Compare commits

...

163 Commits

Author SHA1 Message Date
Josh McKinney
26163effdf refactor: modularize
WIP - this is just a PoC to understand whether this would work fine, termion and termwiz disabled for now
2024-09-27 15:01:08 -07:00
Josh McKinney
bc10af5931 chore(style): make Debug output for Text/Line/Span/Style more concise (#1383)
Given:

```rust
Text::from_iter([
    Line::from("without line fields"),
    Line::from("with line fields").bold().centered(),
    Line::from_iter([
        Span::from("without span fields"),
        Span::from("with span fields")
            .green()
            .on_black()
            .italic()
            .not_dim(),
    ]),
])
```

Debug:
```
Text [Line [Span("without line fields")], Line { style: Style::new().add_modifier(Modifier::BOLD), alignment: Some(Center), spans: [Span("with line fields")] }, Line [Span("without span fields"), Span { style: Style::new().green().on_black().add_modifier(Modifier::ITALIC).remove_modifier(Modifier::DIM), content: "with span fields" }]]
```

Fixes: https://github.com/ratatui/ratatui/issues/1382
---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-09-26 20:38:23 +03:00
dependabot[bot]
784f67a912 chore(deps): update octocrab requirement from 0.39.0 to 0.40.0 (#1386)
Updates the requirements on
[octocrab](https://github.com/XAMPPRocky/octocrab) to permit the latest
version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/XAMPPRocky/octocrab/releases">octocrab's
releases</a>.</em></p>
<blockquote>
<h2>v0.40.0</h2>
<h3>Added</h3>
<ul>
<li>Support <code>remove_assignees</code> on issue API (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/686">#686</a>)</li>
<li>add missing fields in <code>CreateForkBuilder</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/682">#682</a>)</li>
<li>Add <code>Gist::public</code> field (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/678">#678</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(refs)</em> [<strong>breaking</strong>] remove
<code>Reference::Commit</code> variant (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/697">#697</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Fix typo in cfg_attr statement (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/693">#693</a>)</li>
<li>Handle empty author object in pr_commits (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/656">#656</a>)</li>
<li>Add <code>DeviceCodes::poll_until_available</code> method (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/679">#679</a>)</li>
<li>Uncomment pr_commits function (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/680">#680</a>)</li>
<li>Only add base_path if req_pandq does not contain it (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/684">#684</a>)</li>
<li>Update code scanning alert (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/673">#673</a>)</li>
<li>Added <code>merged_by</code> and <code>closed_by</code> fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/674">#674</a>)</li>
<li>Update and Fixes to the Code Scanning Models &amp; Webhooks (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/675">#675</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/XAMPPRocky/octocrab/blob/main/CHANGELOG.md">octocrab's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.39.0...v0.40.0">0.40.0</a>
- 2024-09-22</h2>
<h3>Added</h3>
<ul>
<li>Support <code>remove_assignees</code> on issue API (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/686">#686</a>)</li>
<li>add missing fields in <code>CreateForkBuilder</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/682">#682</a>)</li>
<li>Add <code>Gist::public</code> field (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/678">#678</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(refs)</em> [<strong>breaking</strong>] remove
<code>Reference::Commit</code> variant (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/697">#697</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Fix typo in cfg_attr statement (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/693">#693</a>)</li>
<li>Handle empty author object in pr_commits (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/656">#656</a>)</li>
<li>Add <code>DeviceCodes::poll_until_available</code> method (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/679">#679</a>)</li>
<li>Uncomment pr_commits function (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/680">#680</a>)</li>
<li>Only add base_path if req_pandq does not contain it (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/684">#684</a>)</li>
<li>Update code scanning alert (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/673">#673</a>)</li>
<li>Added <code>merged_by</code> and <code>closed_by</code> fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/674">#674</a>)</li>
<li>Update and Fixes to the Code Scanning Models &amp; Webhooks (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/675">#675</a>)</li>
</ul>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.38.0...v0.39.0">0.39.0</a>
- 2024-07-30</h2>
<h3>Added</h3>
<ul>
<li>support permission on list_collaborators (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/630">#630</a>)</li>
<li>add check run pull requests and list parameters (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/494">#494</a>)</li>
<li>implement hook deliveries (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/668">#668</a>)</li>
<li>allow sending non String payload with execute (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/665">#665</a>)</li>
<li>added /user/blocks functionality (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/657">#657</a>)</li>
<li>add method to create repo webhook (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/640">#640</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>use put instead of get for set_thread_subscription (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/661">#661</a>)</li>
<li><em>(builder)</em> Change add_retry_config signature to match others
in OctocrabBuilder (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/643">#643</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>getting Code Scanning (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/669">#669</a>)</li>
<li>added missing /repos/{owner}/{repo}/pulls/... handlers (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/546">#546</a>)
(<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/605">#605</a>)</li>
<li>Properly mark feature-gated functionality in docs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/662">#662</a>)</li>
<li>repos/releases improvements (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/666">#666</a>)</li>
<li>Add AutoRebaseEnabled to models.rs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/660">#660</a>)</li>
<li>cargo fmt (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/658">#658</a>)</li>
<li>Fix issue <a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/635">#635</a>
(<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/637">#637</a>)</li>
<li>Update issues.rs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/634">#634</a>)</li>
<li>Add head repo to create pr (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/636">#636</a>)</li>
<li>Added support for make_latest in UpdateReleaseBuilder (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/646">#646</a>)</li>
<li>Changing the user name from required to optional parameter (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/650">#650</a>)</li>
<li>Update models.rs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/651">#651</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="bd8b648282"><code>bd8b648</code></a>
chore: release (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/685">#685</a>)</li>
<li><a
href="a8bcee4c29"><code>a8bcee4</code></a>
fix(refs)!: remove <code>Reference::Commit</code> variant (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/697">#697</a>)</li>
<li><a
href="3167540eb0"><code>3167540</code></a>
Fix typo in cfg_attr statement (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/693">#693</a>)</li>
<li><a
href="01767f30eb"><code>01767f3</code></a>
Handle empty author object in pr_commits (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/656">#656</a>)</li>
<li><a
href="689ee43ab0"><code>689ee43</code></a>
feat: Support <code>remove_assignees</code> on issue API (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/686">#686</a>)</li>
<li><a
href="8aa113e079"><code>8aa113e</code></a>
Add <code>DeviceCodes::poll_until_available</code> method (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/679">#679</a>)</li>
<li><a
href="501ef7439d"><code>501ef74</code></a>
feat: add missing fields in <code>CreateForkBuilder</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/682">#682</a>)</li>
<li><a
href="807cc7592d"><code>807cc75</code></a>
Uncomment pr_commits function (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/680">#680</a>)</li>
<li><a
href="0459e31986"><code>0459e31</code></a>
Only add base_path if req_pandq does not contain it (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/684">#684</a>)</li>
<li><a
href="70e9b9e49f"><code>70e9b9e</code></a>
feat: Add <code>Gist::public</code> field (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/678">#678</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.39.0...v0.40.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-09-23 21:21:17 +03:00
Josh McKinney
f4880b40cc chore: lock unicode-width version to 0.1.13 (#1384)
0.1.14 contains breaking changes which we'll need to investigate.
This commit puts a lock on the current version for now.

Changes
https://github.com/unicode-rs/unicode-width/compare/v0.1.13...v0.1.14
2024-09-21 02:58:29 -07:00
Josh McKinney
67c0ea243b chore(block): deprecate block::Title (#1372)
`ratatui::widgets::block::Title` is deprecated in favor of using `Line`
to represent titles.
This removes an unnecessary layer of wrapping (string -> Span -> Line ->
Title).

This struct will be removed in a future release of Ratatui (likely
0.31).
For more information see:
<https://github.com/ratatui/ratatui/issues/738>

To update your code:
```rust
Block::new().title(Title::from("foo"));
// becomes any of
Block::new().title("foo");
Block::new().title(Line::from("foo"));

Block::new().title(Title::from("foo").position(Position::TOP));
// becomes any of
Block::new().title_top("foo");
Block::new().title_top(Line::from("foo"));

Block::new().title(Title::from("foo").position(Position::BOTTOM));
// becomes any of
Block::new().title_bottom("foo");
Block::new().title_bottom(Line::from("foo"));
```
2024-09-20 10:21:26 +03:00
Anthony Rodgers
b9653ba05a fix: prevent calender render panic when terminal height is small (#1380)
Fixes: #1379
2024-09-19 06:02:26 -07:00
dependabot[bot]
9875d9facc chore(deps): bump DavidAnson/markdownlint-cli2-action from 16 to 17 (#1376)
Bumps
[DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action)
from 16 to 17.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/davidanson/markdownlint-cli2-action/releases">DavidAnson/markdownlint-cli2-action's
releases</a>.</em></p>
<blockquote>
<h2>Update markdownlint version (markdownlint-cli2 v0.14.0, markdownlint
v0.35.0).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.13.0, markdownlint
v0.34.0).</h2>
<p>No release notes provided.</p>
<p>Update markdownlint version (markdownlint-cli2 v0.12.1, markdownlint
v0.33.0).</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.11.0, markdownlint
v0.32.1), remove deprecated &quot;command&quot; input.</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.10.0, markdownlint
v0.31.1).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.9.2, markdownlint
v0.30.0).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.8.1, markdownlint
v0.29.0), add &quot;config&quot; and &quot;fix&quot; inputs, deprecate
&quot;command&quot; input.</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.7.1, markdownlint
v0.28.2).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.7.0, markdownlint
v0.28.1), include link to rule information in title of annotations
(clickable in GitHub).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.6.0, markdownlint
v0.27.0).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.5.1, markdownlint
v0.26.2).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.4.0, markdownlint
v0.25.1)</h2>
<p>No release notes provided.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="db43aef879"><code>db43aef</code></a>
Update to version 17.0.0.</li>
<li><a
href="c0decc52d0"><code>c0decc5</code></a>
Bump <code>@​stylistic/eslint-plugin</code> from 2.7.2 to 2.8.0</li>
<li><a
href="dd2171bb17"><code>dd2171b</code></a>
Bump eslint from 9.9.1 to 9.10.0</li>
<li><a
href="85b2286968"><code>85b2286</code></a>
Bump <code>@​eslint/js</code> from 9.9.1 to 9.10.0</li>
<li><a
href="95aa6ed6ed"><code>95aa6ed</code></a>
Freshen generated index.js file.</li>
<li><a
href="476aead54e"><code>476aead</code></a>
Bump markdownlint-cli2 from 0.13.0 to 0.14.0</li>
<li><a
href="da0291d977"><code>da0291d</code></a>
Freshen generated index.js file.</li>
<li><a
href="235535bdb7"><code>235535b</code></a>
Add <code>@​stylistic/eslint-plugin</code> to ESLint configuration.</li>
<li><a
href="20384985f1"><code>2038498</code></a>
Bump eslint from 9.9.0 to 9.9.1</li>
<li><a
href="ea9d2c1e2e"><code>ea9d2c1</code></a>
Bump <code>@​eslint/js</code> from 9.9.0 to 9.9.1</li>
<li>Additional commits viewable in <a
href="https://github.com/davidanson/markdownlint-cli2-action/compare/v16...v17">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=DavidAnson/markdownlint-cli2-action&package-manager=github_actions&previous-version=16&new-version=17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 09:24:38 -07:00
Josh McKinney
b88717b65f docs(constraint): add note about percentages (#1368)
Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2024-09-13 14:42:52 +03:00
Vitalii Kryvenko
5635b930c7 ci: add cargo-machete and remove unused deps (#1362)
https://github.com/bnjbvr/cargo-machete
2024-09-08 18:54:33 -07:00
Hossein Nedaee
870bc6a64a docs: use Frame::area() instead of size() in examples (#1361)
`Frame::size()` is deprecated
2024-09-08 13:20:59 -07:00
FujiApple
da821b431e fix: clippy lints from rust 1.81.0 (#1356) 2024-09-06 22:54:36 -07:00
Patryk Wychowaniec
68886d1787 fix: add unstable-backend-writer feature (#1352)
https://github.com/ratatui/ratatui/pull/991 created a new unstable
feature, but forgot to add it to Cargo.toml, making it impossible to use
on newer versions of rustc - this commit fixes it.
2024-09-06 15:33:14 -07:00
Patryk Wychowaniec
0f48239778 fix(terminal): resize() now resizes fixed viewports (#1353)
`Terminal::resize()` on a fixed viewport used to do nothing due to
an accidentally shadowed variable. This now works as intended.
2024-09-04 12:28:30 -07:00
Valentin271
b13e2f9473 docs(backend): added link to stdio FAQ (#1349) 2024-09-01 15:30:13 -07:00
Orhun Parmaksız
c777beb658 chore(ci): bump git-cliff-action to v4 (#1350)
See: https://github.com/orhun/git-cliff-action/releases/tag/v4.0.0
2024-09-01 16:23:35 +02:00
Mo
20c88aaa5b refactor: Avoid unneeded allocations (#1345) 2024-08-26 13:59:30 -07:00
Orhun Parmaksız
e02947be61 style(example): update panic message in minimal template (#1344) 2024-08-25 04:45:34 -07:00
Orhun Parmaksız
3a90e2a761 chore(release): prepare for 0.28.1 (#1343)
🧀 

The current release steps in reference to #1337

- Bump version in `Cargo.toml`
- `git cliff -u -p CHANGELOG.md -t v0.28.1`
- Merge the PR
- `git tag v0.28.1`
- `git push origin v0.28.1`

We can probably automate away most of these with `release-plz` when it
fully supports `git-cliff`'s GitHub integration.
2024-08-25 12:23:26 +03:00
Orhun Parmaksız
65da535745 chore(ci): update release strategy (#1337)
closes #1232 

Now we can trigger point releases by pushing a tag (follow the
instructions in `RELEASE.md`). This will create a release with generated
changelog.

There is still a lack of automation (e.g. updating `CHANGELOG.md`), but
this PR is a good start towards improving that.
2024-08-25 11:02:29 +03:00
Tayfun Bocek
9ed85fd1dd docs(table): fix incorrect backticks in TableState docs (#1342) 2024-08-24 14:26:37 -07:00
Neal Fachan
aed60b9839 fix(terminal): Terminal::insert_before would crash when called while the viewport filled the screen (#1329)
Reimplement Terminal::insert_before. The previous implementation would
insert the new lines in chunks into the area between the top of the
screen and the top of the (new) viewport. If the viewport filled the
screen, there would be no area in which to insert lines, and the
function would crash.

The new implementation uses as much of the screen as it needs to, all
the way up to using the whole screen.

This commit:
- adds a scrollback buffer to the `TestBackend` so that tests can
inspect and assert the state of the scrollback buffer in addition to the
screen
- adds functions to `TestBackend` to assert the state of the scrollback
- adds and updates `TestBackend` tests to test the behavior of the
scrollback and the new asserting functions
- reimplements `Terminal::insert_before`, including adding two new
helper functions `Terminal::draw_lines` and `Terminal::scroll_up`.
- updates the documentation for `Terminal::insert_before` to clarify
some of the edge cases
- updates terminal tests to assert the state of the scrollback buffer
- adds a new test for the condition that causes the bug
- adds a conversion constructor `Cell::from(char)`

Fixes: https://github.com/ratatui/ratatui/issues/999
2024-08-23 15:27:54 -07:00
Josh McKinney
3631b34f53 docs(examples): add widget implementation example (#1147)
This new example documents the various ways to implement widgets in
Ratatui. It demonstrates how to implement the `Widget` trait on a type,
a reference, and a mutable reference. It also shows how to use the
`WidgetRef` trait to render boxed widgets.
2024-08-23 14:30:23 -07:00
Mo
0d5f3c091f test: Avoid unneeded allocations in assertions (#1335)
A vector can be compared to an array.
2024-08-22 09:14:16 -07:00
Josh McKinney
ed51c4b342 feat(terminal): Add ratatui::init() and restore() methods (#1289)
These are simple opinionated methods for creating a terminal that is
useful to use in most apps. The new init method creates a crossterm
backend writing to stdout, enables raw mode, enters the alternate
screen, and sets a panic handler that restores the terminal on panic.

A minimal hello world now looks a bit like:

```rust
use ratatui::{
    crossterm::event::{self, Event},
    text::Text,
    Frame,
};

fn main() {
    let mut terminal = ratatui::init();
    loop {
        terminal
            .draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
            .expect("Failed to draw");
        if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
            break;
        }
    }
    ratatui::restore();
}
```

A type alias `DefaultTerminal` is added to represent this terminal
type and to simplify any cases where applications need to pass this
terminal around. It is equivalent to:
`Terminal<CrosstermBackend<Stdout>>`

We also added `ratatui::try_init()` and `try_restore()`, for situations
where you might want to handle initialization errors yourself instead
of letting the panic handler fire and cleanup. Simple Apps should
prefer the `init` and `restore` functions over these functions.

Corresponding functions to allow passing a `TerminalOptions` with
a `Viewport` (e.g. inline, fixed) are also available
(`init_with_options`,
and `try_init_with_options`).

The existing code to create a backend and terminal will remain and
is not deprecated by this approach. This just provides a simple one
line initialization using the common options.

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-22 15:16:35 +03:00
Josh McKinney
23516bce76 chore: rename ratatui-org to ratatui (#1334)
All urls updated to point at https://github.com/ratatui

To update your repository remotes, you can run the following commands:

```shell
git remote set-url origin https://github.com/ratatui/ratatui
```
2024-08-21 11:35:08 -07:00
Matt Armstrong
6d1bd99544 docs: minor grammar fixes (#1330) 2024-08-17 16:17:42 -07:00
Neal Fachan
2fb0b8a741 fix: fix u16 overflow in Terminal::insert_before. (#1323)
If the amount of characters in the screen above the viewport was greater
than u16::MAX, a multiplication would overflow. The multiply was used to
compute the maximum chunk size. The fix is to just do the multiplication
as a usize and also do the subsequent division as a usize.

There is currently another outstanding issue that limits the amount of
characters that can be inserted when calling Terminal::insert_before to
u16::MAX. However, this bug can still occur even if the viewport and the
amount of characters being inserted are both less than u16::MAX, since
it's dependant on how large the screen is above the viewport.

Fixes #1322
2024-08-13 21:06:49 -07:00
Josh McKinney
0256269a7f build: simplify Windows build (#1317)
Termion is not supported on Windows, so we need to avoid building it.

Adds a conditional dependency to the Cargo.toml file to only include
termion when the target is not Windows. This allows contributors to
build using the `--all-features` flag on Windows rather than needing
to specify the features individually.
2024-08-13 10:09:46 -07:00
Lucas Pickering
fdd5d8c092 fix(text): remove trailing newline from single-line Display trait impl (#1320) 2024-08-11 20:30:36 -07:00
EdJoPaTo
8b624f5952 chore(maintainers): remove EdJoPaTo (#1314) 2024-08-11 20:27:11 -07:00
Josh McKinney
57d8b742e5 chore(ci): use cargo-docs-rs to lint docs (#1318) 2024-08-11 20:09:57 -07:00
Josh McKinney
d5477b50d5 docs(examples): use ratatui::crossterm in examples (#1315) 2024-08-10 17:43:13 -07:00
montmorillonite
730dfd4940 docs(examples): show line gauge in demo example (#1309) 2024-08-07 20:25:43 -07:00
EdJoPaTo
097ee86e39 docs: remove superfluous doc(inline) (#1310)
It's no longer needed since #1260
2024-08-07 20:25:07 -07:00
Jack Wills
3fdb5e8987 docs: fix typo in terminal.rs (#1313) 2024-08-07 19:34:21 -07:00
Orhun Parmaksız
ec88bb81e5 chore(release): prepare for 0.28.0 (#1295)
🧀
2024-08-07 14:56:01 +03:00
Josh McKinney
f04bf855cb perf: add buffer benchmarks (#1303) 2024-08-06 23:05:36 -07:00
Alex Saveau
4753b7241b perf(reflow): eliminate most WordWrapper allocations (#1239)
On large paragraphs (~1MB), this saves hundreds of thousands of
allocations.

TL;DR: reuse as much memory as possible across `next_line` calls.
Instead of allocating new buffers each time, allocate the buffers once
and clear them before reuse.

Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
2024-08-06 20:49:05 -07:00
Josh McKinney
36fa3c11c1 chore(deps): bump crossterm to 0.28.1 (#1304)
https://github.com/crossterm-rs/crossterm/blob/master/CHANGELOG.md\#version-0281
2024-08-07 00:09:38 +03:00
Josh McKinney
69e8ed7db8 chore(deps): remove anyhow from dev dependencies (#1305)
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-06 23:17:11 +03:00
Josh McKinney
5f7a7fbe19 docs(examples): update barcharts gifs (#1306) 2024-08-06 23:09:40 +03:00
Josh McKinney
e6d2e04bcf perf: move benchmarks into a single benchmark harness (#1302)
Consolidates the benchmarks into a single executable rather than having
to create a new cargo.toml setting per and makes it easier to rearrange
these when adding new benchmarks.
2024-08-06 05:31:13 -07:00
Josh McKinney
45fcab7497 chore: add rect::rows benchmark (#1301) 2024-08-06 05:30:07 -07:00
EdJoPaTo
1b9bdd425c docs(contributing): fix minor issues (#1300)
Co-authored-by: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com>
2024-08-06 04:12:08 -07:00
EdJoPaTo
c68ee6c64a feat!: add get/set_cursor_position() methods to Terminal and Backend (#1284)
The new methods return/accept `Into<Position>` which can be either a Position or a (u16, u16) tuple.

```rust
backend.set_cursor_position(Position { x: 0, y: 20 })?;
let position = backend.get_cursor_position()?;
terminal.set_cursor_position((0, 20))?;
let position = terminal.set_cursor_position()?;
```
2024-08-06 04:10:28 -07:00
EdJoPaTo
afe15349c8 feat(chart)!: accept IntoIterator for axis labels (#1283)
BREAKING CHANGES: #1273 is already breaking and this only advances the
already breaking part
2024-08-06 11:39:44 +02:00
Josh McKinney
fe4eeab676 docs(examples): simplify the barchart example (#1079)
The `barchart` example has been split into two examples: `barchart` and
`barchart-grouped`. The `barchart` example now shows a simple barchart
with random data, while the `barchart-grouped` example shows a grouped
barchart with fake revenue data.

This simplifies the examples a bit so they don't cover too much at once.

- Simplify the rendering functions
- Fix several clippy lints that were marked as allowed

---------

Co-authored-by: EdJoPaTo <rfc-conform-git-commit-email@funny-long-domain-label-everyone-hates-as-it-is-too-long.edjopato.de>
2024-08-06 01:10:58 -07:00
Josh McKinney
a23ecd9b45 feat(buffer): add Buffer::cell, cell_mut and index implementations (#1084)
Code which previously called `buf.get(x, y)` or `buf.get_mut(x, y)`
should now use index operators, or be transitioned to `buff.cell()` or
`buf.cell_mut()` for safe access that avoids panics by returning
`Option<&Cell>` and `Option<&mut Cell>`.

The new methods accept `Into<Position>` instead of `x` and `y`
coordinates, which makes them more ergonomic to use.

```rust
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));

let cell = buf[(0, 0)];
let cell = buf[Position::new(0, 0)];

let symbol = buf.cell((0, 0)).map(|cell| cell.symbol());
let symbol = buf.cell(Position::new(0, 0)).map(|cell| cell.symbol());

buf[(0, 0)].set_symbol("🐀");
buf[Position::new(0, 0)].set_symbol("🐀");

buf.cell_mut((0, 0)).map(|cell| cell.set_symbol("🐀"));
buf.cell_mut(Position::new(0, 0)).map(|cell| cell.set_symbol("🐀"));
```

The existing `get()` and `get_mut()` methods are marked as deprecated.
These are fairly widely used and we will leave these methods around on
the buffer for a longer time than our normal deprecation approach (2
major release)

Addresses part of: https://github.com/ratatui-org/ratatui/issues/1011

---------

Co-authored-by: EdJoPaTo <rfc-conform-git-commit-email@funny-long-domain-label-everyone-hates-as-it-is-too-long.edjopato.de>
2024-08-06 00:40:47 -07:00
EdJoPaTo
bb71e5ffd4 docs(readme): remove MSRV (#1266)
This notice was useful when the `Cargo.toml` had no standardized field
for this. Now it's easier to look it up in the `Cargo.toml` and it's
also a single point of truth. Updating the README was overlooked for
quite some time so it's better to just omit it rather than having
something wrong that will be forgotten again in the future.
2024-08-05 22:04:48 -07:00
Orhun Parmaksız
f2fa1ae9aa docs(breaking-changes): add missing code block (#1291) 2024-08-05 20:25:01 -07:00
Orhun Parmaksız
f687af7c0d docs(breaking-changes): mention removed lifetime of ToText trait (#1292) 2024-08-05 20:18:58 -07:00
EdJoPaTo
f97e07c08a feat(frame): replace Frame::size() with Frame::area() (#1293)
Area is the more correct term for the result of this method.
The Frame::size() method is marked as deprecated and will be
removed around Ratatui version 0.30 or later.

Fixes: https://github.com/ratatui-org/ratatui/pull/1254#issuecomment-2268061409
2024-08-05 20:15:14 -07:00
EdJoPaTo
bb68bc6968 refactor(backend)!: return Size from Backend::size instead of Rect (#1254)
The `Backend::size` method returns a `Size` instead of a `Rect`.
There is no need for the position here as it was always 0,0.
2024-08-05 17:36:50 -07:00
dependabot[bot]
ffeb8e46b9 chore(deps): update rstest requirement from 0.21.0 to 0.22.0 (#1297)
Updates the requirements on [rstest](https://github.com/la10736/rstest)
to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/la10736/rstest/releases">rstest's
releases</a>.</em></p>
<blockquote>
<h2>Version 0.22.0</h2>
<p>Destructuring input data</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.22.0] 2024/8/4</h2>
<h3>Changed</h3>
<ul>
<li>Now it's possible destructuring input values both for cases, values
and fixtures. See <a
href="https://redirect.github.com/la10736/rstest/issues/231">#231</a>
for details</li>
</ul>
<h3>Add</h3>
<ul>
<li>Implemented <code>#[ignore]</code> attribute to ignore test
parameters during fixtures resolution/injection. See <a
href="https://redirect.github.com/la10736/rstest/issues/228">#228</a>
for details</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Lot of typo in code</li>
</ul>
<h2>[0.21.0] 2024/6/1</h2>
<h3>Changed</h3>
<ul>
<li>Add feature <code>crate-name</code> enabled by default to opt-in
crate rename
support. See <a
href="https://redirect.github.com/la10736/rstest/issues/258">#258</a></li>
</ul>
<h2>[0.20.0] 2024/5/30</h2>
<h3>Add</h3>
<ul>
<li>Implemented <code>#[by_ref]</code> attribute to take get a local
lifetime for test arguments.
See <a
href="https://redirect.github.com/la10736/rstest/issues/241">#241</a>
for more details. Thanks to
<a href="https://github.com/narpfel"><code>@​narpfel</code></a> for
suggesting it and useful discussions.</li>
<li>Support for import <code>rstest</code> with another name. See <a
href="https://redirect.github.com/la10736/rstest/issues/221">#221</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Don't remove Lifetimes from test function if any. See <a
href="https://redirect.github.com/la10736/rstest/issues/230">#230</a>
<a href="https://redirect.github.com/la10736/rstest/issues/241">#241</a>
for more details.</li>
<li><a
href="https://doc.rust-lang.org/std/path/struct.PathBuf.html"><code>PathBuf</code></a>
does no longer need to be
in scope when using <code>#[files]</code> (see <a
href="https://redirect.github.com/la10736/rstest/pull/242">#242</a>)</li>
<li><code>#[from(now::accept::also::path::for::fixture)]</code> See <a
href="https://redirect.github.com/la10736/rstest/issues/246">#246</a>
for more details</li>
</ul>
<h2>[0.19.0] 2024/4/9</h2>
<h3>Changed</h3>
<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><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></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="62134281cf"><code>6213428</code></a>
Prepare 0.22.0</li>
<li><a
href="16591fdcef"><code>16591fd</code></a>
Make clippy happy</li>
<li><a
href="d40e785d74"><code>d40e785</code></a>
Merge pull request <a
href="https://redirect.github.com/la10736/rstest/issues/269">#269</a>
from la10736/fix_typos</li>
<li><a
href="9110f0cff7"><code>9110f0c</code></a>
Fix typo</li>
<li><a
href="696eaf63c1"><code>696eaf6</code></a>
Merge pull request <a
href="https://redirect.github.com/la10736/rstest/issues/268">#268</a>
from la10736/arg_destruct</li>
<li><a
href="39490761ca"><code>3949076</code></a>
Fixed warning in beta</li>
<li><a
href="d35ade2521"><code>d35ade2</code></a>
Docs and make clippy happy</li>
<li><a
href="40087a7aa3"><code>40087a7</code></a>
Implementation and integration tests</li>
<li><a
href="fcf732dc34"><code>fcf732d</code></a>
Merge pull request <a
href="https://redirect.github.com/la10736/rstest/issues/267">#267</a>
from marcobacis/ignore_parameter</li>
<li><a
href="cf9dd0bf51"><code>cf9dd0b</code></a>
update docs, simplified unit test</li>
<li>Additional commits viewable in <a
href="https://github.com/la10736/rstest/compare/v0.21.0...v0.22.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-08-05 17:31:01 -07:00
Josh McKinney
2fd5ae64bf docs(widgets): document stability of WidgetRef (#1288)
Addresses some confusion about when to implement `WidgetRef` vs `impl
Widget for &W`. Notes the stability rationale and links to an issue that
helps explain the context of where we're at in working this out.
2024-08-05 17:29:14 -07:00
Orhun Parmaksız
82b70fd329 chore(ci): integrate cargo-semver-checks (#1166)
>
[`cargo-semver-checks`](https://github.com/obi1kenobi/cargo-semver-checks):
Lint your crate API changes for semver violations.
2024-08-05 19:28:38 +03:00
dependabot[bot]
94328a2977 chore(deps): bump EmbarkStudios/cargo-deny-action from 1 to 2 (#1296)
Bumps
[EmbarkStudios/cargo-deny-action](https://github.com/embarkstudios/cargo-deny-action)
from 1 to 2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embarkstudios/cargo-deny-action/releases">EmbarkStudios/cargo-deny-action's
releases</a>.</em></p>
<blockquote>
<h2>Release 2.0.1 - cargo-deny 0.16.1</h2>
<h3>Fixed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/691">PR#691</a>
fixed an issue where workspace dependencies that used the current dir
'.' path component would incorrectly trigger the
<code>unused-workspace-dependency</code> lint.</li>
</ul>
<h2>Release 2.0.0 - cargo-deny 0.16.0</h2>
<h2><code>Action</code></h2>
<h3>Added</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny-action/pull/78">PR#78</a>
added SSH support, thanks <a
href="https://github.com/nagua"><code>@​nagua</code></a>!</li>
</ul>
<h3>Changed</h3>
<ul>
<li>This release includes breaking changes in cargo-deny, so this
release begins the <code>v2</code> tag, using <code>v1</code> will be
stable but not follow future <code>cargo-deny</code> releases.</li>
</ul>
<h2><code>cargo-deny</code></h2>
<h3>Removed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/681">PR#681</a>
finished the deprecation introduced in <a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/611">PR#611</a>,
making the usage of the deprecated fields into errors.</li>
</ul>
<h4><code>[advisories]</code></h4>
<p>The following fields have all been removed in favor of denying all
advisories by default. To ignore an advisory the <a
href="https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html#the-ignore-field-optional"><code>ignore</code></a>
field can be used as before.</p>
<ul>
<li><code>vulnerability</code> - Vulnerability advisories are now
<code>deny</code> by default</li>
<li><code>unmaintained</code> - Unmaintained advisories are now
<code>deny</code> by default</li>
<li><code>unsound</code> - Unsound advisories are now <code>deny</code>
by default</li>
<li><code>notice</code> - Notice advisories are now <code>deny</code> by
default</li>
<li><code>severity-threshold</code> - The severity of vulnerabilities is
now irrelevant</li>
</ul>
<h4><code>[licenses]</code></h4>
<p>The following fields have all been removed in favor of denying all
licenses that are not explicitly allowed via either <a
href="https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html#the-allow-field-optional"><code>allow</code></a>
or <a
href="https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html#the-exceptions-field-optional"><code>exceptions</code></a>.</p>
<ul>
<li><code>unlicensed</code> - Crates whose license(s) cannot be
confidently determined are now always errors. The <a
href="https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html#the-clarify-field-optional"><code>clarify</code></a>
field can be used to help cargo-deny determine the license.</li>
<li><code>allow-osi-fsf-free</code> - The OSI/FSF Free attributes are
now irrelevant, only whether it is explicitly allowed.</li>
<li><code>copyleft</code> - The copyleft attribute is now irrelevant,
only whether it is explicitly allowed.</li>
<li><code>default</code> - The default is now <code>deny</code>.</li>
<li><code>deny</code> - All licenses are now denied by default, this
field added nothing.</li>
</ul>
<h3>Changed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/685">PR#685</a>
follows up on <a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/673">PR#673</a>,
moving the fields that were added to their own separate <a
href="https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html#the-workspace-dependencies-field-optional"><code>bans.workspace-dependencies</code></a>
section. This is an unannounced breaking change but is fairly minor and
0.15.0 was never released on github actions so the amount of people
affected by this will be (hopefully) small. This also makes the
workspace duplicate detection off by default since the field is
optional, <em>but</em> makes it so that if not specified workspace
duplicates are now <code>deny</code> instead of <code>warn</code>.</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/685">PR#685</a>
resolved <a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/issues/682">#682</a>
by adding the <code>include-path-dependencies</code> field, allowing
path dependencies to be ignored if it is <code>false</code>.</li>
</ul>
<h2>Release 1.6.3 - cargo-deny 0.14.21</h2>
<h3>Fixed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/643">PR#643</a>
resolved <a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/issues/629">#629</a>
by making the hosted git (github, gitlab, bitbucket) org/user name
comparison case-insensitive. Thanks <a
href="https://github.com/pmnlla"><code>@​pmnlla</code></a>!</li>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/649">PR#649</a>
fixed an issue where depending on the same crate multiple times by using
different <code>cfg()/triple</code> targets could cause features to be
resolved incorrectly and thus crates to be not pulled into the graph
used for checking.</li>
</ul>
<h2>[0.14.20] - 2024-03-23</h2>
<h3>Fixed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/642">PR#642</a>
resolved <a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/issues/641">#641</a>
by pinning <code>gix-transport</code> (and its unique dependencies) to
0.41.2 as a workaround for <code>cargo install</code> not using the
lockfile. See <a
href="https://redirect.github.com/Byron/gitoxide/issues/1328">this
issue</a> for more information.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8371184bd1"><code>8371184</code></a>
Bump to 0.16.1</li>
<li><a
href="08954043da"><code>0895404</code></a>
Move to v2 tag</li>
<li><a
href="10d8902cf9"><code>10d8902</code></a>
Bump to 0.16.0</li>
<li><a
href="d425dbf412"><code>d425dbf</code></a>
Update .gitignore</li>
<li><a
href="53bface5b1"><code>53bface</code></a>
Update image base</li>
<li><a
href="3f8dc3eed7"><code>3f8dc3e</code></a>
Add ability to fetch git dependecies in cargo via ssh (<a
href="https://redirect.github.com/embarkstudios/cargo-deny-action/issues/78">#78</a>)</li>
<li>See full diff in <a
href="https://github.com/embarkstudios/cargo-deny-action/compare/v1...v2">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 19:18:21 +03:00
Orhun Parmaksız
d468463fc6 docs(breaking-changes): fix the PR link (#1294) 2024-08-05 15:02:26 +03:00
Josh McKinney
6e7b4e4d55 docs(examples): add async example (#1248)
This example demonstrates how to use Ratatui with widgets that fetch
data asynchronously. It uses the `octocrab` crate to fetch a list of
pull requests from the GitHub API. You will need an environment
variable named `GITHUB_TOKEN` with a valid GitHub personal access
token. The token does not need any special permissions.

Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>
2024-08-04 22:26:56 -07:00
EdJoPaTo
864cd9ffef fix(TestBackend): prevent area mismatch (#1252)
Removes the height and width fields from TestBackend, which can get
out of sync with the Buffer, which currently clamps to 255,255.

This changes the `TestBackend` serde representation. It should be
possible to read older data, but data generated after this change
can't be read by older versions.
2024-08-04 18:34:08 -07:00
EdJoPaTo
c08b522d34 fix(chart): allow removing all the axis labels (#1282)
`axis.labels(vec![])` removes all the labels correctly.

This makes calling axis.labels with an empty Vec the equivalent
of not calling axis.labels. It's likely that this is never used, but it
prevents weird cases by removing the mix-up of `Option::None`
and `Vec::is_empty`, and simplifies the implementation code.
2024-08-04 16:41:20 -07:00
Josh McKinney
716c93136e docs: document crossterm breaking change (#1281) 2024-08-04 15:15:18 -07:00
Josh McKinney
5eeb1ccbc4 docs(github): Create CODE_OF_CONDUCT.md (#1279) 2024-08-04 17:26:02 +03:00
Josh McKinney
f77503050f docs: update main lib.rs / README examples (#1280) 2024-08-04 05:09:28 -07:00
dependabot[bot]
cd0d31c2dc chore(deps): update crossterm requirement from 0.27 to 0.28 (#1278)
Updates the requirements on
[crossterm](https://github.com/crossterm-rs/crossterm) to permit the
latest version.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/crossterm-rs/crossterm/blob/master/CHANGELOG.md">crossterm's
changelog</a>.</em></p>
<blockquote>
<h1>Version 0.28.1</h1>
<h2>Fixed 🐛</h2>
<ul>
<li>Fix broken build on linux when using <code>use-dev-tty</code> with
(<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/906">#906</a>)</li>
</ul>
<h2>Breaking ⚠️</h2>
<ul>
<li>Fix desync with mio and signalhook between repo and published crate.
(upgrade to mio 1.0)</li>
</ul>
<h1>Version 0.28</h1>
<h2>Added </h2>
<ul>
<li>Capture double click mouse events on windows (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/826">#826</a>)</li>
<li>(De)serialize Reset color (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/824">#824</a>)</li>
<li>Add functions to allow constructing <code>Attributes</code> in a
const context (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/817">#817</a>)</li>
<li>Implement <code>Display</code> for <code>KeyCode</code> and
<code>KeyModifiers</code> (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/862">#862</a>)</li>
</ul>
<h2>Changed ⚙️</h2>
<ul>
<li>Use Rustix by default instead of libc. Libc can be re-enabled if
necessary with the <code>libc</code> feature flag (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/892">#892</a>)</li>
<li><code>FileDesc</code> now requires a lifetime annotation.</li>
<li>Improve available color detection (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/885">#885</a>)</li>
<li>Speed up <code>SetColors</code> by ~15-25% (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/879">#879</a>)</li>
<li>Remove unsafe and unnecessary size argument from
<code>FileDesc::read()</code> (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/821">#821</a>)</li>
</ul>
<h2>Breaking ⚠️</h2>
<ul>
<li>Fix duplicate bit masks for caps lock and num lock (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/863">#863</a>).
This breaks serialization of <code>KeyEventState</code></li>
</ul>
<h1>Version 0.27.1</h1>
<h2>Added </h2>
<ul>
<li>Add support for (de)serializing <code>Reset</code>
<code>Color</code></li>
</ul>
<h1>Version 0.27</h1>
<h2>Added </h2>
<ul>
<li>Add <code>NO_COLOR</code> support (<a
href="https://no-color.org/">https://no-color.org/</a>)</li>
<li>Add option to force overwrite <code>NO_COLOR</code> (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/802">#802</a>)</li>
<li>Add support for scroll left/right events on windows and unix systems
(<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/788">#788</a>).</li>
<li>Add <code>window_size</code> function to fetch pixel width/height of
screen for more sophisticated rendering in terminals.</li>
<li>Add support for deserializing hex color strings to
<code>Color</code> e.g #fffff.</li>
</ul>
<h2>Changed ⚙️</h2>
<ul>
<li>Make the events module an optional feature <code>events</code> (to
make crossterm more lightweight) (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/776">#776</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/crossterm-rs/crossterm/commits">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-08-04 03:34:38 -07:00
EdJoPaTo
41a910004d chore(github): use the GitHub organization team as codeowners (#1081)
Use GitHub organization team in CODEOWNERS and create MAINTAINERS.md
2024-08-04 11:28:54 +03:00
Josh McKinney
8433d0958b docs: update demo image (#1276)
Follow up to https://github.com/ratatui-org/ratatui/pull/1203
2024-08-04 11:06:37 +03:00
Josh McKinney
8d4a1026ab feat(barchart)!: allow axes to accept Lines (#1273)
Fixes: https://github.com/ratatui-org/ratatui/issues/1272
2024-08-03 16:16:57 -07:00
Josh McKinney
edc2af9822 chore: replace big_text with hardcoded logo (#1203)
big_text.rs was a copy of the code from tui-big-text and was getting
gradually out of sync with the original crate. It was also rendering
something a bit different than the Ratatui logo. This commit replaces
the big_text.rs file with a much smaller string representation of the
Ratatui logo.

![demo2](https://raw.githubusercontent.com/ratatui-org/ratatui/images/examples/demo2-destroy.gif)
2024-08-03 16:08:59 -07:00
Josh McKinney
a80a8a6a47 style(format): lint markdown (#1131)
- **chore: Fix line endings for changelog**
- **chore: cleanup markdown lints**
- **ci: add Markdown linter**
- **build: add markdown lint to the makefile**

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-03 20:26:04 +03:00
Alex Saveau
29c8c84fd0 fix: ignore newlines in Span's Display impl (#1270) 2024-08-02 22:54:48 -07:00
Josh McKinney
c2d38509b4 chore: use LF line endings for CHANGELOG.md instead of CRLF (#1269) 2024-08-02 21:03:04 -07:00
josueBarretogit
b70cd03c02 feat: add ListState / TableState scroll_down_by() / scroll_up_by() methods (#1267)
Implement new methods `scroll_down_by(u16)` and `scroll_up_by(u16)` for
both `Liststate` and `Tablestate`.

Closes: #1207
2024-08-02 19:09:26 -07:00
EdJoPaTo
476ac87c99 ci: split up lint job (#1264)
This helps with identifying what failed right from the title. Also steps
after a failing one are now always executed.

Also shortens the steps a bit by removing obvious names.
2024-08-02 17:31:18 -07:00
EdJoPaTo
8857037bff docs(terminal): fix imports (#1263) 2024-08-02 21:37:05 +03:00
EdJoPaTo
e707ff11d1 refactor: internally use Position struct (#1256) 2024-08-02 20:55:41 +03:00
EdJoPaTo
a9fe4284ac chore: update cargo-deny config (#1265)
Update `cargo-deny` config (noticed in
https://github.com/ratatui-org/ratatui/pull/1263#pullrequestreview-2215488414)

See https://github.com/EmbarkStudios/cargo-deny/pull/611
2024-08-02 20:53:57 +03:00
EdJoPaTo
ffc4300558 chore: remove executable flag for rs files (#1262) 2024-08-02 05:11:23 -07:00
Josh McKinney
84cb16483a fix(terminal)!: make terminal module private (#1260)
This is a simplification of the public API that is helpful for new users
that are not familiar with how rust re-exports work, and helps avoid
clashes with other modules in the backends that are named terminal.

BREAKING CHANGE: The `terminal` module is now private and can not be
used directly. The types under this module are exported from the root of
the crate.

```diff
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
```

Fixes: https://github.com/ratatui-org/ratatui/issues/1210
2024-08-02 04:18:00 -07:00
EdJoPaTo
5b89bd04a8 feat(layout): add Size::ZERO and Position::ORIGIN constants (#1253) 2024-08-02 03:56:39 -07:00
EdJoPaTo
32d0695cc2 test(buffer): ensure emojis are rendered (#1258) 2024-08-02 11:06:53 +02:00
Alex Saveau
cd93547db8 fix: remove unnecessary synchronization in layout cache (#1245)
Layout::init_cache no longer returns bool and takes a NonZeroUsize instead of usize

The cache is a thread-local, so doesn't make much sense to require
synchronized initialization.
2024-08-01 23:06:49 -07:00
Orhun Parmaksız
c245c13cc1 chore(ci): onboard bencher for tracking benchmarks (#1174)
https://bencher.dev/console/projects/ratatui-org

Closes: #1092
2024-08-01 22:35:51 -07:00
EdJoPaTo
b2aa843b31 feat(layout): enable serde for Margin, Position, Rect, Size (#1255) 2024-08-01 20:51:14 -07:00
EdJoPaTo
3ca920e881 fix(span): prevent panic on rendering out of y bounds (#1257) 2024-08-01 20:17:42 -07:00
Josh McKinney
b344f95b7c fix: only apply style to first line when rendering a Line (#1247)
A `Line` widget should only apply its style to the first line when
rendering and not the entire area. This is because the `Line` widget
should only render a single line of text. This commit fixes the issue by
clamping the area to a single line before rendering the text.
2024-07-27 17:23:21 -07:00
Josh McKinney
b304bb99bd chore(changelog): fixup git-cliff formatting (#1214)
- Replaced backticks surrounding body with blockquotes. This formats
  significantly better.
- Added a few postprocessors to remove junk like the PR template,
  horizontal lines and some trailing whitespace

---

Note: there is extra non-automatically generated stuff in the changelog
that this would remove - the changes to CHANGELOG.md should not be
merged as-is, and this is worth waiting for @orhun to check out.

Compare:
5e7fbe8c32/CHANGELOG.md
To: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-07-27 11:39:00 +03:00
Tayfun Bocek
be3eb75ea5 perf(table): avoid extra allocations when rendering Table (#1242)
When rendering a `Table` the `Text` stored inside of a `Cell` gets
cloned before rendering. This removes the clone and uses `WidgetRef`
instead, saving us from allocating a `Vec<Line<'_>>` inside `Text`. Also
avoids an allocation when rendering the highlight symbol if it contains
an owned value.
2024-07-26 21:48:29 +03:00
Dheepak Krishnamurthy
efef0d0dc0 chore(ci): change label from breaking change to Type: Breaking Change (#1243)
This PR changes the label that is auto attached to a PR with a breaking
change per the conventional commits specification.
2024-07-26 21:48:05 +03:00
Tayfun Bocek
663486f1e8 perf(list): avoid extra allocations when rendering List (#1244)
When rendering a `List`, each `ListItem` would be cloned. Removing the
clone, and replacing `Widget::render` with `WidgetRef::render_ref` saves
us allocations caused by the clone of the `Text<'_>` stored inside of
`ListItem`.

Based on the results of running the "list" benchmark locally;
Performance is improved by %1-3 for all `render` benchmarks for `List`.
2024-07-26 21:47:30 +03:00
Alex Saveau
7ddfbc0010 fix: unnecessary allocations when creating Lines (#1237)
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
2024-07-26 01:55:04 -07:00
Josh McKinney
3725262ca3 feat(text): Add Add and AddAssign implementations for Line, Span, and Text (#1236)
This enables:

```rust
let line = Span::raw("Red").red() + Span::raw("blue").blue();
let line = Line::raw("Red").red() + Span::raw("blue").blue();
let line = Line::raw("Red").red() + Line::raw("Blue").blue();
let text = Line::raw("Red").red() + Line::raw("Blue").blue();
let text = Text::raw("Red").red() + Line::raw("Blue").blue();

let mut line = Line::raw("Red").red();
line += Span::raw("Blue").blue();

let mut text = Text::raw("Red").red();
text += Line::raw("Blue").blue();

line.extend(vec![Span::raw("1"), Span::raw("2"), Span::raw("3")]);
```
2024-07-26 01:37:49 -07:00
Josh McKinney
84f334163b fix: clippy lints from rust 1.80.0 (#1238) 2024-07-26 00:55:07 -07:00
airblast
03f3124c1d fix(paragraph): line_width, and line_count include block borders (#1235)
The `line_width`, and `line_count` methods for `Paragraph` would not
take into account the `Block` if one was set. This will now correctly
calculate the values including the `Block`'s width/height.

Fixes: #1233
2024-07-23 13:13:50 -07:00
Josh McKinney
c34fb77818 feat(text)!: remove unnecessary lifetime from ToText trait (#1234)
BREAKING CHANGE: The ToText trait no longer has a lifetime parameter.
This change simplifies the trait and makes it easier implement.
2024-07-22 04:24:30 -07:00
Josh McKinney
6ce447c4f3 docs(block): add docs about style inheritance (#1190)
Fixes: https://github.com/ratatui-org/ratatui/issues/1129
2024-07-19 20:02:08 -07:00
Josh McKinney
272d0591a7 docs(paragraph): update main docs (#1202) 2024-07-18 15:59:48 -07:00
Josh McKinney
e81663bec0 refactor(list): split up list.rs into smaller modules (#1204) 2024-07-18 15:59:35 -07:00
EdJoPaTo
7e1bab049b fix(buffer): dont render control characters (#1226) 2024-07-17 13:22:00 +02:00
EdJoPaTo
379dab9cdb build: cleanup dev dependencies (#1231) 2024-07-16 04:35:54 -07:00
Josh McKinney
5b51018501 feat(chart): add GraphType::Bar (#1205)
![Demo](https://vhs.charm.sh/vhs-50v7I5n7lQF7tHCb1VCmFc.gif)
2024-07-15 20:47:50 -07:00
Josh McKinney
7bab9f0d80 chore: add more CompactString::const_new instead of new (#1230) 2024-07-15 20:29:32 -07:00
dependabot[bot]
6d210b3b6b chore(deps): update compact_str requirement from 0.7.1 to 0.8.0 (#1229)
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>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-07-15 14:02:44 -07:00
Liv Haze
935a7187c2 docs(examples): add missing examples to README (#1225)
Resolves: #1014
2024-07-09 21:13:26 -07:00
Josh McKinney
3bb374df88 feat(terminal): add Terminal::try_draw() method (#1209)
This makes it easier to write fallible rendering methods that can use
the `?` operator

```rust
terminal.try_draw(|frame| {
    some_method_that_can_fail()?;
    another_faillible_method()?;
    Ok(())
})?;
```
2024-07-09 00:46:55 -07:00
Liv Haze
50e5674a20 docs(examples): fix typos in tape files (#1224) 2024-07-08 21:57:04 -07:00
Liv Haze
810da72f26 docs(examples): fix hyperlink example tape (#1222) 2024-07-07 20:50:46 -07:00
Emi
7c0665cb0e docs(layout): fix typo in example (#1217) 2024-07-05 04:56:29 -07:00
Josh McKinney
ccf83e6d76 chore: update labels in issue templates (#1212) 2024-06-29 08:38:01 -07:00
Josh McKinney
60bd7f4814 chore(deps): use instabilty instead of stabilty (#1208)
https://github.com/ratatui-org/instability
2024-06-27 08:44:52 -07:00
leohscl
55e0880d2f docs(block): update block documentation (#1206)
Update block documentation with constructor methods and setter methods
in the main doc comment Added an example for using it to surround
widgets

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

The following items have been added to the prelude:
- `layout::{Position, Size}` - these items are used by code that needs
  to interact with the layout system. These are newer items that were
  added in the last few releases, which should be used more liberally.
2024-06-04 20:59:51 -07:00
Josh McKinney
1520ed9d10 feat(layout): impl Display for Position and Size (#1162) 2024-06-03 18:54:50 -07:00
dependabot[bot]
2a74f9d8c1 chore(deps): update rstest requirement from 0.19.0 to 0.21.0 (#1163)
Updates the requirements on [rstest](https://github.com/la10736/rstest)
to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/la10736/rstest/releases">rstest's
releases</a>.</em></p>
<blockquote>
<h2>0.21.0</h2>
<p>Use <code>crate-name</code> feature to enable the crate rename
support (enabled by default)</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/la10736/rstest/blob/master/CHANGELOG.md">rstest's
changelog</a>.</em></p>
<blockquote>
<h2>[0.21.0] 2024/6/1</h2>
<h3>Changed</h3>
<ul>
<li>Add feature <code>crate-name</code> enabled by default to opt-in
crate rename
support. See <a
href="https://redirect.github.com/la10736/rstest/issues/258">#258</a></li>
</ul>
<h2>[0.20.0] 2024/5/30</h2>
<h3>Add</h3>
<ul>
<li>Implemented <code>#[by_ref]</code> attribute to take get a local
lifetime for test arguments.
See <a
href="https://redirect.github.com/la10736/rstest/issues/241">#241</a>
for more details. Thanks to
<a href="https://github.com/narpfel"><code>@​narpfel</code></a> for
suggesting it and useful discussions.</li>
<li>Support for import <code>rstest</code> with another name. See <a
href="https://redirect.github.com/la10736/rstest/issues/221">#221</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Don't remove Lifetimes from test function if any. See <a
href="https://redirect.github.com/la10736/rstest/issues/230">#230</a>
<a href="https://redirect.github.com/la10736/rstest/issues/241">#241</a>
for more details.</li>
<li><a
href="https://doc.rust-lang.org/std/path/struct.PathBuf.html"><code>PathBuf</code></a>
does no longer need to be
in scope when using <code>#[files]</code> (see <a
href="https://redirect.github.com/la10736/rstest/pull/242">#242</a>)</li>
<li><code>#[from(now::accept::also::path::for::fixture)]</code> See <a
href="https://redirect.github.com/la10736/rstest/issues/246">#246</a>
for more details</li>
</ul>
<h2>[0.19.0] 2024/4/9</h2>
<h3>Changed</h3>
<ul>
<li>Defined <code>rust-version</code> for each crate (see <a
href="https://redirect.github.com/la10736/rstest/issues/227">#227</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>
<p><code>#[once]</code> fixtures now require the returned type to be
<a
href="https://doc.rust-lang.org/std/marker/trait.Sync.html"><code>Sync</code></a>
to prevent UB
when tests are executed in parallel. (see <a
href="https://redirect.github.com/la10736/rstest/issues/235">#235</a>
for more details)</p>
</li>
<li>
<p><code>#[future(awt)]</code> and <code>#[awt]</code> now properly
handle mutable (<code>mut</code>) parameters by treating futures as
immutable and
treating the awaited rebinding as mutable.</p>
</li>
</ul>
<h2>[0.18.2] 2023/8/13</h2>
<h3>Changed</h3>
<ul>
<li>Now <code>#[files]</code> accept also parent folders (see <a
href="https://redirect.github.com/la10736/rstest/issues/205">#205</a>
for more details).</li>
</ul>
<h2>[0.18.1] 2023/7/5</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="38da6bfb96"><code>38da6bf</code></a>
Prepare 0.21.0 Release</li>
<li><a
href="ca69788392"><code>ca69788</code></a>
bump version rstest_test to 0.13.0</li>
<li><a
href="b6b43c6740"><code>b6b43c6</code></a>
Clean chackoutlist</li>
<li><a
href="fef4f7b4f4"><code>fef4f7b</code></a>
Implemented Opt-in crate-name support Fix <a
href="https://redirect.github.com/la10736/rstest/issues/258">#258</a></li>
<li><a
href="236be92a8a"><code>236be92</code></a>
Build should use build tests target</li>
<li><a
href="8fde5be94f"><code>8fde5be</code></a>
Prepare next changelog</li>
<li><a
href="f29e6346fe"><code>f29e634</code></a>
Dependency should have a n explicit version to be published</li>
<li><a
href="e27ad2a4db"><code>e27ad2a</code></a>
Removed empty section</li>
<li><a
href="3867794483"><code>3867794</code></a>
Fixed docs</li>
<li><a
href="b90fb8e092"><code>b90fb8e</code></a>
Fix checkout list</li>
<li>Additional commits viewable in <a
href="https://github.com/la10736/rstest/compare/v0.19.0...v0.21.0">compare
view</a></li>
</ul>
</details>
<br />


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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 19:38:06 +03:00
dependabot[bot]
d7ed6c8bad chore(deps)!: update termion requirement from 3.0 to 4.0 (#1106)
Updates the requirements on termion to permit the latest version.

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

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

---

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

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

</details>

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

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


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

[//]: # (dependabot-automerge-start)
Dependabot will merge this PR once CI passes on it, as requested by
@joshka.

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

---

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

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


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-06-02 18:18:36 -07:00
Josh McKinney
8061813f32 refactor: expand glob imports (#1152)
Consensus is that explicit imports make it easier to understand the
example code. This commit removes the prelude import from all examples
and replaces it with the necessary imports, and expands other glob
imports (widget::*, Constraint::*, KeyCode::*, etc.) everywhere else.
Prelude glob imports not in examples are not covered by this PR.

See https://github.com/ratatui-org/ratatui/issues/1150 for more details.
2024-05-29 04:42:29 -07:00
Josh McKinney
74a32afbae feat: re-export backends from the ratatui crate (#1151)
`crossterm`, `termion`, and `termwiz` can now be accessed as
`ratatui::{crossterm, termion, termwiz}` respectively. This makes it
possible to just add the Ratatui crate as a dependency and use the
backend of choice without having to add the backend crates as
dependencies.

To update existing code, replace all instances of `crossterm::` with
`ratatui::crossterm::`, `termion::` with `ratatui::termion::`, and
`termwiz::` with `ratatui::termwiz::`.
2024-05-28 13:23:39 -07:00
EdJoPaTo
4ce67fc84e perf(buffer)!: filled moves the cell to be filled (#1148) 2024-05-27 11:07:27 -07:00
EdJoPaTo
df4b706674 style: enable more rustfmt settings (#1125) 2024-05-26 19:50:10 +02:00
EdJoPaTo
8b447ec4d6 perf(rect)!: Rect::inner takes Margin directly instead of reference (#1008)
BREAKING CHANGE: Margin needs to be passed without reference now.

```diff
-let area = area.inner(&Margin {
+let area = area.inner(Margin {
     vertical: 0,
     horizontal: 2,
 });
```
2024-05-26 19:44:46 +02:00
EdJoPaTo
7a48c5b11b feat(cell): add EMPTY and (const) new method (#1143)
This simplifies calls to `Buffer::filled` in tests.
2024-05-25 14:08:56 -07:00
Josh McKinney
8cfc316bcc chore: alphabetize examples in Cargo.toml (#1145) 2024-05-25 14:03:37 -07:00
EdJoPaTo
2f8a9363fc docs: fix links on docs.rs (#1144)
This also results in a more readable Cargo.toml as the locations of the
things are more obvious now.

Includes rewording of the underline-color feature.

Logs of the errors: https://docs.rs/crate/ratatui/0.26.3/builds/1224962
Also see #989
2024-05-25 10:38:33 -07:00
EdJoPaTo
d92997105b refactor: dont manually impl Default for defaults (#1142)
Replace `impl Default` by `#[derive(Default)]` when its implementation
equals.
2024-05-25 10:34:48 -07:00
EdJoPaTo
42cda6d287 fix: prevent panic from string_slice (#1140)
<https://rust-lang.github.io/rust-clippy/master/index.html#string_slice>
2024-05-25 15:15:08 +02:00
EdJoPaTo
4f7791079e refactor(padding): Add Padding::ZERO as a constant (#1133)
Deprecate Padding::zero()
2024-05-24 23:48:05 -07:00
EdJoPaTo
cf67ed9b88 refactor(lint): use clippy::or_fun_call (#1138)
<https://rust-lang.github.io/rust-clippy/master/index.html#or_fun_call>
2024-05-24 18:33:19 -07:00
EdJoPaTo
8a60a561c9 refactor: needless_pass_by_ref_mut (#1137)
<https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_ref_mut>
2024-05-24 18:32:22 -07:00
Dheepak Krishnamurthy
35941809e1 feat!: Make Stylize's .bg(color) generic (#1103) 2024-05-24 18:05:45 -07:00
EdJoPaTo
73fd367a74 refactor(block): group builder pattern methods (#1134) 2024-05-24 18:04:35 -07:00
EdJoPaTo
1de9a82b7a refactor: simplify if let (#1135)
While looking through lints
[`clippy::option_if_let_else`](https://rust-lang.github.io/rust-clippy/master/index.html#option_if_let_else)
found these. Other findings are more complex so I skipped them.
2024-05-24 18:03:51 -07:00
EdJoPaTo
d6587bc6b0 test(style): use rstest (#1136)
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
2024-05-24 18:02:59 -07:00
Matt Armstrong
f429f688da docs(examples): Remove lifetimes from the List example (#1132)
Simplify the List example by removing lifetimes not strictly necessary
to demonstrate how Ratatui lists work. Instead, the sample strings are
copied into each `TodoItem`. To further simplify, I changed the code to
use a new TodoItem::new function, rather than an implementation of the
`From` trait.
2024-05-24 15:09:24 -07:00
Valentin271
4770e71581 refactor(list)!: remove deprecated start_corner and Corner (#759)
`List::start_corner` was deprecated in v0.25. Use `List::direction` and
`ListDirection` instead.

```diff
- list.start_corner(Corner::TopLeft);
- list.start_corner(Corner::TopRight);
// This is not an error, BottomRight rendered top to bottom previously
- list.start_corner(Corner::BottomRight);
// all becomes
+ list.direction(ListDirection::TopToBottom);
```

```diff
- list.start_corner(Corner::BottomLeft);
// becomes
+ list.direction(ListDirection::BottomToTop);
```

`layout::Corner` is removed entirely.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-05-24 11:52:01 -07:00
Mikołaj Nowak
eef1afe915 feat(LineGauge): allow LineGauge background styles (#565)
This PR deprecates `gauge_style` in favor of `filled_style` and
`unfilled_style` which can have it's foreground and background styled.

`cargo run --example=line_gauge --features=crossterm`

https://github.com/ratatui-org/ratatui/assets/5149215/5fb2ce65-8607-478f-8be4-092e08612f5b

Implements: <https://github.com/ratatui-org/ratatui/issues/424>

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-05-24 11:42:52 -07:00
EdJoPaTo
257db6257f refactor(cell): must_use and simplify style() (#1124)
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
2024-05-21 23:26:32 -07:00
EdJoPaTo
70df102de0 build(bench): improve benchmark consistency (#1126)
Codegen units are optimized on their own. Per default bench / release
have 16 codegen units. What ends up in a codeget unit is rather random
and can influence a benchmark result as a code change can move stuff
into a different codegen unit → prevent / allow LLVM optimizations
unrelated to the actual change.

More details: https://doc.rust-lang.org/cargo/reference/profiles.html
2024-05-21 23:12:31 -07:00
EdJoPaTo
bf2036987f refactor(cell): reset instead of applying default (#1127)
Using reset is clearer to me what actually happens. On the other case a
struct is created to override the old one completely which basically
does the same in a less clear way.
2024-05-21 22:11:45 -07:00
Enrico Borba
0b5fd6bf8e feat: add writer() and writer_mut() to termion and crossterm backends (#991)
It is sometimes useful to obtain access to the writer if we want to see
what has been written so far. For example, when using &mut [u8] as a
writer.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-05-21 10:34:07 -07:00
246 changed files with 21326 additions and 16145 deletions

10
.github/CODEOWNERS vendored
View File

@@ -1,11 +1,11 @@
# See https://help.github.com/articles/about-codeowners/
# See <https://help.github.com/articles/about-codeowners/>
# for more info about CODEOWNERS file
# It uses the same pattern rule for gitignore file
# https://git-scm.com/docs/gitignore#_pattern_format
# <https://git-scm.com/docs/gitignore#_pattern_format>
# Maintainers
* @orhun @joshka @kdheepak @Valentin271 @EdJoPaTo
# Past maintainers
# @mindoodoo @sayanarijit
* @ratatui/maintainers

View File

@@ -2,7 +2,7 @@
name: Bug report
about: Create an issue about a bug you encountered
title: ''
labels: bug
labels: 'Type: Bug'
assignees: ''
---
@@ -17,26 +17,22 @@ A detailed and complete issue is more likely to be processed quickly.
A clear and concise description of what the bug is.
-->
## To Reproduce
<!--
Try to reduce the issue to a simple code sample exhibiting the problem.
Ideally, fork the project and add a test or an example.
-->
## Expected behavior
<!--
A clear and concise description of what you expected to happen.
-->
## Screenshots
<!--
If applicable, add screenshots, gifs or videos to help explain your problem.
-->
## Environment
<!--
Add a description of the systems where you are observing the issue. For example:

View File

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

View File

@@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
labels: 'Type: Enhancement'
assignees: ''
---

25
.github/workflows/bench_base.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Run Benchmarks
on:
push:
branches:
- main
jobs:
benchmark_base_branch:
name: Continuous Benchmarking with Bencher
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: bencherdev/bencher@main
- name: Track base branch benchmarks with Bencher
run: |
bencher run \
--project ratatui-org \
--token '${{ secrets.BENCHER_API_TOKEN }}' \
--branch main \
--testbed ubuntu-latest \
--adapter rust_criterion \
--err \
cargo bench

25
.github/workflows/bench_run_fork_pr.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Run and Cache Benchmarks
on:
pull_request:
types: [opened, reopened, edited, synchronize]
jobs:
benchmark_fork_pr_branch:
name: Run Fork PR Benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run Benchmarks
run: cargo bench > benchmark_results.txt
- name: Upload Benchmark Results
uses: actions/upload-artifact@v4
with:
name: benchmark_results.txt
path: ./benchmark_results.txt
- name: Upload GitHub Pull Request Event
uses: actions/upload-artifact@v4
with:
name: event.json
path: ${{ github.event_path }}

View File

@@ -0,0 +1,75 @@
name: Track Benchmarks with Bencher
on:
workflow_run:
workflows: [Run and Cache Benchmarks]
types: [completed]
permissions:
contents: read
pull-requests: write
jobs:
track_fork_pr_branch:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
env:
BENCHMARK_RESULTS: benchmark_results.txt
PR_EVENT: event.json
steps:
- name: Download Benchmark Results
uses: actions/github-script@v7
with:
script: |
async function downloadArtifact(artifactName) {
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == artifactName
})[0];
if (!matchArtifact) {
core.setFailed(`Failed to find artifact: ${artifactName}`);
}
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/${artifactName}.zip`, Buffer.from(download.data));
}
await downloadArtifact(process.env.BENCHMARK_RESULTS);
await downloadArtifact(process.env.PR_EVENT);
- name: Unzip Benchmark Results
run: |
unzip $BENCHMARK_RESULTS.zip
unzip $PR_EVENT.zip
- name: Export PR Event Data
uses: actions/github-script@v7
with:
script: |
let fs = require('fs');
let prEvent = JSON.parse(fs.readFileSync(process.env.PR_EVENT, {encoding: 'utf8'}));
core.exportVariable("PR_HEAD", `${prEvent.number}/merge`);
core.exportVariable("PR_BASE", prEvent.pull_request.base.ref);
core.exportVariable("PR_BASE_SHA", prEvent.pull_request.base.sha);
core.exportVariable("PR_NUMBER", prEvent.number);
- uses: bencherdev/bencher@main
- name: Track Benchmarks with Bencher
run: |
bencher run \
--project ratatui-org \
--token '${{ secrets.BENCHER_API_TOKEN }}' \
--branch '${{ env.PR_HEAD }}' \
--branch-start-point '${{ env.PR_BASE }}' \
--branch-start-point-hash '${{ env.PR_BASE_SHA }}' \
--testbed ubuntu-latest \
--adapter rust_criterion \
--err \
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
--ci-number '${{ env.PR_NUMBER }}' \
--file "$BENCHMARK_RESULTS"

View File

@@ -71,7 +71,7 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['breaking change']
labels: ['Type: Breaking Change']
})
do-not-merge:

16
.github/workflows/check-semver.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Check Semver
on:
pull_request:
branches:
- main
jobs:
check-semver:
name: Check semver
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Check semver
uses: obi1kenobi/cargo-semver-checks-action@v2

View File

@@ -17,71 +17,72 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
# don't install husky hooks during CI as they are only needed for for pre-push
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
# lint, clippy and coveraget jobs are intentionally early in the workflow to catch simple
# formatting, typos, and missing tests as early as possible. This allows us to fix these and
# resubmit the PR without having to wait for the comprehensive matrix of tests to complete.
# lint, clippy and coverage jobs are intentionally early in the workflow to catch simple formatting,
# typos, and missing tests as early as possible. This allows us to fix these and resubmit the PR
# without having to wait for the comprehensive matrix of tests to complete.
jobs:
lint:
rustfmt:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo make lint-format
- name: Check documentation
run: cargo make lint-docs
- name: Check typos
uses: crate-ci/typos@master
- name: Lint dependencies
uses: EmbarkStudios/cargo-deny-action@v1
- run: cargo +nightly fmt --all --check
typos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@master
dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
cargo-machete:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: bnjbvr/cargo-machete@v0.6.2
clippy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Run cargo make clippy-all
run: cargo make clippy
- uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make clippy
markdownlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v17
with:
globs: |
'**/*.md'
'!target'
coverage:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools
- name: Install cargo-llvm-cov and cargo-make
uses: taiki-e/install-action@v2
- uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov,cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Generate coverage
run: cargo make coverage
- name: Upload to codecov.io
uses: codecov/codecov-action@v4
- uses: Swatinem/rust-cache@v2
- run: cargo make coverage
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
@@ -94,21 +95,30 @@ jobs:
toolchain: ["1.74.0", "stable"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust {{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Run cargo make check
run: cargo make check
- uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make check
env:
RUST_BACKTRACE: full
lint-docs:
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/install@cargo-docs-rs
- uses: Swatinem/rust-cache@v2
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account
# the package.metadata.docs.rs configured in Cargo.toml.
# https://github.com/dtolnay/cargo-docs-rs
- run: cargo +nightly docs-rs
test-doc:
strategy:
fail-fast: false
@@ -116,16 +126,11 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Test docs
run: cargo make test-doc
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make test-doc
env:
RUST_BACKTRACE: full
@@ -142,19 +147,14 @@ jobs:
backend: termion
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust ${{ matrix.toolchain }}}
uses: dtolnay/rust-toolchain@master
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Install cargo-nextest
uses: taiki-e/install-action@nextest
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Test ${{ matrix.backend }}
run: cargo make test-backend ${{ matrix.backend }}
- uses: taiki-e/install-action@v2
with:
tool: cargo-make,nextest
- uses: Swatinem/rust-cache@v2
- run: cargo make test-backend ${{ matrix.backend }}
env:
RUST_BACKTRACE: full

View File

@@ -1,4 +1,4 @@
name: Continuous Deployment
name: Release alpha version
on:
workflow_dispatch:
@@ -6,9 +6,6 @@ on:
# At 00:00 on Saturday
# https://crontab.guru/#0_0_*_*_6
- cron: "0 0 * * 6"
push:
tags:
- "v*.*.*"
defaults:
run:
@@ -20,7 +17,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ !startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4
@@ -30,14 +26,14 @@ jobs:
- name: Calculate the next release
run: .github/workflows/calculate-alpha-release.bash
- name: Publish on crates.io
uses: actions-rs/cargo@v1
with:
command: publish
args: --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Publish
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
- name: Generate a changelog
uses: orhun/git-cliff-action@v3
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
@@ -50,17 +46,3 @@ jobs:
tag: ${{ env.NEXT_TAG }}
prerelease: true
bodyFile: BODY.md
publish-stable:
name: Create a stable release
runs-on: ubuntu-latest
if: ${{ startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Publish on crates.io
uses: actions-rs/cargo@v1
with:
command: publish
args: --token ${{ secrets.CARGO_TOKEN }}

45
.github/workflows/release-stable.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Release stable version
on:
push:
tags:
- "v*.*.*"
jobs:
publish-stable:
name: Create an stable release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --latest --strip header
env:
OUTPUT: BODY.md
- name: Publish on GitHub
uses: ncipollo/release-action@v1
with:
prerelease: false
bodyFile: BODY.md
publish-crate:
name: Publish crate
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Publish
run: cargo publish --token ${{ secrets.CARGO_TOKEN }}

View File

@@ -4,12 +4,30 @@ This document contains a list of breaking changes in each version and some notes
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
GitHub with a [breaking change] label.
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
[breaking change]: (https://github.com/ratatui/ratatui/issues?q=label%3A%22breaking+change%22)
## Summary
This is a quick summary of the sections below:
- [v0.28.0](#v0280)
`Backend::size` returns `Size` instead of `Rect`
- `Backend` trait migrates to `get/set_cursor_position`
- Ratatui now requires Crossterm 0.28.0
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
- `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize`
- `ratatui::terminal` module is now private
- `ToText` no longer has a lifetime
- `Frame::size` is deprecated and renamed to `Frame::area`
- [v0.27.0](#v0270)
- List no clamps the selected index to list
- Prelude items added / removed
- 'termion' updated to 4.0
- `Rect::inner` takes `Margin` directly instead of reference
- `Buffer::filled` takes `Cell` directly instead of reference
- `Stylize::bg()` now accepts `Into<Color>`
- Removed deprecated `List::start_corner`
- `LineGauge::gauge_style` is deprecated
- [v0.26.0](#v0260)
- `Flex::Start` is the new default flex mode for `Layout`
- `patch_style` & `reset_style` now consume and return `Self`
@@ -47,11 +65,221 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
## v0.28.0
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
[#1254]: https://github.com/ratatui/ratatui/pull/1254
The `Backend::size` method returns a `Size` instead of a `Rect`.
There is no need for the position here as it was always 0,0.
### `Backend` trait migrates to `get/set_cursor_position` ([#1284])
[#1284]: https://github.com/ratatui/ratatui/pull/1284
If you just use the types implementing the `Backend` trait, you will see deprecation hints but
nothing is a breaking change for you.
If you implement the Backend trait yourself, you need to update the implementation and add the
`get/set_cursor_position` method. You can remove the `get/set_cursor` methods as they are deprecated
and a default implementation for them exists.
### Ratatui now requires Crossterm 0.28.0 ([#1278])
[#1278]: https://github.com/ratatui/ratatui/pull/1278
Crossterm is updated to version 0.28.0, which is a semver incompatible version with the previous
version (0.27.0). Ratatui re-exports the version of crossterm that it is compatible with under
`ratatui::crossterm`, which can be used to avoid incompatible versions in your dependency list.
### `Axis::labels()` now accepts `IntoIterator<Into<Line>>` ([#1273] and [#1283])
[#1273]: https://github.com/ratatui/ratatui/pull/1173
[#1283]: https://github.com/ratatui/ratatui/pull/1283
Previously Axis::labels accepted `Vec<Span>`. Any code that uses conversion methods that infer the
type will need to be rewritten as the compiler cannot infer the correct type.
```diff
- Axis::default().labels(vec!["a".into(), "b".into()])
+ Axis::default().labels(["a", "b"])
```
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1245])
[#1245]: https://github.com/ratatui/ratatui/pull/1245
```diff
- let is_initialized = Layout::init_cache(100);
+ Layout::init_cache(NonZeroUsize::new(100).unwrap());
```
### `ratatui::terminal` module is now private ([#1160])
[#1160]: https://github.com/ratatui/ratatui/pull/1160
The `terminal` module is now private and can not be used directly. The types under this module are
exported from the root of the crate. This reduces clashes with other modules in the backends that
are also named terminal, and confusion about module exports for newer Rust users.
```diff
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
```
### `ToText` no longer has a lifetime ([#1234])
[#1234]: https://github.com/ratatui/ratatui/pull/1234
This change simplifies the trait and makes it easier to implement.
### `Frame::size` is deprecated and renamed to `Frame::area`
[#1293]: https://github.com/ratatui/ratatui/pull/1293
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
## [v0.27.0](https://github.com/ratatui/ratatui/releases/tag/v0.27.0)
### List no clamps the selected index to list ([#1159])
[#1149]: https://github.com/ratatui/ratatui/pull/1149
The `List` widget now clamps the selected index to the bounds of the list when navigating with
`first`, `last`, `previous`, and `next`, as well as when setting the index directly with `select`.
Previously selecting an index past the end of the list would show treat the list as having a
selection which was not visible. Now the last item in the list will be selected instead.
### Prelude items added / removed ([#1149])
The following items have been removed from the prelude:
- `style::Styled` - this trait is useful for widgets that want to
support the Stylize trait, but it adds complexity as widgets have two
`style` methods and a `set_style` method.
- `symbols::Marker` - this item is used by code that needs to draw to
the `Canvas` widget, but it's not a common item that would be used by
most users of the library.
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
are rarely used by code that needs to interact with the terminal, and
they're generally only ever used once in any app.
The following items have been added to the prelude:
- `layout::{Position, Size}` - these items are used by code that needs
to interact with the layout system. These are newer items that were
added in the last few releases, which should be used more liberally.
This may cause conflicts for types defined elsewhere with a similar
name.
To update your app:
```diff
// if your app uses Styled::style() or Styled::set_style():
-use ratatui::prelude::*;
+use ratatui::{prelude::*, style::Styled};
// if your app uses symbols::Marker:
-use ratatui::prelude::*;
+use ratatui::{prelude::*, symbols::Marker}
// if your app uses terminal::{CompletedFrame, TerminalOptions, Viewport}
-use ratatui::prelude::*;
+use ratatui::{prelude::*, terminal::{CompletedFrame, TerminalOptions, Viewport}};
// to disambiguate existing types named Position or Size:
- use some_crate::{Position, Size};
- let size: Size = ...;
- let position: Position = ...;
+ let size: some_crate::Size = ...;
+ let position: some_crate::Position = ...;
```
### Termion is updated to 4.0 [#1106]
Changelog: <https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
A change is only necessary if you were matching on all variants of the `MouseEvent` enum without a
wildcard. In this case, you need to either handle the two new variants, `MouseLeft` and
`MouseRight`, or add a wildcard.
[#1106]: https://github.com/ratatui/ratatui/pull/1106
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
[#1008]: https://github.com/ratatui/ratatui/pull/1008
`Margin` needs to be passed without reference now.
```diff
-let area = area.inner(&Margin {
+let area = area.inner(Margin {
vertical: 0,
horizontal: 2,
});
```
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
[#1148]: https://github.com/ratatui/ratatui/pull/1148
`Buffer::filled` moves the `Cell` instead of taking a reference.
```diff
-Buffer::filled(area, &Cell::new("X"));
+Buffer::filled(area, Cell::new("X"));
```
### `Stylize::bg()` now accepts `Into<Color>` ([#1103])
[#1103]: https://github.com/ratatui/ratatui/pull/1103
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
flexible types from calling scopes, though it can break some type inference in the calling scope.
### Remove deprecated `List::start_corner` and `layout::Corner` ([#759])
[#759]: https://github.com/ratatui/ratatui/pull/759
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
```diff
- list.start_corner(Corner::TopLeft);
- list.start_corner(Corner::TopRight);
// This is not an error, BottomRight rendered top to bottom previously
- list.start_corner(Corner::BottomRight);
// all becomes
+ list.direction(ListDirection::TopToBottom);
```
```diff
- list.start_corner(Corner::BottomLeft);
// becomes
+ list.direction(ListDirection::BottomToTop);
```
`layout::Corner` was removed entirely.
### `LineGauge::gauge_style` is deprecated ([#565])
[#565]: https://github.com/ratatui/ratatui/pull/1148
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
```diff
let gauge = LineGauge::default()
- .gauge_style(Style::default().fg(Color::Red).bg(Color::Blue)
+ .filled_style(Style::default().fg(Color::Green))
+ .unfilled_style(Style::default().fg(Color::White));
```
## [v0.26.0](https://github.com/ratatui/ratatui/releases/tag/v0.26.0)
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
[#881]: https://github.com/ratatui-org/ratatui/pull/881
[#881]: https://github.com/ratatui/ratatui/pull/881
Previously, constraints would stretch to fill all available space, violating constraints if
necessary.
@@ -72,9 +300,9 @@ existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Le
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
[#774]: https://github.com/ratatui-org/ratatui/pull/774
[#774]: https://github.com/ratatui/ratatui/pull/774
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
can some break type inference in the calling scope for empty containers.
@@ -89,9 +317,9 @@ This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
[#776]: https://github.com/ratatui-org/ratatui/pull/776
[#776]: https://github.com/ratatui/ratatui/pull/776
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
types from calling scopes, though it can break some type inference in the calling scope.
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
@@ -105,7 +333,7 @@ by removing the call to `.collect()`.
### Table::default() now sets segment_size to None and column_spacing to ([#751])
[#751]: https://github.com/ratatui-org/ratatui/pull/751
[#751]: https://github.com/ratatui/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.
@@ -115,7 +343,7 @@ To use the previous default values, call `table.segment_size(Default::default())
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
[#754]: https://github.com/ratatui-org/ratatui/pull/754
[#754]: https://github.com/ratatui/ratatui/pull/754
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable
reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent
@@ -132,8 +360,6 @@ The following example shows how to migrate for `Line`, but the same applies for
### Remove deprecated `Block::title_on_bottom` ([#757])
[#757]: https://github.com/ratatui-org/ratatui/pull/757
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
```diff
@@ -143,7 +369,7 @@ The following example shows how to migrate for `Line`, but the same applies for
### `Block` style methods cannot be used in a const context ([#720])
[#720]: https://github.com/ratatui-org/ratatui/pull/720
[#720]: https://github.com/ratatui/ratatui/pull/720
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
@@ -151,7 +377,7 @@ longer can be called from a constant context.
### `Line` now has a `style` field that applies to the entire line ([#708])
[#708]: https://github.com/ratatui-org/ratatui/pull/708
[#708]: https://github.com/ratatui/ratatui/pull/708
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
@@ -175,11 +401,11 @@ the `Span::style` field.
.alignment(Alignment::Left);
```
## [v0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.25.0)
## [v0.25.0](https://github.com/ratatui/ratatui/releases/tag/v0.25.0)
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
[#691]: https://github.com/ratatui-org/ratatui/pull/691
[#691]: https://github.com/ratatui/ratatui/pull/691
These items were deprecated since 0.10.
@@ -193,7 +419,7 @@ These items were deprecated since 0.10.
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
[#672]: https://github.com/ratatui-org/ratatui/pull/672
[#672]: https://github.com/ratatui/ratatui/pull/672
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
@@ -208,7 +434,7 @@ E.g.
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
[#635]: https://github.com/ratatui-org/ratatui/pull/635
[#635]: https://github.com/ratatui/ratatui/pull/635
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
widget in the default configuration would not show any indication of the selected tab.
@@ -220,7 +446,7 @@ widget in the default configuration would not show any indication of the selecte
### `Table::new()` now requires specifying the widths of the columns ([#664])
[#664]: https://github.com/ratatui-org/ratatui/pull/664
[#664]: https://github.com/ratatui/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:
@@ -246,7 +472,7 @@ or complex, it may be convenient to replace `Table::new` with `Table::default().
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
[#663]: https://github.com/ratatui-org/ratatui/pull/663
[#663]: https://github.com/ratatui/ratatui/pull/663
Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy
`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove
@@ -262,7 +488,7 @@ E.g.
### Layout::new() now accepts direction and constraint parameters ([#557])
[#557]: https://github.com/ratatui-org/ratatui/pull/557
[#557]: https://github.com/ratatui/ratatui/pull/557
Previously layout new took no parameters. Existing code should either use `Layout::default()` or
the new constructor.
@@ -279,18 +505,18 @@ let layout = layout::default()
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
```
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
## [v0.24.0](https://github.com/ratatui/ratatui/releases/tag/v0.24.0)
### `ScrollbarState` field type changed from `u16` to `usize` ([#456])
[#456]: https://github.com/ratatui-org/ratatui/pull/456
[#456]: https://github.com/ratatui/ratatui/pull/456
In order to support larger content lengths, the `position`, `content_length` and
`viewport_content_length` methods on `ScrollbarState` now take `usize` instead of `u16`
### `BorderType::line_symbols` renamed to `border_symbols` ([#529])
[#529]: https://github.com/ratatui-org/ratatui/issues/529
[#529]: https://github.com/ratatui/ratatui/issues/529
Applications can now set custom borders on a `Block` by calling `border_set()`. The
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
@@ -304,7 +530,7 @@ Applications can now set custom borders on a `Block` by calling `border_set()`.
### Generic `Backend` parameter removed from `Frame` ([#530])
[#530]: https://github.com/ratatui-org/ratatui/issues/530
[#530]: https://github.com/ratatui/ratatui/issues/530
`Frame` is no longer generic over Backend. Code that accepted `Frame<Backend>` will now need to
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
@@ -318,7 +544,7 @@ instance of a Frame. E.g.:
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
[#466]: https://github.com/ratatui-org/ratatui/issues/466
[#466]: https://github.com/ratatui/ratatui/issues/466
In order to support using `Stylize` shorthands (e.g. `"foo".red()`) on temporary `String` values, a
new implementation of `Stylize` was added that returns a `Span<'static>`. This causes the value to
@@ -336,7 +562,7 @@ longer compile. E.g.
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
[#426]: https://github.com/ratatui-org/ratatui/issues/426
[#426]: https://github.com/ratatui/ratatui/issues/426
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
`Buffer::set_line`.
@@ -349,11 +575,11 @@ longer compile. E.g.
+ buffer.set_line(0, 0, line, 10);
```
## [v0.23.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.23.0)
## [v0.23.0](https://github.com/ratatui/ratatui/releases/tag/v0.23.0)
### `Scrollbar::track_symbol()` now takes an `Option<&str>` instead of `&str` ([#360])
[#360]: https://github.com/ratatui-org/ratatui/issues/360
[#360]: https://github.com/ratatui/ratatui/issues/360
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
@@ -365,7 +591,7 @@ The track symbol of `Scrollbar` is now optional, this method now takes an option
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
[#330]: https://github.com/ratatui-org/ratatui/issues/330
[#330]: https://github.com/ratatui/ratatui/issues/330
The symbols for defining scrollbars have been moved to the `symbols` module from the
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the
@@ -379,31 +605,31 @@ new module locations. E.g.:
### MSRV updated to 1.67 ([#361])
[#361]: https://github.com/ratatui-org/ratatui/issues/361
[#361]: https://github.com/ratatui/ratatui/issues/361
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)
## [v0.22.0](https://github.com/ratatui/ratatui/releases/tag/v0.22.0)
### `bitflags` updated to 2.3 ([#205])
[#205]: https://github.com/ratatui-org/ratatui/issues/205
[#205]: https://github.com/ratatui/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)
## [v0.21.0](https://github.com/ratatui/ratatui/releases/tag/v0.21.0)
### MSRV is 1.65.0 ([#171])
[#171]: https://github.com/ratatui-org/ratatui/issues/171
[#171]: https://github.com/ratatui/ratatui/issues/171
The minimum supported rust version is now 1.65.0.
### `Terminal::with_options()` stabilized to allow configuring the viewport ([#114])
[#114]: https://github.com/ratatui-org/ratatui/issues/114
[#114]: https://github.com/ratatui/ratatui/issues/114
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
and `ViewPort` was changed from a struct to an enum.
@@ -420,7 +646,7 @@ let terminal = Terminal::with_options(backend, TerminalOptions {
### Code that binds `Into<Text<'a>>` now requires type annotations ([#168])
[#168]: https://github.com/ratatui-org/ratatui/issues/168
[#168]: https://github.com/ratatui/ratatui/issues/168
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
@@ -434,7 +660,7 @@ previously did not need to use type annotations to fail to compile. To fix this,
### `Marker::Block` now renders as a block rather than a bar character ([#133])
[#133]: https://github.com/ratatui-org/ratatui/issues/133
[#133]: https://github.com/ratatui/ratatui/issues/133
Code using the `Block` marker that previously rendered using a half block character (`'▀'``) now
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
@@ -446,20 +672,20 @@ the existing code.
+ let canvas = Canvas::default().marker(Marker::Bar);
```
## [v0.20.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.20.0)
## [v0.20.0](https://github.com/ratatui/ratatui/releases/tag/v0.20.0)
v0.20.0 was the first release of Ratatui - versions prior to this were release as tui-rs. See the
[Changelog](./CHANGELOG.md) for more details.
### MSRV is update to 1.63.0 ([#80])
[#80]: https://github.com/ratatui-org/ratatui/issues/80
[#80]: https://github.com/ratatui/ratatui/issues/80
The minimum supported rust version is 1.63.0
### List no longer ignores empty string in items ([#42])
[#42]: https://github.com/ratatui-org/ratatui/issues/42
[#42]: https://github.com/ratatui/ratatui/issues/42
The following code now renders 3 items instead of 2. Code which relies on the previous behavior will
need to manually filter empty items prior to display.

11852
CHANGELOG.md

File diff suppressed because it is too large Load Diff

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
https://forum.ratatui.rs/ or https://discord.gg/pMCEU9hNEj.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -8,7 +8,7 @@ creating a new issue before making the change, or starting a discussion on
## Reporting issues
Before reporting an issue on the [issue tracker](https://github.com/ratatui-org/ratatui/issues),
Before reporting an issue on the [issue tracker](https://github.com/ratatui/ratatui/issues),
please check that it has not already been reported by searching for some related keywords. Please
also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues
found.
@@ -17,10 +17,10 @@ found.
All contributions are obviously welcome. Please include as many details as possible in your PR
description to help the reviewer (follow the provided template). Make sure to highlight changes
which may need additional attention or you are uncertain about. Any idea with a large scale impact
which may need additional attention, or you are uncertain about. Any idea with a large scale impact
on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
### Keep PRs small, intentional and focused
### Keep PRs small, intentional, and focused
Try to do one pull request per change. The time taken to review a PR grows exponential with the size
of the change. Small focused PRs will generally be much more faster to review. PRs that include both
@@ -32,7 +32,7 @@ guarantee that the behavior is unchanged.
### Code formatting
Run `cargo make format` before committing to ensure that code is consistently formatted with
rustfmt. Configuration is in [rustfmt.toml](./rustfmt.toml).
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml).
### Search `tui-rs` for similar work
@@ -79,14 +79,14 @@ defaults depending on your platform of choice. Building the project should be as
`cargo make build`.
```shell
git clone https://github.com/ratatui-org/ratatui.git
git clone https://github.com/ratatui/ratatui.git
cd ratatui
cargo make build
```
### Tests
The [test coverage](https://app.codecov.io/gh/ratatui-org/ratatui) of the crate is reasonably
The [test coverage](https://app.codecov.io/gh/ratatui/ratatui) of the crate is reasonably
good, but this can always be improved. Focus on keeping the tests simple and obvious and write unit
tests for all new or modified code. Beside the usual doc and unit tests, one of the most valuable
test you can write for Ratatui is a test against the `TestBackend`. It allows you to assert the
@@ -147,7 +147,7 @@ fn foo() {}
```
- Max line length is 100 characters
See [vscode rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
See [VS Code rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
- Doc comments are above macros
i.e.
@@ -163,19 +163,19 @@ i.e. ``[`Block`]``, **NOT** ``[Block]``
### Deprecation notice
We generally want to wait at least two versions before removing deprecated items so users have
We generally want to wait at least two versions before removing deprecated items, so users have
time to update. However, if a deprecation is blocking for us to implement a new feature we may
*consider* removing it in a one version notice.
### Use of unsafe for optimization purposes
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However, there
may be specific cases that this becomes necessary in order to avoid slowness. Please see [this
discussion](https://github.com/ratatui-org/ratatui/discussions/66) for more about the decision.
discussion](https://github.com/ratatui/ratatui/discussions/66) for more about the decision.
## Continuous Integration
We use Github Actions for the CI where we perform the following checks:
We use GitHub Actions for the CI where we perform the following checks:
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
- The tests (docs, lib, tests and examples) should pass.
@@ -191,12 +191,12 @@ This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in Fe
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
([@fdehau](https://github.com/fdehau)).
The original repository contains all the issues, PRs and discussion that were raised originally, and
The original repository contains all the issues, PRs, and discussion that were raised originally, and
it is useful to refer to when contributing code, documentation, or issues with Ratatui.
We imported all the PRs from the original repository and implemented many of the smaller ones and
We imported all the PRs from the original repository, implemented many of the smaller ones, and
made notes on the leftovers. These are marked as draft PRs and labelled as [imported from
tui](https://github.com/ratatui-org/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
tui](https://github.com/ratatui/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
We have documented the current state of those PRs, and anyone is welcome to pick them up and
continue the work on them.

View File

@@ -1,338 +1,57 @@
[package]
name = "ratatui"
version = "0.26.3" # 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/"
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 = [
"assets/*",
".github",
"Makefile.toml",
"CONTRIBUTING.md",
"*.log",
"tags",
]
autoexamples = true
edition = "2021"
rust-version = "1.74.0"
[workspace]
resolver = "2"
members = ["ratatui*"]
# disabled for now because of the orphan rule on conversions
# <https://github.com/ratatui/ratatui/issues/1388#issuecomment-2379895747>
exclude = ["ratatui-termion", "ratatui-termwiz"]
[badges]
[workspace.dependencies]
ratatui = { path = "ratatui" }
ratatui-core = { path = "ratatui-core" }
ratatui-crossterm = { path = "ratatui-crossterm" }
ratatui-termion = { path = "ratatui-termion" }
ratatui-termwiz = { path = "ratatui-termwiz" }
ratatui-widgets = { path = "ratatui-widgets" }
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
compact_str = "0.7.1"
crossterm = { version = "0.27", optional = true }
document-features = { version = "0.2.7", optional = true }
itertools = "0.12"
compact_str = "0.8.0"
crossterm = { version = "0.28.1" }
document-features = { version = "0.2.7" }
instability = "0.3.1"
itertools = "0.13"
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"] }
palette = { version = "0.7.6" }
serde = { version = "1", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
termion = { version = "4.0.0" }
termwiz = { version = "0.22.0" }
time = { version = "0.3.11", features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-truncate = "1"
unicode-width = "0.1"
unicode-width = "=0.1.13"
[dev-dependencies]
anyhow = "1.0.71"
# dev-dependencies
argh = "0.1.12"
better-panic = "0.3.0"
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
derive_builder = "0.20.0"
fakeit = "1.1"
font8x8 = "0.3.1"
futures = "0.3.30"
indoc = "2"
palette = "0.7.3"
octocrab = "0.40.0"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.19.0"
rstest = "0.22.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"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
module_name_repetitions = "allow"
must_use_candidate = "allow"
wildcard_imports = "allow"
# nursery or restricted
as_underscore = "warn"
deref_by_slicing = "warn"
else_if_without_else = "warn"
empty_line_after_doc_comments = "warn"
equatable_if_let = "warn"
fn_to_numeric_cast_any = "warn"
format_push_string = "warn"
map_err_ignore = "warn"
missing_const_for_fn = "warn"
mixed_read_write_in_expression = "warn"
mod_module_files = "warn"
needless_raw_strings = "warn"
redundant_type_annotations = "warn"
rest_pat_in_fully_bound_structs = "warn"
string_lit_chars_any = "warn"
string_to_string = "warn"
use_self = "warn"
[features]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
#!
## By default, we enable the crossterm backend as this is a reasonable choice for most applications
## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature
## which allows you to set the underline color of text.
default = ["crossterm", "underline-color"]
#! Generally an application will only use one backend, so you should only enable one of the following features:
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
crossterm = ["dep:crossterm"]
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = ["dep:termion"]
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
termwiz = ["dep:termwiz"]
#! The following optional features are available for all backends:
## enables serialization and deserialization of style and color types using the [Serde crate].
## This is useful if you want to save themes to a file.
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
## enables the [`border!`] macro.
macros = []
## enables all widgets.
all-widgets = ["widget-calendar"]
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
#! dependencies. The available features are:
## enables the [`calendar`] widget module and adds a dependency on the [Time crate].
widget-calendar = ["dep:time"]
#! Underline color is only supported by the [`CrosstermBackend`] backend, and is not supported
#! on Windows 7.
## enables the backend code that sets the underline color.
underline-color = ["dep:crossterm"]
#! The following features are unstable and may change in the future:
## Enable all unstable features.
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"]
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
## which are experimental and may change in the future.
## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
unstable-rendered-line-info = []
## Enables the `WidgetRef` and `StatefulWidgetRef` traits which are experimental and may change in
## the future.
unstable-widget-ref = []
[package.metadata.docs.rs]
all-features = true
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
rustdoc-args = ["--cfg", "docsrs"]
[[bench]]
name = "barchart"
harness = false
[[bench]]
name = "block"
harness = false
[[bench]]
name = "line"
harness = false
[[bench]]
name = "list"
harness = false
[lib]
bench = false
[[bench]]
name = "paragraph"
harness = false
[[bench]]
name = "sparkline"
harness = false
[[example]]
name = "barchart"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "block"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "canvas"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "calendar"
required-features = ["crossterm", "widget-calendar"]
doc-scrape-examples = true
[[example]]
name = "chart"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "colors"
required-features = ["crossterm"]
# this example is a bit verbose, so we don't want to include it in the docs
doc-scrape-examples = false
[[example]]
name = "colors_rgb"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "custom_widget"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "demo"
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
doc-scrape-examples = false
[[example]]
name = "demo2"
required-features = ["crossterm", "widget-calendar"]
doc-scrape-examples = true
[[example]]
name = "docsrs"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "gauge"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "hello_world"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "layout"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "constraints"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "flex"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "constraint-explorer"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "list"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "minimal"
required-features = ["crossterm"]
# prefer to show the more featureful examples in the docs
doc-scrape-examples = false
[[example]]
name = "modifiers"
required-features = ["crossterm"]
# this example is a bit verbose, so we don't want to include it in the docs
doc-scrape-examples = false
[[example]]
name = "panic"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "paragraph"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "popup"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "ratatui-logo"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "scrollbar"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "sparkline"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "table"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "tabs"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "user_input"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "inline"
required-features = ["crossterm"]
doc-scrape-examples = true
[[test]]
name = "state_serde"
required-features = ["serde"]
tokio = { version = "1.39.2", features = [
"rt",
"macros",
"time",
"rt-multi-thread",
] }
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

15
MAINTAINERS.md Normal file
View File

@@ -0,0 +1,15 @@
# Maintainers
This file documents current and past maintainers.
- [orhun](https://github.com/orhun)
- [joshka](https://github.com/joshka)
- [kdheepak](https://github.com/kdheepak)
- [Valentin271](https://github.com/Valentin271)
## Past Maintainers
- [fdehau](https://github.com/fdehau)
- [mindoodoo](https://github.com/mindoodoo)
- [sayanarijit](https://github.com/sayanarijit)
- [EdJoPaTo](https://github.com/EdJoPaTo)

View File

@@ -5,24 +5,17 @@ skip_core_tasks = true
[env]
# 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.
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" }
NON_BACKEND_FEATURES = "all-widgets,macros,serde"
[tasks.default]
alias = "ci"
[tasks.ci]
description = "Run continuous integration tasks"
dependencies = ["lint-style", "clippy", "check", "test"]
dependencies = ["lint", "clippy", "check", "test"]
[tasks.lint-style]
description = "Lint code style (formatting, typos, docs)"
[tasks.lint]
description = "Lint code style (formatting, typos, docs, markdown)"
dependencies = ["lint-format", "lint-typos", "lint-docs"]
[tasks.lint-format]
@@ -48,33 +41,27 @@ toolchain = "nightly"
command = "cargo"
args = [
"rustdoc",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--all-features",
"--",
"-Zunstable-options",
"--check",
"-Dwarnings",
]
[tasks.lint-markdown]
description = "Check markdown files for errors and warnings"
command = "markdownlint-cli2"
args = ["**/*.md", "!target"]
[tasks.check]
description = "Check code for errors and warnings"
command = "cargo"
args = [
"check",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
args = ["check", "--all-targets", "--all-features"]
[tasks.build]
description = "Compile the project"
command = "cargo"
args = [
"build",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
args = ["build", "--all-targets", "--all-features"]
[tasks.clippy]
description = "Run Clippy for linting"
@@ -82,10 +69,9 @@ command = "cargo"
args = [
"clippy",
"--all-targets",
"--all-features",
"--tests",
"--benches",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--",
"-D",
"warnings",
@@ -103,18 +89,12 @@ run_task = { name = ["test-lib", "test-doc"] }
description = "Run default tests"
dependencies = ["install-nextest"]
command = "cargo"
args = [
"nextest",
"run",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
args = ["nextest", "run", "--all-targets", "--all-features"]
[tasks.test-doc]
description = "Run documentation tests"
command = "cargo"
args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
args = ["test", "--doc", "--all-features"]
[tasks.test-backend]
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
@@ -127,7 +107,7 @@ args = [
"--all-targets",
"--no-default-features",
"--features",
"${ALL_FEATURES},${@}",
"${NON_BACKEND_FEATURES},${@}",
]
[tasks.coverage]
@@ -138,8 +118,7 @@ args = [
"--lcov",
"--output-path",
"target/lcov.info",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--all-features",
]
[tasks.run-example]

229
README.md
View File

@@ -4,14 +4,18 @@
- [Ratatui](#ratatui)
- [Installation](#installation)
- [Introduction](#introduction)
- [Other Documentation](#other-documentation)
- [Other documentation](#other-documentation)
- [Quickstart](#quickstart)
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
- [Drawing the UI](#drawing-the-ui)
- [Handling events](#handling-events)
- [Example](#example)
- [Layout](#layout)
- [Text and styling](#text-and-styling)
- [Status of this fork](#status-of-this-fork)
- [Rust version requirements](#rust-version-requirements)
- [Widgets](#widgets)
- [Built in](#built-in)
- [Third\-party libraries, bootstrapping templates and
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
- [Third-party libraries, bootstrapping templates and widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
- [Apps](#apps)
- [Alternatives](#alternatives)
- [Acknowledgments](#acknowledgments)
@@ -21,14 +25,14 @@
<!-- cargo-rdme start -->
![Demo](https://github.com/ratatui-org/ratatui/blob/1d39444e3dea6f309cf9035be2417ac711c1abc9/examples/demo2-destroy.gif?raw=true)
![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true)
<div align="center">
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
[![Matrix Badge]][Matrix]<br>
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
[![Forum Badge]][Forum]<br>
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
@@ -43,10 +47,10 @@ Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its deve
## Installation
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
Add `ratatui` as a dependency to your cargo.toml:
```shell
cargo add ratatui crossterm
cargo add ratatui
```
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
@@ -67,6 +71,7 @@ terminal user interfaces and showcases the features of Ratatui, along with a hel
## Other documentation
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
- [Ratatui Forum][Forum] - a place to ask questions and discuss the library
- [API Docs] - the full API documentation for the library on docs.rs.
- [Examples] - a collection of examples that demonstrate how to use the library.
- [Contributing] - Please read this if you are interested in contributing to the project.
@@ -110,7 +115,8 @@ module] and the [Backends] section of the [Ratatui Website] for more info.
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website]
using the provided [`render_widget`] method. After this closure returns, a diff is performed and
only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
for more info.
### Handling events
@@ -125,12 +131,18 @@ Website] for more info. For example, if you are using [Crossterm], you can use t
```rust
use std::io::{self, stdout};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
ExecutableCommand,
},
widgets::{Block, Paragraph},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
fn main() -> io::Result<()> {
enable_raw_mode()?;
@@ -161,9 +173,8 @@ fn handle_events() -> io::Result<bool> {
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!")
.block(Block::bordered().title("Greeting")),
frame.size(),
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
frame.area(),
);
}
```
@@ -180,40 +191,27 @@ area. This lets you describe a responsive terminal UI by nesting layouts. See th
section of the [Ratatui Website] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
use ratatui::{
layout::{Constraint, Layout},
widgets::Block,
Frame,
};
fn ui(frame: &mut Frame) {
let main_layout = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
],
)
.split(frame.size());
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),
main_layout[0],
);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Status Bar"),
main_layout[2],
);
let [title_area, main_area, status_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.areas(frame.area());
let [left_area, right_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(main_area);
let inner_layout = Layout::new(
Direction::Horizontal,
[Constraint::Percentage(50), Constraint::Percentage(50)],
)
.split(main_layout[1]);
frame.render_widget(
Block::bordered().title("Left"),
inner_layout[0],
);
frame.render_widget(
Block::bordered().title("Right"),
inner_layout[1],
);
frame.render_widget(Block::bordered().title("Title Bar"), title_area);
frame.render_widget(Block::bordered().title("Status Bar"), status_area);
frame.render_widget(Block::bordered().title("Left"), left_area);
frame.render_widget(Block::bordered().title("Right"), right_area);
}
```
@@ -234,48 +232,41 @@ short-hand syntax to apply a style to widgets and text. See the [Styling Text] s
[Ratatui Website] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
use ratatui::{
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span},
widgets::{Block, Paragraph},
Frame,
};
fn ui(frame: &mut Frame) {
let areas = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
],
)
.split(frame.size());
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
let span1 = Span::raw("Hello ");
let span2 = Span::styled(
"World",
Style::new()
.fg(Color::Green)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
);
let span3 = "!".red().on_light_yellow().italic();
let line = Line::from(vec![
Span::raw("Hello "),
Span::styled(
"World",
Style::new()
.fg(Color::Green)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
),
"!".red().on_light_yellow().italic(),
]);
frame.render_widget(line, areas[0]);
let line = Line::from(vec![span1, span2, span3]);
let text: Text = Text::from(vec![line]);
// using the short-hand syntax and implicit conversions
let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
frame.render_widget(paragraph, areas[1]);
frame.render_widget(Paragraph::new(text), areas[0]);
// or using the short-hand syntax and implicit conversions
frame.render_widget(
Paragraph::new("Hello World!".red().on_white().bold()),
areas[1],
);
// style the whole widget instead of just the text
let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
frame.render_widget(paragraph, areas[2]);
// to style the whole widget instead of just the text
frame.render_widget(
Paragraph::new("Hello World!").style(Style::new().red().on_white()),
areas[2],
);
// or using the short-hand syntax
frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
// use the simpler short-hand syntax
let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
frame.render_widget(paragraph, areas[3]);
}
```
@@ -293,21 +284,21 @@ Running this example produces the following output:
[Handling Events]: https://ratatui.rs/concepts/event-handling/
[Layout]: https://ratatui.rs/how-to/layout/
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
[templates]: https://github.com/ratatui-org/templates/
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
[Report a bug]: https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
[Request a Feature]: https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
[Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
[templates]: https://github.com/ratatui/templates/
[Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
[Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
[Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
[Create a Pull Request]: https://github.com/ratatui/ratatui/compare
[git-cliff]: https://git-cliff.org
[Conventional Commits]: https://www.conventionalcommits.org
[API Docs]: https://docs.rs/ratatui
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
[Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
[docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
[docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
[`Frame`]: terminal::Frame
[`render_widget`]: terminal::Frame::render_widget
[`Widget`]: widgets::Widget
@@ -326,24 +317,23 @@ Running this example produces the following output:
[Termion]: https://crates.io/crates/termion
[Termwiz]: https://crates.io/crates/termwiz
[tui-rs]: https://crates.io/crates/tui
[GitHub Sponsors]: https://github.com/sponsors/ratatui-org
[GitHub Sponsors]: https://github.com/sponsors/ratatui
[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&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&color=1370D3&logoColor=1370D3
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
[Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
[Discord Badge]: 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&logoColor=E05D44
[Matrix Badge]:
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
[Forum]: https://forum.ratatui.rs
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
<!-- cargo-rdme end -->
@@ -358,17 +348,14 @@ In order to organize ourselves, we currently use a [Discord server](https://disc
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
While we do utilize Discord for coordinating, it's not essential for contributing.
Our primary open-source workflow is centered around GitHub.
For significant discussions, we rely on GitHub — please open an issue, a discussion or a PR.
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub.
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a
Pull Request].
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
you are interested in working on a PR or issue opened in the previous repository.
## Rust version requirements
Since version 0.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
## Widgets
### Built in
@@ -404,7 +391,7 @@ be installed with `cargo install cargo-make`).
`ratatui::text::Text`
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`ratatui::style::Color`
- [templates](https://github.com/ratatui-org/templates) — Starter templates for
- [templates](https://github.com/ratatui/templates) — Starter templates for
bootstrapping a Rust TUI application with Ratatui & crossterm
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
Tui-rs + Crossterm apps
@@ -428,7 +415,7 @@ be installed with `cargo install cargo-make`).
## Apps
Check out [awesome-ratatui](https://github.com/ratatui-org/awesome-ratatui) for a curated list of
Check out [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) for a curated list of
awesome apps/libraries built with `ratatui`!
## Alternatives
@@ -439,7 +426,7 @@ to build text user interfaces in Rust.
## Acknowledgments
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
awesome logo** for the ratatui project and ratatui-org organization.
awesome logo** for the ratatui project and ratatui organization.
## License

View File

@@ -1,5 +1,12 @@
# Creating a Release
Our release strategy is:
> Release major versions with detailed summaries when necessary, while releasing minor versions
> weekly or as needed without extensive announcements.
>
> Versioning scheme being `0.x.y`, where `x` is the major version and `y` is the minor version.
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub
actions](.github/workflows/cd.yml) and triggered by pushing a tag.
@@ -12,7 +19,7 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
```
1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push.
1. Grab the permalink from <https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif> and
1. Grab the permalink from <https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif> and
append `?raw=true` to redirect to the actual image url. Then update the link in the main README.
Avoid adding the gif to the git repo as binary files tend to bloat repositories.
@@ -21,16 +28,16 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
can be used for generating the entries.
1. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)
1. Commit and push the changes.
1. Create a new tag: `git tag -a v[X.Y.Z]`
1. Create a new tag: `git tag -a v[0.x.y]`
1. Push the tag: `git push --tags`
1. Wait for [Continuous Deployment](https://github.com/ratatui-org/ratatui/actions) workflow to
1. Wait for [Continuous Deployment](https://github.com/ratatui/ratatui/actions) workflow to
finish.
## Alpha Releases
Alpha releases are automatically released every Saturday via [cd.yml](./.github/workflows/cd.yml)
and can be manually be created when necessary by triggering the [Continuous
Deployment](https://github.com/ratatui-org/ratatui/actions/workflows/cd.yml) workflow.
Deployment](https://github.com/ratatui/ratatui/actions/workflows/cd.yml) workflow.
We automatically release an alpha release with a patch level bump + alpha.num weekly (and when we
need to manually). E.g. the last release was 0.22.0, and the most recent alpha release is
@@ -40,5 +47,5 @@ These releases will have whatever happened to be in main at the time of release,
for apps that need to get releases from crates.io, but may contain more bugs and be generally less
tested than normal releases.
See [#147](https://github.com/ratatui-org/ratatui/issues/147) and
[#359](https://github.com/ratatui-org/ratatui/pull/359) for more info on the alpha release process.
See [#147](https://github.com/ratatui/ratatui/issues/147) and
[#359](https://github.com/ratatui/ratatui/pull/359) for more info on the alpha release process.

View File

@@ -6,4 +6,4 @@ We only support the latest version of this crate.
## Reporting a Vulnerability
To report secuirity vulnerability, please use the form at https://github.com/ratatui-org/ratatui/security/advisories/new
To report secuirity vulnerability, please use the form at <https://github.com/ratatui/ratatui/security/advisories/new>

View File

@@ -2,7 +2,7 @@
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "ratatui-org"
owner = "ratatui"
repo = "ratatui"
[changelog]
@@ -11,6 +11,8 @@ header = """
# Changelog
All notable changes to this project will be documented in this file.
<!-- ignore lint rules that are often triggered by content generated from commits / git-cliff -->
<!-- markdownlint-disable line-length no-bare-urls ul-style emphasis-style -->
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
@@ -22,25 +24,23 @@ body = """
{%- if not version %}
## [unreleased]
{% else -%}
## [{{ version }}](https://github.com/ratatui-org/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
## [{{ version }}](https://github.com/ratatui/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif -%}
{% macro commit(commit) -%}
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }}) \
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/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 %}
````text {#- 4 backticks escape any backticks in body #}
{{commit.body | indent(prefix=" ") }}
````
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
{%- endif %}
{%- for footer in commit.footers %}
{%- for footer in commit.footers %}\n
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
{{ footer.token | indent(prefix=" ") }}{{ footer.separator }}{{ footer.value }}
>
{{ footer.token | indent(prefix=" > ", first=true, blank=true) }}
{{- footer.separator }}
{{- footer.value| indent(prefix=" > ", first=false, blank=true) }}
{%- endif %}
{%- endfor %}
{% endmacro -%}
@@ -87,6 +87,11 @@ trim = false
footer = """
<!-- generated by git-cliff -->
"""
postprocessors = [
{ pattern = '<!-- Please read CONTRIBUTING.md before submitting any pull request. -->', replace = "" },
{ pattern = '>---+\n', replace = '' },
{ pattern = ' +\n', replace = "\n" },
]
[git]
# parse the commits based on https://www.conventionalcommits.org
@@ -126,6 +131,7 @@ commit_parsers = [
{ message = "^(Buffer|buffer|Frame|frame|Gauge|gauge|Paragraph|paragraph):", group = "<!-- 07 -->Miscellaneous Tasks" },
{ message = "^\\[", group = "<!-- 07 -->Miscellaneous Tasks" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers

View File

@@ -3,7 +3,7 @@ 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
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
allowed-duplicate-crates = [
"bitflags",
"windows-targets",

View File

@@ -1,9 +1,7 @@
# configuration for https://github.com/EmbarkStudios/cargo-deny
[licenses]
default = "deny"
unlicensed = "deny"
copyleft = "deny"
version = 2
confidence-threshold = 0.8
allow = [
"Apache-2.0",
@@ -16,8 +14,7 @@ allow = [
]
[advisories]
unmaintained = "deny"
yanked = "deny"
version = 2
[bans]
multiple-versions = "allow"

View File

@@ -1,298 +0,0 @@
//! # [Ratatui] `BarChart` example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::*,
widgets::{Bar, BarChart, BarGroup, Block, Paragraph},
};
struct Company<'a> {
revenue: [u64; 4],
label: &'a str,
bar_style: Style,
}
struct App<'a> {
data: Vec<(&'a str, u64)>,
months: [&'a str; 4],
companies: [Company<'a>; 3],
}
const TOTAL_REVENUE: &str = "Total Revenue";
impl<'a> App<'a> {
fn new() -> Self {
App {
data: vec![
("B1", 9),
("B2", 12),
("B3", 5),
("B4", 8),
("B5", 2),
("B6", 4),
("B7", 5),
("B8", 9),
("B9", 14),
("B10", 15),
("B11", 1),
("B12", 0),
("B13", 4),
("B14", 6),
("B15", 4),
("B16", 6),
("B17", 4),
("B18", 7),
("B19", 13),
("B20", 8),
("B21", 11),
("B22", 9),
("B23", 3),
("B24", 5),
],
companies: [
Company {
label: "Comp.A",
revenue: [9500, 12500, 5300, 8500],
bar_style: Style::default().fg(Color::Green),
},
Company {
label: "Comp.B",
revenue: [1500, 2500, 3000, 500],
bar_style: Style::default().fg(Color::Yellow),
},
Company {
label: "Comp.C",
revenue: [10500, 10600, 9000, 4200],
bar_style: Style::default().fg(Color::White),
},
],
months: ["Mars", "Apr", "May", "Jun"],
}
}
fn on_tick(&mut self) {
let value = self.data.pop().unwrap();
self.data.insert(0, value);
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui(frame: &mut Frame, app: &App) {
let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [top, bottom] = vertical.areas(frame.size());
let [left, right] = horizontal.areas(bottom);
let barchart = BarChart::default()
.block(Block::bordered().title("Data1"))
.data(&app.data)
.bar_width(9)
.bar_style(Style::default().fg(Color::Yellow))
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
frame.render_widget(barchart, top);
draw_bar_with_group_labels(frame, app, left);
draw_horizontal_bars(frame, app, right);
}
#[allow(clippy::cast_precision_loss)]
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
app.months
.iter()
.enumerate()
.map(|(i, &month)| {
let bars: Vec<Bar> = app
.companies
.iter()
.map(|c| {
let mut bar = Bar::default()
.value(c.revenue[i])
.style(c.bar_style)
.value_style(
Style::default()
.bg(c.bar_style.fg.unwrap())
.fg(Color::Black),
);
if combine_values_and_labels {
bar = bar.text_value(format!(
"{} ({:.1} M)",
c.label,
(c.revenue[i] as f64) / 1000.
));
} else {
bar = bar
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.))
.label(c.label.into());
}
bar
})
.collect();
BarGroup::default()
.label(Line::from(month).centered())
.bars(&bars)
})
.collect()
}
#[allow(clippy::cast_possible_truncation)]
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
const LEGEND_HEIGHT: u16 = 6;
let groups = create_groups(app, false);
let mut barchart = BarChart::default()
.block(Block::bordered().title("Data1"))
.bar_width(7)
.group_gap(3);
for group in groups {
barchart = barchart.data(group);
}
f.render_widget(barchart, area);
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: legend_width,
y: area.y,
x: area.right() - legend_width,
};
draw_legend(f, legend_area);
}
}
#[allow(clippy::cast_possible_truncation)]
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
const LEGEND_HEIGHT: u16 = 6;
let groups = create_groups(app, true);
let mut barchart = BarChart::default()
.block(Block::bordered().title("Data1"))
.bar_width(1)
.group_gap(1)
.bar_gap(0)
.direction(Direction::Horizontal);
for group in groups {
barchart = barchart.data(group);
}
f.render_widget(barchart, area);
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: legend_width,
y: area.y,
x: area.right() - legend_width,
};
draw_legend(f, legend_area);
}
}
fn draw_legend(f: &mut Frame, area: Rect) {
let text = vec![
Line::from(Span::styled(
TOTAL_REVENUE,
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::White),
)),
Line::from(Span::styled(
"- Company A",
Style::default().fg(Color::Green),
)),
Line::from(Span::styled(
"- Company B",
Style::default().fg(Color::Yellow),
)),
Line::from(Span::styled(
"- Company C",
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

@@ -1,815 +0,0 @@
//! [tui-big-text] is a rust crate that renders large pixel text as a [Ratatui] widget using the
//! glyphs from the [font8x8] crate.
//!
//! ![Hello World example](https://vhs.charm.sh/vhs-2UxNc2SJgiNqHoowbsXAMW.gif)
//!
//! # Installation
//!
//! ```shell
//! cargo add ratatui tui-big-text
//! ```
//!
//! # Usage
//!
//! Create a [`BigText`] widget using `BigTextBuilder` and pass it to [`Frame::render_widget`] to
//! render be rendered. The builder allows you to customize the [`Style`] of the widget and the
//! [`PixelSize`] of the glyphs. The [`PixelSize`] can be used to control how many character cells
//! are used to represent a single pixel of the 8x8 font.
//!
//! # Example
//!
//! ```rust
//! use anyhow::Result;
//! use ratatui::prelude::*;
//! use tui_big_text::{BigTextBuilder, PixelSize};
//!
//! fn render(frame: &mut Frame) -> Result<()> {
//! let big_text = BigTextBuilder::default()
//! .pixel_size(PixelSize::Full)
//! .style(Style::new().blue())
//! .lines(vec![
//! "Hello".red().into(),
//! "World".white().into(),
//! "~~~~~".into(),
//! ])
//! .build()?;
//! frame.render_widget(big_text, frame.size());
//! Ok(())
//! }
//! ```
//!
//! [tui-big-text]: https://crates.io/crates/tui-big-text
//! [Ratatui]: https://crates.io/crates/ratatui
//! [font8x8]: https://crates.io/crates/font8x8
//! [`BigText`]: crate::BigText
//! [`PixelSize`]: crate::PixelSize
//! [`Frame::render_widget`]: ratatui::Frame::render_widget
//! [`Style`]: ratatui::style::Style
use std::cmp::min;
use derive_builder::Builder;
use font8x8::UnicodeFonts;
use ratatui::{prelude::*, text::StyledGrapheme};
#[allow(unused)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
pub enum PixelSize {
#[default]
/// A pixel from the 8x8 font is represented by a full character cell in the terminal.
Full,
/// A pixel from the 8x8 font is represented by a half (upper/lower) character cell in the
/// terminal.
HalfHeight,
/// A pixel from the 8x8 font is represented by a half (left/right) character cell in the
/// terminal.
HalfWidth,
/// A pixel from the 8x8 font is represented by a quadrant of a character cell in the terminal.
Quadrant,
}
/// Displays one or more lines of text using 8x8 pixel characters.
///
/// The text is rendered using the [font8x8](https://crates.io/crates/font8x8) crate.
///
/// Using the `pixel_size` method, you can also chose, how 'big' a pixel should be.
/// Currently a pixel of the 8x8 font can be represented by one full or half
/// (horizontal/vertical/both) character cell of the terminal.
///
/// # Examples
///
/// ```rust
/// use ratatui::prelude::*;
/// use tui_big_text::{BigTextBuilder, PixelSize};
///
/// BigText::builder()
/// .pixel_size(PixelSize::Full)
/// .style(Style::new().white())
/// .lines(vec![
/// "Hello".red().into(),
/// "World".blue().into(),
/// "=====".into(),
/// ])
/// .build();
/// ```
///
/// Renders:
///
/// ```plain
/// ██ ██ ███ ███
/// ██ ██ ██ ██
/// ██ ██ ████ ██ ██ ████
/// ██████ ██ ██ ██ ██ ██ ██
/// ██ ██ ██████ ██ ██ ██ ██
/// ██ ██ ██ ██ ██ ██ ██
/// ██ ██ ████ ████ ████ ████
///
/// ██ ██ ███ ███
/// ██ ██ ██ ██
/// ██ ██ ████ ██ ███ ██ ██
/// ██ █ ██ ██ ██ ███ ██ ██ █████
/// ███████ ██ ██ ██ ██ ██ ██ ██
/// ███ ███ ██ ██ ██ ██ ██ ██
/// ██ ██ ████ ████ ████ ███ ██
///
/// ███ ██ ███ ██ ███ ██ ███ ██ ███ ██
/// ██ ███ ██ ███ ██ ███ ██ ███ ██ ███
/// ```
#[derive(Debug, Builder, Clone, PartialEq, Eq, Hash)]
pub struct BigText<'a> {
/// The text to display
#[builder(setter(into))]
lines: Vec<Line<'a>>,
/// The style of the widget
///
/// Defaults to `Style::default()`
#[builder(default)]
style: Style,
/// The size of single glyphs
///
/// Defaults to `BigTextSize::default()` (=> `BigTextSize::Full`)
#[builder(default)]
pixel_size: PixelSize,
}
impl Widget for BigText<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = layout(area, self.pixel_size);
for (line, line_layout) in self.lines.iter().zip(layout) {
for (g, cell) in line.styled_graphemes(self.style).zip(line_layout) {
render_symbol(&g, cell, buf, self.pixel_size);
}
}
}
}
/// Returns how many cells are needed to display a full 8x8 glyphe using the given font size
const fn cells_per_glyph(size: PixelSize) -> (u16, u16) {
match size {
PixelSize::Full => (8, 8),
PixelSize::HalfHeight => (8, 4),
PixelSize::HalfWidth => (4, 8),
PixelSize::Quadrant => (4, 4),
}
}
/// Chunk the area into as many x*y cells as possible returned as a 2D iterator of `Rect`s
/// representing the rows of cells.
/// The size of each cell depends on given font size
fn layout(
area: Rect,
pixel_size: PixelSize,
) -> impl IntoIterator<Item = impl IntoIterator<Item = Rect>> {
let (width, height) = cells_per_glyph(pixel_size);
(area.top()..area.bottom())
.step_by(height as usize)
.map(move |y| {
(area.left()..area.right())
.step_by(width as usize)
.map(move |x| {
let width = min(area.right() - x, width);
let height = min(area.bottom() - y, height);
Rect::new(x, y, width, height)
})
})
}
/// Render a single grapheme into a cell by looking up the corresponding 8x8 bitmap in the
/// `BITMAPS` array and setting the corresponding cells in the buffer.
fn render_symbol(grapheme: &StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_size: PixelSize) {
buf.set_style(area, grapheme.style);
let c = grapheme.symbol.chars().next().unwrap(); // TODO: handle multi-char graphemes
if let Some(glyph) = font8x8::BASIC_FONTS.get(c) {
render_glyph(glyph, area, buf, pixel_size);
}
}
/// Get the correct unicode symbol for two vertical "pixels"
const fn get_symbol_half_height(top: u8, bottom: u8) -> char {
match top {
0 => match bottom {
0 => ' ',
_ => '▄',
},
_ => match bottom {
0 => '▀',
_ => '█',
},
}
}
/// Get the correct unicode symbol for two horizontal "pixels"
const fn get_symbol_half_width(left: u8, right: u8) -> char {
match left {
0 => match right {
0 => ' ',
_ => '▐',
},
_ => match right {
0 => '▌',
_ => '█',
},
}
}
/// Get the correct unicode symbol for 2x2 "pixels"
const fn get_symbol_half_size(
top_left: u8,
top_right: u8,
bottom_left: u8,
bottom_right: u8,
) -> char {
const QUADRANT_SYMBOLS: [char; 16] = [
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
];
let top_left = if top_left > 0 { 1 } else { 0 };
let top_right = if top_right > 0 { 1 << 1 } else { 0 };
let bottom_left = if bottom_left > 0 { 1 << 2 } else { 0 };
let bottom_right = if bottom_right > 0 { 1 << 3 } else { 0 };
QUADRANT_SYMBOLS[top_left + top_right + bottom_left + bottom_right]
}
/// Render a single 8x8 glyph into a cell by setting the corresponding cells in the buffer.
fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: PixelSize) {
let (width, height) = cells_per_glyph(pixel_size);
let glyph_vertical_index = (0..glyph.len()).step_by(8 / height as usize);
let glyph_horizontal_bit_selector = (0..8).step_by(8 / width as usize);
for (row, y) in glyph_vertical_index.zip(area.top()..area.bottom()) {
for (col, x) in glyph_horizontal_bit_selector
.clone()
.zip(area.left()..area.right())
{
let cell = buf.get_mut(x, y);
let symbol_character = match pixel_size {
PixelSize::Full => match glyph[row] & (1 << col) {
0 => ' ',
_ => '█',
},
PixelSize::HalfHeight => {
let top = glyph[row] & (1 << col);
let bottom = glyph[row + 1] & (1 << col);
get_symbol_half_height(top, bottom)
}
PixelSize::HalfWidth => {
let left = glyph[row] & (1 << col);
let right = glyph[row] & (1 << (col + 1));
get_symbol_half_width(left, right)
}
PixelSize::Quadrant => {
let top_left = glyph[row] & (1 << col);
let top_right = glyph[row] & (1 << (col + 1));
let bottom_left = glyph[row + 1] & (1 << col);
let bottom_right = glyph[row + 1] & (1 << (col + 1));
get_symbol_half_size(top_left, top_right, bottom_left, bottom_right)
}
};
cell.set_char(symbol_character);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[test]
fn build() -> Result<()> {
let lines = vec![Line::from(vec!["Hello".red(), "World".blue()])];
let style = Style::new().green();
let pixel_size = PixelSize::default();
assert_eq!(
BigTextBuilder::default()
.lines(lines.clone())
.style(style)
.build()?,
BigText {
lines,
style,
pixel_size
}
);
Ok(())
}
#[test]
fn render_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
" ████ ██ ███ ████ ██ ",
"██ ██ ██ ██ ",
"███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ",
" ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
" ███ ██ ██ ██ ██ ██ ██ ██████ ██ █ ██ ██ ██ ██████ ",
"██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ",
" ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ",
" █████ ",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 6));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"██████ █ ███",
"█ ██ █ ██ ██",
" ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██",
" ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████",
" ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██",
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 16));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"██ ██ ███ █ ██ ",
"███ ███ ██ ██ ",
"███████ ██ ██ ██ █████ ███ ",
"███████ ██ ██ ██ ██ ██ ",
"██ █ ██ ██ ██ ██ ██ ██ ",
"██ ██ ██ ██ ██ ██ █ ██ ",
"██ ██ ███ ██ ████ ██ ████ ",
" ",
"████ ██ ",
" ██ ",
" ██ ███ █████ ████ █████ ",
" ██ ██ ██ ██ ██ ██ ██ ",
" ██ █ ██ ██ ██ ██████ ████ ",
" ██ ██ ██ ██ ██ ██ ██ ",
"███████ ████ ██ ██ ████ █████ ",
" ",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
" ████ █ ███ ███ ".bold(),
"██ ██ ██ ██ ██ ".bold(),
"███ █████ ██ ██ ██ ████ ██ ".bold(),
" ███ ██ ██ ██ ██ ██ ██ █████ ".bold(),
" ███ ██ ██ ██ ██ ██████ ██ ██ ".bold(),
"██ ██ ██ █ █████ ██ ██ ██ ██ ".bold(),
" ████ ██ ██ ████ ████ ███ ██ ".bold(),
" █████ ".bold(),
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.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([
"██████ ███ ",
" ██ ██ ██ ",
" ██ ██ ████ ██ ",
" █████ ██ ██ █████ ",
" ██ ██ ██████ ██ ██ ",
" ██ ██ ██ ██ ██ ",
"███ ██ ████ ███ ██ ",
" ",
" ████ ",
" ██ ██ ",
"██ ██ ███ ████ ████ █████ ",
"██ ███ ██ ██ ██ ██ ██ ██ ██ ",
"██ ███ ██ ██ ██████ ██████ ██ ██ ",
" ██ ██ ██ ██ ██ ██ ██ ",
" █████ ████ ████ ████ ██ ██ ",
" ",
"██████ ███ ",
" ██ ██ ██ ",
" ██ ██ ██ ██ ██ ████ ",
" █████ ██ ██ ██ ██ ██ ",
" ██ ██ ██ ██ ██ ██████ ",
" ██ ██ ██ ██ ██ ██ ",
"██████ ████ ███ ██ ████ ",
" ",
]);
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_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ",
"▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ",
"▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ",
" ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 3));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"█▀██▀█ ▄█ ▀██",
" ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██",
" ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"██▄ ▄██ ▀██ ▄█ ▀▀ ",
"███████ ██ ██ ██ ▀██▀▀ ▀██ ",
"██ ▀ ██ ██ ██ ██ ██ ▄ ██ ",
"▀▀ ▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀▀▀ ",
"▀██▀ ▀▀ ",
" ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ▄█▀▀▀▀ ",
" ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ",
"▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"▄█▀▀█▄ ▄█ ▀██ ▀██ ".bold(),
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ".bold(),
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ".bold(),
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ".bold(),
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.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([
"▀██▀▀█▄ ▀██ ",
" ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ",
" ██ ▀█▄ ██▀▀▀▀ ██ ██ ",
"▀▀▀ ▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
" ▄█▀▀█▄ ",
"██ ▀█▄█▀█▄ ▄█▀▀█▄ ▄█▀▀█▄ ██▀▀█▄ ",
"▀█▄ ▀██ ██ ▀▀ ██▀▀▀▀ ██▀▀▀▀ ██ ██ ",
" ▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ",
"▀██▀▀█▄ ▀██ ",
" ██▄▄█▀ ██ ██ ██ ▄█▀▀█▄ ",
" ██ ██ ██ ██ ██ ██▀▀▀▀ ",
"▀▀▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ",
]);
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_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"▐█▌ █ ▐█ ██ █ ",
"█ █ █ ▐▌ ",
"█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ",
"▐█ █ █ █ █ █ █ █ █ ▐▌ █ █ █ █ █ ",
" ▐█ █ █ █ █ █ █ ███ ▐▌ ▌ █ █ █ ███ ",
"█ █ █ █ █ ▐██ █ █ ▐▌▐▌ █ █ █ █ ",
"▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ",
" ██▌ ",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 6));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"███ ▐ ▐█",
"▌█▐ █ █",
" █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █",
" █ ▐█▐▌█ █ █ █ █ █ █ █ █ █ ▐██",
" █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █",
" █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 16));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"█ ▐▌ ▐█ ▐ █ ",
"█▌█▌ █ █ ",
"███▌█ █ █ ▐██ ▐█ ",
"███▌█ █ █ █ █ ",
"█▐▐▌█ █ █ █ █ ",
"█ ▐▌█ █ █ █▐ █ ",
"█ ▐▌▐█▐▌▐█▌ ▐▌ ▐█▌ ",
" ",
"██ █ ",
"▐▌ ",
"▐▌ ▐█ ██▌ ▐█▌ ▐██ ",
"▐▌ █ █ █ █ █ █ ",
"▐▌ ▌ █ █ █ ███ ▐█▌ ",
"▐▌▐▌ █ █ █ █ █ ",
"███▌▐█▌ █ █ ▐█▌ ██▌ ",
" ",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"▐█▌ ▐ ▐█ ▐█ ".bold(),
"█ █ █ █ █ ".bold(),
"█▌ ▐██ █ █ █ ▐█▌ █ ".bold(),
"▐█ █ █ █ █ █ █ ▐██ ".bold(),
" ▐█ █ █ █ █ ███ █ █ ".bold(),
"█ █ █▐ ▐██ █ █ █ █ ".bold(),
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌".bold(),
" ██▌ ".bold(),
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.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([
"███ ▐█ ",
"▐▌▐▌ █ ",
"▐▌▐▌▐█▌ █ ",
"▐██ █ █ ▐██ ",
"▐▌█ ███ █ █ ",
"▐▌▐▌█ █ █ ",
"█▌▐▌▐█▌ ▐█▐▌ ",
" ",
" ██ ",
"▐▌▐▌ ",
"█ █▐█ ▐█▌ ▐█▌ ██▌ ",
"█ ▐█▐▌█ █ █ █ █ █ ",
"█ █▌▐▌▐▌███ ███ █ █ ",
"▐▌▐▌▐▌ █ █ █ █ ",
" ██▌██ ▐█▌ ▐█▌ █ █ ",
" ",
"███ ▐█ ",
"▐▌▐▌ █ ",
"▐▌▐▌ █ █ █ ▐█▌ ",
"▐██ █ █ █ █ █ ",
"▐▌▐▌ █ █ █ ███ ",
"▐▌▐▌ █ █ █ █ ",
"███ ▐█▌ ▐█▐▌▐█▌ ",
" ",
]);
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_eq!(buf, expected);
Ok(())
}
#[test]
fn check_half_size_symbols() -> Result<()> {
assert_eq!(get_symbol_half_size(0, 0, 0, 0), ' ');
assert_eq!(get_symbol_half_size(1, 0, 0, 0), '▘');
assert_eq!(get_symbol_half_size(0, 1, 0, 0), '▝');
assert_eq!(get_symbol_half_size(1, 1, 0, 0), '▀');
assert_eq!(get_symbol_half_size(0, 0, 1, 0), '▖');
assert_eq!(get_symbol_half_size(1, 0, 1, 0), '▌');
assert_eq!(get_symbol_half_size(0, 1, 1, 0), '▞');
assert_eq!(get_symbol_half_size(1, 1, 1, 0), '▛');
assert_eq!(get_symbol_half_size(0, 0, 0, 1), '▗');
assert_eq!(get_symbol_half_size(1, 0, 0, 1), '▚');
assert_eq!(get_symbol_half_size(0, 1, 0, 1), '▐');
assert_eq!(get_symbol_half_size(1, 1, 0, 1), '▜');
assert_eq!(get_symbol_half_size(0, 0, 1, 1), '▄');
assert_eq!(get_symbol_half_size(1, 0, 1, 1), '▙');
assert_eq!(get_symbol_half_size(0, 1, 1, 1), '▟');
assert_eq!(get_symbol_half_size(1, 1, 1, 1), '█');
Ok(())
}
#[test]
fn render_half_size_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"▟▀▙ ▀ ▝█ ▜▛ ▀ ",
"▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ",
"▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ",
"▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 3));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"▛█▜ ▟ ▝█",
" █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█",
" █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"█▖▟▌ ▝█ ▟ ▀ ",
"███▌█ █ █ ▝█▀ ▝█ ",
"█▝▐▌█ █ █ █▗ █ ",
"▀ ▝▘▝▀▝▘▝▀▘ ▝▘ ▝▀▘ ",
"▜▛ ▀ ",
"▐▌ ▝█ █▀▙ ▟▀▙ ▟▀▀ ",
"▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ",
"▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ",
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"▟▀▙ ▟ ▝█ ▝█ ".bold(),
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ".bold(),
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ".bold(),
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘".bold(),
]);
assert_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.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([
"▜▛▜▖ ▝█ ",
"▐▙▟▘▟▀▙ ▗▄█ ",
"▐▌▜▖█▀▀ █ █ ",
"▀▘▝▘▝▀▘ ▝▀▝▘ ",
"▗▛▜▖ ",
"█ ▜▟▜▖▟▀▙ ▟▀▙ █▀▙ ",
"▜▖▜▌▐▌▝▘█▀▀ █▀▀ █ █ ",
" ▀▀▘▀▀ ▝▀▘ ▝▀▘ ▀ ▀ ",
"▜▛▜▖▝█ ",
"▐▙▟▘ █ █ █ ▟▀▙ ",
"▐▌▐▌ █ █ █ █▀▀ ",
"▀▀▀ ▝▀▘ ▝▀▝▘▝▀▘ ",
]);
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_eq!(buf, expected);
Ok(())
}
}

View File

@@ -1,18 +0,0 @@
use color_eyre::{config::HookBuilder, Result};
use crate::term;
pub fn init_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = term::restore();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = term::restore();
panic(info);
}));
Ok(())
}

View File

@@ -1,45 +0,0 @@
//! # [Ratatui] Demo2 example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(
clippy::enum_glob_use,
clippy::missing_errors_doc,
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::wildcard_imports
)]
mod app;
mod big_text;
mod colors;
mod destroy;
mod errors;
mod tabs;
mod term;
mod theme;
pub use app::*;
use color_eyre::Result;
pub use colors::*;
pub use term::*;
pub use theme::*;
fn main() -> Result<()> {
errors::init_hooks()?;
let terminal = &mut term::init()?;
App::default().run(terminal)?;
term::restore()?;
Ok(())
}

View File

@@ -1,42 +0,0 @@
use std::{
io::{self, stdout},
time::Duration,
};
use color_eyre::{eyre::WrapErr, Result};
use crossterm::{
event::{self, Event},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::prelude::*;
pub fn init() -> Result<Terminal<impl Backend>> {
// this size is to match the size of the terminal when running the demo
// using vhs in a 1280x640 sized window (github social preview size)
let options = TerminalOptions {
viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
};
let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;
enable_raw_mode().context("enable raw mode")?;
stdout()
.execute(EnterAlternateScreen)
.wrap_err("enter alternate screen")?;
Ok(terminal)
}
pub fn restore() -> Result<()> {
disable_raw_mode().context("disable raw mode")?;
stdout()
.execute(LeaveAlternateScreen)
.wrap_err("leave alternate screen")?;
Ok(())
}
pub fn next_event(timeout: Duration) -> Result<Option<Event>> {
if !event::poll(timeout)? {
return Ok(None);
}
let event = event::read()?;
Ok(Some(event))
}

View File

@@ -1,95 +0,0 @@
//! # [Ratatui] Hello World example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
io::{self, Stdout},
time::Duration,
};
use anyhow::{Context, Result};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::Paragraph};
/// This is a bare minimum example. There are many approaches to running an application loop, so
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
/// teardown of a terminal application.
///
/// A more robust application would probably want to handle errors and ensure that the terminal is
/// restored to a sane state before exiting. This example does not do that. It also does not handle
/// events or update the application state. It just draws a greeting and exits when the user
/// presses 'q'.
fn main() -> Result<()> {
let mut terminal = setup_terminal().context("setup failed")?;
run(&mut terminal).context("app loop failed")?;
restore_terminal(&mut terminal).context("restore terminal failed")?;
Ok(())
}
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and
/// hide the cursor. This example does not handle errors. A more robust application would probably
/// want to handle errors and ensure that the terminal is restored to a sane state before exiting.
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
let mut stdout = io::stdout();
enable_raw_mode().context("failed to enable raw mode")?;
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
}
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
/// the cursor.
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode().context("failed to disable raw mode")?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)
.context("unable to switch to main screen")?;
terminal.show_cursor().context("unable to show cursor")
}
/// Run the application loop. This is where you would handle events and update the application
/// state. This example exits when the user presses 'q'. Other styles of application loops are
/// possible, for example, you could have multiple application states and switch between them based
/// on events, or you could have a single application state and update it based on events.
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
loop {
terminal.draw(crate::render_app)?;
if should_quit()? {
break;
}
}
Ok(())
}
/// Render the application. This is where you would draw the application UI. This example just
/// draws a greeting.
fn render_app(frame: &mut Frame) {
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
frame.render_widget(greeting, frame.size());
}
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
/// events. There is a 250ms timeout on the event poll so that the application can exit in a timely
/// manner, and to ensure that the terminal is rendered at least once every 250ms.
fn should_quit() -> Result<bool> {
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
if let Event::Key(key) = event::read().context("event read failed")? {
return Ok(KeyCode::Char('q') == key.code);
}
}
Ok(false)
}

View File

@@ -1,362 +0,0 @@
//! # [Ratatui] List example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
use std::{error::Error, io, io::stdout};
use color_eyre::config::HookBuilder;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
const TODO_HEADER_BG: Color = tailwind::BLUE.c950;
const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
const ALT_ROW_COLOR: Color = tailwind::SLATE.c900;
const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300;
const TEXT_COLOR: Color = tailwind::SLATE.c200;
const COMPLETED_TEXT_COLOR: Color = tailwind::GREEN.c500;
#[derive(Copy, Clone)]
enum Status {
Todo,
Completed,
}
struct TodoItem<'a> {
todo: &'a str,
info: &'a str,
status: Status,
}
struct StatefulList<'a> {
state: ListState,
items: Vec<TodoItem<'a>>,
last_selected: Option<usize>,
}
/// This struct holds the current state of the app. In particular, it has the `items` field which is
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
/// widget with its state and have access to features such as natural scrolling.
///
/// Check the event handling at the bottom to see how to change the state on incoming events.
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
struct App<'a> {
items: StatefulList<'a>,
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
init_error_hooks()?;
let terminal = init_terminal()?;
// create app and run it
App::new().run(terminal)?;
restore_terminal()?;
Ok(())
}
fn init_error_hooks() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> color_eyre::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
impl<'a> App<'a> {
fn new() -> Self {
Self {
items: StatefulList::with_items([
("Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust", Status::Todo),
("Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui.", Status::Completed),
("Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!", Status::Todo),
("Walk with your dog", "Max is bored, go walk with him!", Status::Todo),
("Pay the bills", "Pay the train subscription!!!", Status::Completed),
("Refactor list example", "If you see this info that means I completed this task!", Status::Completed),
]),
}
}
/// Changes the status of the selected list item
fn change_status(&mut self) {
if let Some(i) = self.items.state.selected() {
self.items.items[i].status = match self.items.items[i].status {
Status::Completed => Status::Todo,
Status::Todo => Status::Completed,
}
}
}
fn go_top(&mut self) {
self.items.state.select(Some(0));
}
fn go_bottom(&mut self) {
self.items.state.select(Some(self.items.items.len() - 1));
}
}
impl App<'_> {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> io::Result<()> {
loop {
self.draw(&mut terminal)?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
use KeyCode::*;
match key.code {
Char('q') | Esc => return Ok(()),
Char('h') | Left => self.items.unselect(),
Char('j') | Down => self.items.next(),
Char('k') | Up => self.items.previous(),
Char('l') | Right | Enter => self.change_status(),
Char('g') => self.go_top(),
Char('G') => self.go_bottom(),
_ => {}
}
}
}
}
}
fn draw(&mut self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
terminal.draw(|f| f.render_widget(self, f.size()))?;
Ok(())
}
}
impl Widget for &mut App<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
// Create a space for header, todo list and the footer.
let vertical = Layout::vertical([
Constraint::Length(2),
Constraint::Min(0),
Constraint::Length(2),
]);
let [header_area, rest_area, footer_area] = vertical.areas(area);
// Create two chunks with equal vertical screen space. One for the list and the other for
// the info block.
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [upper_item_list_area, lower_item_list_area] = vertical.areas(rest_area);
render_title(header_area, buf);
self.render_todo(upper_item_list_area, buf);
self.render_info(lower_item_list_area, buf);
render_footer(footer_area, buf);
}
}
impl App<'_> {
fn render_todo(&mut self, area: Rect, buf: &mut Buffer) {
// We create two blocks, one is for the header (outer) and the other is for list (inner).
let outer_block = Block::new()
.borders(Borders::NONE)
.title_alignment(Alignment::Center)
.title("TODO List")
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG);
let inner_block = Block::new()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(NORMAL_ROW_COLOR);
// We get the inner area from outer_block. We'll use this area later to render the table.
let outer_area = area;
let inner_area = outer_block.inner(outer_area);
// We can render the header in outer_area.
outer_block.render(outer_area, buf);
// Iterate through all elements in the `items` and stylize them.
let items: Vec<ListItem> = self
.items
.items
.iter()
.enumerate()
.map(|(i, todo_item)| todo_item.to_list_item(i))
.collect();
// Create a List from all list items and highlight the currently selected one
let items = List::new(items)
.block(inner_block)
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED)
.fg(SELECTED_STYLE_FG),
)
.highlight_symbol(">")
.highlight_spacing(HighlightSpacing::Always);
// We can now render the item list
// (look careful we are using StatefulWidget's render.)
// ratatui::widgets::StatefulWidget::render as stateful_render
StatefulWidget::render(items, inner_area, buf, &mut self.items.state);
}
fn render_info(&self, area: Rect, buf: &mut Buffer) {
// We get the info depending on the item's state.
let info = if let Some(i) = self.items.state.selected() {
match self.items.items[i].status {
Status::Completed => "✓ DONE: ".to_string() + self.items.items[i].info,
Status::Todo => "TODO: ".to_string() + self.items.items[i].info,
}
} else {
"Nothing to see here...".to_string()
};
// We show the list item's info under the list in this paragraph
let outer_info_block = Block::new()
.borders(Borders::NONE)
.title_alignment(Alignment::Center)
.title("TODO Info")
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG);
let inner_info_block = Block::new()
.borders(Borders::NONE)
.padding(Padding::horizontal(1))
.bg(NORMAL_ROW_COLOR);
// This is a similar process to what we did for list. outer_info_area will be used for
// header inner_info_area will be used for the list info.
let outer_info_area = area;
let inner_info_area = outer_info_block.inner(outer_info_area);
// We can render the header. Inner info will be rendered later
outer_info_block.render(outer_info_area, buf);
let info_paragraph = Paragraph::new(info)
.block(inner_info_block)
.fg(TEXT_COLOR)
.wrap(Wrap { trim: false });
// We can now render the item info
info_paragraph.render(inner_info_area, buf);
}
}
fn render_title(area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui List Example")
.bold()
.centered()
.render(area, buf);
}
fn render_footer(area: Rect, buf: &mut Buffer) {
Paragraph::new("\nUse ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
.centered()
.render(area, buf);
}
impl StatefulList<'_> {
fn with_items<'a>(items: [(&'a str, &'a str, Status); 6]) -> StatefulList<'a> {
StatefulList {
state: ListState::default(),
items: items.iter().map(TodoItem::from).collect(),
last_selected: None,
}
}
fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => self.last_selected.unwrap_or(0),
};
self.state.select(Some(i));
}
fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => self.last_selected.unwrap_or(0),
};
self.state.select(Some(i));
}
fn unselect(&mut self) {
let offset = self.state.offset();
self.last_selected = self.state.selected();
self.state.select(None);
*self.state.offset_mut() = offset;
}
}
impl TodoItem<'_> {
fn to_list_item(&self, index: usize) -> ListItem {
let bg_color = match index % 2 {
0 => NORMAL_ROW_COLOR,
_ => ALT_ROW_COLOR,
};
let line = match self.status {
Status::Todo => Line::styled(format!("{}", self.todo), TEXT_COLOR),
Status::Completed => Line::styled(
format!("{}", self.todo),
(COMPLETED_TEXT_COLOR, bg_color),
),
};
ListItem::new(line).bg(bg_color)
}
}
impl<'a> From<&(&'a str, &'a str, Status)> for TodoItem<'a> {
fn from((todo, info, status): &(&'a str, &'a str, Status)) -> Self {
Self {
todo,
info,
status: *status,
}
}
}

View File

@@ -1,44 +0,0 @@
//! # [Ratatui] Minimal example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, text::Text, Terminal};
/// This is a bare minimum example. There are many approaches to running an application loop, so
/// this is not meant to be prescriptive. See the [examples] folder for more complete examples.
/// In particular, the [hello-world] example is a good starting point.
///
/// [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
/// [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
enable_raw_mode()?;
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
loop {
terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.size()))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break;
}
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -1,150 +0,0 @@
//! # [Ratatui] Panic Hook example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
//! How to use a panic hook to reset the terminal before printing the panic to
//! the terminal.
//!
//! When exiting normally or when handling `Result::Err`, we can reset the
//! terminal manually at the end of `main` just before we print the error.
//!
//! Because a panic interrupts the normal control flow, manually resetting the
//! terminal at the end of `main` won't do us any good. Instead, we need to
//! make sure to set up a panic hook that first resets the terminal before
//! handling the panic. This both reuses the standard panic hook to ensure a
//! consistent panic handling UX and properly resets the terminal to not
//! distort the output.
//!
//! That's why this example is set up to show both situations, with and without
//! the chained panic hook, to see the difference.
use std::{error::Error, io};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::*,
widgets::{Block, Paragraph},
};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
#[derive(Default)]
struct App {
hook_enabled: bool,
}
impl App {
fn chain_hook(&mut self) {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic| {
reset_terminal().unwrap();
original_hook(panic);
}));
self.hook_enabled = true;
}
}
fn main() -> Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::default();
let res = run_tui(&mut terminal, &mut app);
reset_terminal()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
/// Initializes the terminal.
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
enable_raw_mode()?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
/// Resets the terminal.
fn reset_terminal() -> Result<()> {
disable_raw_mode()?;
crossterm::execute!(io::stdout(), LeaveAlternateScreen)?;
Ok(())
}
/// Runs the TUI loop.
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, app))?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('p') => {
panic!("intentional demo panic");
}
KeyCode::Char('e') => {
app.chain_hook();
}
_ => {
return Ok(());
}
}
}
}
}
/// Render the TUI.
fn ui(f: &mut Frame, app: &App) {
let text = vec![
if app.hook_enabled {
Line::from("HOOK IS CURRENTLY **ENABLED**")
} else {
Line::from("HOOK IS CURRENTLY **DISABLED**")
},
Line::from(""),
Line::from("press `p` to panic"),
Line::from("press `e` to enable the terminal-resetting panic hook"),
Line::from("press any other key to quit without panic"),
Line::from(""),
Line::from("when you panic without the chained hook,"),
Line::from("you will likely have to reset your terminal afterwards"),
Line::from("with the `reset` command"),
Line::from(""),
Line::from("with the chained panic hook enabled,"),
Line::from("you should see the panic report as you would without ratatui"),
Line::from(""),
Line::from("try first without the panic handler to see the difference"),
];
let paragraph = Paragraph::new(text)
.block(Block::bordered().title("Panic Handler Demo"))
.centered();
f.render_widget(paragraph, f.size());
}

View File

@@ -1,163 +0,0 @@
//! # [Ratatui] Paragraph example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::*,
widgets::{Block, Paragraph, Wrap},
};
struct App {
scroll: u16,
}
impl App {
const fn new() -> Self {
Self { scroll: 0 }
}
fn on_tick(&mut self) {
self.scroll += 1;
self.scroll %= 10;
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let block = Block::new().black();
f.render_widget(block, size);
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
let text = vec![
Line::from("This is a line "),
Line::from("This is a line ".red()),
Line::from("This is a line".on_blue()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.on_green()),
Line::from("This is a line".green().italic()),
Line::from(vec![
"Masked text: ".into(),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
]),
];
let create_block = |title| {
Block::bordered()
.style(Style::default().fg(Color::Gray))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), no wrap"));
f.render_widget(paragraph, layout[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Right alignment, with wrap"))
.right_aligned()
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::Gray))
.block(create_block("Center alignment, with wrap, with scroll"))
.centered()
.wrap(Wrap { trim: true })
.scroll((app.scroll, 0));
f.render_widget(paragraph, layout[3]);
}

View File

@@ -1,127 +0,0 @@
//! # [Ratatui] Popup example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
// See also https://github.com/joshka/tui-popup and
// https://github.com/sephiroth74/tui-confirm-dialog
use std::{error::Error, io};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::*,
widgets::{Block, Clear, Paragraph, Wrap},
};
struct App {
show_popup: bool,
}
impl App {
const fn new() -> Self {
Self { show_popup: false }
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('p') => app.show_popup = !app.show_popup,
_ => {}
}
}
}
}
}
fn ui(f: &mut Frame, app: &App) {
let area = f.size();
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [instructions, content] = vertical.areas(area);
let text = if app.show_popup {
"Press p to close the popup"
} else {
"Press p to show the popup"
};
let paragraph = Paragraph::new(text.slow_blink())
.centered()
.wrap(Wrap { trim: true });
f.render_widget(paragraph, instructions);
let block = Block::bordered().title("Content").on_blue();
f.render_widget(block, content);
if app.show_popup {
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);
}
}
/// helper function to create a centered rect using up certain percentage of the available rect `r`
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}

View File

@@ -1,231 +0,0 @@
//! # [Ratatui] Scrollbar example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![warn(clippy::pedantic)]
#![allow(clippy::wildcard_imports)]
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, symbols::scrollbar, widgets::*};
#[derive(Default)]
struct App {
pub vertical_scroll_state: ScrollbarState,
pub horizontal_scroll_state: ScrollbarState,
pub vertical_scroll: usize,
pub horizontal_scroll: usize,
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::default();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('j') | KeyCode::Down => {
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
}
KeyCode::Char('k') | KeyCode::Up => {
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
}
KeyCode::Char('h') | KeyCode::Left => {
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
}
KeyCode::Char('l') | KeyCode::Right => {
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
}
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
last_tick = Instant::now();
}
}
}
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
fn ui(f: &mut Frame, app: &mut App) {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let chunks = Layout::vertical([
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(size);
let text = vec![
Line::from("This is a line "),
Line::from("This is a line ".red()),
Line::from("This is a line".on_dark_gray()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.clone()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
]),
Line::from("This is a line "),
Line::from("This is a line ".red()),
Line::from("This is a line".on_dark_gray()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.clone()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
]),
];
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
let title = Block::new()
.title_alignment(Alignment::Center)
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold());
f.render_widget(title, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block("Vertical scrollbar with arrows"))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[1]);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some("")),
chunks[1],
&mut app.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Vertical scrollbar without arrows, without track symbol and mirrored",
))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[2]);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
.symbols(scrollbar::VERTICAL)
.begin_symbol(None)
.track_symbol(None)
.end_symbol(None),
chunks[2].inner(&Margin {
vertical: 1,
horizontal: 0,
}),
&mut app.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
))
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[3]);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("🬋")
.end_symbol(None),
chunks[3].inner(&Margin {
vertical: 0,
horizontal: 1,
}),
&mut app.horizontal_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Horizontal scrollbar without arrows & custom thumb and track symbol",
))
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[4]);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("")
.track_symbol(Some("")),
chunks[4].inner(&Margin {
vertical: 0,
horizontal: 1,
}),
&mut app.horizontal_scroll_state,
);
}

View File

@@ -1,180 +0,0 @@
//! # [Ratatui] Sparkline example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Sparkline},
};
#[derive(Clone)]
struct RandomSignal {
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
fn new(lower: u64, upper: u64) -> Self {
Self {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
}
}
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.distribution.sample(&mut self.rng))
}
}
struct App {
signal: RandomSignal,
data1: Vec<u64>,
data2: Vec<u64>,
data3: Vec<u64>,
}
impl App {
fn new() -> Self {
let mut signal = RandomSignal::new(0, 100);
let data1 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
Self {
signal,
data1,
data2,
data3,
}
}
fn on_tick(&mut self) {
let value = self.signal.next().unwrap();
self.data1.pop();
self.data1.insert(0, value);
let value = self.signal.next().unwrap();
self.data2.pop();
self.data2.insert(0, value);
let value = self.signal.next().unwrap();
self.data3.pop();
self.data3.insert(0, value);
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(f.size());
let sparkline = Sparkline::default()
.block(
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::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data2"),
)
.data(&app.data2)
.style(Style::default().bg(Color::Green));
f.render_widget(sparkline, chunks[1]);
// Multiline
let sparkline = Sparkline::default()
.block(
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data3"),
)
.data(&app.data3)
.style(Style::default().fg(Color::Red));
f.render_widget(sparkline, chunks[2]);
}

50
ratatui-core/Cargo.toml Normal file
View File

@@ -0,0 +1,50 @@
[package]
name = "ratatui-core"
version = "0.1.0"
edition = "2021"
[features]
underline-color = []
unstable-widget-ref = []
[dependencies]
bitflags.workspace = true
cassowary.workspace = true
compact_str.workspace = true
document-features.workspace = true
instability.workspace = true
itertools.workspace = true
lru.workspace = true
paste.workspace = true
palette = { workspace = true, optional = true }
serde = { workspace = true, optional = true, features = ["derive"] }
strum = { workspace = true, features = ["derive"] }
termwiz = { workspace = true, optional = true }
unicode-segmentation.workspace = true
unicode-truncate.workspace = true
unicode-width.workspace = true
[dev-dependencies]
argh = "0.1.12"
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
crossterm = { version = "0.28.1", features = ["event-stream"] }
fakeit = "1.1"
font8x8 = "0.3.1"
futures = "0.3.30"
indoc = "2"
octocrab = "0.40.0"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.22.0"
serde_json = "1.0.109"
tokio = { version = "1.39.2", features = [
"rt",
"macros",
"time",
"rt-multi-thread",
] }
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

View File

@@ -96,30 +96,18 @@
//! [Crossterm]: https://crates.io/crates/crossterm
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz
//! [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
//! [Backend Comparison]:
//! https://ratatui.rs/concepts/backends/comparison/
//! [Ratatui Website]: https://ratatui-org.github.io/ratatui-book
//! [Ratatui Website]: https://ratatui.rs
use std::io;
use strum::{Display, EnumString};
use crate::{buffer::Cell, layout::Size, prelude::Rect};
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termion")]
pub use self::termion::TermionBackend;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "termwiz")]
mod termwiz;
#[cfg(feature = "termwiz")]
pub use self::termwiz::TermwizBackend;
use crate::{
buffer::Cell,
layout::{Position, Size},
};
mod test;
pub use self::test::TestBackend;
@@ -192,25 +180,25 @@ pub trait Backend {
/// # std::io::Result::Ok(())
/// ```
///
/// [`show_cursor`]: Backend::show_cursor
/// [`show_cursor`]: Self::show_cursor
fn hide_cursor(&mut self) -> io::Result<()>;
/// Show the cursor on the terminal screen.
///
/// See [`hide_cursor`] for an example.
///
/// [`hide_cursor`]: Backend::hide_cursor
/// [`hide_cursor`]: Self::hide_cursor
fn show_cursor(&mut self) -> io::Result<()>;
/// Get the current cursor position on the terminal screen.
///
/// The returned tuple contains the x and y coordinates of the cursor. The origin
/// (0, 0) is at the top left corner of the screen.
/// The returned tuple contains the x and y coordinates of the cursor.
/// The origin (0, 0) is at the top left corner of the screen.
///
/// See [`set_cursor`] for an example.
/// See [`set_cursor_position`] for an example.
///
/// [`set_cursor`]: Backend::set_cursor
fn get_cursor(&mut self) -> io::Result<(u16, u16)>;
/// [`set_cursor_position`]: Self::set_cursor_position
fn get_cursor_position(&mut self) -> io::Result<Position>;
/// Set the cursor position on the terminal screen to the given x and y coordinates.
///
@@ -220,14 +208,31 @@ pub trait Backend {
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::layout::Position;
/// # let mut backend = TestBackend::new(80, 25);
/// backend.set_cursor(10, 20)?;
/// assert_eq!(backend.get_cursor()?, (10, 20));
/// backend.set_cursor_position(Position { x: 10, y: 20 })?;
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
/// # std::io::Result::Ok(())
/// ```
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()>;
/// Get the current cursor position on the terminal screen.
///
/// [`get_cursor`]: Backend::get_cursor
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
/// The returned tuple contains the x and y coordinates of the cursor. The origin
/// (0, 0) is at the top left corner of the screen.
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
/// Set the cursor position on the terminal screen to the given x and y coordinates.
///
/// The origin (0, 0) is at the top left corner of the screen.
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.set_cursor_position(Position { x, y })
}
/// Clears the whole terminal screen
///
@@ -261,7 +266,7 @@ pub trait Backend {
/// This method will return an error if the terminal screen could not be cleared. It will also
/// return an error if the `clear_type` is not supported by the backend.
///
/// [`clear`]: Backend::clear
/// [`clear`]: Self::clear
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => self.clear(),
@@ -275,19 +280,19 @@ pub trait Backend {
}
}
/// Get the size of the terminal screen in columns/rows as a [`Rect`].
/// Get the size of the terminal screen in columns/rows as a [`Size`].
///
/// The returned [`Rect`] contains the width and height of the terminal screen.
/// The returned [`Size`] contains the width and height of the terminal screen.
///
/// # Example
///
/// ```rust,no_run
/// ```rust
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = TestBackend::new(80, 25);
/// assert_eq!(backend.size()?, Rect::new(0, 0, 80, 25));
/// assert_eq!(backend.size()?, Size::new(80, 25));
/// # std::io::Result::Ok(())
/// ```
fn size(&self) -> io::Result<Rect>;
fn size(&self) -> io::Result<Size>;
/// Get the size of the terminal screen in columns/rows and pixels as a [`WindowSize`].
///

View File

@@ -3,7 +3,7 @@
use std::{
fmt::{self, Write},
io,
io, iter,
};
use unicode_width::UnicodeWidthStr;
@@ -11,7 +11,7 @@ use unicode_width::UnicodeWidthStr;
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::{Buffer, Cell},
layout::{Rect, Size},
layout::{Position, Rect, Size},
};
/// A [`Backend`] implementation used for integration testing that renders to an memory buffer.
@@ -34,9 +34,8 @@ use crate::{
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TestBackend {
width: u16,
buffer: Buffer,
height: u16,
scrollback: Buffer,
cursor: bool,
pos: (u16, u16),
}
@@ -74,9 +73,8 @@ impl TestBackend {
/// Creates a new `TestBackend` with the specified width and height.
pub fn new(width: u16, height: u16) -> Self {
Self {
width,
height,
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
scrollback: Buffer::empty(Rect::new(0, 0, width, 0)),
cursor: false,
pos: (0, 0),
}
@@ -87,11 +85,29 @@ impl TestBackend {
&self.buffer
}
/// Returns a reference to the internal scrollback buffer of the `TestBackend`.
///
/// The scrollback buffer represents the part of the screen that is currently hidden from view,
/// but that could be accessed by scrolling back in the terminal's history. This would normally
/// be done using the terminal's scrollbar or an equivalent keyboard shortcut.
///
/// The scrollback buffer starts out empty. Lines are appended when they scroll off the top of
/// the main buffer. This happens when lines are appended to the bottom of the main buffer
/// using [`Backend::append_lines`].
///
/// The scrollback buffer has a maximum height of [`u16::MAX`]. If lines are appended to the
/// bottom of the scrollback buffer when it is at its maximum height, a corresponding number of
/// lines will be removed from the top.
pub const fn scrollback(&self) -> &Buffer {
&self.scrollback
}
/// Resizes the `TestBackend` to the specified width and height.
pub fn resize(&mut self, width: u16, height: u16) {
self.buffer.resize(Rect::new(0, 0, width, height));
self.width = width;
self.height = height;
let scrollback_height = self.scrollback.area.height;
self.scrollback
.resize(Rect::new(0, 0, width, scrollback_height));
}
/// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
@@ -99,6 +115,7 @@ impl TestBackend {
/// 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)]
@@ -108,11 +125,42 @@ impl TestBackend {
crate::assert_buffer_eq!(&self.buffer, expected);
}
/// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected buffer.
///
/// This is a shortcut for `assert_eq!(self.scrollback(), &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_scrollback(&self, expected: &Buffer) {
assert_eq!(&self.scrollback, expected);
}
/// Asserts that the `TestBackend`'s scrollback buffer is empty.
///
/// # Panics
///
/// When the scrollback buffer is not equal, a panic occurs with a detailed error message
/// showing the differences between the expected and actual buffers.
pub fn assert_scrollback_empty(&self) {
let expected = Buffer {
area: Rect {
width: self.scrollback.area.width,
..Rect::ZERO
},
content: vec![],
};
self.assert_scrollback(&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]
@@ -123,6 +171,37 @@ impl TestBackend {
{
self.assert_buffer(&Buffer::with_lines(expected));
}
/// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected lines.
///
/// This is a shortcut for `assert_eq!(self.scrollback(), &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_scrollback_lines<'line, Lines>(&self, expected: Lines)
where
Lines: IntoIterator,
Lines::Item: Into<crate::text::Line<'line>>,
{
self.assert_scrollback(&Buffer::with_lines(expected));
}
/// Asserts that the `TestBackend`'s cursor position is equal to the expected one.
///
/// This is a shortcut for `assert_eq!(self.get_cursor_position().unwrap(), expected)`.
///
/// # Panics
///
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual position.
#[track_caller]
pub fn assert_cursor_position<P: Into<Position>>(&mut self, position: P) {
let actual = self.get_cursor_position().unwrap();
assert_eq!(actual, position.into());
}
}
impl fmt::Display for TestBackend {
@@ -139,8 +218,7 @@ impl Backend for TestBackend {
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, c) in content {
let cell = self.buffer.get_mut(x, y);
*cell = c.clone();
self.buffer[(x, y)] = c.clone();
}
Ok(())
}
@@ -155,12 +233,12 @@ impl Backend for TestBackend {
Ok(())
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
Ok(self.pos)
fn get_cursor_position(&mut self) -> io::Result<Position> {
Ok(self.pos.into())
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.pos = (x, y);
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
self.pos = position.into().into();
Ok(())
}
@@ -170,26 +248,29 @@ impl Backend for TestBackend {
}
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => self.clear()?,
let region = match clear_type {
ClearType::All => return self.clear(),
ClearType::AfterCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
self.buffer.content[index..].fill(Cell::default());
&mut self.buffer.content[index..]
}
ClearType::BeforeCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1);
self.buffer.content[..index].fill(Cell::default());
&mut self.buffer.content[..index]
}
ClearType::CurrentLine => {
let line_start_index = self.buffer.index_of(0, self.pos.1);
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
self.buffer.content[line_start_index..=line_end_index].fill(Cell::default());
let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
&mut self.buffer.content[line_start_index..=line_end_index]
}
ClearType::UntilNewLine => {
let index = self.buffer.index_of(self.pos.0, self.pos.1);
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
self.buffer.content[index..=line_end_index].fill(Cell::default());
let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
&mut self.buffer.content[index..=line_end_index]
}
};
for cell in region {
cell.reset();
}
Ok(())
}
@@ -206,46 +287,55 @@ impl Backend for TestBackend {
/// the cursor y position then that number of empty lines (at most the buffer's height in this
/// case but this limit is instead replaced with scrolling in most backend implementations) will
/// be added after the current position and the cursor will be moved to the last row.
fn append_lines(&mut self, n: u16) -> io::Result<()> {
let (cur_x, cur_y) = self.get_cursor()?;
fn append_lines(&mut self, line_count: u16) -> io::Result<()> {
let Position { x: cur_x, y: cur_y } = self.get_cursor_position()?;
let Rect { width, height, .. } = self.buffer.area;
// the next column ensuring that we don't go past the last column
let new_cursor_x = cur_x.saturating_add(1).min(self.width.saturating_sub(1));
let new_cursor_x = cur_x.saturating_add(1).min(width.saturating_sub(1));
let max_y = self.height.saturating_sub(1);
let max_y = height.saturating_sub(1);
let lines_after_cursor = max_y.saturating_sub(cur_y);
if n > lines_after_cursor {
let rotate_by = n.saturating_sub(lines_after_cursor).min(max_y);
if rotate_by == self.height - 1 {
self.clear()?;
}
if line_count > lines_after_cursor {
// We need to insert blank lines at the bottom and scroll the lines from the top into
// scrollback.
let scroll_by: usize = (line_count - lines_after_cursor).into();
let width: usize = self.buffer.area.width.into();
let cells_to_scrollback = self.buffer.content.len().min(width * scroll_by);
self.set_cursor(0, rotate_by)?;
self.clear_region(ClearType::BeforeCursor)?;
self.buffer
.content
.rotate_left((self.width * rotate_by).into());
append_to_scrollback(
&mut self.scrollback,
self.buffer.content.splice(
0..cells_to_scrollback,
iter::repeat_with(Default::default).take(cells_to_scrollback),
),
);
self.buffer.content.rotate_left(cells_to_scrollback);
append_to_scrollback(
&mut self.scrollback,
iter::repeat_with(Default::default).take(width * scroll_by - cells_to_scrollback),
);
}
let new_cursor_y = cur_y.saturating_add(n).min(max_y);
self.set_cursor(new_cursor_x, new_cursor_y)?;
let new_cursor_y = cur_y.saturating_add(line_count).min(max_y);
self.set_cursor_position(Position::new(new_cursor_x, new_cursor_y))?;
Ok(())
}
fn size(&self) -> io::Result<Rect> {
Ok(Rect::new(0, 0, self.width, self.height))
fn size(&self) -> io::Result<Size> {
Ok(self.buffer.area.as_size())
}
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 {
const WINDOW_PIXEL_SIZE: Size = Size {
width: 640,
height: 480,
};
Ok(WindowSize {
columns_rows: (self.width, self.height).into(),
columns_rows: self.buffer.area.as_size(),
pixels: WINDOW_PIXEL_SIZE,
})
}
@@ -255,8 +345,25 @@ impl Backend for TestBackend {
}
}
/// Append the provided cells to the bottom of a scrollback buffer. The number of cells must be a
/// multiple of the buffer's width. If the scrollback buffer ends up larger than 65535 lines tall,
/// then lines will be removed from the top to get it down to size.
fn append_to_scrollback(scrollback: &mut Buffer, cells: impl IntoIterator<Item = Cell>) {
scrollback.content.extend(cells);
let width = scrollback.area.width as usize;
let new_height = (scrollback.content.len() / width).min(u16::MAX as usize);
let keep_from = scrollback
.content
.len()
.saturating_sub(width * u16::MAX as usize);
scrollback.content.drain(0..keep_from);
scrollback.area.height = new_height as u16;
}
#[cfg(test)]
mod tests {
use itertools::Itertools as _;
use super::*;
#[test]
@@ -264,9 +371,8 @@ mod tests {
assert_eq!(
TestBackend::new(10, 2),
TestBackend {
width: 10,
height: 2,
buffer: Buffer::with_lines([" "; 2]),
scrollback: Buffer::empty(Rect::new(0, 0, 10, 0)),
cursor: false,
pos: (0, 0),
}
@@ -317,6 +423,13 @@ mod tests {
backend.assert_buffer_lines(["aaaaaaaaaa"; 2]);
}
#[test]
#[should_panic = "assertion `left == right` failed"]
fn assert_scrollback_panics() {
let backend = TestBackend::new(10, 2);
backend.assert_scrollback_lines(["aaaaaaaaaa"; 2]);
}
#[test]
fn display() {
let backend = TestBackend::new(10, 2);
@@ -326,8 +439,7 @@ mod tests {
#[test]
fn draw() {
let mut backend = TestBackend::new(10, 2);
let mut cell = Cell::default();
cell.set_symbol("a");
let cell = Cell::new("a");
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
backend.assert_buffer_lines(["a "; 2]);
@@ -348,23 +460,30 @@ mod tests {
}
#[test]
fn get_cursor() {
fn get_cursor_position() {
let mut backend = TestBackend::new(10, 2);
assert_eq!(backend.get_cursor().unwrap(), (0, 0));
assert_eq!(backend.get_cursor_position().unwrap(), Position::ORIGIN);
}
#[test]
fn set_cursor() {
fn assert_cursor_position() {
let mut backend = TestBackend::new(10, 2);
backend.assert_cursor_position(Position::ORIGIN);
}
#[test]
fn set_cursor_position() {
let mut backend = TestBackend::new(10, 10);
backend.set_cursor(5, 5).unwrap();
backend
.set_cursor_position(Position { x: 5, y: 5 })
.unwrap();
assert_eq!(backend.pos, (5, 5));
}
#[test]
fn clear() {
let mut backend = TestBackend::new(4, 2);
let mut cell = Cell::default();
cell.set_symbol("a");
let cell = Cell::new("a");
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
backend.clear().unwrap();
@@ -403,7 +522,9 @@ mod tests {
"aaaaaaaaaa",
]);
backend.set_cursor(3, 2).unwrap();
backend
.set_cursor_position(Position { x: 3, y: 2 })
.unwrap();
backend.clear_region(ClearType::AfterCursor).unwrap();
backend.assert_buffer_lines([
"aaaaaaaaaa",
@@ -425,7 +546,9 @@ mod tests {
"aaaaaaaaaa",
]);
backend.set_cursor(5, 3).unwrap();
backend
.set_cursor_position(Position { x: 5, y: 3 })
.unwrap();
backend.clear_region(ClearType::BeforeCursor).unwrap();
backend.assert_buffer_lines([
" ",
@@ -447,7 +570,9 @@ mod tests {
"aaaaaaaaaa",
]);
backend.set_cursor(3, 1).unwrap();
backend
.set_cursor_position(Position { x: 3, y: 1 })
.unwrap();
backend.clear_region(ClearType::CurrentLine).unwrap();
backend.assert_buffer_lines([
"aaaaaaaaaa",
@@ -469,7 +594,9 @@ mod tests {
"aaaaaaaaaa",
]);
backend.set_cursor(3, 0).unwrap();
backend
.set_cursor_position(Position { x: 3, y: 0 })
.unwrap();
backend.clear_region(ClearType::UntilNewLine).unwrap();
backend.assert_buffer_lines([
"aaa ",
@@ -491,22 +618,22 @@ mod tests {
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
backend.set_cursor_position(Position::ORIGIN).unwrap();
// If the cursor is not at the last line in the terminal the addition of a
// newline simply moves the cursor down and to the right
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 1));
backend.assert_cursor_position(Position { x: 1, y: 1 });
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (2, 2));
backend.assert_cursor_position(Position { x: 2, y: 2 });
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (3, 3));
backend.assert_cursor_position(Position { x: 3, y: 3 });
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (4, 4));
backend.assert_cursor_position(Position { x: 4, y: 4 });
// As such the buffer should remain unchanged
backend.assert_buffer_lines([
@@ -516,6 +643,7 @@ mod tests {
"dddddddddd",
"eeeeeeeeee",
]);
backend.assert_scrollback_empty();
}
#[test]
@@ -531,21 +659,24 @@ mod tests {
// If the cursor is at the last line in the terminal the addition of a
// newline will scroll the contents of the buffer
backend.set_cursor(0, 4).unwrap();
backend
.set_cursor_position(Position { x: 0, y: 4 })
.unwrap();
backend.append_lines(1).unwrap();
backend.buffer = Buffer::with_lines([
backend.assert_buffer_lines([
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
]);
backend.assert_scrollback_lines(["aaaaaaaaaa"]);
// It also moves the cursor to the right, as is common of the behaviour of
// terminals in raw-mode
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_cursor_position(Position { x: 1, y: 4 });
}
#[test]
@@ -559,13 +690,13 @@ mod tests {
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
backend.set_cursor_position(Position::ORIGIN).unwrap();
// If the cursor is not at the last line in the terminal the addition of multiple
// newlines simply moves the cursor n lines down and to the right by 1
backend.append_lines(4).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_cursor_position(Position { x: 1, y: 4 });
// As such the buffer should remain unchanged
backend.assert_buffer_lines([
@@ -575,6 +706,7 @@ mod tests {
"dddddddddd",
"eeeeeeeeee",
]);
backend.assert_scrollback_empty();
}
#[test]
@@ -588,10 +720,12 @@ mod tests {
"eeeeeeeeee",
]);
backend.set_cursor(0, 3).unwrap();
backend
.set_cursor_position(Position { x: 0, y: 3 })
.unwrap();
backend.append_lines(3).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
"cccccccccc",
@@ -600,6 +734,7 @@ mod tests {
" ",
" ",
]);
backend.assert_scrollback_lines(["aaaaaaaaaa", "bbbbbbbbbb"]);
}
#[test]
@@ -613,10 +748,12 @@ mod tests {
"eeeeeeeeee",
]);
backend.set_cursor(0, 4).unwrap();
backend
.set_cursor_position(Position { x: 0, y: 4 })
.unwrap();
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
" ",
@@ -625,6 +762,13 @@ mod tests {
" ",
" ",
]);
backend.assert_scrollback_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
}
#[test]
@@ -638,10 +782,10 @@ mod tests {
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
backend.set_cursor_position(Position::ORIGIN).unwrap();
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
"bbbbbbbbbb",
@@ -650,12 +794,121 @@ mod tests {
"eeeeeeeeee",
" ",
]);
backend.assert_scrollback_lines(["aaaaaaaaaa"]);
}
#[test]
fn append_multiple_lines_where_cursor_at_end_appends_more_than_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend
.set_cursor_position(Position { x: 0, y: 4 })
.unwrap();
backend.append_lines(8).unwrap();
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
" ",
" ",
" ",
" ",
" ",
]);
backend.assert_scrollback_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
" ",
" ",
]);
}
#[test]
fn append_lines_truncates_beyond_u16_max() -> io::Result<()> {
let mut backend = TestBackend::new(10, 5);
// Fill the scrollback with 65535 + 10 lines.
let row_count = u16::MAX as usize + 10;
for row in 0..=row_count {
if row > 4 {
backend.set_cursor_position(Position { x: 0, y: 4 })?;
backend.append_lines(1)?;
}
let cells = format!("{row:>10}").chars().map(Cell::from).collect_vec();
let content = cells
.iter()
.enumerate()
.map(|(column, cell)| (column as u16, 4.min(row) as u16, cell));
backend.draw(content)?;
}
// check that the buffer contains the last 5 lines appended
backend.assert_buffer_lines([
" 65541",
" 65542",
" 65543",
" 65544",
" 65545",
]);
// TODO: ideally this should be something like:
// let lines = (6..=65545).map(|row| format!("{row:>10}"));
// backend.assert_scrollback_lines(lines);
// but there's some truncation happening in Buffer::with_lines that needs to be fixed
assert_eq!(
Buffer {
area: Rect::new(0, 0, 10, 5),
content: backend.scrollback.content[0..10 * 5].to_vec(),
},
Buffer::with_lines([
" 6",
" 7",
" 8",
" 9",
" 10",
]),
"first 5 lines of scrollback should have been truncated"
);
assert_eq!(
Buffer {
area: Rect::new(0, 0, 10, 5),
content: backend.scrollback.content[10 * 65530..10 * 65535].to_vec(),
},
Buffer::with_lines([
" 65536",
" 65537",
" 65538",
" 65539",
" 65540",
]),
"last 5 lines of scrollback should have been appended"
);
// These checks come after the content checks as otherwise we won't see the failing content
// when these checks fail.
// Make sure the scrollback is the right size.
assert_eq!(backend.scrollback.area.width, 10);
assert_eq!(backend.scrollback.area.height, 65535);
assert_eq!(backend.scrollback.content.len(), 10 * 65535);
Ok(())
}
#[test]
fn size() {
let backend = TestBackend::new(10, 2);
assert_eq!(backend.size().unwrap(), Rect::new(0, 0, 10, 2));
assert_eq!(backend.size().unwrap(), Size::new(10, 2));
}
#[test]

View File

@@ -2,7 +2,6 @@
//! A module for the [`Buffer`] and [`Cell`] types.
mod assert;
#[allow(clippy::module_inception)]
mod buffer;
mod cell;

View File

@@ -18,7 +18,7 @@ macro_rules! assert_buffer_eq {
.into_iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = expected.get(x, y);
let expected_cell = &expected[(x, y)];
format!("{i}: at ({x}, {y})\n expected: {expected_cell:?}\n actual: {cell:?}")
})
.collect::<Vec<String>>()

View File

@@ -1,9 +1,12 @@
use std::fmt;
use std::{
fmt,
ops::{Index, IndexMut},
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{buffer::Cell, prelude::*};
use crate::{buffer::Cell, layout::Position, prelude::*};
/// A buffer that maps to the desired content of the terminal after the draw call
///
@@ -15,16 +18,34 @@ use crate::{buffer::Cell, prelude::*};
/// # Examples:
///
/// ```
/// use ratatui::{buffer::Cell, prelude::*};
/// use ratatui::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// style::{Color, Style},
/// };
///
/// # fn foo() -> Option<()> {
/// let mut buf = Buffer::empty(Rect {
/// x: 0,
/// y: 0,
/// width: 10,
/// height: 5,
/// });
/// buf.get_mut(0, 2).set_symbol("x");
/// assert_eq!(buf.get(0, 2).symbol(), "x");
///
/// // indexing using Position
/// buf[Position { x: 0, y: 0 }].set_symbol("A");
/// assert_eq!(buf[Position { x: 0, y: 0 }].symbol(), "A");
///
/// // indexing using (x, y) tuple (which is converted to Position)
/// buf[(0, 1)].set_symbol("B");
/// assert_eq!(buf[(0, 1)].symbol(), "x");
///
/// // getting an Option instead of panicking if the position is outside the buffer
/// let cell = buf.cell_mut(Position { x: 0, y: 2 })?;
/// cell.set_symbol("C");
///
/// let cell = buf.cell(Position { x: 0, y: 2 })?;
/// assert_eq!(cell.symbol(), "C");
///
/// buf.set_string(
/// 3,
@@ -32,13 +53,12 @@ use crate::{buffer::Cell, prelude::*};
/// "string",
/// Style::default().fg(Color::Red).bg(Color::White),
/// );
/// let cell = buf.get(5, 0);
/// let cell = &buf[(5, 0)]; // cannot move out of buf, so we borrow it
/// assert_eq!(cell.symbol(), "r");
/// assert_eq!(cell.fg, Color::Red);
/// assert_eq!(cell.bg, Color::White);
///
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol(), "x");
/// # Some(())
/// # }
/// ```
#[derive(Default, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -54,14 +74,14 @@ impl Buffer {
/// Returns a Buffer with all cells set to the default one
#[must_use]
pub fn empty(area: Rect) -> Self {
Self::filled(area, &Cell::default())
Self::filled(area, Cell::EMPTY)
}
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
#[must_use]
pub fn filled(area: Rect, cell: &Cell) -> Self {
pub fn filled(area: Rect, cell: Cell) -> Self {
let size = area.area() as usize;
let content = vec![cell.clone(); size];
let content = vec![cell; size];
Self { area, content }
}
@@ -92,20 +112,101 @@ impl Buffer {
&self.area
}
/// Returns a reference to Cell at the given coordinates
/// Returns a reference to the [`Cell`] at the given coordinates
///
/// Callers should use [`Buffer[]`](Self::index) or [`Buffer::cell`] instead of this method.
///
/// Note: idiomatically methods named `get` usually return `Option<&T>`, but this method panics
/// instead. This is kept for backwards compatibility. See [`cell`](Self::cell) for a safe
/// alternative.
///
/// # Panics
///
/// Panics if the index is out of bounds.
#[track_caller]
#[deprecated(note = "Use Buffer[] or Buffer::cell instead")]
#[must_use]
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
/// Returns a mutable reference to the [`Cell`] at the given coordinates.
///
/// Callers should use [`Buffer[]`](Self::index_mut) or [`Buffer::cell_mut`] instead of this
/// method.
///
/// Note: idiomatically methods named `get_mut` usually return `Option<&mut T>`, but this method
/// panics instead. This is kept for backwards compatibility. See [`cell_mut`](Self::cell_mut)
/// for a safe alternative.
///
/// # Panics
///
/// Panics if the position is outside the `Buffer`'s area.
#[track_caller]
#[deprecated(note = "Use Buffer[] or Buffer::cell_mut instead")]
#[must_use]
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
let i = self.index_of(x, y);
&mut self.content[i]
}
/// Returns a reference to the [`Cell`] at the given position or [`None`] if the position is
/// outside the `Buffer`'s area.
///
/// This method accepts any value that can be converted to [`Position`] (e.g. `(x, y)` or
/// `Position::new(x, y)`).
///
/// For a method that panics when the position is outside the buffer instead of returning
/// `None`, use [`Buffer[]`](Self::index).
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
///
/// assert_eq!(buffer.cell(Position::new(0, 0)), Some(&Cell::default()));
/// assert_eq!(buffer.cell(Position::new(10, 10)), None);
/// assert_eq!(buffer.cell((0, 0)), Some(&Cell::default()));
/// assert_eq!(buffer.cell((10, 10)), None);
/// ```
#[must_use]
pub fn cell<P: Into<Position>>(&self, position: P) -> Option<&Cell> {
let position = position.into();
let index = self.index_of_opt(position)?;
self.content.get(index)
}
/// Returns a mutable reference to the [`Cell`] at the given position or [`None`] if the
/// position is outside the `Buffer`'s area.
///
/// This method accepts any value that can be converted to [`Position`] (e.g. `(x, y)` or
/// `Position::new(x, y)`).
///
/// For a method that panics when the position is outside the buffer instead of returning
/// `None`, use [`Buffer[]`](Self::index_mut).
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
///
/// if let Some(cell) = buffer.cell_mut(Position::new(0, 0)) {
/// cell.set_symbol("A");
/// }
/// if let Some(cell) = buffer.cell_mut((0, 0)) {
/// cell.set_style(Style::default().fg(Color::Red));
/// }
/// ```
#[must_use]
pub fn cell_mut<P: Into<Position>>(&mut self, position: P) -> Option<&mut Cell> {
let position = position.into();
let index = self.index_of_opt(position)?;
self.content.get_mut(index)
}
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
///
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
@@ -114,8 +215,7 @@ impl Buffer {
///
/// ```
/// # use ratatui::prelude::*;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
/// // Global coordinates to the top corner of this buffer's area
/// assert_eq!(buffer.index_of(200, 100), 0);
/// ```
@@ -126,23 +226,37 @@ impl Buffer {
///
/// ```should_panic
/// # use ratatui::prelude::*;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
/// // starts at (200, 100).
/// buffer.index_of(0, 0); // Panics
/// ```
#[track_caller]
#[must_use]
pub fn index_of(&self, x: u16, y: u16) -> usize {
debug_assert!(
x >= self.area.left()
&& x < self.area.right()
&& y >= self.area.top()
&& y < self.area.bottom(),
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
self.area
);
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
self.index_of_opt(Position { x, y }).unwrap_or_else(|| {
panic!(
"index outside of buffer: the area is {area:?} but index is ({x}, {y})",
area = self.area,
)
})
}
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
///
/// Returns `None` if the given coordinates are outside of the Buffer's area.
///
/// Note that this is private because of <https://github.com/ratatui/ratatui/issues/1122>
#[must_use]
const fn index_of_opt(&self, position: Position) -> Option<usize> {
let area = self.area;
if !area.contains(position) {
return None;
}
// remove offset
let y = position.y - self.area.y;
let x = position.x - self.area.x;
Some((y * self.area.width + x) as usize)
}
/// Returns the (global) coordinates of a cell given its index
@@ -170,6 +284,7 @@ impl Buffer {
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
/// buffer.pos_of(100); // Panics
/// ```
#[must_use]
pub fn pos_of(&self, i: usize) -> (u16, u16) {
debug_assert!(
i < self.content.len(),
@@ -192,7 +307,7 @@ impl Buffer {
}
/// 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. Skips zero-width graphemes and control characters.
///
/// Use [`Buffer::set_string`] when the maximum amount of characters can be printed.
pub fn set_stringn<T, S>(
@@ -210,6 +325,7 @@ impl Buffer {
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)
.filter(|symbol| !symbol.contains(|char: char| char.is_control()))
.map(|symbol| (symbol, symbol.width() as u16))
.filter(|(_symbol, width)| *width > 0)
.map_while(|(symbol, width)| {
@@ -218,12 +334,12 @@ impl Buffer {
});
let style = style.into();
for (symbol, width) in graphemes {
self.get_mut(x, y).set_symbol(symbol).set_style(style);
self[(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),
while x < next_symbol {
self.get_mut(x, y).reset();
self[(x, y)].reset();
x += 1;
}
}
@@ -266,7 +382,7 @@ impl Buffer {
let area = self.area.intersection(area);
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
self.get_mut(x, y).set_style(style);
self[(x, y)].set_style(style);
}
}
}
@@ -278,22 +394,22 @@ impl Buffer {
if self.content.len() > length {
self.content.truncate(length);
} else {
self.content.resize(length, Cell::default());
self.content.resize(length, Cell::EMPTY);
}
self.area = area;
}
/// Reset all cells in the buffer
pub fn reset(&mut self) {
for c in &mut self.content {
c.reset();
for cell in &mut self.content {
cell.reset();
}
}
/// Merge an other buffer into this one
pub fn merge(&mut self, other: &Self) {
let area = self.area.union(other.area);
self.content.resize(area.area() as usize, Cell::default());
self.content.resize(area.area() as usize, Cell::EMPTY);
// Move original content to the appropriate space
let size = self.area.area() as usize;
@@ -303,7 +419,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::default();
self.content[i].reset();
}
}
@@ -372,6 +488,60 @@ impl Buffer {
}
}
impl<P: Into<Position>> Index<P> for Buffer {
type Output = Cell;
/// Returns a reference to the [`Cell`] at the given position.
///
/// This method accepts any value that can be converted to [`Position`] (e.g. `(x, y)` or
/// `Position::new(x, y)`).
///
/// # Panics
///
/// May panic if the given position is outside the buffer's area. For a method that returns
/// `None` instead of panicking, use [`Buffer::cell`](Self::cell).
///
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// let buf = Buffer::empty(Rect::new(0, 0, 10, 10));
/// let cell = &buf[(0, 0)];
/// let cell = &buf[Position::new(0, 0)];
/// ```
fn index(&self, position: P) -> &Self::Output {
let position = position.into();
let index = self.index_of(position.x, position.y);
&self.content[index]
}
}
impl<P: Into<Position>> IndexMut<P> for Buffer {
/// Returns a mutable reference to the [`Cell`] at the given position.
///
/// This method accepts any value that can be converted to [`Position`] (e.g. `(x, y)` or
/// `Position::new(x, y)`).
///
/// # Panics
///
/// May panic if the given position is outside the buffer's area. For a method that returns
/// `None` instead of panicking, use [`Buffer::cell_mut`](Self::cell_mut).
///
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
/// buf[(0, 0)].set_symbol("A");
/// buf[Position::new(0, 0)].set_symbol("B");
/// ```
fn index_mut(&mut self, position: P) -> &mut Self::Output {
let position = position.into();
let index = self.index_of(position.x, position.y);
&mut self.content[index]
}
}
impl fmt::Debug for Buffer {
/// Writes a debug representation of the buffer to the given formatter.
///
@@ -453,12 +623,6 @@ mod tests {
use super::*;
fn cell(s: &str) -> Cell {
let mut cell = Cell::default();
cell.set_symbol(s);
cell
}
#[test]
fn debug_empty_buffer() {
let buffer = Buffer::empty(Rect::ZERO);
@@ -559,17 +723,90 @@ mod tests {
let buf = Buffer::empty(rect);
// There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
buf.pos_of(100);
let _ = buf.pos_of(100);
}
#[rstest]
#[case::left(9, 10)]
#[case::top(10, 9)]
#[case::right(20, 10)]
#[case::bottom(10, 20)]
#[should_panic(
expected = "index outside of buffer: the area is Rect { x: 10, y: 10, width: 10, height: 10 } but index is"
)]
fn index_of_panics_on_out_of_bounds(#[case] x: u16, #[case] y: u16) {
let _ = Buffer::empty(Rect::new(10, 10, 10, 10)).index_of(x, y);
}
#[test]
#[should_panic(expected = "outside the buffer")]
fn index_of_panics_on_out_of_bounds() {
let rect = Rect::new(0, 0, 10, 10);
let buf = Buffer::empty(rect);
fn test_cell() {
let buf = Buffer::with_lines(["Hello", "World"]);
// width is 10; zero-indexed means that 10 would be the 11th cell.
buf.index_of(10, 0);
let mut expected = Cell::default();
expected.set_symbol("H");
assert_eq!(buf.cell((0, 0)), Some(&expected));
assert_eq!(buf.cell((10, 10)), None);
assert_eq!(buf.cell(Position::new(0, 0)), Some(&expected));
assert_eq!(buf.cell(Position::new(10, 10)), None);
}
#[test]
fn test_cell_mut() {
let mut buf = Buffer::with_lines(["Hello", "World"]);
let mut expected = Cell::default();
expected.set_symbol("H");
assert_eq!(buf.cell_mut((0, 0)), Some(&mut expected));
assert_eq!(buf.cell_mut((10, 10)), None);
assert_eq!(buf.cell_mut(Position::new(0, 0)), Some(&mut expected));
assert_eq!(buf.cell_mut(Position::new(10, 10)), None);
}
#[test]
fn index() {
let buf = Buffer::with_lines(["Hello", "World"]);
let mut expected = Cell::default();
expected.set_symbol("H");
assert_eq!(buf[(0, 0)], expected);
}
#[rstest]
#[case::left(9, 10)]
#[case::top(10, 9)]
#[case::right(20, 10)]
#[case::bottom(10, 20)]
#[should_panic(
expected = "index outside of buffer: the area is Rect { x: 10, y: 10, width: 10, height: 10 } but index is"
)]
fn index_out_of_bounds_panics(#[case] x: u16, #[case] y: u16) {
let rect = Rect::new(10, 10, 10, 10);
let buf = Buffer::empty(rect);
let _ = buf[(x, y)];
}
#[test]
fn index_mut() {
let mut buf = Buffer::with_lines(["Cat", "Dog"]);
buf[(0, 0)].set_symbol("B");
buf[Position::new(0, 1)].set_symbol("L");
assert_eq!(buf, Buffer::with_lines(["Bat", "Log"]));
}
#[rstest]
#[case::left(9, 10)]
#[case::top(10, 9)]
#[case::right(20, 10)]
#[case::bottom(10, 20)]
#[should_panic(
expected = "index outside of buffer: the area is Rect { x: 10, y: 10, width: 10, height: 10 } but index is"
)]
fn index_mut_out_of_bounds_panics(#[case] x: u16, #[case] y: u16) {
let mut buf = Buffer::empty(Rect::new(10, 10, 10, 10));
buf[(x, y)].set_symbol("A");
}
#[test]
@@ -615,16 +852,18 @@ mod tests {
#[test]
fn set_string_zero_width() {
assert_eq!("\u{200B}".width(), 0);
let area = Rect::new(0, 0, 1, 1);
let mut buffer = Buffer::empty(area);
// Leading grapheme with zero width
let s = "\u{1}a";
let s = "\u{200B}a";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(["a"]));
// Trailing grapheme with zero with
let s = "a\u{1}";
let s = "a\u{200B}";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(["a"]));
}
@@ -749,14 +988,14 @@ mod tests {
let prev = Buffer::empty(area);
let next = Buffer::empty(area);
let diff = prev.diff(&next);
assert_eq!(diff, vec![]);
assert_eq!(diff, []);
}
#[test]
fn diff_empty_filled() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::empty(area);
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
let next = Buffer::filled(area, Cell::new("a"));
let diff = prev.diff(&next);
assert_eq!(diff.len(), 40 * 40);
}
@@ -764,10 +1003,10 @@ mod tests {
#[test]
fn diff_filled_filled() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
let prev = Buffer::filled(area, Cell::new("a"));
let next = Buffer::filled(area, Cell::new("a"));
let diff = prev.diff(&next);
assert_eq!(diff, vec![]);
assert_eq!(diff, []);
}
#[test]
@@ -789,22 +1028,23 @@ mod tests {
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![
(2, 1, &cell("I")),
(3, 1, &cell("T")),
(4, 1, &cell("L")),
(5, 1, &cell("E")),
[
(2, 1, &Cell::new("I")),
(3, 1, &Cell::new("T")),
(4, 1, &Cell::new("L")),
(5, 1, &Cell::new("E")),
]
);
}
#[test]
#[rustfmt::skip]
fn diff_multi_width() {
#[rustfmt::skip]
let prev = Buffer::with_lines([
"┌Title─┐ ",
"└──────┘ ",
]);
#[rustfmt::skip]
let next = Buffer::with_lines([
"┌称号──┐ ",
"└──────┘ ",
@@ -812,12 +1052,12 @@ mod tests {
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![
(1, 0, &cell("")),
[
(1, 0, &Cell::new("")),
// Skipped "i"
(3, 0, &cell("")),
(3, 0, &Cell::new("")),
// Skipped "l"
(5, 0, &cell("")),
(5, 0, &Cell::new("")),
]
);
}
@@ -830,7 +1070,11 @@ mod tests {
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![(1, 0, &cell("")), (2, 0, &cell("")), (4, 0, &cell("")),]
[
(1, 0, &Cell::new("")),
(2, 0, &Cell::new("")),
(4, 0, &Cell::new("")),
]
);
}
@@ -843,59 +1087,25 @@ mod tests {
}
let diff = prev.diff(&next);
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
assert_eq!(diff, [(0, 0, &Cell::new("4"))],);
}
#[test]
fn merge() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 2,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
#[rstest]
#[case(Rect::new(0, 0, 2, 2), Rect::new(0, 2, 2, 2), ["11", "11", "22", "22"])]
#[case(Rect::new(2, 2, 2, 2), Rect::new(0, 0, 2, 2), ["22 ", "22 ", " 11", " 11"])]
fn merge<'line, Lines>(#[case] one: Rect, #[case] two: Rect, #[case] expected: Lines)
where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let mut one = Buffer::filled(one, Cell::new("1"));
let two = Buffer::filled(two, Cell::new("2"));
one.merge(&two);
assert_eq!(one, Buffer::with_lines(["11", "11", "22", "22"]));
assert_eq!(one, Buffer::with_lines(expected));
}
#[test]
fn merge2() {
let mut one = Buffer::filled(
Rect {
x: 2,
y: 2,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_eq!(one, Buffer::with_lines(["22 ", "22 ", " 11", " 11"]));
}
#[test]
fn merge3() {
fn merge_with_offset() {
let mut one = Buffer::filled(
Rect {
x: 3,
@@ -903,7 +1113,7 @@ mod tests {
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
Cell::new("1"),
);
let two = Buffer::filled(
Rect {
@@ -912,67 +1122,48 @@ mod tests {
width: 3,
height: 4,
},
Cell::default().set_symbol("2"),
Cell::new("2"),
);
one.merge(&two);
let mut merged = Buffer::with_lines(["222 ", "222 ", "2221", "2221"]);
merged.area = Rect {
let mut expected = Buffer::with_lines(["222 ", "222 ", "2221", "2221"]);
expected.area = Rect {
x: 1,
y: 1,
width: 4,
height: 4,
};
assert_eq!(one, merged);
assert_eq!(one, expected);
}
#[test]
fn merge_skip() {
let mut one = Buffer::filled(
Rect {
#[rstest]
#[case(false, true, [false, false, true, true, true, true])]
#[case(true, false, [true, true, false, false, false, false])]
fn merge_skip(#[case] skip_one: bool, #[case] skip_two: bool, #[case] expected: [bool; 6]) {
let mut one = {
let area = Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
};
let mut cell = Cell::new("1");
cell.skip = skip_one;
Buffer::filled(area, cell)
};
let two = {
let area = Rect {
x: 0,
y: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2").set_skip(true),
);
};
let mut cell = Cell::new("2");
cell.skip = skip_two;
Buffer::filled(area, cell)
};
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![false, false, true, true, true, true]);
}
#[test]
fn merge_skip2() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1").set_skip(true),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![true, true, false, false, false, false]);
let skipped = one.content().iter().map(|c| c.skip).collect::<Vec<_>>();
assert_eq!(skipped, expected);
}
#[test]
@@ -983,4 +1174,74 @@ mod tests {
buf.set_string(0, 1, "bar", Style::new().blue());
assert_eq!(buf, Buffer::with_lines(["foo".red(), "bar".blue()]));
}
#[test]
fn control_sequence_rendered_full() {
let text = "I \x1b[0;36mwas\x1b[0m here!";
let mut buffer = Buffer::filled(Rect::new(0, 0, 25, 3), Cell::new("x"));
buffer.set_string(1, 1, text, Style::new());
let expected = Buffer::with_lines([
"xxxxxxxxxxxxxxxxxxxxxxxxx",
"xI [0;36mwas[0m here!xxxx",
"xxxxxxxxxxxxxxxxxxxxxxxxx",
]);
assert_eq!(buffer, expected);
}
#[test]
fn control_sequence_rendered_partially() {
let text = "I \x1b[0;36mwas\x1b[0m here!";
let mut buffer = Buffer::filled(Rect::new(0, 0, 11, 3), Cell::new("x"));
buffer.set_string(1, 1, text, Style::new());
#[rustfmt::skip]
let expected = Buffer::with_lines([
"xxxxxxxxxxx",
"xI [0;36mwa",
"xxxxxxxxxxx",
]);
assert_eq!(buffer, expected);
}
/// Emojis normally contain various characters which should stay part of the Emoji.
/// This should work fine by utilizing unicode_segmentation but a testcase is probably helpful
/// due to the nature of never perfect Unicode implementations and all of its quirks.
#[rstest]
// Shrug without gender or skintone. Has a width of 2 like all emojis have.
#[case::shrug("🤷", "🤷xxxxx")]
// Technically this is a (brown) bear, a zero-width joiner and a snowflake
// As it is joined its a single emoji and should therefore have a width of 2.
// It's correctly detected as a single grapheme but it's width is 4 for some reason
#[case::polarbear("🐻‍❄️", "🐻xxx")]
// Technically this is an eye, a zero-width joiner and a speech bubble
// Both eye and speech bubble include a 'display as emoji' variation selector
#[case::eye_speechbubble("👁️‍🗨️", "👁🗨xxx")]
fn renders_emoji(#[case] input: &str, #[case] expected: &str) {
use unicode_width::UnicodeWidthChar;
dbg!(input);
dbg!(input.len());
dbg!(input
.graphemes(true)
.map(|symbol| (symbol, symbol.escape_unicode().to_string(), symbol.width()))
.collect::<Vec<_>>());
dbg!(input
.chars()
.map(|char| (
char,
char.escape_unicode().to_string(),
char.width(),
char.is_control()
))
.collect::<Vec<_>>());
let mut buffer = Buffer::filled(Rect::new(0, 0, 7, 1), Cell::new("x"));
buffer.set_string(0, 0, input, Style::new());
let expected = Buffer::with_lines([expected]);
assert_eq!(buffer, expected);
}
}

View File

@@ -0,0 +1,297 @@
use compact_str::CompactString;
use crate::prelude::*;
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cell {
/// The string to be drawn in the cell.
///
/// This accepts unicode grapheme clusters which might take up more than one cell.
///
/// This is a [`CompactString`] which is a wrapper around [`String`] that uses a small inline
/// buffer for short strings.
///
/// See <https://github.com/ratatui/ratatui/pull/601> for more information.
symbol: CompactString,
/// The foreground color of the cell.
pub fg: Color,
/// The background color of the cell.
pub bg: Color,
/// The underline color of the cell.
#[cfg(feature = "underline-color")]
pub underline_color: Color,
/// The modifier of the cell.
pub modifier: Modifier,
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
pub skip: bool,
}
impl Cell {
/// An empty `Cell`
pub const EMPTY: Self = Self::new(" ");
/// Creates a new `Cell` with the given symbol.
///
/// This works at compile time and puts the symbol onto the stack. Fails to build when the
/// symbol doesnt fit onto the stack and requires to be placed on the heap. Use
/// `Self::default().set_symbol()` in that case. See [`CompactString::const_new`] for more
/// details on this.
pub const fn new(symbol: &'static str) -> Self {
Self {
symbol: CompactString::const_new(symbol),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
}
/// Gets the symbol of the cell.
#[must_use]
pub fn symbol(&self) -> &str {
self.symbol.as_str()
}
/// Sets the symbol of the cell.
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol = CompactString::new(symbol);
self
}
/// Appends a symbol to the cell.
///
/// This is particularly useful for adding zero-width characters to the cell.
pub(crate) fn append_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol.push_str(symbol);
self
}
/// Sets the symbol of the cell to a single character.
pub fn set_char(&mut self, ch: char) -> &mut Self {
let mut buf = [0; 4];
self.symbol = CompactString::new(ch.encode_utf8(&mut buf));
self
}
/// Sets the foreground color of the cell.
pub fn set_fg(&mut self, color: Color) -> &mut Self {
self.fg = color;
self
}
/// Sets the background color of the cell.
pub fn set_bg(&mut self, color: Color) -> &mut Self {
self.bg = color;
self
}
/// Sets the style of the cell.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Self {
let style = style.into();
if let Some(c) = style.fg {
self.fg = c;
}
if let Some(c) = style.bg {
self.bg = c;
}
#[cfg(feature = "underline-color")]
if let Some(c) = style.underline_color {
self.underline_color = c;
}
self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
self
}
/// Returns the style of the cell.
#[must_use]
pub const fn style(&self) -> Style {
Style {
fg: Some(self.fg),
bg: Some(self.bg),
#[cfg(feature = "underline-color")]
underline_color: Some(self.underline_color),
add_modifier: self.modifier,
sub_modifier: Modifier::empty(),
}
}
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
///
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
pub fn set_skip(&mut self, skip: bool) -> &mut Self {
self.skip = skip;
self
}
/// Resets the cell to the empty state.
pub fn reset(&mut self) {
self.symbol = CompactString::const_new(" ");
self.fg = Color::Reset;
self.bg = Color::Reset;
#[cfg(feature = "underline-color")]
{
self.underline_color = Color::Reset;
}
self.modifier = Modifier::empty();
self.skip = false;
}
}
impl Default for Cell {
fn default() -> Self {
Self::EMPTY
}
}
impl From<char> for Cell {
fn from(ch: char) -> Self {
let mut cell = Self::EMPTY;
cell.set_char(ch);
cell
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
let cell = Cell::new("");
assert_eq!(
cell,
Cell {
symbol: CompactString::const_new(""),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
);
}
#[test]
fn empty() {
let cell = Cell::EMPTY;
assert_eq!(cell.symbol(), " ");
}
#[test]
fn set_symbol() {
let mut cell = Cell::EMPTY;
cell.set_symbol(""); // Multi-byte character
assert_eq!(cell.symbol(), "");
cell.set_symbol("👨‍👩‍👧‍👦"); // Multiple code units combined with ZWJ
assert_eq!(cell.symbol(), "👨‍👩‍👧‍👦");
}
#[test]
fn append_symbol() {
let mut cell = Cell::EMPTY;
cell.set_symbol(""); // Multi-byte character
cell.append_symbol("\u{200B}"); // zero-width space
assert_eq!(cell.symbol(), "\u{200B}");
}
#[test]
fn set_char() {
let mut cell = Cell::EMPTY;
cell.set_char('あ'); // Multi-byte character
assert_eq!(cell.symbol(), "");
}
#[test]
fn set_fg() {
let mut cell = Cell::EMPTY;
cell.set_fg(Color::Red);
assert_eq!(cell.fg, Color::Red);
}
#[test]
fn set_bg() {
let mut cell = Cell::EMPTY;
cell.set_bg(Color::Red);
assert_eq!(cell.bg, Color::Red);
}
#[test]
fn set_style() {
let mut cell = Cell::EMPTY;
cell.set_style(Style::new().fg(Color::Red).bg(Color::Blue));
assert_eq!(cell.fg, Color::Red);
assert_eq!(cell.bg, Color::Blue);
}
#[test]
fn set_skip() {
let mut cell = Cell::EMPTY;
cell.set_skip(true);
assert!(cell.skip);
}
#[test]
fn reset() {
let mut cell = Cell::EMPTY;
cell.set_symbol("");
cell.set_fg(Color::Red);
cell.set_bg(Color::Blue);
cell.set_skip(true);
cell.reset();
assert_eq!(cell.symbol(), " ");
assert_eq!(cell.fg, Color::Reset);
assert_eq!(cell.bg, Color::Reset);
assert!(!cell.skip);
}
#[test]
fn style() {
let cell = Cell::EMPTY;
assert_eq!(
cell.style(),
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
#[cfg(feature = "underline-color")]
underline_color: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
);
}
#[test]
fn default() {
let cell = Cell::default();
assert_eq!(cell.symbol(), " ");
}
#[test]
fn cell_eq() {
let cell1 = Cell::new("");
let cell2 = Cell::new("");
assert_eq!(cell1, cell2);
}
#[test]
fn cell_ne() {
let cell1 = Cell::new("");
let cell2 = Cell::new("");
assert_ne!(cell1, cell2);
}
}

View File

@@ -2,10 +2,8 @@
mod alignment;
mod constraint;
mod corner;
mod direction;
mod flex;
#[allow(clippy::module_inception)]
mod layout;
mod margin;
mod position;
@@ -14,7 +12,6 @@ mod size;
pub use alignment::Alignment;
pub use constraint::Constraint;
pub use corner::Corner;
pub use direction::Direction;
pub use flex::Flex;
pub use layout::Layout;

View File

@@ -1,6 +1,5 @@
use std::fmt;
use itertools::Itertools;
use strum::EnumIs;
/// A constraint that defines the size of a layout element.
@@ -117,8 +116,12 @@ pub enum Constraint {
/// Applies a percentage of the available space to the element
///
/// Converts the given percentage to a floating-point value and multiplies that with area.
/// This value is rounded back to a integer as part of the layout split calculation.
/// Converts the given percentage to a floating-point value and multiplies that with area. This
/// value is rounded back to a integer as part of the layout split calculation.
///
/// **Note**: As this value only accepts a `u16`, certain percentages that cannot be
/// represented exactly (e.g. 1/3) are not possible. You might want to use
/// [`Constraint::Ratio`] or [`Constraint::Fill`] in such cases.
///
/// # Examples
///
@@ -229,7 +232,7 @@ impl Constraint {
where
T: IntoIterator<Item = u16>,
{
lengths.into_iter().map(Self::Length).collect_vec()
lengths.into_iter().map(Self::Length).collect()
}
/// Convert an iterator of ratios into a vector of constraints
@@ -246,10 +249,7 @@ impl Constraint {
where
T: IntoIterator<Item = (u32, u32)>,
{
ratios
.into_iter()
.map(|(n, d)| Self::Ratio(n, d))
.collect_vec()
ratios.into_iter().map(|(n, d)| Self::Ratio(n, d)).collect()
}
/// Convert an iterator of percentages into a vector of constraints
@@ -266,7 +266,7 @@ impl Constraint {
where
T: IntoIterator<Item = u16>,
{
percentages.into_iter().map(Self::Percentage).collect_vec()
percentages.into_iter().map(Self::Percentage).collect()
}
/// Convert an iterator of maxes into a vector of constraints
@@ -283,7 +283,7 @@ impl Constraint {
where
T: IntoIterator<Item = u16>,
{
maxes.into_iter().map(Self::Max).collect_vec()
maxes.into_iter().map(Self::Max).collect()
}
/// Convert an iterator of mins into a vector of constraints
@@ -300,7 +300,7 @@ impl Constraint {
where
T: IntoIterator<Item = u16>,
{
mins.into_iter().map(Self::Min).collect_vec()
mins.into_iter().map(Self::Min).collect()
}
/// Convert an iterator of proportional factors into a vector of constraints
@@ -310,17 +310,14 @@ impl Constraint {
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_mins([1, 2, 3]);
/// let constraints = Constraint::from_fills([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_fills<T>(proportional_factors: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
proportional_factors
.into_iter()
.map(Self::Fill)
.collect_vec()
proportional_factors.into_iter().map(Self::Fill).collect()
}
}

View File

@@ -1,4 +1,4 @@
use std::{cell::RefCell, collections::HashMap, iter, num::NonZeroUsize, rc::Rc, sync::OnceLock};
use std::{cell::RefCell, collections::HashMap, iter, num::NonZeroUsize, rc::Rc};
use cassowary::{
strength::REQUIRED,
@@ -37,7 +37,9 @@ type Cache = LruCache<(Rect, Layout), (Segments, Spacers)>;
const FLOAT_PRECISION_MULTIPLIER: f64 = 100.0;
thread_local! {
static LAYOUT_CACHE: OnceLock<RefCell<Cache>> = const { OnceLock::new() };
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(Cache::new(
NonZeroUsize::new(Layout::DEFAULT_CACHE_SIZE).unwrap(),
));
}
/// A layout is a set of constraints that can be applied to a given area to split it into smaller
@@ -105,7 +107,7 @@ thread_local! {
/// example](https://camo.githubusercontent.com/77d22f3313b782a81e5e033ef82814bb48d786d2598699c27f8e757ccee62021/68747470733a2f2f7668732e636861726d2e73682f7668732d315a4e6f4e4c4e6c4c746b4a58706767396e435635652e676966)
///
/// [`cassowary-rs`]: https://crates.io/crates/cassowary
/// [Examples]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
/// [Examples]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Layout {
direction: Direction,
@@ -209,22 +211,9 @@ impl Layout {
/// that subsequent calls with the same parameters are faster. The cache is a `LruCache`, and
/// grows until `cache_size` is reached.
///
/// Returns true if the cell's value was set by this call.
/// Returns false if the cell's value was not set by this call, this means that another thread
/// has set this value or that the cache size is already initialized.
///
/// Note that a custom cache size will be set only if this function:
/// * is called before [`Layout::split()`] otherwise, the cache size is
/// [`Self::DEFAULT_CACHE_SIZE`].
/// * is called for the first time, subsequent calls do not modify the cache size.
pub fn init_cache(cache_size: usize) -> bool {
LAYOUT_CACHE
.with(|c| {
c.set(RefCell::new(LruCache::new(
NonZeroUsize::new(cache_size).unwrap(),
)))
})
.is_ok()
/// By default, the cache size is [`Self::DEFAULT_CACHE_SIZE`].
pub fn init_cache(cache_size: NonZeroUsize) {
LAYOUT_CACHE.with_borrow_mut(|c| c.resize(cache_size));
}
/// Set the direction of the layout.
@@ -439,7 +428,7 @@ impl Layout {
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let area = frame.area();
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = layout.areas(area);
///
@@ -471,7 +460,7 @@ impl Layout {
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let area = frame.area();
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = layout.areas(area);
/// let [before, inbetween, after] = layout.spacers(area);
@@ -571,17 +560,10 @@ impl Layout {
/// );
/// ```
pub fn split_with_spacers(&self, area: Rect) -> (Segments, Spacers) {
LAYOUT_CACHE.with(|c| {
c.get_or_init(|| {
RefCell::new(LruCache::new(
NonZeroUsize::new(Self::DEFAULT_CACHE_SIZE).unwrap(),
))
})
.borrow_mut()
.get_or_insert((area, self.clone()), || {
self.try_split(area).expect("failed to split")
})
.clone()
LAYOUT_CACHE.with_borrow_mut(|c| {
let key = (area, self.clone());
c.get_or_insert(key, || self.try_split(area).expect("failed to split"))
.clone()
})
}
@@ -608,7 +590,7 @@ impl Layout {
// This is equivalent to storing the solver in `Layout` and calling `solver.reset()` here.
let mut solver = Solver::new();
let inner_area = area.inner(&self.margin);
let inner_area = area.inner(self.margin);
let (area_start, area_end) = match self.direction {
Direction::Horizontal => (
f64::from(inner_area.x) * FLOAT_PRECISION_MULTIPLIER,
@@ -1099,37 +1081,14 @@ mod tests {
}
#[test]
fn custom_cache_size() {
assert!(Layout::init_cache(10));
assert!(!Layout::init_cache(15));
LAYOUT_CACHE.with(|c| {
assert_eq!(c.get().unwrap().borrow().cap().get(), 10);
fn cache_size() {
LAYOUT_CACHE.with_borrow(|c| {
assert_eq!(c.cap().get(), Layout::DEFAULT_CACHE_SIZE);
});
}
#[test]
fn default_cache_size() {
let target = Rect {
x: 2,
y: 2,
width: 10,
height: 10,
};
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(10),
Constraint::Max(5),
Constraint::Min(1),
])
.split(target);
assert!(!Layout::init_cache(15));
LAYOUT_CACHE.with(|c| {
assert_eq!(
c.get().unwrap().borrow().cap().get(),
Layout::DEFAULT_CACHE_SIZE
);
Layout::init_cache(NonZeroUsize::new(10).unwrap());
LAYOUT_CACHE.with_borrow(|c| {
assert_eq!(c.cap().get(), 10);
});
}
@@ -1336,7 +1295,7 @@ mod tests {
use crate::{
layout::flex::Flex,
prelude::{Constraint::*, *},
widgets::Paragraph,
// widgets::Paragraph, // TODO
};
/// Test that the given constraints applied to the given area result in the expected layout.
@@ -1356,9 +1315,9 @@ mod tests {
.flex(flex)
.split(area);
let mut buffer = Buffer::empty(area);
for (i, c) in ('a'..='z').take(constraints.len()).enumerate() {
for (c, &area) in ('a'..='z').take(constraints.len()).zip(layout.iter()) {
let s = c.to_string().repeat(area.width as usize);
Paragraph::new(s).render(layout[i], &mut buffer);
// Paragraph::new(s).render(area, &mut buffer); // TODO
}
assert_eq!(buffer, Buffer::with_lines([expected]));
}
@@ -1881,14 +1840,11 @@ mod tests {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Max(5),
Constraint::Min(1),
]
.as_ref(),
)
.constraints([
Constraint::Percentage(10),
Constraint::Max(5),
Constraint::Min(1),
])
.split(target);
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
@@ -1932,7 +1888,7 @@ mod tests {
);
// minimal bug from
// https://github.com/ratatui-org/ratatui/pull/404#issuecomment-1681850644
// https://github.com/ratatui/ratatui/pull/404#issuecomment-1681850644
// TODO: check if this bug is now resolved?
let layout = Layout::default()
.constraints([Min(1), Length(0), Min(1)])

View File

@@ -1,6 +1,7 @@
use std::fmt;
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Margin {
pub horizontal: u16,
pub vertical: u16,

View File

@@ -1,4 +1,6 @@
#![warn(missing_docs)]
use std::fmt;
use crate::layout::Rect;
/// Position in the terminal
@@ -22,6 +24,7 @@ use crate::layout::Rect;
/// let (x, y) = position.into();
/// ```
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Position {
/// The x coordinate of the position
///
@@ -37,6 +40,9 @@ pub struct Position {
}
impl Position {
/// Position at the origin, the top left edge at 0,0
pub const ORIGIN: Self = Self { x: 0, y: 0 };
/// Create a new position
pub const fn new(x: u16, y: u16) -> Self {
Self { x, y }
@@ -61,6 +67,12 @@ impl From<Rect> for Position {
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -94,4 +106,10 @@ mod tests {
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
#[test]
fn to_string() {
let position = Position::new(1, 2);
assert_eq!(position.to_string(), "(1, 2)");
}
}

View File

@@ -32,7 +32,8 @@ pub struct Rect {
/// Positive numbers move to the right/bottom and negative to the left/top.
///
/// See [`Rect::offset`]
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Offset {
/// How much to move on the X axis
pub x: i32,
@@ -58,7 +59,7 @@ impl Rect {
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
/// clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
let max_area = u16::max_value();
let max_area = u16::MAX;
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
let aspect_ratio = f64::from(width) / f64::from(height);
@@ -119,9 +120,8 @@ 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 const 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);
@@ -236,7 +236,7 @@ impl Rect {
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let area = frame.area();
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
/// # }
/// ```
@@ -291,7 +291,7 @@ impl Rect {
/// # use ratatui::prelude::*;
/// fn render(area: Rect, buf: &mut Buffer) {
/// for position in area.positions() {
/// buf.get_mut(position.x, position.y).set_symbol("x");
/// buf[(position.x, position.y)].set_symbol("x");
/// }
/// }
/// ```
@@ -406,7 +406,7 @@ mod tests {
#[test]
fn inner() {
assert_eq!(
Rect::new(1, 2, 3, 4).inner(&Margin::new(1, 2)),
Rect::new(1, 2, 3, 4).inner(Margin::new(1, 2)),
Rect::new(2, 4, 1, 0)
);
}

View File

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

View File

@@ -1,4 +1,6 @@
#![warn(missing_docs)]
use std::fmt;
use crate::prelude::*;
/// A simple size struct
@@ -6,6 +8,7 @@ use crate::prelude::*;
/// The width and height are stored as `u16` values and represent the number of columns and rows
/// respectively.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Size {
/// The width in columns
pub width: u16,
@@ -14,6 +17,9 @@ pub struct Size {
}
impl Size {
/// A zero sized Size
pub const ZERO: Self = Self::new(0, 0);
/// Create a new `Size` struct
pub const fn new(width: u16, height: u16) -> Self {
Self { width, height }
@@ -32,6 +38,12 @@ impl From<Rect> for Size {
}
}
impl fmt::Display for Size {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}x{}", self.width, self.height)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -56,4 +68,9 @@ mod tests {
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
#[test]
fn display() {
assert_eq!(Size::new(10, 20).to_string(), "10x20");
}
}

View File

@@ -1,11 +1,11 @@
//! ![Demo](https://github.com/ratatui-org/ratatui/blob/1d39444e3dea6f309cf9035be2417ac711c1abc9/examples/demo2-destroy.gif?raw=true)
//! ![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true)
//!
//! <div align="center">
//!
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
//! Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
//! [![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
//! [![Matrix Badge]][Matrix]<br>
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
//! Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
//! Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
//! [![Forum Badge]][Forum]<br>
//!
//! [Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
//! [Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
@@ -20,10 +20,10 @@
//!
//! ## Installation
//!
//! Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
//! Add `ratatui` as a dependency to your cargo.toml:
//!
//! ```shell
//! cargo add ratatui crossterm
//! cargo add ratatui
//! ```
//!
//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
@@ -44,6 +44,7 @@
//! ## Other documentation
//!
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
//! - [Ratatui Forum][Forum] - a place to ask questions and discuss the library
//! - [API Docs] - the full API documentation for the library on docs.rs.
//! - [Examples] - a collection of examples that demonstrate how to use the library.
//! - [Contributing] - Please read this if you are interested in contributing to the project.
@@ -101,47 +102,28 @@
//! ### Example
//!
//! ```rust,no_run
//! use std::io::{self, stdout};
//!
//! use crossterm::{
//! event::{self, Event, KeyCode},
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
//! ExecutableCommand,
//! use ratatui::{
//! crossterm::event::{self, Event, KeyCode, KeyEventKind},
//! widgets::{Block, Paragraph},
//! };
//! use ratatui::{prelude::*, widgets::*};
//!
//! fn main() -> io::Result<()> {
//! enable_raw_mode()?;
//! stdout().execute(EnterAlternateScreen)?;
//! let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
//!
//! let mut should_quit = false;
//! while !should_quit {
//! terminal.draw(ui)?;
//! should_quit = handle_events()?;
//! }
//!
//! disable_raw_mode()?;
//! stdout().execute(LeaveAlternateScreen)?;
//! Ok(())
//! }
//!
//! fn handle_events() -> io::Result<bool> {
//! if event::poll(std::time::Duration::from_millis(50))? {
//! fn main() -> std::io::Result<()> {
//! let mut terminal = ratatui::init();
//! loop {
//! terminal.draw(|frame| {
//! frame.render_widget(
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
//! frame.area(),
//! );
//! })?;
//! if let Event::Key(key) = event::read()? {
//! if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
//! return Ok(true);
//! if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
//! break;
//! }
//! }
//! }
//! Ok(false)
//! }
//!
//! fn ui(frame: &mut Frame) {
//! frame.render_widget(
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
//! frame.size(),
//! );
//! ratatui::restore();
//! Ok(())
//! }
//! ```
//!
@@ -157,34 +139,27 @@
//! section of the [Ratatui Website] for more info.
//!
//! ```rust,no_run
//! use ratatui::{prelude::*, widgets::*};
//! use ratatui::{
//! layout::{Constraint, Layout},
//! widgets::Block,
//! Frame,
//! };
//!
//! fn ui(frame: &mut Frame) {
//! let main_layout = Layout::new(
//! Direction::Vertical,
//! [
//! Constraint::Length(1),
//! Constraint::Min(0),
//! Constraint::Length(1),
//! ],
//! )
//! .split(frame.size());
//! frame.render_widget(
//! Block::new().borders(Borders::TOP).title("Title Bar"),
//! main_layout[0],
//! );
//! frame.render_widget(
//! Block::new().borders(Borders::TOP).title("Status Bar"),
//! main_layout[2],
//! );
//! fn draw(frame: &mut Frame) {
//! let [title_area, main_area, status_area] = Layout::vertical([
//! Constraint::Length(1),
//! Constraint::Min(0),
//! Constraint::Length(1),
//! ])
//! .areas(frame.area());
//! let [left_area, right_area] =
//! Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
//! .areas(main_area);
//!
//! let inner_layout = Layout::new(
//! Direction::Horizontal,
//! [Constraint::Percentage(50), Constraint::Percentage(50)],
//! )
//! .split(main_layout[1]);
//! frame.render_widget(Block::bordered().title("Left"), inner_layout[0]);
//! frame.render_widget(Block::bordered().title("Right"), inner_layout[1]);
//! frame.render_widget(Block::bordered().title("Title Bar"), title_area);
//! frame.render_widget(Block::bordered().title("Status Bar"), status_area);
//! frame.render_widget(Block::bordered().title("Left"), left_area);
//! frame.render_widget(Block::bordered().title("Right"), right_area);
//! }
//! ```
//!
@@ -205,48 +180,41 @@
//! [Ratatui Website] for more info.
//!
//! ```rust,no_run
//! use ratatui::{prelude::*, widgets::*};
//! use ratatui::{
//! layout::{Constraint, Layout},
//! style::{Color, Modifier, Style, Stylize},
//! text::{Line, Span},
//! widgets::{Block, Paragraph},
//! Frame,
//! };
//!
//! fn ui(frame: &mut Frame) {
//! let areas = Layout::new(
//! Direction::Vertical,
//! [
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Min(0),
//! ],
//! )
//! .split(frame.size());
//! fn draw(frame: &mut Frame) {
//! let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
//!
//! let span1 = Span::raw("Hello ");
//! let span2 = Span::styled(
//! "World",
//! Style::new()
//! .fg(Color::Green)
//! .bg(Color::White)
//! .add_modifier(Modifier::BOLD),
//! );
//! let span3 = "!".red().on_light_yellow().italic();
//! let line = Line::from(vec![
//! Span::raw("Hello "),
//! Span::styled(
//! "World",
//! Style::new()
//! .fg(Color::Green)
//! .bg(Color::White)
//! .add_modifier(Modifier::BOLD),
//! ),
//! "!".red().on_light_yellow().italic(),
//! ]);
//! frame.render_widget(line, areas[0]);
//!
//! let line = Line::from(vec![span1, span2, span3]);
//! let text: Text = Text::from(vec![line]);
//! // using the short-hand syntax and implicit conversions
//! let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
//! frame.render_widget(paragraph, areas[1]);
//!
//! frame.render_widget(Paragraph::new(text), areas[0]);
//! // or using the short-hand syntax and implicit conversions
//! frame.render_widget(
//! Paragraph::new("Hello World!".red().on_white().bold()),
//! areas[1],
//! );
//! // style the whole widget instead of just the text
//! let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
//! frame.render_widget(paragraph, areas[2]);
//!
//! // to style the whole widget instead of just the text
//! frame.render_widget(
//! Paragraph::new("Hello World!").style(Style::new().red().on_white()),
//! areas[2],
//! );
//! // or using the short-hand syntax
//! frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
//! // use the simpler short-hand syntax
//! let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
//! frame.render_widget(paragraph, areas[3]);
//! }
//! ```
//!
@@ -255,22 +223,6 @@
//! ![docsrs-styling]
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
#![cfg_attr(
feature = "document-features",
doc = "[`CrossTermBackend`]: backend::CrosstermBackend"
)]
#![cfg_attr(
feature = "document-features",
doc = "[`TermionBackend`]: backend::TermionBackend"
)]
#![cfg_attr(
feature = "document-features",
doc = "[`TermwizBackend`]: backend::TermwizBackend"
)]
#![cfg_attr(
feature = "document-features",
doc = "[`calendar`]: widgets::calendar::Monthly"
)]
//!
//! [Ratatui Website]: https://ratatui.rs/
//! [Installation]: https://ratatui.rs/installation/
@@ -282,21 +234,21 @@
//! [Handling Events]: https://ratatui.rs/concepts/event-handling/
//! [Layout]: https://ratatui.rs/how-to/layout/
//! [Styling Text]: https://ratatui.rs/how-to/render/style-text/
//! [templates]: https://github.com/ratatui-org/templates/
//! [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
//! [Report a bug]: https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
//! [Request a Feature]: https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
//! [Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
//! [templates]: https://github.com/ratatui/templates/
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
//! [Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
//! [Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
//! [Create a Pull Request]: https://github.com/ratatui/ratatui/compare
//! [git-cliff]: https://git-cliff.org
//! [Conventional Commits]: https://www.conventionalcommits.org
//! [API Docs]: https://docs.rs/ratatui
//! [Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
//! [Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
//! [Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
//! [Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
//! [Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
//! [Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
//! [FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
//! [docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
//! [docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
//! [docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
//! [docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
//! [docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
//! [docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
//! [`Frame`]: terminal::Frame
//! [`render_widget`]: terminal::Frame::render_widget
//! [`Widget`]: widgets::Widget
@@ -315,43 +267,52 @@
//! [Termion]: https://crates.io/crates/termion
//! [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
//! [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
//! [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
//! [GitHub Sponsors]: https://github.com/sponsors/ratatui
//! [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/ratatui/ci.yml?style=flat-square&logo=github
//! [CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
//! [Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
//! [Codecov]: https://app.codecov.io/gh/ratatui/ratatui
//! [Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
//! [Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
//! [Discord Badge]: 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
//! [Matrix Badge]:
//! https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
//! [Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
//! [Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
//! [Forum]: https://forum.ratatui.rs
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/ratatui-org/ratatui/main/assets/logo.png",
html_favicon_url = "https://raw.githubusercontent.com/ratatui-org/ratatui/main/assets/favicon.ico"
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
)]
/// re-export the `crossterm` crate so that users don't have to add it as a dependency
#[cfg(feature = "crossterm")]
pub use crossterm;
#[cfg(feature = "crossterm")]
pub use terminal::{
init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal,
};
pub use terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
/// re-export the `termion` crate so that users don't have to add it as a dependency
#[cfg(all(not(windows), feature = "termion"))]
pub use termion;
/// re-export the `termwiz` crate so that users don't have to add it as a dependency
#[cfg(feature = "termwiz")]
pub use termwiz;
pub mod backend;
pub mod buffer;
pub mod layout;
pub mod prelude;
pub mod style;
pub mod symbols;
pub mod terminal;
mod terminal;
pub mod text;
pub mod widgets;
#[doc(inline)]
pub use self::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
pub mod prelude;

View File

@@ -0,0 +1,41 @@
//! A prelude for conveniently writing applications using this library.
//!
//! ```rust,no_run
//! use ratatui::prelude::*;
//! ```
//!
//! Aside from the main types that are used in the library, this prelude also re-exports several
//! modules to make it easy to qualify types that would otherwise collide. E.g.:
//!
//! ```rust
//! use ratatui::{prelude::*, widgets::*};
//!
//! #[derive(Debug, Default, PartialEq, Eq)]
//! struct Line;
//!
//! assert_eq!(Line::default(), Line);
//! assert_eq!(text::Line::default(), ratatui::text::Line::from(vec![]));
//! ```
// TODO: re-export the following modules:
// #[cfg(feature = "crossterm")]
// pub use crate::backend::CrosstermBackend;
// #[cfg(all(not(windows), feature = "termion"))]
// pub use crate::backend::TermionBackend;
// #[cfg(feature = "termwiz")]
// pub use crate::backend::TermwizBackend;
pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef};
pub use crate::{
backend::{self, Backend},
buffer::{self, Buffer},
layout::{self, Alignment, Constraint, Direction, Layout, Margin, Position, Rect, Size},
style::{self, Color, Modifier, Style, Stylize},
symbols::{self},
text::{self, Line, Masked, Span, Text},
widgets::{
// block::BlockExt, // TODO
StatefulWidget,
Widget,
},
Frame, Terminal,
};

View File

@@ -71,13 +71,15 @@
use std::fmt;
use bitflags::bitflags;
pub use color::{Color, ParseColorError};
use stylize::ColorDebugKind;
pub use stylize::{Styled, Stylize};
mod color;
mod stylize;
pub use color::{Color, ParseColorError};
pub use stylize::{Styled, Stylize};
pub mod palette;
#[cfg(feature = "palette")]
mod palette_conversion;
mod stylize;
bitflags! {
/// Modifier changes the way a piece of text is displayed.
@@ -179,7 +181,7 @@ impl fmt::Debug for Modifier {
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -190,7 +192,7 @@ impl fmt::Debug for Modifier {
/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED,
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
/// buffer[(0, 0)].style(),
/// );
/// ```
///
@@ -208,7 +210,7 @@ impl fmt::Debug for Modifier {
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -219,10 +221,10 @@ impl fmt::Debug for Modifier {
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
/// buffer[(0, 0)].style(),
/// );
/// ```
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Option<Color>,
@@ -233,9 +235,52 @@ pub struct Style {
pub sub_modifier: Modifier,
}
impl Default for Style {
fn default() -> Self {
Self::new()
/// A custom debug implementation that prints only the fields that are not the default, and unwraps
/// the `Option`s.
impl fmt::Debug for Style {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("Style::new()")?;
if let Some(fg) = self.fg {
fg.stylize_debug(ColorDebugKind::Foreground).fmt(f)?;
}
if let Some(bg) = self.bg {
bg.stylize_debug(ColorDebugKind::Background).fmt(f)?;
}
#[cfg(feature = "underline-color")]
if let Some(underline_color) = self.underline_color {
underline_color
.stylize_debug(ColorDebugKind::Underline)
.fmt(f)?;
}
for modifier in self.add_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".bold()")?,
Modifier::DIM => f.write_str(".dim()")?,
Modifier::ITALIC => f.write_str(".italic()")?,
Modifier::UNDERLINED => f.write_str(".underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".rapid_blink()")?,
Modifier::REVERSED => f.write_str(".reversed()")?,
Modifier::HIDDEN => f.write_str(".hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".crossed_out()")?,
_ => f.write_fmt(format_args!(".add_modifier(Modifier::{modifier:?})"))?,
}
}
for modifier in self.sub_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".not_bold()")?,
Modifier::DIM => f.write_str(".not_dim()")?,
Modifier::ITALIC => f.write_str(".not_italic()")?,
Modifier::UNDERLINED => f.write_str(".not_underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".not_slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".not_rapid_blink()")?,
Modifier::REVERSED => f.write_str(".not_reversed()")?,
Modifier::HIDDEN => f.write_str(".not_hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".not_crossed_out()")?,
_ => f.write_fmt(format_args!(".remove_modifier(Modifier::{modifier:?})"))?,
}
}
Ok(())
}
}
@@ -554,6 +599,16 @@ mod tests {
use super::*;
#[rstest]
#[case(Style::new(), "Style::new()")]
#[case(Style::new().red(), "Style::new().red()")]
#[case(Style::new().on_blue(), "Style::new().on_blue()")]
#[case(Style::new().bold(), "Style::new().bold()")]
#[case(Style::new().not_italic(), "Style::new().not_italic()")]
fn debug(#[case] style: Style, #[case] expected: &'static str) {
assert_eq!(format!("{style:?}"), expected);
}
#[test]
fn combined_patch_gives_same_result_as_individual_patch() {
let styles = [
@@ -600,9 +655,9 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
for m in mods {
buffer.get_mut(0, 0).set_style(Style::reset());
buffer.get_mut(0, 0).set_style(Style::new().add_modifier(m));
let style = buffer.get(0, 0).style();
buffer[(0, 0)].set_style(Style::reset());
buffer[(0, 0)].set_style(Style::new().add_modifier(m));
let style = buffer[(0, 0)].style();
assert!(style.add_modifier.contains(m));
assert!(!style.sub_modifier.contains(m));
}
@@ -652,151 +707,80 @@ mod tests {
);
}
#[allow(clippy::too_many_lines)]
#[rstest]
#[case(Style::new().black(), Color::Black)]
#[case(Style::new().red(), Color::Red)]
#[case(Style::new().green(), Color::Green)]
#[case(Style::new().yellow(), Color::Yellow)]
#[case(Style::new().blue(), Color::Blue)]
#[case(Style::new().magenta(), Color::Magenta)]
#[case(Style::new().cyan(), Color::Cyan)]
#[case(Style::new().white(), Color::White)]
#[case(Style::new().gray(), Color::Gray)]
#[case(Style::new().dark_gray(), Color::DarkGray)]
#[case(Style::new().light_red(), Color::LightRed)]
#[case(Style::new().light_green(), Color::LightGreen)]
#[case(Style::new().light_yellow(), Color::LightYellow)]
#[case(Style::new().light_blue(), Color::LightBlue)]
#[case(Style::new().light_magenta(), Color::LightMagenta)]
#[case(Style::new().light_cyan(), Color::LightCyan)]
#[case(Style::new().white(), Color::White)]
fn fg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
assert_eq!(stylized, Style::new().fg(expected));
}
#[rstest]
#[case(Style::new().on_black(), Color::Black)]
#[case(Style::new().on_red(), Color::Red)]
#[case(Style::new().on_green(), Color::Green)]
#[case(Style::new().on_yellow(), Color::Yellow)]
#[case(Style::new().on_blue(), Color::Blue)]
#[case(Style::new().on_magenta(), Color::Magenta)]
#[case(Style::new().on_cyan(), Color::Cyan)]
#[case(Style::new().on_white(), Color::White)]
#[case(Style::new().on_gray(), Color::Gray)]
#[case(Style::new().on_dark_gray(), Color::DarkGray)]
#[case(Style::new().on_light_red(), Color::LightRed)]
#[case(Style::new().on_light_green(), Color::LightGreen)]
#[case(Style::new().on_light_yellow(), Color::LightYellow)]
#[case(Style::new().on_light_blue(), Color::LightBlue)]
#[case(Style::new().on_light_magenta(), Color::LightMagenta)]
#[case(Style::new().on_light_cyan(), Color::LightCyan)]
#[case(Style::new().on_white(), Color::White)]
fn bg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
assert_eq!(stylized, Style::new().bg(expected));
}
#[rstest]
#[case(Style::new().bold(), Modifier::BOLD)]
#[case(Style::new().dim(), Modifier::DIM)]
#[case(Style::new().italic(), Modifier::ITALIC)]
#[case(Style::new().underlined(), Modifier::UNDERLINED)]
#[case(Style::new().slow_blink(), Modifier::SLOW_BLINK)]
#[case(Style::new().rapid_blink(), Modifier::RAPID_BLINK)]
#[case(Style::new().reversed(), Modifier::REVERSED)]
#[case(Style::new().hidden(), Modifier::HIDDEN)]
#[case(Style::new().crossed_out(), Modifier::CROSSED_OUT)]
fn add_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
assert_eq!(stylized, Style::new().add_modifier(expected));
}
#[rstest]
#[case(Style::new().not_bold(), Modifier::BOLD)]
#[case(Style::new().not_dim(), Modifier::DIM)]
#[case(Style::new().not_italic(), Modifier::ITALIC)]
#[case(Style::new().not_underlined(), Modifier::UNDERLINED)]
#[case(Style::new().not_slow_blink(), Modifier::SLOW_BLINK)]
#[case(Style::new().not_rapid_blink(), Modifier::RAPID_BLINK)]
#[case(Style::new().not_reversed(), Modifier::REVERSED)]
#[case(Style::new().not_hidden(), Modifier::HIDDEN)]
#[case(Style::new().not_crossed_out(), Modifier::CROSSED_OUT)]
fn remove_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
assert_eq!(stylized, Style::new().remove_modifier(expected));
}
#[test]
fn style_can_be_stylized() {
// foreground colors
assert_eq!(Style::new().black(), Style::new().fg(Color::Black));
assert_eq!(Style::new().red(), Style::new().fg(Color::Red));
assert_eq!(Style::new().green(), Style::new().fg(Color::Green));
assert_eq!(Style::new().yellow(), Style::new().fg(Color::Yellow));
assert_eq!(Style::new().blue(), Style::new().fg(Color::Blue));
assert_eq!(Style::new().magenta(), Style::new().fg(Color::Magenta));
assert_eq!(Style::new().cyan(), Style::new().fg(Color::Cyan));
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
assert_eq!(Style::new().gray(), Style::new().fg(Color::Gray));
assert_eq!(Style::new().dark_gray(), Style::new().fg(Color::DarkGray));
assert_eq!(Style::new().light_red(), Style::new().fg(Color::LightRed));
assert_eq!(
Style::new().light_green(),
Style::new().fg(Color::LightGreen)
);
assert_eq!(
Style::new().light_yellow(),
Style::new().fg(Color::LightYellow)
);
assert_eq!(Style::new().light_blue(), Style::new().fg(Color::LightBlue));
assert_eq!(
Style::new().light_magenta(),
Style::new().fg(Color::LightMagenta)
);
assert_eq!(Style::new().light_cyan(), Style::new().fg(Color::LightCyan));
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
// Background colors
assert_eq!(Style::new().on_black(), Style::new().bg(Color::Black));
assert_eq!(Style::new().on_red(), Style::new().bg(Color::Red));
assert_eq!(Style::new().on_green(), Style::new().bg(Color::Green));
assert_eq!(Style::new().on_yellow(), Style::new().bg(Color::Yellow));
assert_eq!(Style::new().on_blue(), Style::new().bg(Color::Blue));
assert_eq!(Style::new().on_magenta(), Style::new().bg(Color::Magenta));
assert_eq!(Style::new().on_cyan(), Style::new().bg(Color::Cyan));
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
assert_eq!(Style::new().on_gray(), Style::new().bg(Color::Gray));
assert_eq!(
Style::new().on_dark_gray(),
Style::new().bg(Color::DarkGray)
);
assert_eq!(
Style::new().on_light_red(),
Style::new().bg(Color::LightRed)
);
assert_eq!(
Style::new().on_light_green(),
Style::new().bg(Color::LightGreen)
);
assert_eq!(
Style::new().on_light_yellow(),
Style::new().bg(Color::LightYellow)
);
assert_eq!(
Style::new().on_light_blue(),
Style::new().bg(Color::LightBlue)
);
assert_eq!(
Style::new().on_light_magenta(),
Style::new().bg(Color::LightMagenta)
);
assert_eq!(
Style::new().on_light_cyan(),
Style::new().bg(Color::LightCyan)
);
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
// Add Modifiers
assert_eq!(
Style::new().bold(),
Style::new().add_modifier(Modifier::BOLD)
);
assert_eq!(Style::new().dim(), Style::new().add_modifier(Modifier::DIM));
assert_eq!(
Style::new().italic(),
Style::new().add_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::new().underlined(),
Style::new().add_modifier(Modifier::UNDERLINED)
);
assert_eq!(
Style::new().slow_blink(),
Style::new().add_modifier(Modifier::SLOW_BLINK)
);
assert_eq!(
Style::new().rapid_blink(),
Style::new().add_modifier(Modifier::RAPID_BLINK)
);
assert_eq!(
Style::new().reversed(),
Style::new().add_modifier(Modifier::REVERSED)
);
assert_eq!(
Style::new().hidden(),
Style::new().add_modifier(Modifier::HIDDEN)
);
assert_eq!(
Style::new().crossed_out(),
Style::new().add_modifier(Modifier::CROSSED_OUT)
);
// Remove Modifiers
assert_eq!(
Style::new().not_bold(),
Style::new().remove_modifier(Modifier::BOLD)
);
assert_eq!(
Style::new().not_dim(),
Style::new().remove_modifier(Modifier::DIM)
);
assert_eq!(
Style::new().not_italic(),
Style::new().remove_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::new().not_underlined(),
Style::new().remove_modifier(Modifier::UNDERLINED)
);
assert_eq!(
Style::new().not_slow_blink(),
Style::new().remove_modifier(Modifier::SLOW_BLINK)
);
assert_eq!(
Style::new().not_rapid_blink(),
Style::new().remove_modifier(Modifier::RAPID_BLINK)
);
assert_eq!(
Style::new().not_reversed(),
Style::new().remove_modifier(Modifier::REVERSED)
);
assert_eq!(
Style::new().not_hidden(),
Style::new().remove_modifier(Modifier::HIDDEN)
);
assert_eq!(
Style::new().not_crossed_out(),
Style::new().remove_modifier(Modifier::CROSSED_OUT)
);
// reset
fn reset_can_be_stylized() {
assert_eq!(Style::new().reset(), Style::reset());
}

View File

@@ -2,6 +2,8 @@
use std::{fmt, str::FromStr};
use crate::style::stylize::{ColorDebug, ColorDebugKind};
/// ANSI Color
///
/// All colors from the [ANSI color table] are supported (though some names are not exactly the
@@ -113,7 +115,7 @@ pub enum Color {
/// If the terminal does not support true color, code using the [`TermwizBackend`] will
/// fallback to the default text color. Crossterm and Termion do not have this capability and
/// the display will be unpredictable (e.g. Terminal.app may display glitched blinking text).
/// See <https://github.com/ratatui-org/ratatui/issues/475> for an example of this problem.
/// See <https://github.com/ratatui/ratatui/issues/475> for an example of this problem.
///
/// See also: <https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit>
///
@@ -313,16 +315,7 @@ impl FromStr for Color {
_ => {
if let Ok(index) = s.parse::<u8>() {
Self::Indexed(index)
} else if let (Ok(r), Ok(g), Ok(b)) = {
if !s.starts_with('#') || s.len() != 7 {
return Err(ParseColorError);
}
(
u8::from_str_radix(&s[1..3], 16),
u8::from_str_radix(&s[3..5], 16),
u8::from_str_radix(&s[5..7], 16),
)
} {
} else if let Some((r, g, b)) = parse_hex_color(s) {
Self::Rgb(r, g, b)
} else {
return Err(ParseColorError);
@@ -333,6 +326,16 @@ impl FromStr for Color {
}
}
fn parse_hex_color(input: &str) -> Option<(u8, u8, u8)> {
if !input.starts_with('#') || input.len() != 7 {
return None;
}
let r = u8::from_str_radix(input.get(1..3)?, 16).ok()?;
let g = u8::from_str_radix(input.get(3..5)?, 16).ok()?;
let b = u8::from_str_radix(input.get(5..7)?, 16).ok()?;
Some((r, g, b))
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
@@ -360,6 +363,10 @@ impl fmt::Display for Color {
}
impl Color {
pub(crate) const fn stylize_debug(self, kind: ColorDebugKind) -> ColorDebug {
ColorDebug { kind, color: self }
}
/// Converts a HSL representation to a `Color::Rgb` instance.
///
/// The `from_hsl` function converts the Hue, Saturation and Lightness values to a
@@ -587,6 +594,7 @@ mod tests {
"abcdef0", // 7 chars is not a color
" bcdefa", // doesn't start with a '#'
"#abcdef00", // too many chars
"#1🦀2", // len 7 but on char boundaries shouldnt panic
"resett", // typo
"lightblackk", // typo
];

View File

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

View File

@@ -1,3 +1,5 @@
use std::fmt;
use paste::paste;
use crate::{
@@ -23,6 +25,75 @@ pub trait Styled {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item;
}
/// A helper struct to make it easy to debug using the `Stylize` method names
pub(crate) struct ColorDebug {
pub kind: ColorDebugKind,
pub color: Color,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub(crate) enum ColorDebugKind {
Foreground,
Background,
#[cfg(feature = "underline-color")]
Underline,
}
impl fmt::Debug for ColorDebug {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(feature = "underline-color")]
let is_underline = self.kind == ColorDebugKind::Underline;
#[cfg(not(feature = "underline-color"))]
let is_underline = false;
if is_underline
|| matches!(
self.color,
Color::Reset | Color::Indexed(_) | Color::Rgb(_, _, _)
)
{
match self.kind {
ColorDebugKind::Foreground => write!(f, ".fg(")?,
ColorDebugKind::Background => write!(f, ".bg(")?,
#[cfg(feature = "underline-color")]
ColorDebugKind::Underline => write!(f, ".underline_color(")?,
}
write!(f, "Color::{:?}", self.color)?;
write!(f, ")")?;
return Ok(());
}
match self.kind {
ColorDebugKind::Foreground => write!(f, ".")?,
ColorDebugKind::Background => write!(f, ".on_")?,
// TODO: .underline_color_xxx is not implemented on Stylize yet, but it should be
#[cfg(feature = "underline-color")]
ColorDebugKind::Underline => {
unreachable!("covered by the first part of the if statement")
}
}
match self.color {
Color::Black => write!(f, "black")?,
Color::Red => write!(f, "red")?,
Color::Green => write!(f, "green")?,
Color::Yellow => write!(f, "yellow")?,
Color::Blue => write!(f, "blue")?,
Color::Magenta => write!(f, "magenta")?,
Color::Cyan => write!(f, "cyan")?,
Color::Gray => write!(f, "gray")?,
Color::DarkGray => write!(f, "dark_gray")?,
Color::LightRed => write!(f, "light_red")?,
Color::LightGreen => write!(f, "light_green")?,
Color::LightYellow => write!(f, "light_yellow")?,
Color::LightBlue => write!(f, "light_blue")?,
Color::LightMagenta => write!(f, "light_magenta")?,
Color::LightCyan => write!(f, "light_cyan")?,
Color::White => write!(f, "white")?,
_ => unreachable!("covered by the first part of the if statement"),
}
write!(f, "()")
}
}
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
/// etc) and one for setting the background color (`on_red()`, `on_blue()`, etc.). Each method sets
/// the color of the style to the corresponding color.
@@ -137,9 +208,9 @@ macro_rules! modifier {
/// ```
pub trait Stylize<'a, T>: Sized {
#[must_use = "`bg` returns the modified style without modifying the original"]
fn bg(self, color: Color) -> T;
fn bg<C: Into<Color>>(self, color: C) -> T;
#[must_use = "`fg` returns the modified style without modifying the original"]
fn fg<S: Into<Color>>(self, color: S) -> T;
fn fg<C: Into<Color>>(self, color: C) -> T;
#[must_use = "`reset` returns the modified style without modifying the original"]
fn reset(self) -> T;
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
@@ -179,12 +250,12 @@ impl<'a, T, U> Stylize<'a, T> for U
where
U: Styled<Item = T>,
{
fn bg(self, color: Color) -> T {
let style = self.style().bg(color);
fn bg<C: Into<Color>>(self, color: C) -> T {
let style = self.style().bg(color.into());
self.set_style(style)
}
fn fg<S: Into<Color>>(self, color: S) -> T {
fn fg<C: Into<Color>>(self, color: C) -> T {
let style = self.style().fg(color.into());
self.set_style(style)
}
@@ -231,6 +302,7 @@ impl Styled for String {
#[cfg(test)]
mod tests {
use itertools::Itertools;
use rstest::rstest;
use super::*;
@@ -346,7 +418,7 @@ mod tests {
// issue as above without the `Styled` trait impl for `String`
let items = [String::from("a"), String::from("b")];
let sss = items.iter().map(|s| format!("{s}{s}").red()).collect_vec();
assert_eq!(sss, vec![Span::from("aa").red(), Span::from("bb").red()]);
assert_eq!(sss, [Span::from("aa").red(), Span::from("bb").red()]);
}
#[test]
@@ -423,4 +495,82 @@ mod tests {
Span::styled("hello", all_modifier_black)
);
}
#[rstest]
#[case(ColorDebugKind::Foreground, Color::Black, ".black()")]
#[case(ColorDebugKind::Foreground, Color::Red, ".red()")]
#[case(ColorDebugKind::Foreground, Color::Green, ".green()")]
#[case(ColorDebugKind::Foreground, Color::Yellow, ".yellow()")]
#[case(ColorDebugKind::Foreground, Color::Blue, ".blue()")]
#[case(ColorDebugKind::Foreground, Color::Magenta, ".magenta()")]
#[case(ColorDebugKind::Foreground, Color::Cyan, ".cyan()")]
#[case(ColorDebugKind::Foreground, Color::Gray, ".gray()")]
#[case(ColorDebugKind::Foreground, Color::DarkGray, ".dark_gray()")]
#[case(ColorDebugKind::Foreground, Color::LightRed, ".light_red()")]
#[case(ColorDebugKind::Foreground, Color::LightGreen, ".light_green()")]
#[case(ColorDebugKind::Foreground, Color::LightYellow, ".light_yellow()")]
#[case(ColorDebugKind::Foreground, Color::LightBlue, ".light_blue()")]
#[case(ColorDebugKind::Foreground, Color::LightMagenta, ".light_magenta()")]
#[case(ColorDebugKind::Foreground, Color::LightCyan, ".light_cyan()")]
#[case(ColorDebugKind::Foreground, Color::White, ".white()")]
#[case(
ColorDebugKind::Foreground,
Color::Indexed(10),
".fg(Color::Indexed(10))"
)]
#[case(
ColorDebugKind::Foreground,
Color::Rgb(255, 0, 0),
".fg(Color::Rgb(255, 0, 0))"
)]
#[case(ColorDebugKind::Background, Color::Black, ".on_black()")]
#[case(ColorDebugKind::Background, Color::Red, ".on_red()")]
#[case(ColorDebugKind::Background, Color::Green, ".on_green()")]
#[case(ColorDebugKind::Background, Color::Yellow, ".on_yellow()")]
#[case(ColorDebugKind::Background, Color::Blue, ".on_blue()")]
#[case(ColorDebugKind::Background, Color::Magenta, ".on_magenta()")]
#[case(ColorDebugKind::Background, Color::Cyan, ".on_cyan()")]
#[case(ColorDebugKind::Background, Color::Gray, ".on_gray()")]
#[case(ColorDebugKind::Background, Color::DarkGray, ".on_dark_gray()")]
#[case(ColorDebugKind::Background, Color::LightRed, ".on_light_red()")]
#[case(ColorDebugKind::Background, Color::LightGreen, ".on_light_green()")]
#[case(ColorDebugKind::Background, Color::LightYellow, ".on_light_yellow()")]
#[case(ColorDebugKind::Background, Color::LightBlue, ".on_light_blue()")]
#[case(ColorDebugKind::Background, Color::LightMagenta, ".on_light_magenta()")]
#[case(ColorDebugKind::Background, Color::LightCyan, ".on_light_cyan()")]
#[case(ColorDebugKind::Background, Color::White, ".on_white()")]
#[case(
ColorDebugKind::Background,
Color::Indexed(10),
".bg(Color::Indexed(10))"
)]
#[case(
ColorDebugKind::Background,
Color::Rgb(255, 0, 0),
".bg(Color::Rgb(255, 0, 0))"
)]
#[cfg(feature = "underline-color")]
#[case(
ColorDebugKind::Underline,
Color::Black,
".underline_color(Color::Black)"
)]
#[cfg(feature = "underline-color")]
#[case(ColorDebugKind::Underline, Color::Red, ".underline_color(Color::Red)")]
#[cfg(feature = "underline-color")]
#[case(
ColorDebugKind::Underline,
Color::Green,
".underline_color(Color::Green)"
)]
#[cfg(feature = "underline-color")]
#[case(
ColorDebugKind::Underline,
Color::Yellow,
".underline_color(Color::Yellow)"
)]
fn stylize_debug(#[case] kind: ColorDebugKind, #[case] color: Color, #[case] expected: &str) {
let debug = color.stylize_debug(kind);
assert_eq!(format!("{debug:?}"), expected);
}
}

228
ratatui-core/src/symbols.rs Normal file
View File

@@ -0,0 +1,228 @@
use strum::{Display, EnumString};
pub mod border;
pub mod line;
pub mod block {
pub const FULL: &str = "";
pub const SEVEN_EIGHTHS: &str = "";
pub const THREE_QUARTERS: &str = "";
pub const FIVE_EIGHTHS: &str = "";
pub const HALF: &str = "";
pub const THREE_EIGHTHS: &str = "";
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
pub three_quarters: &'static str,
pub five_eighths: &'static str,
pub half: &'static str,
pub three_eighths: &'static str,
pub one_quarter: &'static str,
pub one_eighth: &'static str,
pub empty: &'static str,
}
impl Default for Set {
fn default() -> Self {
NINE_LEVELS
}
}
pub const THREE_LEVELS: Set = Set {
full: FULL,
seven_eighths: FULL,
three_quarters: HALF,
five_eighths: HALF,
half: HALF,
three_eighths: HALF,
one_quarter: HALF,
one_eighth: " ",
empty: " ",
};
pub const NINE_LEVELS: Set = Set {
full: FULL,
seven_eighths: SEVEN_EIGHTHS,
three_quarters: THREE_QUARTERS,
five_eighths: FIVE_EIGHTHS,
half: HALF,
three_eighths: THREE_EIGHTHS,
one_quarter: ONE_QUARTER,
one_eighth: ONE_EIGHTH,
empty: " ",
};
}
pub mod half_block {
pub const UPPER: char = '▀';
pub const LOWER: char = '▄';
pub const FULL: char = '█';
}
pub mod bar {
pub const FULL: &str = "";
pub const SEVEN_EIGHTHS: &str = "";
pub const THREE_QUARTERS: &str = "";
pub const FIVE_EIGHTHS: &str = "";
pub const HALF: &str = "";
pub const THREE_EIGHTHS: &str = "";
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
pub three_quarters: &'static str,
pub five_eighths: &'static str,
pub half: &'static str,
pub three_eighths: &'static str,
pub one_quarter: &'static str,
pub one_eighth: &'static str,
pub empty: &'static str,
}
impl Default for Set {
fn default() -> Self {
NINE_LEVELS
}
}
pub const THREE_LEVELS: Set = Set {
full: FULL,
seven_eighths: FULL,
three_quarters: HALF,
five_eighths: HALF,
half: HALF,
three_eighths: HALF,
one_quarter: HALF,
one_eighth: " ",
empty: " ",
};
pub const NINE_LEVELS: Set = Set {
full: FULL,
seven_eighths: SEVEN_EIGHTHS,
three_quarters: THREE_QUARTERS,
five_eighths: FIVE_EIGHTHS,
half: HALF,
three_eighths: THREE_EIGHTHS,
one_quarter: ONE_QUARTER,
one_eighth: ONE_EIGHTH,
empty: " ",
};
}
pub const DOT: &str = "";
pub mod braille {
pub const BLANK: u16 = 0x2800;
pub const DOTS: [[u16; 2]; 4] = [
[0x0001, 0x0008],
[0x0002, 0x0010],
[0x0004, 0x0020],
[0x0040, 0x0080],
];
}
/// Marker to use when plotting data points
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Marker {
/// One point per cell in shape of dot (`•`)
#[default]
Dot,
/// One point per cell in shape of a block (`█`)
Block,
/// One point per cell in the shape of a bar (`▄`)
Bar,
/// Use the [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) block to
/// represent data points.
///
/// This is a 2x4 grid of dots, where each dot can be either on or off.
///
/// Note: Support for this marker is limited to terminals and fonts that support Unicode
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
/// characters (`<60>`) instead of Braille dots (`⠓`, `⣇`, `⣿`).
Braille,
/// Use the unicode block and half block characters (`█`, `▄`, and `▀`) to represent points in
/// a grid that is double the resolution of the terminal. Because each terminal cell is
/// generally about twice as tall as it is wide, this allows for a square grid of pixels.
HalfBlock,
}
pub mod scrollbar {
use super::{block, line};
/// Scrollbar Set
/// ```text
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ end
/// │ │ └──── track
/// │ └──────── thumb
/// └─────────── begin
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub track: &'static str,
pub thumb: &'static str,
pub begin: &'static str,
pub end: &'static str,
}
pub const DOUBLE_VERTICAL: Set = Set {
track: line::DOUBLE_VERTICAL,
thumb: block::FULL,
begin: "",
end: "",
};
pub const DOUBLE_HORIZONTAL: Set = Set {
track: line::DOUBLE_HORIZONTAL,
thumb: block::FULL,
begin: "",
end: "",
};
pub const VERTICAL: Set = Set {
track: line::VERTICAL,
thumb: block::FULL,
begin: "",
end: "",
};
pub const HORIZONTAL: Set = Set {
track: line::HORIZONTAL,
thumb: block::FULL,
begin: "",
end: "",
};
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn marker_tostring() {
assert_eq!(Marker::Dot.to_string(), "Dot");
assert_eq!(Marker::Block.to_string(), "Block");
assert_eq!(Marker::Bar.to_string(), "Bar");
assert_eq!(Marker::Braille.to_string(), "Braille");
}
#[test]
fn marker_from_str() {
assert_eq!("Dot".parse::<Marker>(), Ok(Marker::Dot));
assert_eq!("Block".parse::<Marker>(), Ok(Marker::Block));
assert_eq!("Bar".parse::<Marker>(), Ok(Marker::Bar));
assert_eq!("Braille".parse::<Marker>(), Ok(Marker::Braille));
assert_eq!("".parse::<Marker>(), Err(ParseError::VariantNotFound));
}
}

View File

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

View File

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

View File

@@ -18,7 +18,7 @@
//! let backend = CrosstermBackend::new(stdout());
//! let mut terminal = Terminal::new(backend)?;
//! terminal.draw(|frame| {
//! let area = frame.size();
//! let area = frame.area();
//! frame.render_widget(Paragraph::new("Hello world!"), area);
//! })?;
//! # std::io::Result::Ok(())
@@ -32,7 +32,6 @@
//! [`Buffer`]: crate::buffer::Buffer
mod frame;
#[allow(clippy::module_inception)]
mod terminal;
mod viewport;

View File

@@ -16,7 +16,7 @@ pub struct Frame<'a> {
///
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
pub(crate) cursor_position: Option<(u16, u16)>,
pub(crate) cursor_position: Option<Position>,
/// The area of the viewport
pub(crate) viewport_area: Rect,
@@ -42,13 +42,25 @@ pub struct CompletedFrame<'a> {
}
impl Frame<'_> {
/// The size of the current frame
/// The area of the current frame
///
/// This is guaranteed not to change during rendering, so may be called multiple times.
///
/// If your app listens for a resize event from the backend, it should ignore the values from
/// the event for any calculations that are used to render the current frame and use this value
/// instead as this is the size of the buffer that is used to render the current frame.
/// instead as this is the area of the buffer that is used to render the current frame.
pub const fn area(&self) -> Rect {
self.viewport_area
}
/// The area of the current frame
///
/// This is guaranteed not to change during rendering, so may be called multiple times.
///
/// If your app listens for a resize event from the backend, it should ignore the values from
/// the event for any calculations that are used to render the current frame and use this value
/// instead as this is the area of the buffer that is used to render the current frame.
#[deprecated = "use .area() as it's the more correct name"]
pub const fn size(&self) -> Rect {
self.viewport_area
}
@@ -94,7 +106,7 @@ impl Frame<'_> {
/// # }
/// ```
#[allow(clippy::needless_pass_by_value)]
#[stability::unstable(feature = "widget-ref")]
#[instability::unstable(feature = "widget-ref")]
pub fn render_widget_ref<W: WidgetRef>(&mut self, widget: W, area: Rect) {
widget.render_ref(area, self.buffer);
}
@@ -152,7 +164,7 @@ impl Frame<'_> {
/// # }
/// ```
#[allow(clippy::needless_pass_by_value)]
#[stability::unstable(feature = "widget-ref")]
#[instability::unstable(feature = "widget-ref")]
pub fn render_stateful_widget_ref<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidgetRef,
@@ -163,11 +175,22 @@ impl Frame<'_> {
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
/// with it.
/// Note that this will interfere with calls to [`Terminal::hide_cursor`],
/// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and
/// stick with it.
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) {
self.cursor_position = Some(position.into());
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to [`Terminal::hide_cursor`],
/// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and
/// stick with it.
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
pub fn set_cursor(&mut self, x: u16, y: u16) {
self.cursor_position = Some((x, y));
self.set_cursor_position(Position { x, y });
}
/// Gets the buffer that this `Frame` draws into as a mutable reference.

View File

@@ -0,0 +1,727 @@
use std::io;
use crate::{
backend::ClearType, buffer::Cell, prelude::*, CompletedFrame, TerminalOptions, Viewport,
};
/// An interface to interact and draw [`Frame`]s on the user's terminal.
///
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
/// state of the buffers, cursor and viewport.
///
/// The [`Terminal`] is generic over a [`Backend`] implementation which is used to interface with
/// the underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
/// information.
///
/// The `Terminal` struct maintains two buffers: the current and the previous.
/// When the widgets are drawn, the changes are accumulated in the current buffer.
/// At the end of each draw pass, the two buffers are compared, and only the changes
/// between these buffers are written to the terminal, avoiding any redundant operations.
/// After flushing these changes, the buffers are swapped to prepare for the next draw cycle.
///
/// The terminal also has a viewport which is the area of the terminal that is currently visible to
/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
///
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
/// application with the new size. This will automatically resize the internal buffers to match the
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
///
/// # Examples
///
/// ```rust,no_run
/// # use ratatui::prelude::*;
/// use std::io::stdout;
///
/// use ratatui::widgets::Paragraph;
///
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// })?;
/// # std::io::Result::Ok(())
/// ```
///
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Termion]: https://crates.io/crates/termion
/// [Termwiz]: https://crates.io/crates/termwiz
/// [`backend`]: crate::backend
/// [`Backend`]: crate::backend::Backend
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Terminal<B>
where
B: Backend,
{
/// The backend used to interface with the terminal
backend: B,
/// Holds the results of the current and previous draw calls. The two are compared at the end
/// of each draw pass to output the necessary updates to the terminal
buffers: [Buffer; 2],
/// Index of the current buffer in the previous array
current: usize,
/// Whether the cursor is currently hidden
hidden_cursor: bool,
/// Viewport
viewport: Viewport,
/// Area of the viewport
viewport_area: Rect,
/// Last known area of the terminal. Used to detect if the internal buffers have to be resized.
last_known_area: Rect,
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
/// and the terminal resized.
last_known_cursor_pos: Position,
/// Number of frames rendered up until current time.
frame_count: usize,
}
/// Options to pass to [`Terminal::with_options`]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Options {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.hidden_cursor {
if let Err(err) = self.show_cursor() {
eprintln!("Failed to show the cursor: {err}");
}
}
}
}
impl<B> Terminal<B>
where
B: Backend,
{
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// let terminal = Terminal::new(backend)?;
/// # std::io::Result::Ok(())
/// ```
pub fn new(backend: B) -> io::Result<Self> {
Self::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)
}
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
///
/// # Example
///
/// ```rust
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, backend::TestBackend, Viewport, TerminalOptions};
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Self> {
let area = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => {
Rect::from((Position::ORIGIN, backend.size()?))
}
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (area, Position::ORIGIN),
Viewport::Inline(height) => {
compute_inline_size(&mut backend, height, area.as_size(), 0)?
}
Viewport::Fixed(area) => (area, area.as_position()),
};
Ok(Self {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_area: area,
last_known_cursor_pos: cursor_pos,
frame_count: 0,
})
}
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
pub fn get_frame(&mut self) -> Frame {
let count = self.frame_count;
Frame {
cursor_position: None,
viewport_area: self.viewport_area,
buffer: self.current_buffer_mut(),
count,
}
}
/// Gets the current buffer as a mutable reference.
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
/// Gets the backend
pub const fn backend(&self) -> &B {
&self.backend
}
/// Gets the backend as a mutable reference
pub fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
/// Obtains a difference between the previous and the current buffer and passes it to the
/// current backend for drawing.
pub fn flush(&mut self) -> io::Result<()> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
if let Some((col, row, _)) = updates.last() {
self.last_known_cursor_pos = Position { x: *col, y: *row };
}
self.backend.draw(updates.into_iter())
}
/// Updates the Terminal so that internal buffers match the requested area.
///
/// Requested area will be saved to remain consistent when rendering. This leads to a full clear
/// of the screen.
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
let next_area = match self.viewport {
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
.y
.saturating_sub(self.viewport_area.top());
compute_inline_size(
&mut self.backend,
height,
area.as_size(),
offset_in_previous_viewport,
)?
.0
}
Viewport::Fixed(_) | Viewport::Fullscreen => area,
};
self.set_viewport_area(next_area);
self.clear()?;
self.last_known_area = area;
Ok(())
}
fn set_viewport_area(&mut self, area: Rect) {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport_area = area;
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
// fixed viewports do not get autoresized
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let area = Rect::from((Position::ORIGIN, self.size()?));
if area != self.last_known_area {
self.resize(area)?;
}
};
Ok(())
}
/// Draws a single frame to the terminal.
///
/// Returns a [`CompletedFrame`] if successful, otherwise a [`std::io::Error`].
///
/// If the render callback passed to this method can fail, use [`try_draw`] instead.
///
/// Applications should call `draw` or [`try_draw`] in a loop to continuously render the
/// terminal. These methods are the main entry points for drawing to the terminal.
///
/// [`try_draw`]: Terminal::try_draw
///
/// This method will:
///
/// - autoresize the terminal if necessary
/// - call the render callback, passing it a [`Frame`] reference to render to
/// - flush the current internal state by copying the current buffer to the backend
/// - move the cursor to the last known position if it was set during the rendering closure
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
///
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
/// purposes, but it is often not used in regular applicationss.
///
/// The render callback should fully render the entire frame when called, including areas that
/// are unchanged from the previous frame. This is because each frame is compared to the
/// previous frame to determine what has changed, and only the changes are written to the
/// terminal. If the render callback does not fully render the frame, the terminal will not be
/// in a consistent state.
///
/// # Examples
///
/// ```
/// # use ratatui::layout::Position;
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
/// # let mut terminal = ratatui::Terminal::new(backend)?;
/// use ratatui::widgets::Paragraph;
///
/// // with a closure
/// terminal.draw(|frame| {
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// })?;
///
/// // or with a function
/// terminal.draw(render)?;
///
/// fn render(frame: &mut ratatui::Frame) {
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
/// }
/// # std::io::Result::Ok(())
/// ```
pub fn draw<F>(&mut self, render_callback: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame),
{
self.try_draw(|frame| {
render_callback(frame);
io::Result::Ok(())
})
}
/// Tries to draw a single frame to the terminal.
///
/// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
/// [`Result::Err`] containing the [`std::io::Error`] that caused the failure.
///
/// This is the equivalent of [`Terminal::draw`] but the render callback is a function or
/// closure that returns a `Result` instead of nothing.
///
/// Applications should call `try_draw` or [`draw`] in a loop to continuously render the
/// terminal. These methods are the main entry points for drawing to the terminal.
///
/// [`draw`]: Terminal::draw
///
/// This method will:
///
/// - autoresize the terminal if necessary
/// - call the render callback, passing it a [`Frame`] reference to render to
/// - flush the current internal state by copying the current buffer to the backend
/// - move the cursor to the last known position if it was set during the rendering closure
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
///
/// The render callback passed to `try_draw` can return any [`Result`] with an error type that
/// can be converted into an [`std::io::Error`] using the [`Into`] trait. This makes it possible
/// to use the `?` operator to propagate errors that occur during rendering. If the render
/// callback returns an error, the error will be returned from `try_draw` as an
/// [`std::io::Error`] and the terminal will not be updated.
///
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
/// purposes, but it is often not used in regular applicationss.
///
/// The render callback should fully render the entire frame when called, including areas that
/// are unchanged from the previous frame. This is because each frame is compared to the
/// previous frame to determine what has changed, and only the changes are written to the
/// terminal. If the render function does not fully render the frame, the terminal will not be
/// in a consistent state.
///
/// # Examples
///
/// ```should_panic
/// # use ratatui::layout::Position;;
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
/// # let mut terminal = ratatui::Terminal::new(backend)?;
/// use std::io;
///
/// use ratatui::widgets::Paragraph;
///
/// // with a closure
/// terminal.try_draw(|frame| {
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// io::Result::Ok(())
/// })?;
///
/// // or with a function
/// terminal.try_draw(render)?;
///
/// fn render(frame: &mut ratatui::Frame) -> io::Result<()> {
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
/// Ok(())
/// }
/// # io::Result::Ok(())
/// ```
pub fn try_draw<F, E>(&mut self, render_callback: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame) -> Result<(), E>,
E: Into<io::Error>,
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;
let mut frame = self.get_frame();
render_callback(&mut frame).map_err(Into::into)?;
// We can't change the cursor position right away because we have to flush the frame to
// stdout first. But we also can't keep the frame around, since it holds a &mut to
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;
// Draw to stdout
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some(position) => {
self.show_cursor()?;
self.set_cursor_position(position)?;
}
}
self.swap_buffers();
// Flush
self.backend.flush()?;
let completed_frame = CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_area,
count: self.frame_count,
};
// increment frame count before returning from draw
self.frame_count = self.frame_count.wrapping_add(1);
Ok(completed_frame)
}
/// Hides the cursor.
pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
/// Shows the cursor.
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call and is returned as a tuple of
/// `(x, y)` coordinates.
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
/// Sets the cursor position.
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.set_cursor_position(Position { x, y })
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call.
pub fn get_cursor_position(&mut self) -> io::Result<Position> {
self.backend.get_cursor_position()
}
/// Sets the cursor position.
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let position = position.into();
self.backend.set_cursor_position(position)?;
self.last_known_cursor_pos = position;
Ok(())
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
self.backend
.set_cursor_position(self.viewport_area.as_position())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(area) => {
for y in area.top()..area.bottom() {
self.backend.set_cursor_position(Position { x: 0, y })?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
}
}
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
}
/// Clears the inactive buffer and swaps it with the current buffer
pub fn swap_buffers(&mut self) {
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
}
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Size> {
self.backend.size()
}
/// Insert some content before the current inline viewport. This has no effect when the
/// viewport is not inline.
///
/// The `draw_fn` closure will be called to draw into a writable `Buffer` that is `height`
/// lines tall. The content of that `Buffer` will then be inserted before the viewport.
///
/// If the viewport isn't yet at the bottom of the screen, inserted lines will push it towards
/// the bottom. Once the viewport is at the bottom of the screen, inserted lines will scroll
/// the area of the screen above the viewport upwards.
///
/// Before:
/// ```ignore
/// +---------------------+
/// | pre-existing line 1 |
/// | pre-existing line 2 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// | |
/// | |
/// +---------------------+
/// ```
///
/// After inserting 2 lines:
/// ```ignore
/// +---------------------+
/// | pre-existing line 1 |
/// | pre-existing line 2 |
/// | inserted line 1 |
/// | inserted line 2 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// +---------------------+
/// ```
///
/// After inserting 2 more lines:
/// ```ignore
/// +---------------------+
/// | pre-existing line 2 |
/// | inserted line 1 |
/// | inserted line 2 |
/// | inserted line 3 |
/// | inserted line 4 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// ```
///
/// If more lines are inserted than there is space on the screen, then the top lines will go
/// directly into the terminal's scrollback buffer. At the limit, if the viewport takes up the
/// whole screen, all lines will be inserted directly into the scrollback buffer.
///
/// # Examples
///
/// ## Insert a single line before the current viewport
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
/// Paragraph::new(Line::from(vec![
/// Span::raw("This line will be added "),
/// Span::styled("before", Style::default().fg(Color::Blue)),
/// Span::raw(" the current viewport"),
/// ]))
/// .render(buf.area, buf);
/// });
/// ```
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
where
F: FnOnce(&mut Buffer),
{
if !matches!(self.viewport, Viewport::Inline(_)) {
return Ok(());
}
// The approach of this function is to first render all of the lines to insert into a
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
// this buffer onto the screen.
let area = Rect {
x: 0,
y: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
let mut buffer = buffer.content.as_slice();
// Use i32 variables so we don't have worry about overflowed u16s when adding, or about
// negative results when subtracting.
let mut drawn_height: i32 = self.viewport_area.top().into();
let mut buffer_height: i32 = height.into();
let viewport_height: i32 = self.viewport_area.height.into();
let screen_height: i32 = self.last_known_area.height.into();
// The algorithm here is to loop, drawing large chunks of text (up to a screen-full at a
// time), until the remainder of the buffer plus the viewport fits on the screen. We choose
// this loop condition because it guarantees that we can write the remainder of the buffer
// with just one call to Self::draw_lines().
while buffer_height + viewport_height > screen_height {
// We will draw as much of the buffer as possible on this iteration in order to make
// forward progress. So we have:
//
// to_draw = min(buffer_height, screen_height)
//
// We may need to scroll the screen up to make room to draw. We choose the minimal
// possible scroll amount so we don't end up with the viewport sitting in the middle of
// the screen when this function is done. The amount to scroll by is:
//
// scroll_up = max(0, drawn_height + to_draw - screen_height)
//
// We want `scroll_up` to be enough so that, after drawing, we have used the whole
// screen (drawn_height - scroll_up + to_draw = screen_height). However, there might
// already be enough room on the screen to draw without scrolling (drawn_height +
// to_draw <= screen_height). In this case, we just don't scroll at all.
let to_draw = buffer_height.min(screen_height);
let scroll_up = 0.max(drawn_height + to_draw - screen_height);
self.scroll_up(scroll_up as u16)?;
buffer = self.draw_lines((drawn_height - scroll_up) as u16, to_draw as u16, buffer)?;
drawn_height += to_draw - scroll_up;
buffer_height -= to_draw;
}
// There is now enough room on the screen for the remaining buffer plus the viewport,
// though we may still need to scroll up some of the existing text first. It's possible
// that by this point we've drained the buffer, but we may still need to scroll up to make
// room for the viewport.
//
// We want to scroll up the exact amount that will leave us completely filling the screen.
// However, it's possible that the viewport didn't start on the bottom of the screen and
// the added lines weren't enough to push it all the way to the bottom. We deal with this
// case by just ensuring that our scroll amount is non-negative.
//
// We want:
// screen_height = drawn_height - scroll_up + buffer_height + viewport_height
// Or, equivalently:
// scroll_up = drawn_height + buffer_height + viewport_height - screen_height
let scroll_up = 0.max(drawn_height + buffer_height + viewport_height - screen_height);
self.scroll_up(scroll_up as u16)?;
self.draw_lines(
(drawn_height - scroll_up) as u16,
buffer_height as u16,
buffer,
)?;
drawn_height += buffer_height - scroll_up;
self.set_viewport_area(Rect {
y: drawn_height as u16,
..self.viewport_area
});
// Clear the viewport off the screen. We didn't clear earlier for two reasons. First, it
// wasn't necessary because the buffer we drew out of isn't sparse, so it overwrote
// whatever was on the screen. Second, there is a weird bug with tmux where a full screen
// clear plus immediate scrolling causes some garbage to go into the scrollback.
self.clear()?;
Ok(())
}
/// Draw lines at the given vertical offset. The slice of cells must contain enough cells
/// for the requested lines. A slice of the unused cells are returned.
fn draw_lines<'a>(
&mut self,
y_offset: u16,
lines_to_draw: u16,
cells: &'a [Cell],
) -> io::Result<&'a [Cell]> {
let width: usize = self.last_known_area.width.into();
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
if lines_to_draw > 0 {
let iter = to_draw
.iter()
.enumerate()
.map(|(i, c)| ((i % width) as u16, y_offset + (i / width) as u16, c));
self.backend.draw(iter)?;
self.backend.flush()?;
}
Ok(remainder)
}
/// Scroll the whole screen up by the given number of lines.
fn scroll_up(&mut self, lines_to_scroll: u16) -> io::Result<()> {
if lines_to_scroll > 0 {
self.set_cursor_position(Position::new(
0,
self.last_known_area.height.saturating_sub(1),
))?;
self.backend.append_lines(lines_to_scroll)?;
}
Ok(())
}
}
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Size,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, Position)> {
let pos = backend.get_cursor_position()?;
let mut row = pos.y;
let max_height = size.height.min(height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
backend.append_lines(lines_after_cursor)?;
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Ok((
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
))
}

View File

@@ -5,7 +5,7 @@
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//! [`Text`].
//!
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Line`].
@@ -48,14 +48,13 @@ mod grapheme;
pub use grapheme::StyledGrapheme;
mod line;
pub use line::Line;
pub use line::{Line, ToLine};
mod masked;
pub use masked::Masked;
mod span;
pub use span::Span;
pub use span::{Span, ToSpan};
#[allow(clippy::module_inception)]
mod text;
pub use text::Text;
pub use text::{Text, ToText};

View File

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

View File

@@ -4,8 +4,7 @@ use std::{borrow::Cow, fmt};
use unicode_truncate::UnicodeTruncateStr;
use super::StyledGrapheme;
use crate::prelude::*;
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// A line of text, consisting of one or more [`Span`]s.
///
@@ -13,6 +12,9 @@ use crate::prelude::*;
/// text. When a [`Line`] is rendered, it is rendered as a single line of text, with each [`Span`]
/// being rendered in order (left to right).
///
/// Any newlines in the content are removed when creating a [`Line`] using the constructor or
/// conversion methods.
///
/// # Constructor Methods
///
/// - [`Line::default`] creates a line with empty content and the default style.
@@ -147,16 +149,40 @@ use crate::prelude::*;
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Line<'a> {
/// The spans that make up this line of text.
pub spans: Vec<Span<'a>>,
/// The style of this line of text.
pub style: Style,
/// The alignment of this line of text.
pub alignment: Option<Alignment>,
/// The spans that make up this line of text.
pub spans: Vec<Span<'a>>,
}
impl fmt::Debug for Line<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.style == Style::default() && self.alignment.is_none() {
f.write_str("Line ")?;
return f.debug_list().entries(&self.spans).finish();
}
let mut debug = f.debug_struct("Line");
if self.style != Style::default() {
debug.field("style", &self.style);
}
if let Some(alignment) = self.alignment {
debug.field("alignment", &format!("Alignment::{alignment}"));
}
debug.field("spans", &self.spans).finish()
}
}
fn cow_to_spans<'a>(content: impl Into<Cow<'a, str>>) -> Vec<Span<'a>> {
match content.into() {
Cow::Borrowed(s) => s.lines().map(Span::raw).collect(),
Cow::Owned(s) => s.lines().map(|v| Span::raw(v.to_string())).collect(),
}
}
impl<'a> Line<'a> {
@@ -184,17 +210,14 @@ impl<'a> Line<'a> {
T: Into<Cow<'a, str>>,
{
Self {
spans: content
.into()
.lines()
.map(|v| Span::raw(v.to_string()))
.collect(),
spans: cow_to_spans(content),
..Default::default()
}
}
/// Create a line with the given style.
// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
///
/// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
/// [`Cow<str>`], or your own type that implements [`Into<Cow<str>>`]).
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
@@ -218,11 +241,7 @@ impl<'a> Line<'a> {
S: Into<Style>,
{
Self {
spans: content
.into()
.lines()
.map(|v| Span::raw(v.to_string()))
.collect(),
spans: cow_to_spans(content),
style: style.into(),
..Default::default()
}
@@ -503,13 +522,13 @@ impl<'a> IntoIterator for &'a mut Line<'a> {
impl<'a> From<String> for Line<'a> {
fn from(s: String) -> Self {
Self::from(vec![Span::from(s)])
Self::raw(s)
}
}
impl<'a> From<&'a str> for Line<'a> {
fn from(s: &'a str) -> Self {
Self::from(vec![Span::from(s)])
Self::raw(s)
}
}
@@ -546,6 +565,37 @@ where
}
}
/// Adds a `Span` to a `Line`, returning a new `Line` with the `Span` added.
impl<'a> std::ops::Add<Span<'a>> for Line<'a> {
type Output = Self;
fn add(mut self, rhs: Span<'a>) -> Self::Output {
self.spans.push(rhs);
self
}
}
/// Adds two `Line`s together, returning a new `Text` with the contents of the two `Line`s.
impl<'a> std::ops::Add<Self> for Line<'a> {
type Output = Text<'a>;
fn add(self, rhs: Self) -> Self::Output {
Text::from(vec![self, rhs])
}
}
impl<'a> std::ops::AddAssign<Span<'a>> for Line<'a> {
fn add_assign(&mut self, rhs: Span<'a>) {
self.spans.push(rhs);
}
}
impl<'a> Extend<Span<'a>> for Line<'a> {
fn extend<T: IntoIterator<Item = Span<'a>>>(&mut self, iter: T) {
self.spans.extend(iter);
}
}
impl Widget for Line<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
@@ -558,6 +608,7 @@ impl WidgetRef for Line<'_> {
if area.is_empty() {
return;
}
let area = Rect { height: 1, ..area };
let line_width = self.width();
if line_width == 0 {
return;
@@ -647,6 +698,29 @@ fn spans_after_width<'a>(
})
}
/// A trait for converting a value to a [`Line`].
///
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
/// such, `ToLine` shouln't be implemented directly: [`Display`] should be implemented instead, and
/// you get the `ToLine` implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToLine {
/// Converts the value to a [`Line`].
fn to_line(&self) -> Line<'_>;
}
/// # Panics
///
/// In this implementation, the `to_line` method panics if the `Display` implementation returns an
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
/// returns an error itself.
impl<T: fmt::Display> ToLine for T {
fn to_line(&self) -> Line<'_> {
Line::from(self.to_string())
}
}
impl fmt::Display for Line<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for span in &self.spans {
@@ -684,11 +758,11 @@ mod tests {
#[test]
fn raw_str() {
let line = Line::raw("test content");
assert_eq!(line.spans, vec![Span::raw("test content")]);
assert_eq!(line.spans, [Span::raw("test content")]);
assert_eq!(line.alignment, None);
let line = Line::raw("a\nb");
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
assert_eq!(line.spans, [Span::raw("a"), Span::raw("b")]);
assert_eq!(line.alignment, None);
}
@@ -697,7 +771,7 @@ mod tests {
let style = Style::new().yellow();
let content = "Hello, world!";
let line = Line::styled(content, style);
assert_eq!(line.spans, vec![Span::raw(content)]);
assert_eq!(line.spans, [Span::raw(content)]);
assert_eq!(line.style, style);
}
@@ -706,7 +780,7 @@ mod tests {
let style = Style::new().yellow();
let content = String::from("Hello, world!");
let line = Line::styled(content.clone(), style);
assert_eq!(line.spans, vec![Span::raw(content)]);
assert_eq!(line.spans, [Span::raw(content)]);
assert_eq!(line.style, style);
}
@@ -715,7 +789,7 @@ mod tests {
let style = Style::new().yellow();
let content = Cow::from("Hello, world!");
let line = Line::styled(content.clone(), style);
assert_eq!(line.spans, vec![Span::raw(content)]);
assert_eq!(line.spans, [Span::raw(content)]);
assert_eq!(line.style, style);
}
@@ -804,14 +878,28 @@ mod tests {
fn from_string() {
let s = String::from("Hello, world!");
let line = Line::from(s);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
assert_eq!(line.spans, [Span::from("Hello, world!")]);
let s = String::from("Hello\nworld!");
let line = Line::from(s);
assert_eq!(line.spans, [Span::from("Hello"), Span::from("world!")]);
}
#[test]
fn from_str() {
let s = "Hello, world!";
let line = Line::from(s);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
assert_eq!(line.spans, [Span::from("Hello, world!")]);
let s = "Hello\nworld!";
let line = Line::from(s);
assert_eq!(line.spans, [Span::from("Hello"), Span::from("world!")]);
}
#[test]
fn to_line() {
let line = 42.to_line();
assert_eq!(line.spans, [Span::from("42")]);
}
#[test]
@@ -821,7 +909,7 @@ mod tests {
Span::styled(" world!", Style::default().fg(Color::Green)),
];
let line = Line::from(spans.clone());
assert_eq!(spans, line.spans);
assert_eq!(line.spans, spans);
}
#[test]
@@ -854,7 +942,63 @@ mod tests {
fn from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
let line = Line::from(span.clone());
assert_eq!(vec![span], line.spans);
assert_eq!(line.spans, [span]);
}
#[test]
fn add_span() {
assert_eq!(
Line::raw("Red").red() + Span::raw("blue").blue(),
Line {
spans: vec![Span::raw("Red"), Span::raw("blue").blue()],
style: Style::new().red(),
alignment: None,
},
);
}
#[test]
fn add_line() {
assert_eq!(
Line::raw("Red").red() + Line::raw("Blue").blue(),
Text {
lines: vec![Line::raw("Red").red(), Line::raw("Blue").blue()],
style: Style::default(),
alignment: None,
}
);
}
#[test]
fn add_assign_span() {
let mut line = Line::raw("Red").red();
line += Span::raw("Blue").blue();
assert_eq!(
line,
Line {
spans: vec![Span::raw("Red"), Span::raw("Blue").blue()],
style: Style::new().red(),
alignment: None,
},
);
}
#[test]
fn extend() {
let mut line = Line::from("Hello, ");
line.extend([Span::raw("world!")]);
assert_eq!(line.spans, [Span::raw("Hello, "), Span::raw("world!")]);
let mut line = Line::from("Hello, ");
line.extend([Span::raw("world! "), Span::raw("How are you?")]);
assert_eq!(
line.spans,
[
Span::raw("Hello, "),
Span::raw("world! "),
Span::raw("How are you?")
]
);
}
#[test]
@@ -864,7 +1008,7 @@ mod tests {
Span::styled(" world!", Style::default().fg(Color::Green)),
]);
let s: String = line.into();
assert_eq!("Hello, world!", s);
assert_eq!(s, "Hello, world!");
}
#[test]
@@ -997,6 +1141,17 @@ mod tests {
assert_eq!(buf, expected);
}
#[test]
fn render_only_styles_first_line() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 2));
hello_world().render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(["Hello world! ", " "]);
expected.set_style(Rect::new(0, 0, 20, 1), ITALIC);
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
assert_eq!(buf, expected);
}
#[test]
fn render_truncates() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
@@ -1055,7 +1210,7 @@ mod tests {
assert_eq!(buf, Buffer::with_lines(["lo wo"]));
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[test]
fn regression_1032() {
@@ -1071,7 +1226,7 @@ mod tests {
/// 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
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[test]
fn crab_emoji_width() {
@@ -1082,7 +1237,7 @@ mod tests {
assert_eq!(crab.width(), 2); // display width
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[rstest]
#[case::left_4(Alignment::Left, 4, "1234")]
@@ -1104,7 +1259,7 @@ mod tests {
assert_eq!(buf, Buffer::with_lines([expected]));
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// Part of a regression test for <https://github.com/ratatui/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
@@ -1169,7 +1324,7 @@ mod tests {
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 mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::new("X"));
let area = Rect::new(2, 0, 6, 1);
line.render_ref(area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
@@ -1188,12 +1343,12 @@ mod tests {
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"));
let mut buf = Buffer::filled(area, Cell::new("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
/// Part of a regression test for <https://github.com/ratatui/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
@@ -1207,7 +1362,7 @@ mod tests {
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
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[rstest]
#[case::flag_1(1, " ")]
@@ -1271,6 +1426,13 @@ mod tests {
line.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
#[test]
fn render_with_newlines() {
let mut buf = Buffer::empty(Rect::new(0, 0, 11, 1));
Line::from("Hello\nworld!").render(Rect::new(0, 0, 11, 1), &mut buf);
assert_eq!(buf, Buffer::with_lines(["Helloworld!"]));
}
}
mod iterators {

View File

@@ -125,10 +125,10 @@ mod tests {
let masked = Masked::new("12345", 'x');
let text: Text = (&masked).into();
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
assert_eq!(text.lines, [Line::from("xxxxx")]);
let text: Text = masked.into();
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
assert_eq!(text.lines, [Line::from("xxxxx")]);
}
#[test]

View File

@@ -3,8 +3,7 @@ use std::{borrow::Cow, fmt};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::StyledGrapheme;
use crate::prelude::*;
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// Represents a part of a line that is contiguous and where all characters share the same style.
///
@@ -82,19 +81,31 @@ use crate::prelude::*;
/// use ratatui::prelude::*;
///
/// # fn render_frame(frame: &mut Frame) {
/// frame.render_widget("test content".green().on_yellow().italic(), frame.size());
/// frame.render_widget("test content".green().on_yellow().italic(), frame.area());
/// # }
/// ```
/// [`Line`]: crate::text::Line
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Stylize`]: crate::style::Stylize
/// [`Cow<str>`]: std::borrow::Cow
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Span<'a> {
/// The content of the span as a Clone-on-write string.
pub content: Cow<'a, str>,
/// The style of the span.
pub style: Style,
/// The content of the span as a Clone-on-write string.
pub content: Cow<'a, str>,
}
impl fmt::Debug for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.style == Style::default() {
return write!(f, "Span({:?})", self.content);
}
f.debug_struct("Span")
.field("style", &self.style)
.field("content", &self.content)
.finish()
}
}
impl<'a> Span<'a> {
@@ -343,6 +354,14 @@ where
}
}
impl<'a> std::ops::Add<Self> for Span<'a> {
type Output = Line<'a>;
fn add(self, rhs: Self) -> Self::Output {
Line::from_iter([self, rhs])
}
}
impl<'a> Styled for Span<'a> {
type Item = Self;
@@ -364,45 +383,88 @@ impl Widget for Span<'_> {
impl WidgetRef for Span<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
let Rect {
x: mut current_x,
y,
width,
..
} = area;
let max_x = Ord::min(current_x.saturating_add(width), buf.area.right());
for g in self.styled_graphemes(Style::default()) {
let symbol_width = g.symbol.width();
let next_x = current_x.saturating_add(symbol_width as u16);
if next_x > max_x {
if area.is_empty() {
return;
}
let Rect { mut x, y, .. } = area;
for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
let symbol_width = grapheme.symbol.width();
let next_x = x.saturating_add(symbol_width as u16);
if next_x > area.right() {
break;
}
buf.get_mut(current_x, y)
.set_symbol(g.symbol)
.set_style(g.style);
if i == 0 {
// the first grapheme is always set on the cell
buf[(x, y)]
.set_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else if x == area.x {
// there is one or more zero-width graphemes in the first cell, so the first cell
// must be appended to.
buf[(x, y)]
.append_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else if symbol_width == 0 {
// append zero-width graphemes to the previous cell
buf[(x - 1, y)]
.append_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else {
// just a normal grapheme (not first, not zero-width, not overflowing the area)
buf[(x, y)]
.set_symbol(grapheme.symbol)
.set_style(grapheme.style);
}
// multi-width graphemes must clear the cells of characters that are hidden by the
// grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
// overwritten.
for i in (current_x + 1)..next_x {
buf.get_mut(i, y).reset();
for x_hidden in (x + 1)..next_x {
// it may seem odd that the style of the hidden cells are not set to the style of
// the grapheme, but this is how the existing buffer.set_span() method works.
// buf.get_mut(i, y).set_style(g.style);
buf[(x_hidden, y)].reset();
}
current_x = next_x;
x = next_x;
}
}
}
/// A trait for converting a value to a [`Span`].
///
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
/// such, `ToSpan` shouln't be implemented directly: [`Display`] should be implemented instead, and
/// you get the `ToSpan` implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToSpan {
/// Converts the value to a [`Span`].
fn to_span(&self) -> Span<'_>;
}
/// # Panics
///
/// In this implementation, the `to_span` method panics if the `Display` implementation returns an
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
/// returns an error itself.
impl<T: fmt::Display> ToSpan for T {
fn to_span(&self) -> Span<'_> {
Span::raw(self.to_string())
}
}
impl fmt::Display for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.content, f)
for line in self.content.lines() {
fmt::Display::fmt(line, f)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use buffer::Cell;
use rstest::fixture;
use super::*;
@@ -495,6 +557,12 @@ mod tests {
assert_eq!(span.style, Style::default());
}
#[test]
fn to_span() {
assert_eq!(42.to_span(), Span::raw("42"));
assert_eq!("test".to_span(), Span::raw("test"));
}
#[test]
fn reset_style() {
let span = Span::styled("test content", Style::new().green()).reset_style();
@@ -513,6 +581,8 @@ mod tests {
assert_eq!(Span::raw("").width(), 0);
assert_eq!(Span::raw("test").width(), 4);
assert_eq!(Span::raw("test content").width(), 12);
// Needs reconsideration: https://github.com/ratatui/ratatui/issues/1271
assert_eq!(Span::raw("test\ncontent").width(), 12);
}
#[test]
@@ -526,6 +596,7 @@ mod tests {
assert_eq!(stylized.content, Cow::Borrowed("test content"));
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
}
#[test]
fn display_span() {
let span = Span::raw("test content");
@@ -533,6 +604,12 @@ mod tests {
assert_eq!(format!("{span:.4}"), "test");
}
#[test]
fn display_newline_span() {
let span = Span::raw("test\ncontent");
assert_eq!(format!("{span}"), "testcontent");
}
#[test]
fn display_styled_span() {
let stylized_span = Span::styled("stylized test content", Style::new().green());
@@ -580,8 +657,11 @@ mod tests {
}
#[rstest]
fn render_out_of_bounds(mut small_buf: Buffer) {
let out_of_bounds = Rect::new(20, 20, 10, 1);
#[case::x(20, 0)]
#[case::y(0, 20)]
#[case::both(20, 20)]
fn render_out_of_bounds(mut small_buf: Buffer, #[case] x: u16, #[case] y: u16) {
let out_of_bounds = Rect::new(x, y, 10, 1);
Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
assert_eq!(small_buf, Buffer::empty(small_buf.area));
}
@@ -664,5 +744,103 @@ mod tests {
])]);
assert_eq!(buf, expected);
}
#[test]
fn render_first_zero_width() {
let span = Span::raw("\u{200B}abc");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("\u{200B}a"), Cell::new("b"), Cell::new("c"),]
);
}
#[test]
fn render_second_zero_width() {
let span = Span::raw("a\u{200B}bc");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a\u{200B}"), Cell::new("b"), Cell::new("c")]
);
}
#[test]
fn render_middle_zero_width() {
let span = Span::raw("ab\u{200B}c");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a"), Cell::new("b\u{200B}"), Cell::new("c")]
);
}
#[test]
fn render_last_zero_width() {
let span = Span::raw("abc\u{200B}");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a"), Cell::new("b"), Cell::new("c\u{200B}")]
);
}
#[test]
fn render_with_newlines() {
let span = Span::raw("a\nb");
let mut buf = Buffer::empty(Rect::new(0, 0, 2, 1));
span.render(buf.area, &mut buf);
assert_eq!(buf.content(), [Cell::new("a"), Cell::new("b")]);
}
}
/// Regression test for <https://github.com/ratatui/ratatui/issues/1160> One line contains
/// some Unicode Left-Right-Marks (U+200E)
///
/// The issue was that a zero-width character at the end of the buffer causes the buffer bounds
/// to be exceeded (due to a position + 1 calculation that fails to account for the possibility
/// that the next position might not be available).
#[test]
fn issue_1160() {
let span = Span::raw("Hello\u{200E}");
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[
Cell::new("H"),
Cell::new("e"),
Cell::new("l"),
Cell::new("l"),
Cell::new("o\u{200E}"),
]
);
}
#[test]
fn add() {
assert_eq!(
Span::default() + Span::default(),
Line::from(vec![Span::default(), Span::default()])
);
assert_eq!(
Span::default() + Span::raw("test"),
Line::from(vec![Span::default(), Span::raw("test")])
);
assert_eq!(
Span::raw("test") + Span::default(),
Line::from(vec![Span::raw("test"), Span::default()])
);
assert_eq!(
Span::raw("test") + Span::raw("content"),
Line::from(vec![Span::raw("test"), Span::raw("content")])
);
}
}

View File

@@ -1,9 +1,7 @@
#![warn(missing_docs)]
use std::{borrow::Cow, fmt};
use itertools::{Itertools, Position};
use crate::prelude::*;
use crate::{prelude::*, style::Styled};
/// A string split over one or more lines.
///
@@ -165,14 +163,32 @@ use crate::prelude::*;
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Text<'a> {
/// The lines that make up this piece of text.
pub lines: Vec<Line<'a>>,
/// The style of this text.
pub style: Style,
/// The alignment of this text.
pub alignment: Option<Alignment>,
/// The style of this text.
pub style: Style,
/// The lines that make up this piece of text.
pub lines: Vec<Line<'a>>,
}
impl fmt::Debug for Text<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.style == Style::default() && self.alignment.is_none() {
f.write_str("Text ")?;
f.debug_list().entries(&self.lines).finish()
} else {
let mut debug = f.debug_struct("Text");
if self.style != Style::default() {
debug.field("style", &self.style);
}
if let Some(alignment) = self.alignment {
debug.field("alignment", &format!("Alignment::{alignment}"));
}
debug.field("lines", &self.lines).finish()
}
}
}
impl<'a> Text<'a> {
@@ -570,6 +586,33 @@ where
}
}
impl<'a> std::ops::Add<Line<'a>> for Text<'a> {
type Output = Self;
fn add(mut self, line: Line<'a>) -> Self::Output {
self.push_line(line);
self
}
}
/// Adds two `Text` together.
///
/// This ignores the style and alignment of the second `Text`.
impl<'a> std::ops::Add<Self> for Text<'a> {
type Output = Self;
fn add(mut self, text: Self) -> Self::Output {
self.lines.extend(text.lines);
self
}
}
impl<'a> std::ops::AddAssign<Line<'a>> for Text<'a> {
fn add_assign(&mut self, line: Line<'a>) {
self.push_line(line);
}
}
impl<'a, T> Extend<T> for Text<'a>
where
T: Into<Line<'a>>,
@@ -580,14 +623,36 @@ where
}
}
/// A trait for converting a value to a [`Text`].
///
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
/// such, `ToText` shouldn't be implemented directly: [`Display`] should be implemented instead, and
/// you get the `ToText` implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToText {
/// Converts the value to a [`Text`].
fn to_text(&self) -> Text<'_>;
}
/// # Panics
///
/// In this implementation, the `to_text` method panics if the `Display` implementation returns an
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
/// returns an error itself.
impl<T: fmt::Display> ToText for T {
fn to_text(&self) -> Text {
Text::raw(self.to_string())
}
}
impl fmt::Display for Text<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (position, line) in self.iter().with_position() {
if position == Position::Last {
write!(f, "{line}")?;
} else {
if let Some((last, rest)) = self.lines.split_last() {
for line in rest {
writeln!(f, "{line}")?;
}
write!(f, "{last}")?;
}
Ok(())
}
@@ -744,7 +809,25 @@ mod tests {
#[test]
fn from_line() {
let text = Text::from(Line::from("The first line"));
assert_eq!(text.lines, vec![Line::from("The first line")]);
assert_eq!(text.lines, [Line::from("The first line")]);
}
#[rstest]
#[case(42, Text::from("42"))]
#[case("just\ntesting", Text::from("just\ntesting"))]
#[case(true, Text::from("true"))]
#[case(6.66, Text::from("6.66"))]
#[case('a', Text::from("a"))]
#[case(String::from("hello"), Text::from("hello"))]
#[case(-1, Text::from("-1"))]
#[case("line1\nline2", Text::from("line1\nline2"))]
#[case(
"first line\nsecond line\nthird line",
Text::from("first line\nsecond line\nthird line")
)]
#[case("trailing newline\n", Text::from("trailing newline\n"))]
fn to_text(#[case] value: impl fmt::Display, #[case] expected: Text) {
assert_eq!(value.to_text(), expected);
}
#[test]
@@ -788,6 +871,44 @@ mod tests {
assert_eq!(iter.next(), None);
}
#[test]
fn add_line() {
assert_eq!(
Text::raw("Red").red() + Line::raw("Blue").blue(),
Text {
lines: vec![Line::raw("Red"), Line::raw("Blue").blue()],
style: Style::new().red(),
alignment: None,
}
);
}
#[test]
fn add_text() {
assert_eq!(
Text::raw("Red").red() + Text::raw("Blue").blue(),
Text {
lines: vec![Line::raw("Red"), Line::raw("Blue")],
style: Style::new().red(),
alignment: None,
}
);
}
#[test]
fn add_assign_line() {
let mut text = Text::raw("Red").red();
text += Line::raw("Blue").blue();
assert_eq!(
text,
Text {
lines: vec![Line::raw("Red"), Line::raw("Blue").blue()],
style: Style::new().red(),
alignment: None,
}
);
}
#[test]
fn extend() {
let mut text = Text::from("The first line\nThe second line");
@@ -839,11 +960,12 @@ mod tests {
);
}
#[test]
fn display_raw_text() {
let text = Text::raw("The first line\nThe second line");
assert_eq!(format!("{text}"), "The first line\nThe second line");
#[rstest]
#[case::one_line("The first line")]
#[case::multiple_lines("The first line\nThe second line")]
fn display_raw_text(#[case] value: &str) {
let text = Text::raw(value);
assert_eq!(format!("{text}"), value);
}
#[test]
@@ -935,7 +1057,7 @@ mod tests {
fn push_line_empty() {
let mut text = Text::default();
text.push_line(Line::from("Hello, world!"));
assert_eq!(text.lines, vec![Line::from("Hello, world!")]);
assert_eq!(text.lines, [Line::from("Hello, world!")]);
}
#[test]
@@ -957,7 +1079,7 @@ mod tests {
fn push_span_empty() {
let mut text = Text::default();
text.push_span(Span::raw("Hello, world!"));
assert_eq!(text.lines, vec![Line::from(Span::raw("Hello, world!"))],);
assert_eq!(text.lines, [Line::from(Span::raw("Hello, world!"))]);
}
mod widget {
@@ -1128,4 +1250,46 @@ mod tests {
assert_eq!(result, "Hello world!");
}
}
mod debug {
use super::*;
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn no_style() {
let text = Text::from("single unstyled line");
assert_eq!(text, Text::default());
}
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn text_style() {
let text = Text::from("single styled line")
.red()
.on_black()
.bold()
.not_italic();
assert_eq!(text, Text::default());
}
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn line_style() {
let text = Text::from(vec![
Line::from("first line").red().alignment(Alignment::Right),
Line::from("second line").on_black(),
]);
assert_eq!(text, Text::default());
}
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn span_style() {
let text = Text::from(Line::from(vec![
Span::from("first span").red(),
Span::from("second span").on_black(),
]));
assert_eq!(text, Text::default());
}
}
}

View File

@@ -21,38 +21,8 @@
//! - [`Tabs`]: displays a tab bar and allows selection.
//!
//! [`Canvas`]: crate::widgets::canvas::Canvas
mod barchart;
pub mod block;
mod borders;
#[cfg(feature = "widget-calendar")]
pub mod calendar;
pub mod canvas;
mod chart;
mod clear;
mod gauge;
mod list;
mod paragraph;
mod reflow;
mod scrollbar;
mod sparkline;
mod table;
mod tabs;
pub use self::{
barchart::{Bar, BarChart, BarGroup},
block::{Block, BorderType, Padding},
borders::*,
chart::{Axis, Chart, Dataset, GraphType, LegendPosition},
clear::Clear,
gauge::{Gauge, LineGauge},
list::{List, ListDirection, ListItem, ListState},
paragraph::{Paragraph, Wrap},
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
sparkline::{RenderDirection, Sparkline},
table::{Cell, HighlightSpacing, Row, Table, TableState},
tabs::Tabs,
};
use crate::{buffer::Buffer, layout::Rect};
use crate::{buffer::Buffer, layout::Rect, style::Style};
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
///
@@ -60,14 +30,22 @@ use crate::{buffer::Buffer, layout::Rect};
/// during rendering. This meant that they were not meant to be stored but used as *commands* to
/// draw common figures in the UI.
///
/// Starting with Ratatui 0.26.0, we added a new [`WidgetRef`] trait and implemented this on all the
/// internal widgets. This allows you to store a reference to a widget and render it later. It also
/// Starting with Ratatui 0.26.0, all the internal widgets implement Widget for a reference to
/// themselves. This allows you to store a reference to a widget and render it later. Widget crates
/// should consider also doing this to allow for more flexibility in how widgets are used.
///
/// In Ratatui 0.26.0, we also added an unstable [`WidgetRef`] trait and implemented this on all the
/// internal widgets. In addition to the above benefit of rendering references to widgets, this also
/// allows you to render boxed widgets. This is useful when you want to store a collection of
/// widgets with different types. You can then iterate over the collection and render each widget.
/// See <https://github.com/ratatui/ratatui/issues/1287> for more information.
///
/// The `Widget` trait can still be implemented, however, it is recommended to implement `WidgetRef`
/// and add an implementation of `Widget` that calls `WidgetRef::render_ref`. This pattern should be
/// used where backwards compatibility is required (all the internal widgets use this approach).
/// In general where you expect a widget to immutably work on its data, we recommended to implement
/// `Widget` for a reference to the widget (`impl Widget for &MyWidget`). If you need to store state
/// between draw calls, implement `StatefulWidget` if you want the Widget to be immutable, or
/// implement `Widget` for a mutable reference to the widget (`impl Widget for &mut MyWidget`) if
/// you want the widget to be mutable. The mutable widget pattern is used infrequently in apps, but
/// can be quite useful.
///
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
/// Widget is also implemented for `&str` and `String` types.
@@ -80,7 +58,7 @@ use crate::{buffer::Buffer, layout::Rect};
/// # let mut terminal = Terminal::new(backend).unwrap();
///
/// terminal.draw(|frame| {
/// frame.render_widget(Clear, frame.size());
/// frame.render_widget(Clear, frame.area());
/// });
/// ```
///
@@ -235,9 +213,9 @@ pub trait StatefulWidget {
/// useful when you want to store a collection of widgets with different types. You can then iterate
/// over the collection and render each widget.
///
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal widgets.
/// Implementors should prefer to implement this over the `Widget` trait and add an implementation
/// of `Widget` that calls `WidgetRef::render_ref` where backwards compatibility is required.
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal widgets. It
/// is currently marked as unstable as we are still evaluating the API and may make changes in the
/// future. See <https://github.com/ratatui/ratatui/issues/1287> for more information.
///
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
///
@@ -297,7 +275,7 @@ pub trait StatefulWidget {
/// # }
/// # }
/// ```
#[stability::unstable(feature = "widget-ref")]
#[instability::unstable(feature = "widget-ref")]
pub trait WidgetRef {
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget.
@@ -359,9 +337,9 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
/// to a stateful widget and render it later. It also allows you to render boxed stateful widgets.
///
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal stateful
/// widgets. Implementors should prefer to implement this over the `StatefulWidget` trait and add an
/// implementation of `StatefulWidget` that calls `StatefulWidgetRef::render_ref` where backwards
/// compatibility is required.
/// widgets. It is currently marked as unstable as we are still evaluating the API and may make
/// changes in the future. See <https://github.com/ratatui/ratatui/issues/1287> for more
/// information.
///
/// A blanket implementation of `StatefulWidget` for `&W` where `W` implements `StatefulWidgetRef`
/// is provided.
@@ -398,7 +376,7 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
/// }
/// # }
/// ```
#[stability::unstable(feature = "widget-ref")]
#[instability::unstable(feature = "widget-ref")]
pub trait StatefulWidgetRef {
/// State associated with the stateful widget.
///
@@ -444,7 +422,7 @@ impl Widget for &str {
/// [`Rect`].
impl WidgetRef for &str {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.x, area.y, self, crate::style::Style::default());
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
}
}
@@ -465,7 +443,7 @@ impl Widget for String {
/// without the need to give up ownership of the underlying text.
impl WidgetRef for String {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.x, area.y, self, crate::style::Style::default());
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
}
}
@@ -474,7 +452,7 @@ mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::prelude::*;
use crate::text::Line;
#[fixture]
fn buf() -> Buffer {
@@ -650,6 +628,13 @@ mod tests {
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_area(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
"hello world, just hello".render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
"hello world".render_ref(buf.area, &mut buf);
@@ -677,6 +662,13 @@ mod tests {
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_area(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
String::from("hello world, just hello").render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
String::from("hello world").render_ref(buf.area, &mut buf);

View File

@@ -0,0 +1,20 @@
[package]
name = "ratatui-crossterm"
version = "0.1.0"
edition = "2021"
[features]
default = ["underline-color"]
## enables the backend code that sets the underline color.
## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend,
## and is not supported on Windows 7.
underline-color = []
[dependencies]
ratatui-core = { workspace = true }
crossterm.workspace = true
instability.workspace = true
[dev-dependencies]
rstest.workspace = true

View File

@@ -0,0 +1,681 @@
//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
//! the [Crossterm] crate to interact with the terminal.
//!
//! [Crossterm]: https://crates.io/crates/crossterm
use std::io::{self, Write};
#[cfg(feature = "underline-color")]
use crossterm::style::SetUnderlineColor;
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
style::{
Attribute as CrosstermAttribute, Attributes as CrosstermAttributes,
Color as CrosstermColor, Colors, ContentStyle, Print, SetAttribute, SetBackgroundColor,
SetColors, SetForegroundColor,
},
terminal::{self, Clear},
};
use ratatui_core::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
layout::{Position, Size},
style::{Color, Modifier, Style},
};
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
///
/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is
/// used to send commands to the terminal. It provides methods for drawing content, manipulating
/// the cursor, and clearing the terminal screen.
///
/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
///
/// Usually applications will enable raw mode and switch to alternate screen mode after creating
/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
/// when the application exits). This is not done automatically by the backend because it is
/// possible that the application may want to use the terminal for other purposes (like showing
/// help text) before entering alternate screen mode.
///
/// # Example
///
/// ```rust,no_run
/// use std::io::{stderr, stdout};
///
/// use ratatui::{
/// crossterm::{
/// terminal::{
/// disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
/// },
/// ExecutableCommand,
/// },
/// prelude::*,
/// };
///
/// let mut backend = CrosstermBackend::new(stdout());
/// // or
/// let backend = CrosstermBackend::new(stderr());
/// let mut terminal = Terminal::new(backend)?;
///
/// enable_raw_mode()?;
/// stdout().execute(EnterAlternateScreen)?;
///
/// terminal.clear()?;
/// terminal.draw(|frame| {
/// // -- snip --
/// })?;
///
/// stdout().execute(LeaveAlternateScreen)?;
/// disable_raw_mode()?;
///
/// # std::io::Result::Ok(())
/// ```
///
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
/// for more details on raw mode and alternate screen.
///
/// [`Write`]: std::io::Write
/// [`Terminal`]: crate::terminal::Terminal
/// [`backend`]: crate::backend
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct CrosstermBackend<W: Write> {
/// The writer used to send commands to the terminal.
writer: W,
}
impl<W> CrosstermBackend<W>
where
W: Write,
{
/// Creates a new `CrosstermBackend` with the given writer.
///
/// Most applications will use either [`stdout`](std::io::stdout) or
/// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use.
///
/// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// ```
pub const fn new(writer: W) -> Self {
Self { writer }
}
/// Gets the writer.
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui/ratatui/pull/991"
)]
pub const fn writer(&self) -> &W {
&self.writer
}
/// Gets the writer as a mutable reference.
///
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
/// way that the Terminal implements diffing Buffers.
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui/ratatui/pull/991"
)]
pub fn writer_mut(&mut self) -> &mut W {
&mut self.writer
}
}
impl<W> Write for CrosstermBackend<W>
where
W: Write,
{
/// Writes a buffer of bytes to the underlying buffer.
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
}
/// Flushes the underlying buffer.
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
impl<W> Backend for CrosstermBackend<W>
where
W: Write,
{
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
let mut fg = Color::Reset;
let mut bg = Color::Reset;
#[cfg(feature = "underline-color")]
let mut underline_color = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<Position> = None;
for (x, y, cell) in content {
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
queue!(self.writer, MoveTo(x, y))?;
}
last_pos = Some(Position { x, y });
if cell.modifier != modifier {
let diff = ModifierDiff {
from: modifier,
to: cell.modifier,
};
diff.queue(&mut self.writer)?;
modifier = cell.modifier;
}
if cell.fg != fg || cell.bg != bg {
queue!(
self.writer,
SetColors(Colors::new(
from_ratatui_color(cell.fg),
from_ratatui_color(cell.bg)
))
)?;
fg = cell.fg;
bg = cell.bg;
}
#[cfg(feature = "underline-color")]
if cell.underline_color != underline_color {
let color = from_ratatui_color(cell.underline_color);
queue!(self.writer, SetUnderlineColor(color))?;
underline_color = cell.underline_color;
}
queue!(self.writer, Print(cell.symbol()))?;
}
#[cfg(feature = "underline-color")]
return queue!(
self.writer,
SetForegroundColor(CrosstermColor::Reset),
SetBackgroundColor(CrosstermColor::Reset),
SetUnderlineColor(CrosstermColor::Reset),
SetAttribute(CrosstermAttribute::Reset),
);
#[cfg(not(feature = "underline-color"))]
return queue!(
self.writer,
SetForegroundColor(CrosstermColor::Reset),
SetBackgroundColor(CrosstermColor::Reset),
SetAttribute(CrosstermAttribute::Reset),
);
}
fn hide_cursor(&mut self) -> io::Result<()> {
execute!(self.writer, Hide)
}
fn show_cursor(&mut self) -> io::Result<()> {
execute!(self.writer, Show)
}
fn get_cursor_position(&mut self) -> io::Result<Position> {
crossterm::cursor::position()
.map(|(x, y)| Position { x, y })
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let Position { x, y } = position.into();
execute!(self.writer, MoveTo(x, y))
}
fn clear(&mut self) -> io::Result<()> {
self.clear_region(ClearType::All)
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
execute!(
self.writer,
Clear(match clear_type {
ClearType::All => crossterm::terminal::ClearType::All,
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
})
)
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
for _ in 0..n {
queue!(self.writer, Print("\n"))?;
}
self.writer.flush()
}
fn size(&self) -> io::Result<Size> {
let (width, height) = terminal::size()?;
Ok(Size { width, height })
}
fn window_size(&mut self) -> io::Result<WindowSize> {
let crossterm::terminal::WindowSize {
columns,
rows,
width,
height,
} = terminal::window_size()?;
Ok(WindowSize {
columns_rows: Size {
width: columns,
height: rows,
},
pixels: Size { width, height },
})
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
fn from_ratatui_color(color: Color) -> CrosstermColor {
match color {
Color::Reset => CrosstermColor::Reset,
Color::Black => CrosstermColor::Black,
Color::Red => CrosstermColor::DarkRed,
Color::Green => CrosstermColor::DarkGreen,
Color::Yellow => CrosstermColor::DarkYellow,
Color::Blue => CrosstermColor::DarkBlue,
Color::Magenta => CrosstermColor::DarkMagenta,
Color::Cyan => CrosstermColor::DarkCyan,
Color::Gray => CrosstermColor::Grey,
Color::DarkGray => CrosstermColor::DarkGrey,
Color::LightRed => CrosstermColor::Red,
Color::LightGreen => CrosstermColor::Green,
Color::LightBlue => CrosstermColor::Blue,
Color::LightYellow => CrosstermColor::Yellow,
Color::LightMagenta => CrosstermColor::Magenta,
Color::LightCyan => CrosstermColor::Cyan,
Color::White => CrosstermColor::White,
Color::Indexed(i) => CrosstermColor::AnsiValue(i),
Color::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b },
}
}
fn from_crossterm_color(value: CrosstermColor) -> Color {
match value {
CrosstermColor::Reset => Color::Reset,
CrosstermColor::Black => Color::Black,
CrosstermColor::DarkRed => Color::Red,
CrosstermColor::DarkGreen => Color::Green,
CrosstermColor::DarkYellow => Color::Yellow,
CrosstermColor::DarkBlue => Color::Blue,
CrosstermColor::DarkMagenta => Color::Magenta,
CrosstermColor::DarkCyan => Color::Cyan,
CrosstermColor::Grey => Color::Gray,
CrosstermColor::DarkGrey => Color::DarkGray,
CrosstermColor::Red => Color::LightRed,
CrosstermColor::Green => Color::LightGreen,
CrosstermColor::Blue => Color::LightBlue,
CrosstermColor::Yellow => Color::LightYellow,
CrosstermColor::Magenta => Color::LightMagenta,
CrosstermColor::Cyan => Color::LightCyan,
CrosstermColor::White => Color::White,
CrosstermColor::Rgb { r, g, b } => Color::Rgb(r, g, b),
CrosstermColor::AnsiValue(v) => Color::Indexed(v),
}
}
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
struct ModifierDiff {
pub from: Modifier,
pub to: Modifier,
}
impl ModifierDiff {
fn queue<W>(self, mut w: W) -> io::Result<()>
where
W: io::Write,
{
//use crossterm::Attribute;
let removed = self.from - self.to;
if removed.contains(Modifier::REVERSED) {
queue!(w, SetAttribute(CrosstermAttribute::NoReverse))?;
}
if removed.contains(Modifier::BOLD) {
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
if self.to.contains(Modifier::DIM) {
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
}
}
if removed.contains(Modifier::ITALIC) {
queue!(w, SetAttribute(CrosstermAttribute::NoItalic))?;
}
if removed.contains(Modifier::UNDERLINED) {
queue!(w, SetAttribute(CrosstermAttribute::NoUnderline))?;
}
if removed.contains(Modifier::DIM) {
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
}
if removed.contains(Modifier::CROSSED_OUT) {
queue!(w, SetAttribute(CrosstermAttribute::NotCrossedOut))?;
}
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
queue!(w, SetAttribute(CrosstermAttribute::NoBlink))?;
}
let added = self.to - self.from;
if added.contains(Modifier::REVERSED) {
queue!(w, SetAttribute(CrosstermAttribute::Reverse))?;
}
if added.contains(Modifier::BOLD) {
queue!(w, SetAttribute(CrosstermAttribute::Bold))?;
}
if added.contains(Modifier::ITALIC) {
queue!(w, SetAttribute(CrosstermAttribute::Italic))?;
}
if added.contains(Modifier::UNDERLINED) {
queue!(w, SetAttribute(CrosstermAttribute::Underlined))?;
}
if added.contains(Modifier::DIM) {
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
}
if added.contains(Modifier::CROSSED_OUT) {
queue!(w, SetAttribute(CrosstermAttribute::CrossedOut))?;
}
if added.contains(Modifier::SLOW_BLINK) {
queue!(w, SetAttribute(CrosstermAttribute::SlowBlink))?;
}
if added.contains(Modifier::RAPID_BLINK) {
queue!(w, SetAttribute(CrosstermAttribute::RapidBlink))?;
}
Ok(())
}
}
fn from_crossterm_attribute(value: CrosstermAttribute) -> Modifier {
// `Attribute*s*` (note the *s*) contains multiple `Attribute`
// We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
// the conversion again
from_crossterm_attributes(CrosstermAttributes::from(value))
}
fn from_crossterm_attributes(value: CrosstermAttributes) -> Modifier {
let mut res = Modifier::empty();
if value.has(CrosstermAttribute::Bold) {
res |= Modifier::BOLD;
}
if value.has(CrosstermAttribute::Dim) {
res |= Modifier::DIM;
}
if value.has(CrosstermAttribute::Italic) {
res |= Modifier::ITALIC;
}
if value.has(CrosstermAttribute::Underlined)
|| value.has(CrosstermAttribute::DoubleUnderlined)
|| value.has(CrosstermAttribute::Undercurled)
|| value.has(CrosstermAttribute::Underdotted)
|| value.has(CrosstermAttribute::Underdashed)
{
res |= Modifier::UNDERLINED;
}
if value.has(CrosstermAttribute::SlowBlink) {
res |= Modifier::SLOW_BLINK;
}
if value.has(CrosstermAttribute::RapidBlink) {
res |= Modifier::RAPID_BLINK;
}
if value.has(CrosstermAttribute::Reverse) {
res |= Modifier::REVERSED;
}
if value.has(CrosstermAttribute::Hidden) {
res |= Modifier::HIDDEN;
}
if value.has(CrosstermAttribute::CrossedOut) {
res |= Modifier::CROSSED_OUT;
}
res
}
fn from_crossterm_style(value: ContentStyle) -> Style {
let mut sub_modifier = Modifier::empty();
if value.attributes.has(CrosstermAttribute::NoBold) {
sub_modifier |= Modifier::BOLD;
}
if value.attributes.has(CrosstermAttribute::NoItalic) {
sub_modifier |= Modifier::ITALIC;
}
if value.attributes.has(CrosstermAttribute::NotCrossedOut) {
sub_modifier |= Modifier::CROSSED_OUT;
}
if value.attributes.has(CrosstermAttribute::NoUnderline) {
sub_modifier |= Modifier::UNDERLINED;
}
if value.attributes.has(CrosstermAttribute::NoHidden) {
sub_modifier |= Modifier::HIDDEN;
}
if value.attributes.has(CrosstermAttribute::NoBlink) {
sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
}
if value.attributes.has(CrosstermAttribute::NoReverse) {
sub_modifier |= Modifier::REVERSED;
}
Style {
fg: value.foreground_color.map(from_crossterm_color),
bg: value.background_color.map(from_crossterm_color),
#[cfg(feature = "underline-color")]
underline_color: value.underline_color.map(from_crossterm_color),
add_modifier: from_crossterm_attributes(value.attributes),
sub_modifier,
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case(CrosstermColor::Reset, Color::Reset)]
#[case(CrosstermColor::Black, Color::Black)]
#[case(CrosstermColor::DarkGrey, Color::DarkGray)]
#[case(CrosstermColor::Red, Color::LightRed)]
#[case(CrosstermColor::DarkRed, Color::Red)]
#[case(CrosstermColor::Green, Color::LightGreen)]
#[case(CrosstermColor::DarkGreen, Color::Green)]
#[case(CrosstermColor::Yellow, Color::LightYellow)]
#[case(CrosstermColor::DarkYellow, Color::Yellow)]
#[case(CrosstermColor::Blue, Color::LightBlue)]
#[case(CrosstermColor::DarkBlue, Color::Blue)]
#[case(CrosstermColor::Magenta, Color::LightMagenta)]
#[case(CrosstermColor::DarkMagenta, Color::Magenta)]
#[case(CrosstermColor::Cyan, Color::LightCyan)]
#[case(CrosstermColor::DarkCyan, Color::Cyan)]
#[case(CrosstermColor::White, Color::White)]
#[case(CrosstermColor::Grey, Color::Gray)]
#[case(CrosstermColor::Rgb { r: 0, g: 0, b: 0 }, Color::Rgb(0, 0, 0))]
#[case(CrosstermColor::Rgb { r: 10, g: 20, b: 30 }, Color::Rgb(10, 20, 30))]
#[case(CrosstermColor::AnsiValue(32), Color::Indexed(32))]
#[case(CrosstermColor::AnsiValue(37), Color::Indexed(37))]
fn convert_from_crossterm_color(#[case] value: CrosstermColor, #[case] expected: Color) {
assert_eq!(from_crossterm_color(value), expected);
}
mod modifier {
use super::*;
#[rstest]
#[rstest]
#[case(CrosstermAttribute::Reset, Modifier::empty())]
#[case(CrosstermAttribute::Bold, Modifier::BOLD)]
#[case(CrosstermAttribute::Italic, Modifier::ITALIC)]
#[case(CrosstermAttribute::Underlined, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::DoubleUnderlined, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::Underdotted, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::Dim, Modifier::DIM)]
#[case(CrosstermAttribute::NormalIntensity, Modifier::empty())]
#[case(CrosstermAttribute::CrossedOut, Modifier::CROSSED_OUT)]
#[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
#[case(CrosstermAttribute::OverLined, Modifier::empty())]
#[case(CrosstermAttribute::SlowBlink, Modifier::SLOW_BLINK)]
#[case(CrosstermAttribute::RapidBlink, Modifier::RAPID_BLINK)]
#[case(CrosstermAttribute::Hidden, Modifier::HIDDEN)]
#[case(CrosstermAttribute::NoHidden, Modifier::empty())]
#[case(CrosstermAttribute::Reverse, Modifier::REVERSED)]
fn convert_from_crossterm_attribute(
#[case] value: CrosstermAttribute,
#[case] expected: Modifier,
) {
assert_eq!(from_crossterm_attribute(value), expected);
}
#[rstest]
#[case(&[CrosstermAttribute::Bold], Modifier::BOLD)]
#[case(
&[CrosstermAttribute::Bold, CrosstermAttribute::Italic],
Modifier::BOLD | Modifier::ITALIC
)]
#[case(
&[CrosstermAttribute::Bold, CrosstermAttribute::NotCrossedOut],
Modifier::BOLD
)]
#[case(
&[CrosstermAttribute::Dim, CrosstermAttribute::Underdotted],
Modifier::DIM | Modifier::UNDERLINED
)]
#[case(
&[CrosstermAttribute::Dim, CrosstermAttribute::SlowBlink, CrosstermAttribute::Italic],
Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC
)]
#[case(
&[CrosstermAttribute::Hidden, CrosstermAttribute::NoUnderline, CrosstermAttribute::NotCrossedOut],
Modifier::HIDDEN
)]
#[case(
&[CrosstermAttribute::Reverse],
Modifier::REVERSED
)]
#[case(
&[CrosstermAttribute::Reset],
Modifier::empty()
)]
#[case(
&[CrosstermAttribute::RapidBlink, CrosstermAttribute::CrossedOut],
Modifier::RAPID_BLINK | Modifier::CROSSED_OUT
)]
#[case(
&[CrosstermAttribute::DoubleUnderlined, CrosstermAttribute::OverLined],
Modifier::UNDERLINED
)]
#[case(
&[CrosstermAttribute::Undercurled, CrosstermAttribute::Underdashed],
Modifier::UNDERLINED
)]
#[case(
&[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic],
Modifier::empty()
)]
#[case(
&[CrosstermAttribute::NoBlink, CrosstermAttribute::NoReverse],
Modifier::empty()
)]
fn convert_from_crossterm_attributes(
#[case] value: &[CrosstermAttribute],
#[case] expected: Modifier,
) {
assert_eq!(
from_crossterm_attributes(CrosstermAttributes::from(value)),
expected
);
}
}
#[rstest]
#[case(ContentStyle::default(), Style::default())]
#[case(
ContentStyle {
foreground_color: Some(CrosstermColor::DarkYellow),
..Default::default()
},
Style::default().fg(Color::Yellow)
)]
#[case(
ContentStyle {
background_color: Some(CrosstermColor::DarkYellow),
..Default::default()
},
Style::default().bg(Color::Yellow)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Bold),
..Default::default()
},
Style::default().add_modifier(Modifier::BOLD)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoBold),
..Default::default()
},
Style::default().remove_modifier(Modifier::BOLD)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Italic),
..Default::default()
},
Style::default().add_modifier(Modifier::ITALIC)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoItalic),
..Default::default()
},
Style::default().remove_modifier(Modifier::ITALIC)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(
[CrosstermAttribute::Bold, CrosstermAttribute::Italic].as_ref()
),
..Default::default()
},
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(
[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic].as_ref()
),
..Default::default()
},
Style::default()
.remove_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
)]
#[cfg(feature = "underline-color")]
#[case(
ContentStyle {
underline_color: Some(CrosstermColor::DarkRed),
..Default::default()
},
Style::default().underline_color(Color::Red)
)]
fn convert_from_crossterm_content_style(#[case] value: ContentStyle, #[case] expected: Style) {
assert_eq!(from_crossterm_style(value), expected);
}
}

View File

@@ -0,0 +1,9 @@
[package]
name = "ratatui-termion"
version = "0.1.0"
edition = "2021"
[dependencies]
instability = { workspace = true }
ratatui-core = { workspace = true }
termion.workspace = true

View File

@@ -9,14 +9,13 @@ use std::{
io::{self, Write},
};
use termion::{color as tcolor, style as tstyle};
use crate::{
use ratatui_core::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
prelude::Rect,
layout::{Position, Size},
style::{Color, Modifier, Style},
};
use termion::{color as tcolor, color::Color as _, style as tstyle};
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
///
@@ -40,8 +39,10 @@ use crate::{
/// ```rust,no_run
/// use std::io::{stderr, stdout};
///
/// use ratatui::prelude::*;
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
/// use ratatui::{
/// prelude::*,
/// termion::{raw::IntoRawMode, screen::IntoAlternateScreen},
/// };
///
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
/// let mut backend = TermionBackend::new(writer);
@@ -75,6 +76,11 @@ where
{
/// Creates a new Termion backend with the given writer.
///
/// Most applications will use either [`stdout`](std::io::stdout) or
/// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use.
///
/// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr
///
/// # Example
///
/// ```rust,no_run
@@ -85,6 +91,26 @@ where
pub const fn new(writer: W) -> Self {
Self { writer }
}
/// Gets the writer.
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui/ratatui/pull/991"
)]
pub const fn writer(&self) -> &W {
&self.writer
}
/// Gets the writer as a mutable reference.
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
/// way that the Terminal implements diffing Buffers.
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui/ratatui/pull/991"
)]
pub fn writer_mut(&mut self) -> &mut W {
&mut self.writer
}
}
impl<W> Write for TermionBackend<W>
@@ -136,11 +162,13 @@ where
self.writer.flush()
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer).map(|(x, y)| (x - 1, y - 1))
fn get_cursor_position(&mut self) -> io::Result<Position> {
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer)
.map(|(x, y)| Position { x: x - 1, y: y - 1 })
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let Position { x, y } = position.into();
write!(self.writer, "{}", termion::cursor::Goto(x + 1, y + 1))?;
self.writer.flush()
}
@@ -155,13 +183,13 @@ where
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
let mut last_pos: Option<Position> = None;
for (x, y, cell) in content {
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap();
}
last_pos = Some((x, y));
last_pos = Some(Position { x, y });
if cell.modifier != modifier {
write!(
string,
@@ -193,9 +221,9 @@ where
)
}
fn size(&self) -> io::Result<Rect> {
fn size(&self) -> io::Result<Size> {
let terminal = termion::terminal_size()?;
Ok(Rect::new(0, 0, terminal.0, terminal.1))
Ok(Size::new(terminal.0, terminal.1))
}
fn window_size(&mut self) -> io::Result<WindowSize> {
@@ -223,7 +251,6 @@ struct ModifierDiff {
impl fmt::Display for Fg {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use termion::color::Color as TermionColor;
match self.0 {
Color::Reset => termion::color::Reset.write_fg(f),
Color::Black => termion::color::Black.write_fg(f),
@@ -249,7 +276,6 @@ impl fmt::Display for Fg {
}
impl fmt::Display for Bg {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use termion::color::Color as TermionColor;
match self.0 {
Color::Reset => termion::color::Reset.write_bg(f),
Color::Black => termion::color::Black.write_bg(f),
@@ -275,7 +301,7 @@ impl fmt::Display for Bg {
}
macro_rules! from_termion_for_color {
($termion_color:ident, $color: ident) => {
($termion_color:ident, $color:ident) => {
impl From<tcolor::$termion_color> for Color {
fn from(_: tcolor::$termion_color) -> Self {
Color::$color
@@ -416,7 +442,7 @@ impl fmt::Display for ModifierDiff {
}
macro_rules! from_termion_for_modifier {
($termion_modifier:ident, $modifier: ident) => {
($termion_modifier:ident, $modifier:ident) => {
impl From<tstyle::$termion_modifier> for Modifier {
fn from(_: tstyle::$termion_modifier) -> Self {
Modifier::$modifier

View File

@@ -0,0 +1,7 @@
[package]
name = "ratatui-termwiz"
version = "0.1.0"
edition = "2021"
[dependencies]
ratatui-core = { workspace = true }

View File

@@ -7,20 +7,18 @@
use std::{error::Error, io};
use termwiz::{
caps::Capabilities,
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
surface::{Change, CursorVisibility, Position},
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
};
use crate::{
backend::{Backend, WindowSize},
buffer::Cell,
layout::Size,
prelude::Rect,
style::{Color, Modifier, Style},
termwiz::{
caps::Capabilities,
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
surface::{Change, CursorVisibility, Position},
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
},
};
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
@@ -59,7 +57,7 @@ use crate::{
/// [`Terminal`]: crate::terminal::Terminal
/// [`BufferedTerminal`]: termwiz::terminal::buffered::BufferedTerminal
/// [Termwiz]: https://crates.io/crates/termwiz
/// [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
/// [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
pub struct TermwizBackend {
buffered_terminal: BufferedTerminal<SystemTerminal>,
}
@@ -193,12 +191,16 @@ impl Backend for TermwizBackend {
Ok(())
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
fn get_cursor_position(&mut self) -> io::Result<crate::layout::Position> {
let (x, y) = self.buffered_terminal.cursor_position();
Ok((x as u16, y as u16))
Ok((x as u16, y as u16).into())
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
fn set_cursor_position<P: Into<crate::layout::Position>>(
&mut self,
position: P,
) -> io::Result<()> {
let crate::layout::Position { x, y } = position.into();
self.buffered_terminal.add_change(Change::CursorPosition {
x: Position::Absolute(x as usize),
y: Position::Absolute(y as usize),
@@ -213,9 +215,9 @@ impl Backend for TermwizBackend {
Ok(())
}
fn size(&self) -> io::Result<Rect> {
fn size(&self) -> io::Result<Size> {
let (cols, rows) = self.buffered_terminal.dimensions();
Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows)))
Ok(Size::new(u16_max(cols), u16_max(rows)))
}
fn window_size(&mut self) -> io::Result<WindowSize> {

View File

@@ -0,0 +1,40 @@
[package]
name = "ratatui-widgets"
version = "0.1.0"
edition = "2021"
[features]
# TODO: remove unstable-widget-ref, consider whether to keep all-widgets
default = ["all-widgets", "unstable-widget-ref"]
## enables serialization and deserialization of style and color types using the [`serde`] crate.
## This is useful if you want to save themes to a file.
serde = ["dep:serde", "bitflags/serde"]
## enables all widgets.
all-widgets = ["widget-calendar"]
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
#! dependencies. The available features are:
## enables the [`calendar`](widgets::calendar) widget module and adds a dependency on [`time`].
widget-calendar = ["dep:time"]
unstable-widget-ref = ["ratatui-core/unstable-widget-ref"]
[dependencies]
bitflags.workspace = true
instability.workspace = true
itertools.workspace = true
ratatui-core.workspace = true
serde = { workspace = true, optional = true }
strum.workspace = true
time = { workspace = true, optional = true }
unicode-segmentation.workspace = true
unicode-width.workspace = true
[dev-dependencies]
rstest.workspace = true
indoc.workspace = true
pretty_assertions.workspace = true
time.workspace = true

View File

@@ -1,4 +1,8 @@
use crate::{prelude::*, widgets::Block};
use ratatui_core::{
prelude::{symbols, Buffer, Direction, Line, Rect, Style, Stylize, Widget},
style::Styled,
widgets::WidgetRef,
};
mod bar;
mod bar_group;
@@ -6,6 +10,8 @@ mod bar_group;
pub use bar::Bar;
pub use bar_group::BarGroup;
use crate::{block::BlockExt, Block};
/// A chart showing values as [bars](Bar).
///
/// Here is a possible `BarChart` output.
@@ -431,7 +437,7 @@ impl BarChart<'_> {
} else {
self.bar_set.empty
};
buf.get_mut(bars_area.left() + x, bar_y)
buf[(bars_area.left() + x, bar_y)]
.set_symbol(symbol)
.set_style(bar_style);
}
@@ -507,7 +513,7 @@ impl BarChart<'_> {
let bar_style = self.bar_style.patch(bar.style);
for x in 0..self.bar_width {
buf.get_mut(bar_x + x, area.top() + j)
buf[(bar_x + x, area.top() + j)]
.set_symbol(symbol)
.set_style(bar_style);
}
@@ -613,9 +619,14 @@ impl<'a> Styled for BarChart<'a> {
#[cfg(test)]
mod tests {
use itertools::iproduct;
use ratatui_core::{
layout::Alignment,
style::{Color, Modifier},
text::Span,
};
use super::*;
use crate::widgets::BorderType;
use crate::BorderType;
#[test]
fn default() {
@@ -698,7 +709,7 @@ mod tests {
"f b ",
]);
for (x, y) in iproduct!([0, 2], [0, 1]) {
expected.get_mut(x, y).set_fg(Color::Red);
expected[(x, y)].set_fg(Color::Red);
}
assert_eq!(buffer, expected);
}
@@ -790,8 +801,8 @@ mod tests {
"█1█ █2█ ",
"foo bar ",
]);
expected.get_mut(1, 1).set_fg(Color::Red);
expected.get_mut(5, 1).set_fg(Color::Red);
expected[(1, 1)].set_fg(Color::Red);
expected[(5, 1)].set_fg(Color::Red);
assert_eq!(buffer, expected);
}
@@ -808,8 +819,8 @@ mod tests {
"1 2 ",
"f b ",
]);
expected.get_mut(0, 2).set_fg(Color::Red);
expected.get_mut(2, 2).set_fg(Color::Red);
expected[(0, 2)].set_fg(Color::Red);
expected[(2, 2)].set_fg(Color::Red);
assert_eq!(buffer, expected);
}
@@ -827,7 +838,7 @@ mod tests {
"f b ",
]);
for (x, y) in iproduct!(0..10, 0..3) {
expected.get_mut(x, y).set_fg(Color::Red);
expected[(x, y)].set_fg(Color::Red);
}
assert_eq!(buffer, expected);
}
@@ -958,27 +969,23 @@ mod tests {
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);
expected[(0, 1)].modifier.insert(Modifier::ITALIC);
for x in 0..5 {
expected.get_mut(x, 1).set_fg(Color::Yellow);
expected[(x, 1)].set_fg(Color::Yellow);
}
let expected_color = if let Some(color) = bar_color {
color
} else {
Color::Yellow
};
let expected_color = bar_color.unwrap_or(Color::Yellow);
// second line contains the word "label". Since the bar value is 2,
// then the first 2 characters of "label" are italic red.
// the rest is white (using the Bar's style).
let cell = expected.get_mut(0, 0).set_fg(Color::Red);
let cell = expected[(0, 0)].set_fg(Color::Red);
cell.modifier.insert(Modifier::ITALIC);
let cell = expected.get_mut(1, 0).set_fg(Color::Red);
let cell = expected[(1, 0)].set_fg(Color::Red);
cell.modifier.insert(Modifier::ITALIC);
expected.get_mut(2, 0).set_fg(expected_color);
expected.get_mut(3, 0).set_fg(expected_color);
expected.get_mut(4, 0).set_fg(expected_color);
expected[(2, 0)].set_fg(expected_color);
expected[(3, 0)].set_fg(expected_color);
expected[(4, 0)].set_fg(expected_color);
assert_eq!(buffer, expected);
}
@@ -1031,9 +1038,9 @@ mod tests {
// bold: because of BarChart::label_style
// red: is included with the label itself
let mut expected = Buffer::with_lines(["2████", "G1 "]);
let cell = expected.get_mut(0, 1).set_fg(Color::Red);
let cell = expected[(0, 1)].set_fg(Color::Red);
cell.modifier.insert(Modifier::BOLD);
let cell = expected.get_mut(1, 1).set_fg(Color::Red);
let cell = expected[(1, 1)].set_fg(Color::Red);
cell.modifier.insert(Modifier::BOLD);
assert_eq!(buffer, expected);

View File

@@ -1,8 +1,7 @@
use ratatui_core::prelude::{Buffer, Line, Rect, Style, Widget};
use unicode_width::UnicodeWidthStr;
use crate::prelude::*;
/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget.
/// A bar to be shown by the [`BarChart`](crate::BarChart) widget.
///
/// Here is an explanation of a `Bar`'s components.
/// ```plain
@@ -61,7 +60,7 @@ impl<'a> Bar<'a> {
/// display the label **under** the bar.
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars,
/// display the label **in** the bar.
/// See [`BarChart::direction`](crate::widgets::BarChart::direction) to set the direction.
/// See [`BarChart::direction`](crate::BarChart::direction) to set the direction.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn label(mut self, label: Line<'a>) -> Self {
self.label = Some(label);

View File

@@ -1,5 +1,6 @@
use ratatui_core::prelude::*;
use super::Bar;
use crate::prelude::*;
/// A group of bars to be shown by the Barchart.
///
@@ -78,7 +79,8 @@ impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {
impl<'a, const N: usize> From<&[(&'a str, u64); N]> for BarGroup<'a> {
fn from(value: &[(&'a str, u64); N]) -> Self {
Self::from(value.as_ref())
let value: &[(&'a str, u64)] = value.as_ref();
Self::from(value)
}
}

View File

@@ -6,15 +6,23 @@
//! [title](Block::title) and [padding](Block::padding).
use itertools::Itertools;
use ratatui_core::{
prelude::{Alignment, Buffer, Line, Rect, Style, Stylize, Widget},
style::Styled,
symbols::border,
widgets::WidgetRef,
};
use strum::{Display, EnumString};
use crate::{prelude::*, symbols::border, widgets::Borders};
use self::title::Position;
mod padding;
pub mod title;
pub use padding::Padding;
pub use title::{Position, Title};
pub use title::Title;
use crate::Borders;
/// Base widget to be used to display a box border around all [upper level ones](crate::widgets).
///
@@ -27,14 +35,42 @@ pub use title::{Position, Title};
/// both centered and non-centered titles are rendered, the centered space is calculated based on
/// the full width of the block, rather than the leftover width.
///
/// Titles are not rendered in the corners of the block unless there is no border on that edge.
/// If the block is too small and multiple titles overlap, the border may get cut off at a corner.
/// Titles are not rendered in the corners of the block unless there is no border on that edge. If
/// the block is too small and multiple titles overlap, the border may get cut off at a corner.
///
/// ```plain
/// ┌With at least a left border───
///
/// Without left border───
/// ```
/// # Constructor methods
///
/// - [`Block::new`] creates a new [`Block`] with no border or paddings.
/// - [`Block::bordered`] Create a new block with all borders shown.
///
/// # Setter methods
///
/// These methods are fluent setters. They return a new [`Block`] with the specified property set.
///
/// - [`Block::borders`] Defines which borders to display.
/// - [`Block::border_style`] Defines the style of the borders.
/// - [`Block::border_type`] Sets the symbols used to display the border (e.g. single line, double
/// line, thick or rounded borders).
/// - [`Block::padding`] Defines the padding inside a [`Block`].
/// - [`Block::style`] Sets the base style of the widget.
/// - [`Block::title`] Adds a title to the block.
/// - [`Block::title_alignment`] Sets the default [`Alignment`] for all block titles.
/// - [`Block::title_style`] Applies the style to all titles.
/// - [`Block::title_top`] Adds a title to the top of the block.
/// - [`Block::title_bottom`] Adds a title to the bottom of the block.
/// - [`Block::title_position`] Adds a title to the block.
///
/// # Other Methods
/// - [`Block::inner`] Compute the inner area of a block based on its border visibility rules.
///
/// [`Style`]s are applied first to the entire block, then to the borders, and finally to the
/// titles. If the block is used as a container for another widget, the inner widget can also be
/// styled. See [`Style`] for more information on how merging styles works.
///
/// # Examples
///
@@ -53,17 +89,34 @@ pub use title::{Position, Title};
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// };
///
/// Block::new()
/// .title("Title 1")
/// .title(Title::from("Title 2").position(Position::Bottom));
/// ```
///
/// You can also pass it as parameters of another widget so that the block surrounds them:
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{Block, Borders, List},
/// };
///
/// let surrounding_block = Block::default()
/// .borders(Borders::ALL)
/// .title("Here is a list of items");
/// let items = ["Item 1", "Item 2", "Item 3"];
/// let list = List::new(items).block(surrounding_block);
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Block<'a> {
/// List of titles
titles: Vec<Title<'a>>,
titles: Vec<(Option<Position>, Line<'a>)>,
/// The style to be patched to all titles of the block
titles_style: Style,
/// The default alignment of the titles that don't have one
@@ -168,7 +221,7 @@ impl<'a> Block<'a> {
border_style: Style::new(),
border_set: BorderType::Plain.to_border_set(),
style: Style::new(),
padding: Padding::zero(),
padding: Padding::ZERO,
}
}
@@ -198,9 +251,9 @@ impl<'a> Block<'a> {
/// [spans](crate::text::Span) (`Vec<Span>`).
///
/// By default, the titles will avoid being rendered in the corners of the block but will align
/// against the left or right edge of the block if there is no border on that edge.
/// The following demonstrates this behavior, notice the second title is one character off to
/// the left.
/// against the left or right edge of the block if there is no border on that edge. The
/// following demonstrates this behavior, notice the second title is one character off to the
/// left.
///
/// ```plain
/// ┌With at least a left border───
@@ -211,12 +264,15 @@ impl<'a> Block<'a> {
/// Note: If the block is too small and multiple titles overlap, the border might get cut off at
/// a corner.
///
/// # Example
/// # Examples
///
/// See the [Block example] for a visual representation of how the various borders and styles
/// look when rendered.
///
/// The following example demonstrates:
/// - Default title alignment
/// - Multiple titles (notice "Center" is centered according to the full with of the block, not
/// the leftover space)
/// the leftover space)
/// - Two titles with the same alignment (notice the left titles are separated)
/// ```
/// use ratatui::{
@@ -226,9 +282,9 @@ impl<'a> Block<'a> {
///
/// 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))
/// .title(Title::from("Center").alignment(Alignment::Center));
/// .title(Line::from("Left").left_aligned()) // also on the left
/// .title(Line::from("Right").right_aligned())
/// .title(Line::from("Center").centered());
/// // Renders
/// // ┌Title─Left────Center─────────Right┐
/// ```
@@ -239,12 +295,27 @@ impl<'a> Block<'a> {
/// - [`Block::title_style`]
/// - [`Block::title_alignment`]
/// - [`Block::title_position`]
///
/// # Future improvements
///
/// In a future release of Ratatui this method will be changed to accept `Into<Line>` instead of
/// `Into<Title>`. This allows us to remove the unnecessary `Title` struct and store the
/// position in the block itself. For more information see
/// <https://github.com/ratatui/ratatui/issues/738>.
///
/// [Block example]: https://github.com/ratatui/ratatui/blob/main/examples/README.md#block
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title<T>(mut self, title: T) -> Self
where
T: Into<Title<'a>>,
{
self.titles.push(title.into());
let title = title.into();
let position = title.position;
let mut content = title.content;
if let Some(alignment) = title.alignment {
content = content.alignment(alignment);
}
self.titles.push((position, content));
self
}
@@ -271,8 +342,8 @@ impl<'a> Block<'a> {
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title_top<T: Into<Line<'a>>>(mut self, title: T) -> Self {
let title = Title::from(title).position(Position::Top);
self.titles.push(title);
let line = title.into();
self.titles.push((Some(Position::Top), line));
self
}
@@ -299,17 +370,21 @@ impl<'a> Block<'a> {
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title_bottom<T: Into<Line<'a>>>(mut self, title: T) -> Self {
let title = Title::from(title).position(Position::Bottom);
self.titles.push(title);
let line = title.into();
self.titles.push((Some(Position::Bottom), line));
self
}
/// Applies the style to all titles.
///
/// This style will be applied to all titles of the block. If a title has a style set, it will
/// be applied after this style. This style will be applied after any [`Block::style`] or
/// [`Block::border_style`] is applied.
///
/// See [`Style`] for more information on how merging styles works.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// If a [`Title`] already has a style, the title's style will add on top of this one.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title_style<S: Into<Style>>(mut self, style: S) -> Self {
self.titles_style = style.into();
@@ -333,7 +408,7 @@ impl<'a> Block<'a> {
/// Block::new()
/// .title_alignment(Alignment::Center)
/// // This title won't be aligned in the center
/// .title(Title::from("right").alignment(Alignment::Right))
/// .title(Line::from("right").right_aligned())
/// .title("foo")
/// .title("bar");
/// ```
@@ -354,13 +429,16 @@ impl<'a> Block<'a> {
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// };
///
/// Block::new()
/// .title_position(Position::Bottom)
/// // This title won't be aligned in the center
/// .title(Title::from("top").position(Position::Top))
/// .title_top("top")
/// .title("foo")
/// .title("bar");
/// ```
@@ -372,7 +450,10 @@ impl<'a> Block<'a> {
/// Defines the style of the borders.
///
/// If a [`Block::style`] is defined, `border_style` will be applied on top of it.
/// This style is applied only to the areas covered by borders, and is applied to the block
/// after any [`Block::style`] is applied.
///
/// See [`Style`] for more information on how merging styles works.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
@@ -390,16 +471,39 @@ impl<'a> Block<'a> {
self
}
/// Defines the block style.
/// Defines the style of the entire block.
///
/// This is the most generic [`Style`] a block can receive, it will be merged with any other
/// more specific style. Elements can be styled further with [`Block::title_style`] and
/// [`Block::border_style`].
/// more specific styles. Elements can be styled further with [`Block::title_style`] and
/// [`Block::border_style`], which will be applied on top of this style. If the block is used as
/// a container for another widget (e.g. a [`Paragraph`]), then the style of the widget is
/// generally applied before this style.
///
/// See [`Style`] for more information on how merging styles works.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This will also apply to the widget inside that block, unless the inner widget is styled.
/// # Example
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// let block = Block::new().style(Style::new().red().on_black());
///
/// // For border and title you can additionally apply styles on top of the block level style.
/// let block = Block::new()
/// .style(Style::new().red().bold().italic())
/// .border_style(Style::new().not_italic()) // will be red and bold
/// .title_style(Style::new().not_bold()) // will be red and italic
/// .title("Title");
///
/// // To style the inner widget, you can style the widget itself.
/// let paragraph = Paragraph::new("Content")
/// .block(block)
/// .style(Style::new().white().not_bold()); // will be white, and italic
/// ```
///
/// [`Paragraph`]: crate::Paragraph
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -469,6 +573,38 @@ impl<'a> Block<'a> {
self
}
/// Defines the padding inside a `Block`.
///
/// See [`Padding`] for more information.
///
/// # Examples
///
/// This renders a `Block` with no padding (the default).
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::bordered().padding(Padding::ZERO);
/// // Renders
/// // ┌───────┐
/// // │content│
/// // └───────┘
/// ```
///
/// This example shows a `Block` with padding left and right ([`Padding::horizontal`]).
/// Notice the two spaces before and after the content.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::bordered().padding(Padding::horizontal(2));
/// // Renders
/// // ┌───────────┐
/// // │ content │
/// // └───────────┘
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn padding(mut self, padding: Padding) -> Self {
self.padding = padding;
self
}
/// Compute the inner area of a block based on its border visibility rules.
///
/// # Examples
@@ -480,7 +616,7 @@ impl<'a> Block<'a> {
/// let outer_block = Block::bordered().title("Outer");
/// let inner_block = Block::bordered().title("Inner");
///
/// let outer_area = frame.size();
/// let outer_area = frame.area();
/// let inner_area = outer_block.inner(outer_area);
///
/// frame.render_widget(outer_block, outer_area);
@@ -527,39 +663,7 @@ impl<'a> Block<'a> {
fn has_title_at_position(&self, position: Position) -> bool {
self.titles
.iter()
.any(|title| title.position.unwrap_or(self.titles_position) == position)
}
/// Defines the padding inside a `Block`.
///
/// See [`Padding`] for more information.
///
/// # Examples
///
/// This renders a `Block` with no padding (the default).
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::bordered().padding(Padding::zero());
/// // Renders
/// // ┌───────┐
/// // │content│
/// // └───────┘
/// ```
///
/// This example shows a `Block` with padding left and right ([`Padding::horizontal`]).
/// Notice the two spaces before and after the content.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::bordered().padding(Padding::horizontal(2));
/// // Renders
/// // ┌───────────┐
/// // │ content │
/// // └───────────┘
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn padding(mut self, padding: Padding) -> Self {
self.padding = padding;
self
.any(|(pos, _)| pos.unwrap_or(self.titles_position) == position)
}
}
@@ -628,7 +732,7 @@ impl Block<'_> {
fn render_left_side(&self, area: Rect, buf: &mut Buffer) {
if self.borders.contains(Borders::LEFT) {
for y in area.top()..area.bottom() {
buf.get_mut(area.left(), y)
buf[(area.left(), y)]
.set_symbol(self.border_set.vertical_left)
.set_style(self.border_style);
}
@@ -638,7 +742,7 @@ impl Block<'_> {
fn render_top_side(&self, area: Rect, buf: &mut Buffer) {
if self.borders.contains(Borders::TOP) {
for x in area.left()..area.right() {
buf.get_mut(x, area.top())
buf[(x, area.top())]
.set_symbol(self.border_set.horizontal_top)
.set_style(self.border_style);
}
@@ -649,7 +753,7 @@ impl Block<'_> {
if self.borders.contains(Borders::RIGHT) {
let x = area.right() - 1;
for y in area.top()..area.bottom() {
buf.get_mut(x, y)
buf[(x, y)]
.set_symbol(self.border_set.vertical_right)
.set_style(self.border_style);
}
@@ -660,7 +764,7 @@ impl Block<'_> {
if self.borders.contains(Borders::BOTTOM) {
let y = area.bottom() - 1;
for x in area.left()..area.right() {
buf.get_mut(x, y)
buf[(x, y)]
.set_symbol(self.border_set.horizontal_bottom)
.set_style(self.border_style);
}
@@ -669,7 +773,7 @@ impl Block<'_> {
fn render_bottom_right_corner(&self, buf: &mut Buffer, area: Rect) {
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
buf.get_mut(area.right() - 1, area.bottom() - 1)
buf[(area.right() - 1, area.bottom() - 1)]
.set_symbol(self.border_set.bottom_right)
.set_style(self.border_style);
}
@@ -677,7 +781,7 @@ impl Block<'_> {
fn render_top_right_corner(&self, buf: &mut Buffer, area: Rect) {
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
buf.get_mut(area.right() - 1, area.top())
buf[(area.right() - 1, area.top())]
.set_symbol(self.border_set.top_right)
.set_style(self.border_style);
}
@@ -685,7 +789,7 @@ impl Block<'_> {
fn render_bottom_left_corner(&self, buf: &mut Buffer, area: Rect) {
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
buf.get_mut(area.left(), area.bottom() - 1)
buf[(area.left(), area.bottom() - 1)]
.set_symbol(self.border_set.bottom_left)
.set_style(self.border_style);
}
@@ -693,7 +797,7 @@ impl Block<'_> {
fn render_top_left_corner(&self, buf: &mut Buffer, area: Rect) {
if self.borders.contains(Borders::LEFT | Borders::TOP) {
buf.get_mut(area.left(), area.top())
buf[(area.left(), area.top())]
.set_symbol(self.border_set.top_left)
.set_style(self.border_style);
}
@@ -704,7 +808,7 @@ impl Block<'_> {
/// Currently (due to the way lines are truncated), the right side of the leftmost title will
/// be cut off if the block is too small to fit all titles. This is not ideal and should be
/// the left side of that leftmost that is cut off. This is due to the line being truncated
/// incorrectly. See <https://github.com/ratatui-org/ratatui/issues/932>
/// incorrectly. See <https://github.com/ratatui/ratatui/issues/932>
#[allow(clippy::similar_names)]
fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
let titles = self.filtered_titles(position, Alignment::Right);
@@ -715,7 +819,7 @@ impl Block<'_> {
if titles_area.is_empty() {
break;
}
let title_width = title.content.width() as u16;
let title_width = title.width() as u16;
let title_area = Rect {
x: titles_area
.right()
@@ -725,7 +829,7 @@ impl Block<'_> {
..titles_area
};
buf.set_style(title_area, self.titles_style);
title.content.render_ref(title_area, buf);
title.render_ref(title_area, buf);
// bump the width of the titles area to the left
titles_area.width = titles_area
@@ -747,7 +851,7 @@ impl Block<'_> {
.collect_vec();
let total_width = titles
.iter()
.map(|title| title.content.width() as u16 + 1) // space between titles
.map(|title| title.width() as u16 + 1) // space between titles
.sum::<u16>()
.saturating_sub(1); // no space for the last title
@@ -760,13 +864,13 @@ impl Block<'_> {
if titles_area.is_empty() {
break;
}
let title_width = title.content.width() as u16;
let title_width = title.width() as u16;
let title_area = Rect {
width: title_width.min(titles_area.width),
..titles_area
};
buf.set_style(title_area, self.titles_style);
title.content.render_ref(title_area, buf);
title.render_ref(title_area, buf);
// bump the titles area to the right and reduce its width
titles_area.x = titles_area.x.saturating_add(title_width + 1);
@@ -783,13 +887,13 @@ impl Block<'_> {
if titles_area.is_empty() {
break;
}
let title_width = title.content.width() as u16;
let title_width = title.width() as u16;
let title_area = Rect {
width: title_width.min(titles_area.width),
..titles_area
};
buf.set_style(title_area, self.titles_style);
title.content.render_ref(title_area, buf);
title.render_ref(title_area, buf);
// bump the titles area to the right and reduce its width
titles_area.x = titles_area.x.saturating_add(title_width + 1);
@@ -802,11 +906,12 @@ impl Block<'_> {
&self,
position: Position,
alignment: Alignment,
) -> impl DoubleEndedIterator<Item = &Title> {
self.titles.iter().filter(move |title| {
title.position.unwrap_or(self.titles_position) == position
&& title.alignment.unwrap_or(self.titles_alignment) == alignment
})
) -> impl DoubleEndedIterator<Item = &Line> {
self.titles
.iter()
.filter(move |(pos, _)| pos.unwrap_or(self.titles_position) == position)
.filter(move |(_, line)| line.alignment.unwrap_or(self.titles_alignment) == alignment)
.map(|(_, line)| line)
}
/// An area that is one line tall and spans the width of the block excluding the borders and
@@ -827,6 +932,35 @@ impl Block<'_> {
height: 1,
}
}
/// Calculate the left, and right space the [`Block`] will take up.
///
/// The result takes the [`Block`]'s, [`Borders`], and [`Padding`] into account.
pub(crate) fn horizontal_space(&self) -> (u16, u16) {
let left = self
.padding
.left
.saturating_add(u16::from(self.borders.contains(Borders::LEFT)));
let right = self
.padding
.right
.saturating_add(u16::from(self.borders.contains(Borders::RIGHT)));
(left, right)
}
/// Calculate the top, and bottom space that the [`Block`] will take up.
///
/// Takes the [`Padding`], [`Title`]'s position, and the [`Borders`] that are selected into
/// account when calculating the result.
pub(crate) fn vertical_space(&self) -> (u16, u16) {
let has_top =
self.borders.contains(Borders::TOP) || self.has_title_at_position(Position::Top);
let top = self.padding.top + u16::from(has_top);
let has_bottom =
self.borders.contains(Borders::BOTTOM) || self.has_title_at_position(Position::Bottom);
let bottom = self.padding.bottom + u16::from(has_bottom);
(top, bottom)
}
}
/// An extension trait for [`Block`] that provides some convenience methods.
@@ -860,6 +994,7 @@ impl<'a> Styled for Block<'a> {
#[cfg(test)]
mod tests {
use ratatui_core::style::{Color, Modifier};
use rstest::rstest;
use strum::ParseError;
@@ -911,24 +1046,17 @@ mod tests {
let area = Rect::new(0, 0, 0, 1);
let expected = Rect::new(0, 1, 0, 0);
let block = Block::new().title(Title::from("Test").alignment(alignment));
let block = Block::new().title(Line::from("Test").alignment(alignment));
assert_eq!(block.inner(area), expected);
}
#[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,
) {
#[case::top_top(Block::new().title_top("Test").borders(Borders::TOP), Rect::new(0, 1, 0, 1))]
#[case::top_bot(Block::new().title_top("Test").borders(Borders::BOTTOM), Rect::new(0, 1, 0, 0))]
#[case::bot_top(Block::new().title_bottom("Test").borders(Borders::TOP), Rect::new(0, 1, 0, 0))]
#[case::bot_bot(Block::new().title_bottom("Test").borders(Borders::BOTTOM), Rect::new(0, 0, 0, 1))]
fn inner_takes_into_account_border_and_title(#[case] block: Block, #[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);
}
@@ -938,32 +1066,33 @@ mod tests {
assert!(!block.has_title_at_position(Position::Top));
assert!(!block.has_title_at_position(Position::Bottom));
let block = Block::new().title(Title::from("Test").position(Position::Top));
let block = Block::new().title_top("test");
assert!(block.has_title_at_position(Position::Top));
assert!(!block.has_title_at_position(Position::Bottom));
let block = Block::new().title(Title::from("Test").position(Position::Bottom));
let block = Block::new().title_bottom("test");
assert!(!block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
#[allow(deprecated)] // until Title is removed
let block = Block::new()
.title(Title::from("Test").position(Position::Top))
.title_position(Position::Bottom);
assert!(block.has_title_at_position(Position::Top));
assert!(!block.has_title_at_position(Position::Bottom));
#[allow(deprecated)] // until Title is removed
let block = Block::new()
.title(Title::from("Test").position(Position::Bottom))
.title_position(Position::Top);
assert!(!block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
let block = Block::new()
.title(Title::from("Test").position(Position::Top))
.title(Title::from("Test").position(Position::Bottom));
let block = Block::new().title_top("test").title_bottom("test");
assert!(block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
#[allow(deprecated)] // until Title is removed
let block = Block::new()
.title(Title::from("Test").position(Position::Top))
.title(Title::from("Test"))
@@ -971,6 +1100,7 @@ mod tests {
assert!(block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
#[allow(deprecated)] // until Title is removed
let block = Block::new()
.title(Title::from("Test"))
.title(Title::from("Test").position(Position::Bottom))
@@ -979,6 +1109,117 @@ mod tests {
assert!(block.has_title_at_position(Position::Bottom));
}
#[rstest]
#[case::none(Borders::NONE, (0, 0))]
#[case::top(Borders::TOP, (1, 0))]
#[case::right(Borders::RIGHT, (0, 0))]
#[case::bottom(Borders::BOTTOM, (0, 1))]
#[case::left(Borders::LEFT, (0, 0))]
#[case::top_right(Borders::TOP | Borders::RIGHT, (1, 0))]
#[case::top_bottom(Borders::TOP | Borders::BOTTOM, (1, 1))]
#[case::top_left(Borders::TOP | Borders::LEFT, (1, 0))]
#[case::bottom_right(Borders::BOTTOM | Borders::RIGHT, (0, 1))]
#[case::bottom_left(Borders::BOTTOM | Borders::LEFT, (0, 1))]
#[case::left_right(Borders::LEFT | Borders::RIGHT, (0, 0))]
fn vertical_space_takes_into_account_borders(
#[case] borders: Borders,
#[case] vertical_space: (u16, u16),
) {
let block = Block::new().borders(borders);
assert_eq!(block.vertical_space(), vertical_space);
}
#[rstest]
#[case::top_border_top_p1(Borders::TOP, Padding::new(0, 0, 1, 0), (2, 0))]
#[case::right_border_top_p1(Borders::RIGHT, Padding::new(0, 0, 1, 0), (1, 0))]
#[case::bottom_border_top_p1(Borders::BOTTOM, Padding::new(0, 0, 1, 0), (1, 1))]
#[case::left_border_top_p1(Borders::LEFT, Padding::new(0, 0, 1, 0), (1, 0))]
#[case::top_bottom_border_all_p3(Borders::TOP | Borders::BOTTOM, Padding::new(100, 100, 4, 5), (5, 6))]
#[case::no_border(Borders::NONE, Padding::new(100, 100, 10, 13), (10, 13))]
#[case::all(Borders::ALL, Padding::new(100, 100, 1, 3), (2, 4))]
fn vertical_space_takes_into_account_padding(
#[case] borders: Borders,
#[case] padding: Padding,
#[case] vertical_space: (u16, u16),
) {
let block = Block::new().borders(borders).padding(padding);
assert_eq!(block.vertical_space(), vertical_space);
}
#[test]
fn vertical_space_takes_into_account_titles() {
let block = Block::new().title_top("Test");
assert_eq!(block.vertical_space(), (1, 0));
let block = Block::new().title_bottom("Test");
assert_eq!(block.vertical_space(), (0, 1));
}
#[rstest]
#[case::top_border_top_title(Block::new(), Borders::TOP, Position::Top, (1, 0))]
#[case::right_border_top_title(Block::new(), Borders::RIGHT, Position::Top, (1, 0))]
#[case::bottom_border_top_title(Block::new(), Borders::BOTTOM, Position::Top, (1, 1))]
#[case::left_border_top_title(Block::new(), Borders::LEFT, Position::Top, (1, 0))]
#[case::top_border_top_title(Block::new(), Borders::TOP, Position::Bottom, (1, 1))]
#[case::right_border_top_title(Block::new(), Borders::RIGHT, Position::Bottom, (0, 1))]
#[case::bottom_border_top_title(Block::new(), Borders::BOTTOM, Position::Bottom, (0, 1))]
#[case::left_border_top_title(Block::new(), Borders::LEFT, Position::Bottom, (0, 1))]
fn vertical_space_takes_into_account_borders_and_title(
#[case] block: Block,
#[case] borders: Borders,
#[case] pos: Position,
#[case] vertical_space: (u16, u16),
) {
let block = block.borders(borders).title_position(pos).title("Test");
assert_eq!(block.vertical_space(), vertical_space);
}
#[test]
fn horizontal_space_takes_into_account_borders() {
let block = Block::bordered();
assert_eq!(block.horizontal_space(), (1, 1));
let block = Block::new().borders(Borders::LEFT);
assert_eq!(block.horizontal_space(), (1, 0));
let block = Block::new().borders(Borders::RIGHT);
assert_eq!(block.horizontal_space(), (0, 1));
}
#[test]
fn horizontal_space_takes_into_account_padding() {
let block = Block::new().padding(Padding::new(1, 1, 100, 100));
assert_eq!(block.horizontal_space(), (1, 1));
let block = Block::new().padding(Padding::new(3, 5, 0, 0));
assert_eq!(block.horizontal_space(), (3, 5));
let block = Block::new().padding(Padding::new(0, 1, 100, 100));
assert_eq!(block.horizontal_space(), (0, 1));
let block = Block::new().padding(Padding::new(1, 0, 100, 100));
assert_eq!(block.horizontal_space(), (1, 0));
}
#[rstest]
#[case::all_bordered_all_padded(Block::bordered(), Padding::new(1, 1, 1, 1), (2, 2))]
#[case::all_bordered_left_padded(Block::bordered(), Padding::new(1, 0, 0, 0), (2, 1))]
#[case::all_bordered_right_padded(Block::bordered(), Padding::new(0, 1, 0, 0), (1, 2))]
#[case::all_bordered_top_padded(Block::bordered(), Padding::new(0, 0, 1, 0), (1, 1))]
#[case::all_bordered_bottom_padded(Block::bordered(), Padding::new(0, 0, 0, 1), (1, 1))]
#[case::left_bordered_left_padded(Block::new().borders(Borders::LEFT), Padding::new(1, 0, 0, 0), (2, 0))]
#[case::left_bordered_right_padded(Block::new().borders(Borders::LEFT), Padding::new(0, 1, 0, 0), (1, 1))]
#[case::right_bordered_right_padded(Block::new().borders(Borders::RIGHT), Padding::new(0, 1, 0, 0), (0, 2))]
#[case::right_bordered_left_padded(Block::new().borders(Borders::RIGHT), Padding::new(1, 0, 0, 0), (1, 1))]
fn horizontal_space_takes_into_account_borders_and_padding(
#[case] block: Block,
#[case] padding: Padding,
#[case] horizontal_space: (u16, u16),
) {
let block = block.padding(padding);
assert_eq!(block.horizontal_space(), horizontal_space);
}
#[test]
const fn border_type_can_be_const() {
const _PLAIN: border::Set = BorderType::border_symbols(BorderType::Plain);
@@ -997,7 +1238,7 @@ mod tests {
border_style: Style::new(),
border_set: BorderType::Plain.to_border_set(),
style: Style::new(),
padding: Padding::zero(),
padding: Padding::ZERO,
}
);
}
@@ -1012,7 +1253,7 @@ mod tests {
// .border_style(_DEFAULT_STYLE) // no longer const
// .title_style(_DEFAULT_STYLE) // no longer const
.title_alignment(Alignment::Left)
.title_position(Position::Top)
.title_position(crate::block::Position::Top)
.padding(_DEFAULT_PADDING);
}
@@ -1078,6 +1319,7 @@ mod tests {
use Alignment::*;
use Position::*;
let mut buffer = Buffer::empty(Rect::new(0, 0, 11, 3));
#[allow(deprecated)] // until Title is removed
Block::bordered()
.title(Title::from("A").position(Top).alignment(Left))
.title(Title::from("B").position(Top).alignment(Center))
@@ -1143,13 +1385,13 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
Block::new()
.title_alignment(block_title_alignment)
.title(Title::from("test").alignment(alignment))
.title(Line::from("test").alignment(alignment))
.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
}
/// This is a regression test for bug <https://github.com/ratatui-org/ratatui/issues/929>
/// This is a regression test for bug <https://github.com/ratatui/ratatui/issues/929>
#[test]
fn render_right_aligned_empty_title() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));

View File

@@ -19,8 +19,8 @@
/// Padding::symmetric(5, 6);
/// ```
///
/// [`Block`]: crate::widgets::Block
/// [`padding`]: crate::widgets::Block::padding
/// [`Block`]: crate::Block
/// [`padding`]: crate::Block::padding
/// [CSS padding]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Padding {
@@ -35,6 +35,14 @@ pub struct Padding {
}
impl Padding {
/// `Padding` with all fields set to `0`
pub const ZERO: Self = Self {
left: 0,
right: 0,
top: 0,
bottom: 0,
};
/// Creates a new `Padding` by specifying every field individually.
///
/// Note: the order of the fields does not match the order of the CSS properties.
@@ -48,13 +56,9 @@ impl Padding {
}
/// Creates a `Padding` with all fields set to `0`.
#[deprecated = "use Padding::ZERO"]
pub const fn zero() -> Self {
Self {
left: 0,
right: 0,
top: 0,
bottom: 0,
}
Self::ZERO
}
/// Creates a `Padding` with the same value for `left` and `right`.
@@ -173,7 +177,6 @@ mod tests {
#[test]
fn constructors() {
assert_eq!(Padding::zero(), Padding::new(0, 0, 0, 0));
assert_eq!(Padding::horizontal(1), Padding::new(1, 1, 0, 0));
assert_eq!(Padding::vertical(1), Padding::new(0, 0, 1, 1));
assert_eq!(Padding::uniform(1), Padding::new(1, 1, 1, 1));
@@ -189,7 +192,6 @@ mod tests {
const fn can_be_const() {
const _PADDING: Padding = Padding::new(1, 1, 1, 1);
const _UNI_PADDING: Padding = Padding::uniform(1);
const _NO_PADDING: Padding = Padding::zero();
const _HORIZONTAL: Padding = Padding::horizontal(1);
const _VERTICAL: Padding = Padding::vertical(1);
const _PROPORTIONAL: Padding = Padding::proportional(1);

View File

@@ -1,14 +1,24 @@
//! This module holds the [`Title`] element and its related configuration types.
//! A title is a piece of [`Block`](crate::widgets::Block) configuration.
//! A title is a piece of [`Block`](crate::Block) configuration.
use ratatui_core::{layout::Alignment, text::Line};
use strum::{Display, EnumString};
use crate::{layout::Alignment, text::Line};
/// A [`Block`](crate::widgets::Block) title.
/// A [`Block`](crate::Block) title.
///
/// It can be aligned (see [`Alignment`]) and positioned (see [`Position`]).
///
/// # Future Deprecation
///
/// This type is deprecated and will be removed in a future release. The reason for this is that the
/// position of the title should be stored in the block itself, not in the title. The `Line` type
/// has an alignment method that can be used to align the title. For more information see
/// <https://github.com/ratatui/ratatui/issues/738>.
///
/// Use [`Line`] instead, when the position is not defined as part of the title. When a specific
/// position is needed, use [`Block::title_top`](crate::Block::title_top) or
/// [`Block::title_bottom`](crate::Block::title_bottom) instead.
///
/// # Example
///
/// Title with no style.
@@ -36,7 +46,10 @@ use crate::{layout::Alignment, text::Line};
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// };
///
/// Title::from("Title")
@@ -50,19 +63,19 @@ pub struct Title<'a> {
/// Title alignment
///
/// If [`None`], defaults to the alignment defined with
/// [`Block::title_alignment`](crate::widgets::Block::title_alignment) in the associated
/// [`Block`](crate::widgets::Block).
/// [`Block::title_alignment`](crate::Block::title_alignment) in the associated
/// [`Block`](crate::Block).
pub alignment: Option<Alignment>,
/// Title position
///
/// If [`None`], defaults to the position defined with
/// [`Block::title_position`](crate::widgets::Block::title_position) in the associated
/// [`Block`](crate::widgets::Block).
/// [`Block::title_position`](crate::Block::title_position) in the associated
/// [`Block`](crate::Block).
pub position: Option<Position>,
}
/// Defines the [title](crate::widgets::block::Title) position.
/// Defines the [title](crate::block::Title) position.
///
/// The title can be positioned on top or at the bottom of the block.
/// Defaults to [`Position::Top`].
@@ -85,6 +98,7 @@ pub enum Position {
Bottom,
}
#[deprecated = "use Block::title_top() or Block::title_bottom() instead. This will be removed in a future release."]
impl<'a> Title<'a> {
/// Set the title content.
#[must_use = "method moves the value of self and returns the modified value"]

View File

@@ -55,7 +55,7 @@ impl fmt::Debug for Borders {
/// and RIGHT.
///
/// When used with NONE you should consider omitting this completely. For ALL you should consider
/// [`Block::bordered()`](crate::widgets::Block::bordered) instead.
/// [`Block::bordered()`](crate::Block::bordered) instead.
///
/// ## Examples
///

View File

@@ -10,9 +10,13 @@
//! [`Monthly`] has several controls for what should be displayed
use std::collections::HashMap;
use ratatui_core::{
prelude::{Alignment, Buffer, Color, Constraint, Layout, Line, Rect, Span, Style, Widget},
widgets::WidgetRef,
};
use time::{Date, Duration, OffsetDateTime};
use crate::{prelude::*, widgets::Block};
use crate::{block::BlockExt, Block};
/// Display a month calendar for the month containing `display_date`
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
@@ -177,7 +181,9 @@ impl<DS: DateStyler> Monthly<'_, DS> {
spans.push(self.format_date(curr_day));
curr_day += Duration::DAY;
}
buf.set_line(days_area.x, y, &spans.into(), area.width);
if buf.area.height > y {
buf.set_line(days_area.x, y, &spans.into(), area.width);
}
y += 1;
}
}
@@ -203,7 +209,7 @@ impl CalendarEventStore {
let mut res = Self::default();
res.add(
OffsetDateTime::now_local()
.unwrap_or(OffsetDateTime::now_utc())
.unwrap_or_else(|_| OffsetDateTime::now_utc())
.date(),
style.into(),
);

View File

@@ -22,6 +22,12 @@ mod world;
use std::{fmt, iter::zip};
use itertools::Itertools;
use ratatui_core::{
prelude::{Buffer, Color, Rect, Style, Widget},
symbols::{self, Marker},
text::Line as TextLine,
widgets::WidgetRef,
};
pub use self::{
circle::Circle,
@@ -30,7 +36,7 @@ pub use self::{
points::Points,
rectangle::Rectangle,
};
use crate::{prelude::*, text::Line as TextLine, widgets::Block};
use crate::block::{Block, BlockExt};
/// Something that can be drawn on a [`Canvas`].
///
@@ -425,7 +431,7 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
/// This is used by the [`Canvas`] widget to draw shapes on the grid. It can be useful to think of
/// this as similar to the [`Frame`] struct that is used to draw widgets on the terminal.
///
/// [`Frame`]: crate::prelude::Frame
/// [`Frame`]: ratatui_core::prelude::Frame
#[derive(Debug)]
pub struct Context<'a> {
x_bounds: [f64; 2],
@@ -464,17 +470,17 @@ impl<'a> Context<'a> {
height: u16,
x_bounds: [f64; 2],
y_bounds: [f64; 2],
marker: symbols::Marker,
marker: Marker,
) -> Self {
let dot = symbols::DOT.chars().next().unwrap();
let block = symbols::block::FULL.chars().next().unwrap();
let bar = symbols::bar::HALF.chars().next().unwrap();
let grid: Box<dyn Grid> = match marker {
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
symbols::Marker::Block => Box::new(CharGrid::new(width, height, block)),
symbols::Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
symbols::Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
Marker::Block => Box::new(CharGrid::new(width, height, block)),
Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
Marker::Braille => Box::new(BrailleGrid::new(width, height)),
Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
};
Self {
x_bounds,
@@ -604,7 +610,7 @@ where
y_bounds: [f64; 2],
paint_func: Option<F>,
background_color: Color,
marker: symbols::Marker,
marker: Marker,
}
impl<'a, F> Default for Canvas<'a, F>
@@ -618,7 +624,7 @@ where
y_bounds: [0.0, 0.0],
paint_func: None,
background_color: Color::Reset,
marker: symbols::Marker::Braille,
marker: Marker::Braille,
}
}
}
@@ -717,7 +723,7 @@ where
/// .paint(|ctx| {});
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn marker(mut self, marker: symbols::Marker) -> Self {
pub const fn marker(mut self, marker: Marker) -> Self {
self.marker = marker;
self
}
@@ -771,7 +777,7 @@ where
(index % width) as u16 + canvas_area.left(),
(index / width) as u16 + canvas_area.top(),
);
let cell = buf.get_mut(x, y).set_char(ch);
let cell = buf[(x, y)].set_char(ch);
if colors.0 != Color::Reset {
cell.set_fg(colors.0);
}
@@ -809,17 +815,15 @@ where
#[cfg(test)]
mod tests {
use indoc::indoc;
use ratatui_core::buffer::Cell;
use super::*;
use crate::buffer::Cell;
// helper to test the canvas checks that drawing a vertical and horizontal line
// results in the expected output
fn test_marker(marker: Marker, expected: &str) {
let area = Rect::new(0, 0, 5, 5);
let mut cell = Cell::default();
cell.set_char('x');
let mut buf = Buffer::filled(area, &cell);
let mut buf = Buffer::filled(area, Cell::new("x"));
let horizontal_line = Line {
x1: 0.0,
y1: 0.0,

View File

@@ -1,7 +1,6 @@
use crate::{
style::Color,
widgets::canvas::{Painter, Shape},
};
use ratatui_core::style::Color;
use crate::canvas::{Painter, Shape};
/// A circle with a given center and radius and with a given color
#[derive(Debug, Default, Clone, PartialEq)]
@@ -31,17 +30,12 @@ impl Shape for Circle {
#[cfg(test)]
mod tests {
use crate::{
buffer::Buffer,
layout::Rect,
style::Color,
symbols::Marker,
widgets::{
canvas::{Canvas, Circle},
Widget,
},
use ratatui_core::{
buffer::Buffer, layout::Rect, style::Color, symbols::Marker, widgets::Widget,
};
use crate::canvas::{Canvas, Circle};
#[test]
fn test_it_draws_a_circle() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));

View File

@@ -1,7 +1,6 @@
use crate::{
style::Color,
widgets::canvas::{Painter, Shape},
};
use ratatui_core::style::Color;
use crate::canvas::{Painter, Shape};
/// A line from `(x1, y1)` to `(x2, y2)` with the given color
#[derive(Debug, Default, Clone, PartialEq)]
@@ -112,10 +111,18 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
#[cfg(test)]
mod tests {
use ratatui_core::{
buffer::Buffer,
layout::Rect,
style::{Style, Stylize},
symbols::Marker,
text,
widgets::Widget,
};
use rstest::rstest;
use super::{super::*, *};
use crate::{buffer::Buffer, layout::Rect};
use super::*;
use crate::canvas::Canvas;
#[rstest]
#[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])]
@@ -201,7 +208,7 @@ mod tests {
fn tests<'expected_line, ExpectedLines>(#[case] line: &Line, #[case] expected: ExpectedLines)
where
ExpectedLines: IntoIterator,
ExpectedLines::Item: Into<crate::text::Line<'expected_line>>,
ExpectedLines::Item: Into<text::Line<'expected_line>>,
{
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
let canvas = Canvas::default()

View File

@@ -1,13 +1,10 @@
use ratatui_core::style::Color;
use strum::{Display, EnumString};
use crate::{
style::Color,
widgets::canvas::{
world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION},
Painter, Shape,
},
use crate::canvas::{
world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION},
Painter, Shape,
};
/// Defines how many points are going to be used to draw a [`Map`].
///
/// You generally want a [high](MapResolution::High) resolution map.
@@ -62,10 +59,14 @@ impl Shape for Map {
#[cfg(test)]
mod tests {
use ratatui_core::{
prelude::{Buffer, Color, Rect, Widget},
symbols::Marker,
};
use strum::ParseError;
use super::*;
use crate::{prelude::*, widgets::canvas::Canvas};
use crate::canvas::Canvas;
#[test]
fn map_resolution_to_string() {

View File

@@ -1,7 +1,6 @@
use crate::{
style::Color,
widgets::canvas::{Painter, Shape},
};
use ratatui_core::style::Color;
use crate::canvas::{Painter, Shape};
/// A group of points with a given color
#[derive(Debug, Default, Clone, PartialEq)]

View File

@@ -1,7 +1,6 @@
use crate::{
style::Color,
widgets::canvas::{Line, Painter, Shape},
};
use ratatui_core::style::Color;
use crate::canvas::{Line, Painter, Shape};
/// A rectangle to draw on a [`Canvas`](super::Canvas)
///
@@ -65,8 +64,10 @@ impl Shape for Rectangle {
#[cfg(test)]
mod tests {
use ratatui_core::{prelude::*, symbols::Marker};
use super::*;
use crate::{prelude::*, widgets::canvas::Canvas};
use crate::canvas::Canvas;
#[test]
fn draw_block_lines() {
@@ -98,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());
expected.set_style(buffer.area.inner(Margin::new(1, 1)), Style::reset());
assert_eq!(buffer, expected);
}
@@ -132,8 +133,8 @@ 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());
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_eq!(buffer, expected);
}
@@ -176,8 +177,8 @@ 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());
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_eq!(buffer, expected);
}
}

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