Compare commits

..

19 Commits

Author SHA1 Message Date
Orhun Parmaksız
efd1e47642 chore(release): prepare for 0.26.1 (#945)
🐭
2024-02-12 12:35:48 +01:00
Orhun Parmaksız
410d08b2b5 docs: add link to FOSDEM 2024 talk (#944) 2024-02-12 10:54:05 +01:00
Orhun Parmaksız
a4892ad444 chore: fix typo in docsrs example (#946) 2024-02-12 10:53:56 +01:00
Orhun Parmaksız
18870ce990 chore: fix the method name for setting the Line style (#947) 2024-02-12 10:53:46 +01:00
Orhun Parmaksız
1f208ffd03 docs: add GitHub Sponsors badge (#943) 2024-02-11 10:54:42 +01:00
Josh McKinney
e51ca6e0d2 refactor: finish tidying up table (#942) 2024-02-11 10:54:08 +01:00
Josh McKinney
9182f47026 feat: add Block::title_top and Block::title_top_bottom (#940)
This adds the ability to add titles to the top and bottom of a block
without having to use the `Title` struct (which will be removed in a
future release - likely v0.28.0).

Fixes a subtle bug if the title was created from a right aligned Line
and was also right aligned. The title would be rendered one cell too far
to the right.

```rust
Block::bordered()
    .title_top(Line::raw("A").left_aligned())
    .title_top(Line::raw("B").centered())
    .title_top(Line::raw("C").right_aligned())
    .title_bottom(Line::raw("D").left_aligned())
    .title_bottom(Line::raw("E").centered())
    .title_bottom(Line::raw("F").right_aligned())
    .render(buffer.area, &mut buffer);
// renders
"┌A─────B─────C┐",
"│             │",
"└D─────E─────F┘",
```

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

<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
2024-02-09 12:50:56 -08:00
Josh McKinney
91040c0865 refactor: rearrange block structure (#939) 2024-02-09 01:13:14 -08:00
Josh McKinney
2202059259 fix(block): fix crash on empty right aligned title (#933)
- Simplified implementation of the rendering for block.
- Introduces a subtle rendering change where centered titles that are
  odd in length will now be rendered one character to the left compared
  to before. This aligns with other places that we render centered text
  and is a more consistent behavior. See
  https://github.com/ratatui-org/ratatui/pull/807#discussion_r1455645954
  for another example of this.

Fixes: https://github.com/ratatui-org/ratatui/pull/929
2024-02-07 15:24:14 -08:00
Dheepak Krishnamurthy
8fb46301a0 chore: Remove github action bot that makes comments nudging commit signing (#937)
We can consider reverting this commit once this PR is merged:
https://github.com/1Password/check-signed-commits-action/pull/9
2024-02-07 19:51:30 +01:00
may
0dcdbea083 fix(paragraph): render Line::styled correctly inside a paragraph (#930)
Renders the styled graphemes of the line instead of the contained spans.
2024-02-06 09:42:17 -08:00
Josh McKinney
74a051147a feat(rect): add Rect::positions iterator (#928)
Useful for performing some action on all the cells in a particular area.
E.g.,

```rust
fn render(area: Rect, buf: &mut Buffer) {
   for position in area.positions() {
        buf.get_mut(position.x, position.y).set_symbol("x");
    }
}
```
2024-02-06 09:39:17 -08:00
Josh McKinney
c3fb25898f refactor(rect): move iters to module and add docs (#927) 2024-02-05 20:25:08 -08:00
Josh McKinney
fae5862c6e fix: ensure that buffer::set_line sets the line style (#926)
Fixes a regression in 0.26 where buffer::set_line was no longer setting
the style. This was due to the new style field on Line instead of being
stored only in the spans.

Also adds a configuration for just running unit tests to bacon.toml.
2024-02-05 16:26:23 -08:00
dependabot[bot]
788e6d9fb8 chore(deps): bump codecov/codecov-action from 3 to 4 (#923)
Bumps
[codecov/codecov-action](https://github.com/codecov/codecov-action) from
3 to 4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/codecov/codecov-action/releases">codecov/codecov-action's
releases</a>.</em></p>
<blockquote>
<h2>v4.0.0</h2>
<p>v4 of the Codecov Action uses the <a
href="https://docs.codecov.com/docs/the-codecov-cli">CLI</a> as the
underlying upload. The CLI has helped to power new features including
local upload, the global upload token, and new upcoming features.</p>
<h2>Breaking Changes</h2>
<ul>
<li>The Codecov Action runs as a <code>node20</code> action due to
<code>node16</code> deprecation. See <a
href="https://github.blog/changelog/2023-09-22-github-actions-transitioning-from-node-16-to-node-20/">this
post from GitHub</a> on how to migrate.</li>
<li>Tokenless uploading is unsupported. However, PRs made from forks to
the upstream public repos will support tokenless (e.g. contributors to
OS projects do not need the upstream repo's Codecov token). This <a
href="https://docs.codecov.com/docs/adding-the-codecov-token#github-actions">doc</a>
shows instructions on how to add the Codecov token.</li>
<li>OS platforms have been added, though some may not be automatically
detected. To see a list of platforms, see our <a
href="https://cli.codecov.io">CLI download page</a></li>
<li>Various arguments to the Action have been changed. Please be aware
that the arguments match with the CLI's needs</li>
</ul>
<p><code>v3</code> versions and below will not have access to CLI
features (e.g. global upload token, ATS).</p>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump openpgp from 5.8.0 to 5.9.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/985">codecov/codecov-action#985</a></li>
<li>build(deps): bump actions/checkout from 3.0.0 to 3.5.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1000">codecov/codecov-action#1000</a></li>
<li>build(deps): bump ossf/scorecard-action from 2.1.3 to 2.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1006">codecov/codecov-action#1006</a></li>
<li>build(deps): bump tough-cookie from 4.0.0 to 4.1.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1013">codecov/codecov-action#1013</a></li>
<li>build(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1024">codecov/codecov-action#1024</a></li>
<li>build(deps): bump node-fetch from 3.3.1 to 3.3.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1031">codecov/codecov-action#1031</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.1.4 to
20.4.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1032">codecov/codecov-action#1032</a></li>
<li>build(deps): bump github/codeql-action from 1.0.26 to 2.21.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1033">codecov/codecov-action#1033</a></li>
<li>build commit,report and upload args based on codecovcli by <a
href="https://github.com/dana-yaish"><code>@​dana-yaish</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/943">codecov/codecov-action#943</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.4.5 to
20.5.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1055">codecov/codecov-action#1055</a></li>
<li>build(deps): bump github/codeql-action from 2.21.2 to 2.21.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1051">codecov/codecov-action#1051</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.3 to
20.5.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1058">codecov/codecov-action#1058</a></li>
<li>chore(deps): update outdated deps by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1059">codecov/codecov-action#1059</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.4 to
20.5.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1060">codecov/codecov-action#1060</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
6.4.1 to 6.5.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1065">codecov/codecov-action#1065</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 6.4.1 to 6.5.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1064">codecov/codecov-action#1064</a></li>
<li>build(deps): bump actions/checkout from 3.5.3 to 3.6.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1063">codecov/codecov-action#1063</a></li>
<li>build(deps-dev): bump eslint from 8.47.0 to 8.48.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1061">codecov/codecov-action#1061</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.6 to
20.5.7 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1062">codecov/codecov-action#1062</a></li>
<li>build(deps): bump openpgp from 5.9.0 to 5.10.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1066">codecov/codecov-action#1066</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.7 to
20.5.9 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1070">codecov/codecov-action#1070</a></li>
<li>build(deps): bump github/codeql-action from 2.21.4 to 2.21.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1069">codecov/codecov-action#1069</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 6.5.0 to 6.6.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1072">codecov/codecov-action#1072</a></li>
<li>Update README.md by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1073">codecov/codecov-action#1073</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
6.5.0 to 6.6.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1071">codecov/codecov-action#1071</a></li>
<li>build(deps-dev): bump <code>@​vercel/ncc</code> from 0.36.1 to
0.38.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1074">codecov/codecov-action#1074</a></li>
<li>build(deps): bump <code>@​actions/core</code> from 1.10.0 to 1.10.1
by <a href="https://github.com/dependabot"><code>@​dependabot</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1081">codecov/codecov-action#1081</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 6.6.0 to 6.7.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1080">codecov/codecov-action#1080</a></li>
<li>build(deps): bump actions/checkout from 3.6.0 to 4.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1078">codecov/codecov-action#1078</a></li>
<li>build(deps): bump actions/upload-artifact from 3.1.2 to 3.1.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1077">codecov/codecov-action#1077</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.9 to
20.6.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1075">codecov/codecov-action#1075</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
6.6.0 to 6.7.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1079">codecov/codecov-action#1079</a></li>
<li>build(deps-dev): bump eslint from 8.48.0 to 8.49.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1076">codecov/codecov-action#1076</a></li>
<li>use cli instead of node uploader by <a
href="https://github.com/dana-yaish"><code>@​dana-yaish</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1068">codecov/codecov-action#1068</a></li>
<li>chore(release): 4.0.0-beta.1 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1084">codecov/codecov-action#1084</a></li>
<li>not adding -n if empty to do-upload command by <a
href="https://github.com/dana-yaish"><code>@​dana-yaish</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1085">codecov/codecov-action#1085</a></li>
<li>4.0.0-beta.2 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1086">codecov/codecov-action#1086</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md">codecov/codecov-action's
changelog</a>.</em></p>
<blockquote>
<h2>4.0.0-beta.2</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/1085">#1085</a>
not adding -n if empty to do-upload command</li>
</ul>
<h2>4.0.0-beta.1</h2>
<p><code>v4</code> represents a move from the <a
href="https://github.com/codecov/uploader">universal uploader</a> to the
<a href="https://github.com/codecov/codecov-cli">Codecov CLI</a>.
Although this will unlock new features for our users, the CLI is not yet
at feature parity with the universal uploader.</p>
<h3>Breaking Changes</h3>
<ul>
<li>No current support for <code>aarch64</code> and <code>alpine</code>
architectures.</li>
<li>Tokenless uploading is unsuported</li>
<li>Various arguments to the Action have been removed</li>
</ul>
<h2>3.1.4</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/967">#967</a>
Fix typo in README.md</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/971">#971</a>
fix: add back in working dir</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/969">#969</a>
fix: CLI option names for uploader</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/970">#970</a>
build(deps-dev): bump <code>@​types/node</code> from 18.15.12 to
18.16.3</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/979">#979</a>
build(deps-dev): bump <code>@​types/node</code> from 20.1.0 to
20.1.2</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/981">#981</a>
build(deps-dev): bump <code>@​types/node</code> from 20.1.2 to
20.1.4</li>
</ul>
<h2>3.1.3</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/960">#960</a>
fix: allow for aarch64 build</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/957">#957</a>
build(deps-dev): bump jest-junit from 15.0.0 to 16.0.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/958">#958</a>
build(deps): bump openpgp from 5.7.0 to 5.8.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/959">#959</a>
build(deps-dev): bump <code>@​types/node</code> from 18.15.10 to
18.15.12</li>
</ul>
<h2>3.1.2</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/718">#718</a>
Update README.md</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/851">#851</a>
Remove unsupported path_to_write_report argument</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/898">#898</a>
codeql-analysis.yml</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/901">#901</a>
Update README to contain correct information - inputs and negate
feature</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/955">#955</a>
fix: add in all the extra arguments for uploader</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/819">#819</a>
build(deps): bump openpgp from 5.4.0 to 5.5.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/835">#835</a>
build(deps): bump node-fetch from 3.2.4 to 3.2.10</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/840">#840</a>
build(deps): bump ossf/scorecard-action from 1.1.1 to 2.0.4</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/841">#841</a>
build(deps): bump <code>@​actions/core</code> from 1.9.1 to 1.10.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/843">#843</a>
build(deps): bump <code>@​actions/github</code> from 5.0.3 to 5.1.1</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/869">#869</a>
build(deps): bump node-fetch from 3.2.10 to 3.3.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/872">#872</a>
build(deps-dev): bump jest-junit from 13.2.0 to 15.0.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/879">#879</a>
build(deps): bump decode-uri-component from 0.2.0 to 0.2.2</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e0b68c6749"><code>e0b68c6</code></a>
fix: show both token uses in readme (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1250">#1250</a>)</li>
<li><a
href="1f9f5573d1"><code>1f9f557</code></a>
Add all args (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1245">#1245</a>)</li>
<li><a
href="09686fcfcb"><code>09686fc</code></a>
Update README.md (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1243">#1243</a>)</li>
<li><a
href="f30e4959ba"><code>f30e495</code></a>
fix: update action.yml (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1240">#1240</a>)</li>
<li><a
href="a7b945cea4"><code>a7b945c</code></a>
fix: allow for other archs (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1239">#1239</a>)</li>
<li><a
href="98ab2c591b"><code>98ab2c5</code></a>
Update package.json (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1238">#1238</a>)</li>
<li><a
href="43235cc5ae"><code>43235cc</code></a>
Update README.md (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1237">#1237</a>)</li>
<li><a
href="0cf8684c82"><code>0cf8684</code></a>
chore(ci): bump to node20 (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1236">#1236</a>)</li>
<li><a
href="8e1e730371"><code>8e1e730</code></a>
build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 6.19.1 to 6.20.0 ...</li>
<li><a
href="61293af0e8"><code>61293af</code></a>
build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
6.19.1 to 6.20.0 (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1235">#1235</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/codecov/codecov-action/compare/v3...v4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov/codecov-action&package-manager=github_actions&previous-version=3&new-version=4)](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>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-02-05 16:25:33 -08:00
Jack Wills
14c67fbb52 fix(list): highlight symbol when using a multi-bytes char (#924)
ratatui v0.26.0 brought a regression in the List widget, in which the
highlight symbol width was incorrectly calculated - specifically when
the highlight symbol was a multi-char character, e.g. `▶`.
2024-02-05 20:59:19 +01:00
Mo
096346350e perf: Use drain instead of remove in chart examples (#922) 2024-02-05 04:54:05 -08:00
Valentin271
61a827821d docs(canvas): add documentation to canvas module (#913)
Document the whole `canvas` module. With this, the whole `widgets`
module is documented.
2024-02-03 13:58:29 -08:00
Dheepak Krishnamurthy
fbb5dfaaa9 fix: Scrollbar rendering when no track symbols are provided (#911) 2024-02-03 16:06:44 +01:00
36 changed files with 1599 additions and 767 deletions

View File

@@ -44,24 +44,6 @@ jobs:
header: pr-title-lint-error
delete: true
check-signed:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
# Check commit signature and add comment if needed
- name: Check signed commits in PR
uses: 1Password/check-signed-commits-action@v1
with:
comment: |
Thank you for opening this pull request!
We require commits to be signed and it looks like this PR contains unsigned commits.
Get help in the [CONTRIBUTING.md](https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md#sign-your-commits)
or on [Github doc](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
check-breaking-change-label:
runs-on: ubuntu-latest
env:

View File

@@ -92,7 +92,7 @@ jobs:
- name: Generate coverage
run: cargo make coverage
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true

View File

@@ -154,7 +154,7 @@ longer can be called from a constant context.
[#708]: https://github.com/ratatui-org/ratatui/pull/708
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
itself has a `style` field, which can be set with the `Line::style` method. Any code that creates
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).

View File

@@ -2,6 +2,165 @@
All notable changes to this project will be documented in this file.
## [0.26.1](https://github.com/ratatui-org/ratatui/releases/tag/0.26.1) - 2024-02-12
This is a patch release that fixes bugs and adds enhancements, including new iterators, title options for blocks, and various rendering improvements. ✨
### Features
- [74a0511](https://github.com/ratatui-org/ratatui/commit/74a051147a4059990c31e08d96a8469d8220537b)
*(rect)* Add Rect::positions iterator ([#928](https://github.com/ratatui-org/ratatui/issues/928))
````text
Useful for performing some action on all the cells in a particular area.
E.g.,
```rust
fn render(area: Rect, buf: &mut Buffer) {
for position in area.positions() {
buf.get_mut(position.x, position.y).set_symbol("x");
}
}
```
````
- [9182f47](https://github.com/ratatui-org/ratatui/commit/9182f47026d1630cb749163b6f8b8987474312ae)
*(uncategorized)* Add Block::title_top and Block::title_top_bottom ([#940](https://github.com/ratatui-org/ratatui/issues/940))
````text
This adds the ability to add titles to the top and bottom of a block
without having to use the `Title` struct (which will be removed in a
future release - likely v0.28.0).
Fixes a subtle bug if the title was created from a right aligned Line
and was also right aligned. The title would be rendered one cell too far
to the right.
```rust
Block::bordered()
.title_top(Line::raw("A").left_aligned())
.title_top(Line::raw("B").centered())
.title_top(Line::raw("C").right_aligned())
.title_bottom(Line::raw("D").left_aligned())
.title_bottom(Line::raw("E").centered())
.title_bottom(Line::raw("F").right_aligned())
.render(buffer.area, &mut buffer);
// renders
"┌A─────B─────C┐",
"│ │",
"└D─────E─────F┘",
```
Addresses part of https://github.com/ratatui-org/ratatui/issues/738
````
### Bug Fixes
- [2202059](https://github.com/ratatui-org/ratatui/commit/220205925911ed4377358d2a28ffca9373f11bda)
*(block)* Fix crash on empty right aligned title ([#933](https://github.com/ratatui-org/ratatui/issues/933))
````text
- Simplified implementation of the rendering for block.
- Introduces a subtle rendering change where centered titles that are
odd in length will now be rendered one character to the left compared
to before. This aligns with other places that we render centered text
and is a more consistent behavior. See
https://github.com/ratatui-org/ratatui/pull/807#discussion_r1455645954
for another example of this.
````
Fixes: https://github.com/ratatui-org/ratatui/pull/929
- [14c67fb](https://github.com/ratatui-org/ratatui/commit/14c67fbb52101d10b2d2e26898c408ab8dd3ec2d)
*(list)* Highlight symbol when using a multi-bytes char ([#924](https://github.com/ratatui-org/ratatui/issues/924))
````text
ratatui v0.26.0 brought a regression in the List widget, in which the
highlight symbol width was incorrectly calculated - specifically when
the highlight symbol was a multi-char character, e.g. ``.
````
- [0dcdbea](https://github.com/ratatui-org/ratatui/commit/0dcdbea083aace6d531c0d505837e0911f400675)
*(paragraph)* Render Line::styled correctly inside a paragraph ([#930](https://github.com/ratatui-org/ratatui/issues/930))
````text
Renders the styled graphemes of the line instead of the contained spans.
````
- [fae5862](https://github.com/ratatui-org/ratatui/commit/fae5862c6e0947ee1488a7e4775413dbead67c8b)
*(uncategorized)* Ensure that buffer::set_line sets the line style ([#926](https://github.com/ratatui-org/ratatui/issues/926))
````text
Fixes a regression in 0.26 where buffer::set_line was no longer setting
the style. This was due to the new style field on Line instead of being
stored only in the spans.
Also adds a configuration for just running unit tests to bacon.toml.
````
- [fbb5dfa](https://github.com/ratatui-org/ratatui/commit/fbb5dfaaa903efde0e63114c393dc3063d5f56fd)
*(uncategorized)* Scrollbar rendering when no track symbols are provided ([#911](https://github.com/ratatui-org/ratatui/issues/911))
### Refactor
- [c3fb258](https://github.com/ratatui-org/ratatui/commit/c3fb25898f3e3ffe485ee69631b680679874d2cb)
*(rect)* Move iters to module and add docs ([#927](https://github.com/ratatui-org/ratatui/issues/927))
- [e51ca6e](https://github.com/ratatui-org/ratatui/commit/e51ca6e0d2705e6e0a96aeee78f1e80fcaaf34fc)
*(uncategorized)* Finish tidying up table ([#942](https://github.com/ratatui-org/ratatui/issues/942))
- [91040c0](https://github.com/ratatui-org/ratatui/commit/91040c0865043b8d5e7387509523a41345ed5af3)
*(uncategorized)* Rearrange block structure ([#939](https://github.com/ratatui-org/ratatui/issues/939))
### Documentation
- [61a8278](https://github.com/ratatui-org/ratatui/commit/61a827821dff2bd733377cfc143266edce1dbeec)
*(canvas)* Add documentation to canvas module ([#913](https://github.com/ratatui-org/ratatui/issues/913))
````text
Document the whole `canvas` module. With this, the whole `widgets`
module is documented.
````
- [d2d91f7](https://github.com/ratatui-org/ratatui/commit/d2d91f754c87458c6d07863eca20f3ea8ae319ce)
*(changelog)* Add sponsors section ([#908](https://github.com/ratatui-org/ratatui/issues/908))
- [410d08b](https://github.com/ratatui-org/ratatui/commit/410d08b2b5812d7e29302adc0e8ddf18eb7d1d26)
*(uncategorized)* Add link to FOSDEM 2024 talk ([#944](https://github.com/ratatui-org/ratatui/issues/944))
- [1f208ff](https://github.com/ratatui-org/ratatui/commit/1f208ffd0368b4d269854dc0c550686dcd2d1de0)
*(uncategorized)* Add GitHub Sponsors badge ([#943](https://github.com/ratatui-org/ratatui/issues/943))
### Performance
- [0963463](https://github.com/ratatui-org/ratatui/commit/096346350e19c5de9a4d74bba64796997e9f40da)
*(uncategorized)* Use drain instead of remove in chart examples ([#922](https://github.com/ratatui-org/ratatui/issues/922))
### Miscellaneous Tasks
- [a4892ad](https://github.com/ratatui-org/ratatui/commit/a4892ad444739d7a760bc45bbd954e728c66b2d2)
*(uncategorized)* Fix typo in docsrs example ([#946](https://github.com/ratatui-org/ratatui/issues/946))
- [18870ce](https://github.com/ratatui-org/ratatui/commit/18870ce99063a492674de061441b2cce5dc54c60)
*(uncategorized)* Fix the method name for setting the Line style ([#947](https://github.com/ratatui-org/ratatui/issues/947))
- [8fb4630](https://github.com/ratatui-org/ratatui/commit/8fb46301a00b5d065f9b890496f914d3fdc17495)
*(uncategorized)* Remove github action bot that makes comments nudging commit signing ([#937](https://github.com/ratatui-org/ratatui/issues/937))
````text
We can consider reverting this commit once this PR is merged:
https://github.com/1Password/check-signed-commits-action/pull/9
````
### Contributors
Thank you so much to everyone that contributed to this release!
Here is the list of contributors who have contributed to `ratatui` for the first time!
* @mo8it
* @m4rch3n1ng
## [0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/0.26.0) - 2024-02-02
We are excited to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭

View File

@@ -1,6 +1,6 @@
[package]
name = "ratatui"
version = "0.26.0" # crate version
version = "0.26.1" # 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/"

View File

@@ -26,7 +26,7 @@
<div align="center">
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
Badge]](./LICENSE)<br>
Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
[![Matrix Badge]][Matrix]<br>
@@ -61,6 +61,9 @@ This is in contrast to the retained mode style of rendering where widgets are up
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
for more info.
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
## Other documentation
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
@@ -301,6 +304,7 @@ Running this example produces the following output:
[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
[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
@@ -322,6 +326,7 @@ 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
[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]:
@@ -339,6 +344,7 @@ Running this example produces the following output:
[Matrix Badge]:
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
<!-- cargo-rdme end -->

View File

@@ -44,6 +44,16 @@ command = [
]
need_stdout = true
[jobs.test-unit]
command = [
"cargo", "test",
"--lib",
"--all-features",
"--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.doc]
command = [
"cargo", "+nightly", "doc",
@@ -97,4 +107,6 @@ ctrl-c = "job:check-crossterm"
ctrl-t = "job:check-termion"
ctrl-w = "job:check-termwiz"
v = "job:coverage"
u = "job:coverage-unit-tests-only"
ctrl-v = "job:coverage-unit-tests-only"
u = "job:test-unit"
n = "job:nextest"

View File

@@ -81,14 +81,12 @@ impl App {
}
fn on_tick(&mut self) {
for _ in 0..5 {
self.data1.remove(0);
}
self.data1.drain(0..5);
self.data1.extend(self.signal1.by_ref().take(5));
for _ in 0..10 {
self.data2.remove(0);
}
self.data2.drain(0..10);
self.data2.extend(self.signal2.by_ref().take(10));
self.window[0] += 1.0;
self.window[1] += 1.0;
}

View File

@@ -191,9 +191,7 @@ where
S: Iterator,
{
fn on_tick(&mut self) {
for _ in 0..self.tick_rate {
self.points.remove(0);
}
self.points.drain(0..self.tick_rate);
self.points
.extend(self.source.by_ref().take(self.tick_rate));
}

View File

@@ -21,7 +21,7 @@ use crossterm::{
};
use ratatui::{prelude::*, widgets::*};
/// Example code for libr.rs
/// Example code for lib.rs
///
/// When cargo-rdme supports doc comments that import from code, this will be imported
/// rather than copied to the lib.rs file.

View File

@@ -253,7 +253,7 @@ impl Buffer {
y,
span.content.as_ref(),
remaining_width as usize,
span.style,
line.style.patch(span.style),
);
let w = pos.0.saturating_sub(x);
x = pos.0;
@@ -453,6 +453,11 @@ impl Debug for Buffer {
#[cfg(test)]
mod tests {
use std::iter;
use itertools::Itertools;
use rstest::{fixture, rstest};
use super::*;
use crate::assert_buffer_eq;
@@ -616,6 +621,67 @@ mod tests {
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
}
#[fixture]
fn small_one_line_buffer() -> Buffer {
Buffer::empty(Rect::new(0, 0, 5, 1))
}
#[rstest]
#[case::empty("", " ")]
#[case::one("1", "1 ")]
#[case::full("12345", "12345")]
#[case::overflow("123456", "12345")]
fn set_line_raw(
mut small_one_line_buffer: Buffer,
#[case] content: &str,
#[case] expected: &str,
) {
let line = Line::raw(content);
small_one_line_buffer.set_line(0, 0, &line, 5);
// note: testing with empty / set_string here instead of with_lines because with_lines calls
// set_line
let mut expected_buffer = Buffer::empty(small_one_line_buffer.area);
expected_buffer.set_string(0, 0, expected, Style::default());
assert_buffer_eq!(small_one_line_buffer, expected_buffer);
}
#[rstest]
#[case::empty("", " ")]
#[case::one("1", "1 ")]
#[case::full("12345", "12345")]
#[case::overflow("123456", "12345")]
fn set_line_styled(
mut small_one_line_buffer: Buffer,
#[case] content: &str,
#[case] expected: &str,
) {
let color = Color::Blue;
let line = Line::styled(content, color);
small_one_line_buffer.set_line(0, 0, &line, 5);
// note: manually testing the contents here as the Buffer::with_lines calls set_line
let actual_contents = small_one_line_buffer
.content
.iter()
.map(|c| c.symbol())
.join("");
let actual_styles = small_one_line_buffer
.content
.iter()
.map(|c| c.fg)
.collect_vec();
// set_line only sets the style for non-empty cells (unlike Line::render which sets the
// style for all cells)
let expected_styles = iter::repeat(color)
.take(content.len().min(5))
.chain(iter::repeat(Color::default()).take(5_usize.saturating_sub(content.len())))
.collect_vec();
assert_eq!(actual_contents, expected);
assert_eq!(actual_styles, expected_styles);
}
#[test]
fn set_style() {
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);

View File

@@ -4,13 +4,14 @@ use std::{
fmt,
};
use layout::{Position, Size};
use super::{Position, Size};
use crate::prelude::*;
mod offset;
pub use offset::*;
mod iter;
pub use iter::*;
/// A Rectangular area.
///
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
/// area they are supposed to render to.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
@@ -26,56 +27,17 @@ pub struct Rect {
pub height: u16,
}
/// Manages row divisions within a `Rect`.
/// Amounts by which to move a [`Rect`](super::Rect).
///
/// The `Rows` struct is an iterator that allows iterating through rows of a given `Rect`.
pub struct Rows {
/// The `Rect` associated with the rows.
pub rect: Rect,
/// The y coordinate of the row within the `Rect`.
pub current_row: u16,
}
impl Iterator for Rows {
type Item = Rect;
/// Retrieves the next row within the `Rect`.
///
/// Returns `None` when there are no more rows to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_row >= self.rect.bottom() {
return None;
}
let row = Rect::new(self.rect.x, self.current_row, self.rect.width, 1);
self.current_row += 1;
Some(row)
}
}
/// Manages column divisions within a `Rect`.
/// Positive numbers move to the right/bottom and negative to the left/top.
///
/// The `Columns` struct is an iterator that allows iterating through columns of a given `Rect`.
pub struct Columns {
/// The `Rect` associated with the columns.
pub rect: Rect,
/// The x coordinate of the column within the `Rect`.
pub current_column: u16,
}
impl Iterator for Columns {
type Item = Rect;
/// Retrieves the next column within the `Rect`.
///
/// Returns `None` when there are no more columns to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_column >= self.rect.right() {
return None;
}
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
self.current_column += 1;
Some(column)
}
/// See [`Rect::offset`]
#[derive(Debug, Default, Clone, Copy)]
pub struct Offset {
/// How much to move on the X axis
pub x: i32,
/// How much to move on the Y axis
pub y: i32,
}
impl fmt::Display for Rect {
@@ -271,47 +233,54 @@ impl Rect {
Rect::new(x, y, width, height)
}
/// Creates an iterator over rows within the `Rect`.
/// An iterator over rows within the `Rect`.
///
/// This method returns a `Rows` iterator that allows iterating through rows of the `Rect`.
///
/// # Examples
/// # Example
///
/// ```
/// use ratatui::prelude::*;
/// let area = Rect::new(0, 0, 10, 5);
/// for row in area.rows() {
/// // Perform operations on each row of the area
/// println!("Row: {:?}", row);
/// # use ratatui::prelude::*;
/// fn render(area: Rect, buf: &mut Buffer) {
/// for row in area.rows() {
/// Line::raw("Hello, world!").render(row, buf);
/// }
/// }
/// ```
pub fn rows(&self) -> Rows {
Rows {
rect: *self,
current_row: self.y,
}
pub fn rows(self) -> Rows {
Rows::new(self)
}
/// Creates an iterator over columns within the `Rect`.
/// An iterator over columns within the `Rect`.
///
/// This method returns a `Columns` iterator that allows iterating through columns of the
/// `Rect`.
///
/// # Examples
/// # Example
///
/// ```
/// use ratatui::prelude::*;
/// let area = Rect::new(0, 0, 10, 5);
/// for column in area.columns() {
/// // Perform operations on each column of the area
/// println!("Column: {:?}", column);
/// # use ratatui::{prelude::*, widgets::*};
/// fn render(area: Rect, buf: &mut Buffer) {
/// if let Some(left) = area.columns().next() {
/// Block::new().borders(Borders::LEFT).render(left, buf);
/// }
/// }
/// ```
pub fn columns(&self) -> Columns {
Columns {
rect: *self,
current_column: self.x,
}
pub fn columns(self) -> Columns {
Columns::new(self)
}
/// An iterator over the positions within the `Rect`.
///
/// The positions are returned in a row-major order (left-to-right, top-to-bottom).
///
/// # Example
///
/// ```
/// # 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");
/// }
/// }
/// ```
pub fn positions(self) -> Positions {
Positions::new(self)
}
/// Returns a [`Position`] with the same coordinates as this rect.

145
src/layout/rect/iter.rs Normal file
View File

@@ -0,0 +1,145 @@
use self::layout::Position;
use crate::prelude::*;
/// An iterator over rows within a `Rect`.
pub struct Rows {
/// The `Rect` associated with the rows.
pub rect: Rect,
/// The y coordinate of the row within the `Rect`.
pub current_row: u16,
}
impl Rows {
/// Creates a new `Rows` iterator.
pub fn new(rect: Rect) -> Self {
Self {
rect,
current_row: rect.y,
}
}
}
impl Iterator for Rows {
type Item = Rect;
/// Retrieves the next row within the `Rect`.
///
/// Returns `None` when there are no more rows to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_row >= self.rect.bottom() {
return None;
}
let row = Rect::new(self.rect.x, self.current_row, self.rect.width, 1);
self.current_row += 1;
Some(row)
}
}
/// An iterator over columns within a `Rect`.
pub struct Columns {
/// The `Rect` associated with the columns.
pub rect: Rect,
/// The x coordinate of the column within the `Rect`.
pub current_column: u16,
}
impl Columns {
/// Creates a new `Columns` iterator.
pub fn new(rect: Rect) -> Self {
Self {
rect,
current_column: rect.x,
}
}
}
impl Iterator for Columns {
type Item = Rect;
/// Retrieves the next column within the `Rect`.
///
/// Returns `None` when there are no more columns to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_column >= self.rect.right() {
return None;
}
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
self.current_column += 1;
Some(column)
}
}
/// An iterator over positions within a `Rect`.
///
/// The iterator will yield all positions within the `Rect` in a row-major order.
pub struct Positions {
/// The `Rect` associated with the positions.
pub rect: Rect,
/// The current position within the `Rect`.
pub current_position: Position,
}
impl Positions {
/// Creates a new `Positions` iterator.
pub fn new(rect: Rect) -> Self {
Self {
rect,
current_position: Position::new(rect.x, rect.y),
}
}
}
impl Iterator for Positions {
type Item = Position;
/// Retrieves the next position within the `Rect`.
///
/// Returns `None` when there are no more positions to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_position.y >= self.rect.bottom() {
return None;
}
let position = self.current_position;
self.current_position.x += 1;
if self.current_position.x >= self.rect.right() {
self.current_position.x = self.rect.x;
self.current_position.y += 1;
}
Some(position)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Position;
#[test]
fn rows() {
let rect = Rect::new(0, 0, 2, 2);
let mut rows = Rows::new(rect);
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.next(), None);
}
#[test]
fn columns() {
let rect = Rect::new(0, 0, 2, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.next(), None);
}
#[test]
fn positions() {
let rect = Rect::new(0, 0, 2, 2);
let mut positions = Positions::new(rect);
assert_eq!(positions.next(), Some(Position::new(0, 0)));
assert_eq!(positions.next(), Some(Position::new(1, 0)));
assert_eq!(positions.next(), Some(Position::new(0, 1)));
assert_eq!(positions.next(), Some(Position::new(1, 1)));
assert_eq!(positions.next(), None);
}
}

View File

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

View File

@@ -5,7 +5,7 @@
//! <div align="center">
//!
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
//! Badge]](./LICENSE)<br>
//! Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
//! [![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
//! [![Matrix Badge]][Matrix]<br>
//!
@@ -40,6 +40,9 @@
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
//! for more info.
//!
//! You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
//! terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
//!
//! ## Other documentation
//!
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
@@ -299,6 +302,7 @@
//! [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
//! [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
@@ -320,6 +324,7 @@
//! [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]:
@@ -337,6 +342,7 @@
//! [Matrix Badge]:
//! https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

View File

@@ -1,3 +1,4 @@
#![warn(missing_docs)]
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
//!
//! Widgets are created for each frame as they are consumed after rendered.
@@ -215,7 +216,12 @@ pub trait Widget {
/// }
/// ```
pub trait StatefulWidget {
/// State associated with the stateful widget.
///
/// If you don't need this then you probably want to implement [`Widget`] instead.
type State;
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom stateful widget.
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
}
@@ -290,6 +296,8 @@ pub trait StatefulWidget {
/// ```
#[stability::unstable(feature = "widget-ref")]
pub trait WidgetRef {
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget.
fn render_ref(&self, area: Rect, buf: &mut Buffer);
}
@@ -385,7 +393,12 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
/// ```
#[stability::unstable(feature = "widget-ref")]
pub trait StatefulWidgetRef {
/// State associated with the stateful widget.
///
/// If you don't need this then you probably want to implement [`WidgetRef`] instead.
type State;
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom stateful widget.
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
}

View File

@@ -1,4 +1,3 @@
#![warn(missing_docs)]
use crate::{prelude::*, widgets::Block};
mod bar;

View File

@@ -1,4 +1,3 @@
#![warn(missing_docs)]
//! Elements related to the `Block` base widget.
//!
//! This holds everything needed to display and configure a [`Block`].
@@ -6,6 +5,7 @@
//! In its simplest form, a `Block` is a [border](Borders) around another widget. It can have a
//! [title](Block::title) and [padding](Block::padding).
use itertools::Itertools;
use strum::{Display, EnumString};
use crate::{prelude::*, symbols::border, widgets::Borders};
@@ -16,6 +16,73 @@ pub mod title;
pub use padding::Padding;
pub use title::{Position, Title};
/// Base widget to be used to display a box border around all [upper level ones](crate::widgets).
///
/// The borders can be configured with [`Block::borders`] and others. A block can have multiple
/// [`Title`] using [`Block::title`]. It can also be [styled](Block::style) and
/// [padded](Block::padding).
///
/// You can call the title methods multiple times to add multiple titles. Each title will be
/// rendered with a single space separating titles that are in the same position or alignment. When
/// 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.
///
/// ```plain
/// ┌With at least a left border───
///
/// Without left border───
/// ```
///
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// Block::default()
/// .title("Block")
/// .borders(Borders::LEFT | Borders::RIGHT)
/// .border_style(Style::default().fg(Color::White))
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// ```
///
/// You may also use multiple titles like in the following:
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// };
///
/// Block::default()
/// .title("Title 1")
/// .title(Title::from("Title 2").position(Position::Bottom));
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Block<'a> {
/// List of titles
titles: Vec<Title<'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
titles_alignment: Alignment,
/// The default position of the titles that don't have one
titles_position: Position,
/// Visible borders
borders: Borders,
/// Border style
border_style: Style,
/// The symbols used to render the border. The default is plain lines but one can choose to
/// have rounded or doubled lines instead or a custom set of symbols
border_set: border::Set,
/// Widget style
style: Style,
/// Block padding
padding: Padding,
}
/// The type of border of a [`Block`].
///
/// See the [`borders`](Block::borders) method of `Block` to configure its borders.
@@ -89,79 +156,6 @@ pub enum BorderType {
QuadrantOutside,
}
impl BorderType {
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
pub const fn border_symbols(border_type: BorderType) -> border::Set {
match border_type {
BorderType::Plain => border::PLAIN,
BorderType::Rounded => border::ROUNDED,
BorderType::Double => border::DOUBLE,
BorderType::Thick => border::THICK,
BorderType::QuadrantInside => border::QUADRANT_INSIDE,
BorderType::QuadrantOutside => border::QUADRANT_OUTSIDE,
}
}
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
pub const fn to_border_set(self) -> border::Set {
Self::border_symbols(self)
}
}
/// Base widget to be used to display a box border around all [upper level ones](crate::widgets).
///
/// The borders can be configured with [`Block::borders`] and others. A block can have multiple
/// [`Title`] using [`Block::title`]. It can also be [styled](Block::style) and
/// [padded](Block::padding).
///
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// Block::default()
/// .title("Block")
/// .borders(Borders::LEFT | Borders::RIGHT)
/// .border_style(Style::default().fg(Color::White))
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// ```
///
/// You may also use multiple titles like in the following:
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// };
///
/// Block::default()
/// .title("Title 1")
/// .title(Title::from("Title 2").position(Position::Bottom));
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Block<'a> {
/// List of titles
titles: Vec<Title<'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
titles_alignment: Alignment,
/// The default position of the titles that don't have one
titles_position: Position,
/// Visible borders
borders: Borders,
/// Border style
border_style: Style,
/// The symbols used to render the border. The default is plain lines but one can choose to
/// have rounded or doubled lines instead or a custom set of symbols
border_set: border::Set,
/// Widget style
style: Style,
/// Block padding
padding: Padding,
}
impl<'a> Block<'a> {
/// Creates a new block with no [`Borders`] or [`Padding`].
pub const fn new() -> Self {
@@ -248,6 +242,62 @@ impl<'a> Block<'a> {
self
}
/// Adds a title to the top of the block.
///
/// You can provide any type that can be converted into [`Line`] including: strings, string
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
/// [spans](crate::text::Span) (`Vec<Span>`).
///
/// # Example
///
/// ```
/// # use ratatui::{ prelude::*, widgets::* };
/// Block::bordered()
/// .title_top("Left1") // By default in the top left corner
/// .title_top(Line::from("Left2").left_aligned())
/// .title_top(Line::from("Right").right_aligned())
/// .title_top(Line::from("Center").centered());
///
/// // Renders
/// // ┌Left1─Left2───Center─────────Right┐
/// // │ │
/// // └──────────────────────────────────┘
/// ```
#[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);
self
}
/// Adds a title to the bottom of the block.
///
/// You can provide any type that can be converted into [`Line`] including: strings, string
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
/// [spans](crate::text::Span) (`Vec<Span>`).
///
/// # Example
///
/// ```
/// # use ratatui::{ prelude::*, widgets::* };
/// Block::bordered()
/// .title_bottom("Left1") // By default in the top left corner
/// .title_bottom(Line::from("Left2").left_aligned())
/// .title_bottom(Line::from("Right").right_aligned())
/// .title_bottom(Line::from("Center").centered());
///
/// // Renders
/// // ┌──────────────────────────────────┐
/// // │ │
/// // └Left1─Left2───Center─────────Right┘
/// ```
#[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);
self
}
/// Applies the style to all titles.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
@@ -518,6 +568,25 @@ impl<'a> Block<'a> {
}
}
impl BorderType {
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
pub const fn border_symbols(border_type: BorderType) -> border::Set {
match border_type {
BorderType::Plain => border::PLAIN,
BorderType::Rounded => border::ROUNDED,
BorderType::Double => border::DOUBLE,
BorderType::Thick => border::THICK,
BorderType::QuadrantInside => border::QUADRANT_INSIDE,
BorderType::QuadrantOutside => border::QUADRANT_OUTSIDE,
}
}
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
pub const fn to_border_set(self) -> border::Set {
Self::border_symbols(self)
}
}
impl Widget for Block<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
@@ -526,9 +595,11 @@ impl Widget for Block<'_> {
impl WidgetRef for Block<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
if area.is_empty() {
return;
}
buf.set_style(area, self.style);
self.render_borders(area, buf);
self.render_titles(area, buf);
}
@@ -536,185 +607,228 @@ impl WidgetRef for Block<'_> {
impl Block<'_> {
fn render_borders(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let symbols = self.border_set;
self.render_left_side(area, buf);
self.render_top_side(area, buf);
self.render_right_side(area, buf);
self.render_bottom_side(area, buf);
// Sides
if self.borders.intersects(Borders::LEFT) {
for y in area.top()..area.bottom() {
buf.get_mut(area.left(), y)
.set_symbol(symbols.vertical_left)
.set_style(self.border_style);
}
}
if self.borders.intersects(Borders::TOP) {
for x in area.left()..area.right() {
buf.get_mut(x, area.top())
.set_symbol(symbols.horizontal_top)
.set_style(self.border_style);
}
}
if self.borders.intersects(Borders::RIGHT) {
let x = area.right() - 1;
for y in area.top()..area.bottom() {
buf.get_mut(x, y)
.set_symbol(symbols.vertical_right)
.set_style(self.border_style);
}
}
if self.borders.intersects(Borders::BOTTOM) {
let y = area.bottom() - 1;
for x in area.left()..area.right() {
buf.get_mut(x, y)
.set_symbol(symbols.horizontal_bottom)
.set_style(self.border_style);
}
}
// Corners
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
buf.get_mut(area.right() - 1, area.bottom() - 1)
.set_symbol(symbols.bottom_right)
.set_style(self.border_style);
}
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
buf.get_mut(area.right() - 1, area.top())
.set_symbol(symbols.top_right)
.set_style(self.border_style);
}
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
buf.get_mut(area.left(), area.bottom() - 1)
.set_symbol(symbols.bottom_left)
.set_style(self.border_style);
}
if self.borders.contains(Borders::LEFT | Borders::TOP) {
buf.get_mut(area.left(), area.top())
.set_symbol(symbols.top_left)
.set_style(self.border_style);
}
}
/* Titles Rendering */
fn get_title_y(&self, position: Position, area: Rect) -> u16 {
match position {
Position::Bottom => area.bottom() - 1,
Position::Top => area.top(),
}
}
fn title_filter(&self, title: &Title, alignment: Alignment, position: Position) -> bool {
title.alignment.unwrap_or(self.titles_alignment) == alignment
&& title.position.unwrap_or(self.titles_position) == position
}
fn calculate_title_area_offsets(&self, area: Rect) -> (u16, u16, u16) {
let left_border_dx = u16::from(self.borders.intersects(Borders::LEFT));
let right_border_dx = u16::from(self.borders.intersects(Borders::RIGHT));
let title_area_width = area
.width
.saturating_sub(left_border_dx)
.saturating_sub(right_border_dx);
(left_border_dx, right_border_dx, title_area_width)
}
fn render_left_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
let (left_border_dx, _, title_area_width) = self.calculate_title_area_offsets(area);
let mut current_offset = left_border_dx;
self.titles
.iter()
.filter(|title| self.title_filter(title, Alignment::Left, position))
.for_each(|title| {
let title_x = current_offset;
current_offset += title.content.width() as u16 + 1;
// Clone the title's content, applying block title style then the title style
let mut content = title.content.clone();
for span in content.spans.iter_mut() {
span.style = self.titles_style.patch(span.style);
}
buf.set_line(
title_x + area.left(),
self.get_title_y(position, area),
&content,
title_area_width,
);
});
}
fn render_center_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
let (_, _, title_area_width) = self.calculate_title_area_offsets(area);
let titles = self
.titles
.iter()
.filter(|title| self.title_filter(title, Alignment::Center, position));
let titles_sum = titles
.clone()
.fold(-1, |acc, f| acc + f.content.width() as i16 + 1); // First element isn't spaced
let mut current_offset = area.width.saturating_sub(titles_sum as u16) / 2;
titles.for_each(|title| {
let title_x = current_offset;
current_offset += title.content.width() as u16 + 1;
// Clone the title's content, applying block title style then the title style
let mut content = title.content.clone();
for span in content.spans.iter_mut() {
span.style = self.titles_style.patch(span.style);
}
buf.set_line(
title_x + area.left(),
self.get_title_y(position, area),
&content,
title_area_width,
);
});
}
fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
let (_, right_border_dx, title_area_width) = self.calculate_title_area_offsets(area);
let mut current_offset = right_border_dx;
self.titles
.iter()
.filter(|title| self.title_filter(title, Alignment::Right, position))
.rev() // so that the titles appear in the order they have been set
.for_each(|title| {
current_offset += title.content.width() as u16 + 1;
let title_x = current_offset - 1; // First element isn't spaced
// Clone the title's content, applying block title style then the title style
let mut content = title.content.clone();
for span in content.spans.iter_mut() {
span.style = self.titles_style.patch(span.style);
}
buf.set_line(
area.width.saturating_sub(title_x) + area.left(),
self.get_title_y(position, area),
&content,
title_area_width,
);
});
}
fn render_title_position(&self, position: Position, area: Rect, buf: &mut Buffer) {
// Note: the order in which these functions are called define the overlapping behavior
self.render_right_titles(position, area, buf);
self.render_center_titles(position, area, buf);
self.render_left_titles(position, area, buf);
self.render_bottom_right_corner(buf, area);
self.render_top_right_corner(buf, area);
self.render_bottom_left_corner(buf, area);
self.render_top_left_corner(buf, area);
}
fn render_titles(&self, area: Rect, buf: &mut Buffer) {
self.render_title_position(Position::Top, area, buf);
self.render_title_position(Position::Bottom, area, buf);
}
fn render_title_position(&self, position: Position, area: Rect, buf: &mut Buffer) {
// NOTE: the order in which these functions are called defines the overlapping behavior
self.render_right_titles(position, area, buf);
self.render_center_titles(position, area, buf);
self.render_left_titles(position, area, buf);
}
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)
.set_symbol(self.border_set.vertical_left)
.set_style(self.border_style);
}
}
}
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())
.set_symbol(self.border_set.horizontal_top)
.set_style(self.border_style);
}
}
}
fn render_right_side(&self, area: Rect, buf: &mut Buffer) {
if self.borders.contains(Borders::RIGHT) {
let x = area.right() - 1;
for y in area.top()..area.bottom() {
buf.get_mut(x, y)
.set_symbol(self.border_set.vertical_right)
.set_style(self.border_style);
}
}
}
fn render_bottom_side(&self, area: Rect, buf: &mut Buffer) {
if self.borders.contains(Borders::BOTTOM) {
let y = area.bottom() - 1;
for x in area.left()..area.right() {
buf.get_mut(x, y)
.set_symbol(self.border_set.horizontal_bottom)
.set_style(self.border_style);
}
}
}
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)
.set_symbol(self.border_set.bottom_right)
.set_style(self.border_style);
}
}
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())
.set_symbol(self.border_set.top_right)
.set_style(self.border_style);
}
}
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)
.set_symbol(self.border_set.bottom_left)
.set_style(self.border_style);
}
}
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())
.set_symbol(self.border_set.top_left)
.set_style(self.border_style);
}
}
/// Render titles aligned to the right of the 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
fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
let titles = self.filtered_titles(position, Alignment::Right);
let mut titles_area = self.titles_area(area, position);
// render titles in reverse order to align them to the right
for title in titles.rev() {
if titles_area.is_empty() {
break;
}
let title_width = title.content.width() as u16;
let title_area = Rect {
x: titles_area
.right()
.saturating_sub(title_width)
.max(titles_area.left()),
width: title_width.min(titles_area.width),
..titles_area
};
buf.set_style(title_area, self.titles_style);
title.content.render_ref(title_area, buf);
// bump the width of the titles area to the left
titles_area.width = titles_area
.width
.saturating_sub(title_width)
.saturating_sub(1); // space between titles
}
}
/// Render titles in the center of the block
///
/// Currently this method aligns the titles to the left inside a centered area. This is not
/// ideal and should be fixed in the future to align the titles to the center of the block and
/// truncate both sides of the titles if the block is too small to fit all titles.
fn render_center_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
let titles = self
.filtered_titles(position, Alignment::Center)
.collect_vec();
let total_width = titles
.iter()
.map(|title| title.content.width() as u16 + 1) // space between titles
.sum::<u16>()
.saturating_sub(1); // no space for the last title
let titles_area = self.titles_area(area, position);
let mut titles_area = Rect {
x: titles_area.left() + (titles_area.width.saturating_sub(total_width) / 2),
..titles_area
};
for title in titles {
if titles_area.is_empty() {
break;
}
let title_width = title.content.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);
// bump the titles area to the right and reduce its width
titles_area.x = titles_area.x.saturating_add(title_width + 1);
titles_area.width = titles_area.width.saturating_sub(title_width + 1);
}
}
/// Render titles aligned to the left of the block
fn render_left_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
let titles = self.filtered_titles(position, Alignment::Left);
let mut titles_area = self.titles_area(area, position);
for title in titles {
if titles_area.is_empty() {
break;
}
let title_width = title.content.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);
// bump the titles area to the right and reduce its width
titles_area.x = titles_area.x.saturating_add(title_width + 1);
titles_area.width = titles_area.width.saturating_sub(title_width + 1);
}
}
/// An iterator over the titles that match the position and alignment
fn filtered_titles(
&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
})
}
/// An area that is one line tall and spans the width of the block excluding the borders and
/// is positioned at the top or bottom of the block.
fn titles_area(&self, area: Rect, position: Position) -> Rect {
let left_border = u16::from(self.borders.contains(Borders::LEFT));
let right_border = u16::from(self.borders.contains(Borders::RIGHT));
Rect {
x: area.left() + left_border,
y: match position {
Position::Top => area.top(),
Position::Bottom => area.bottom() - 1,
},
width: area
.width
.saturating_sub(left_border)
.saturating_sub(right_border),
height: 1,
}
}
}
/// An extension trait for [`Block`] that provides some convenience methods.
@@ -753,7 +867,7 @@ mod tests {
use super::*;
use crate::{
assert_buffer_eq,
layout::Rect,
layout::{Alignment, Rect},
style::{Color, Modifier, Stylize},
};
@@ -1087,6 +1201,50 @@ mod tests {
)
}
#[test]
fn title() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
use Alignment::*;
use Position::*;
Block::bordered()
.title(Title::from("A").position(Top).alignment(Left))
.title(Title::from("B").position(Top).alignment(Center))
.title(Title::from("C").position(Top).alignment(Right))
.title(Title::from("D").position(Bottom).alignment(Left))
.title(Title::from("E").position(Bottom).alignment(Center))
.title(Title::from("F").position(Bottom).alignment(Right))
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"┌A─────B─────C┐",
"│ │",
"└D─────E─────F┘",
])
);
}
#[test]
fn title_top_bottom() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::bordered()
.title_top(Line::raw("A").left_aligned())
.title_top(Line::raw("B").centered())
.title_top(Line::raw("C").right_aligned())
.title_bottom(Line::raw("D").left_aligned())
.title_bottom(Line::raw("E").centered())
.title_bottom(Line::raw("F").right_aligned())
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"┌A─────B─────C┐",
"│ │",
"└D─────E─────F┘",
])
);
}
#[test]
fn title_alignment() {
let tests = vec![
@@ -1121,6 +1279,24 @@ mod tests {
}
}
/// This is a regression test for bug https://github.com/ratatui-org/ratatui/issues/929
#[test]
fn render_right_aligned_empty_title() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.title("")
.title_alignment(Alignment::Right)
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ",
" ",
])
);
}
#[test]
fn title_position() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));

View File

@@ -115,18 +115,25 @@ where
T: Into<Line<'a>>,
{
fn from(value: T) -> Self {
Self::default().content(value.into())
let content = value.into();
let alignment = content.alignment;
Self {
content,
alignment,
position: None,
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use strum::ParseError;
use super::*;
#[test]
fn position_tostring() {
fn position_to_string() {
assert_eq!(Position::Top.to_string(), "Top");
assert_eq!(Position::Bottom.to_string(), "Bottom");
}
@@ -137,4 +144,24 @@ mod tests {
assert_eq!("Bottom".parse::<Position>(), Ok(Position::Bottom));
assert_eq!("".parse::<Position>(), Err(ParseError::VariantNotFound));
}
#[test]
fn title_from_line() {
let title = Title::from(Line::raw("Title"));
assert_eq!(title.content, Line::from("Title"));
assert_eq!(title.alignment, None);
assert_eq!(title.position, None);
}
#[rstest]
#[case::left(Alignment::Left)]
#[case::center(Alignment::Center)]
#[case::right(Alignment::Right)]
fn title_from_line_with_alignment(#[case] alignment: Alignment) {
let line = Line::raw("Title").alignment(alignment);
let title = Title::from(line.clone());
assert_eq!(title.content, line);
assert_eq!(title.alignment, Some(alignment));
assert_eq!(title.position, None);
}
}

View File

@@ -1,3 +1,17 @@
//! A [`Canvas`] and a collection of [`Shape`]s.
//!
//! The [`Canvas`] is a blank space on which you can draw anything manually or use one of the
//! predefined [`Shape`]s.
//!
//! The available shapes are:
//!
//! - [`Circle`]: A basic circle
//! - [`Line`]: A line between two points
//! - [`Map`]: A world map
//! - [`Points`]: A scatter of points
//! - [`Rectangle`]: A basic rectangle
//!
//! You can also implement your own custom [`Shape`]s.
mod circle;
mod line;
mod map;
@@ -18,8 +32,14 @@ pub use self::{
};
use crate::{prelude::*, symbols, text::Line as TextLine, widgets::Block};
/// Interface for all shapes that may be drawn on a Canvas widget.
/// Something that can be drawn on a [`Canvas`].
///
/// You may implement your own canvas custom widgets by implementing this trait.
pub trait Shape {
/// Draws this [`Shape`] using the given [`Painter`].
///
/// This is the only method required to implement a custom widget that can be drawn on a
/// [`Canvas`].
fn draw(&self, painter: &mut Painter);
}
@@ -37,10 +57,10 @@ pub struct Label<'a> {
/// multiple shapes on the canvas in specific order.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct Layer {
// a string of characters representing the grid. This will be wrapped to the width of the grid
// A string of characters representing the grid. This will be wrapped to the width of the grid
// when rendering
string: String,
// colors for foreground and background
// Colors for foreground and background of each cell
colors: Vec<(Color, Color)>,
}
@@ -55,16 +75,18 @@ trait Grid: Debug {
fn width(&self) -> u16;
/// Get the height of the grid in number of terminal rows
fn height(&self) -> u16;
/// Get the resolution of the grid in number of dots. This doesn't have to be the same as the
/// number of rows and columns of the grid. For example, a grid of Braille patterns will have a
/// resolution of 2x4 dots per cell. This means that a grid of 10x10 cells will have a
/// resolution of 20x40 dots.
/// Get the resolution of the grid in number of dots.
///
/// This doesn't have to be the same as the number of rows and columns of the grid. For example,
/// a grid of Braille patterns will have a resolution of 2x4 dots per cell. This means that a
/// grid of 10x10 cells will have a resolution of 20x40 dots.
fn resolution(&self) -> (f64, f64);
/// Paint a point of the grid. The point is expressed in number of dots starting at the origin
/// of the grid in the top left corner. Note that this is not the same as the (x, y) coordinates
/// of the canvas.
/// Paint a point of the grid.
///
/// The point is expressed in number of dots starting at the origin of the grid in the top left
/// corner. Note that this is not the same as the `(x, y)` coordinates of the canvas.
fn paint(&mut self, x: usize, y: usize, color: Color);
/// Save the current state of the grid as a layer to be rendered
/// Save the current state of the [`Grid`] as a layer to be rendered
fn save(&self) -> Layer;
/// Reset the grid to its initial state
fn reset(&mut self);
@@ -81,12 +103,12 @@ trait Grid: Debug {
/// to set the individual color of each dot in the braille pattern.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct BrailleGrid {
/// width of the grid in number of terminal columns
/// Width of the grid in number of terminal columns
width: u16,
/// height of the grid in number of terminal rows
/// Height of the grid in number of terminal rows
height: u16,
/// represents the unicode braille patterns. Will take a value between 0x2800 and 0x28FF
/// this is converted to a utf16 string when converting to a layer. See
/// Represents the unicode braille patterns. Will take a value between `0x2800` and `0x28FF`
/// this is converted to an utf16 string when converting to a layer. See
/// <https://en.wikipedia.org/wiki/Braille_Patterns> for more info.
utf16_code_points: Vec<u16>,
/// The color of each cell only supports foreground colors for now as there's no way to
@@ -152,11 +174,11 @@ impl Grid for BrailleGrid {
/// when you want to draw shapes with a low resolution.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct CharGrid {
/// width of the grid in number of terminal columns
/// Width of the grid in number of terminal columns
width: u16,
/// height of the grid in number of terminal rows
/// Height of the grid in number of terminal rows
height: u16,
/// represents a single character for each cell
/// Represents a single character for each cell
cells: Vec<char>,
/// The color of each cell
colors: Vec<Color>,
@@ -232,17 +254,17 @@ impl Grid for CharGrid {
/// character for each cell.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct HalfBlockGrid {
/// width of the grid in number of terminal columns
/// Width of the grid in number of terminal columns
width: u16,
/// height of the grid in number of terminal rows
/// Height of the grid in number of terminal rows
height: u16,
/// represents a single color for each "pixel" arranged in column, row order
/// Represents a single color for each "pixel" arranged in column, row order
pixels: Vec<Vec<Color>>,
}
impl HalfBlockGrid {
/// Create a new HalfBlockGrid with the given width and height measured in terminal columns and
/// rows respectively.
/// Create a new `HalfBlockGrid` with the given width and height measured in terminal columns
/// and rows respectively.
fn new(width: u16, height: u16) -> HalfBlockGrid {
HalfBlockGrid {
width,
@@ -346,33 +368,39 @@ pub struct Painter<'a, 'b> {
}
impl<'a, 'b> Painter<'a, 'b> {
/// Convert the (x, y) coordinates to location of a point on the grid
/// Convert the `(x, y)` coordinates to location of a point on the grid
///
/// (x, y) coordinates are expressed in the coordinate system of the canvas. The origin is in
/// the lower left corner of the canvas (unlike most other coordinates in Ratatui where the
/// origin is the upper left corner). The x and y bounds of the canvas define the specific area
/// of some coordinate system that will be drawn on the canvas. The resolution of the grid is
/// used to convert the (x, y) coordinates to the location of a point on the grid.
/// `(x, y)` coordinates are expressed in the coordinate system of the canvas. The origin is in
/// the lower left corner of the canvas (unlike most other coordinates in `Ratatui` where the
/// origin is the upper left corner). The `x` and `y` bounds of the canvas define the specific
/// area of some coordinate system that will be drawn on the canvas. The resolution of the grid
/// is used to convert the `(x, y)` coordinates to the location of a point on the grid.
///
/// The grid coordinates are expressed in the coordinate system of the grid. The origin is in
/// the top left corner of the grid. The x and y bounds of the grid are always [0, width - 1]
/// and [0, height - 1] respectively. The resolution of the grid is used to convert the (x, y)
/// coordinates to the location of a point on the grid.
/// the top left corner of the grid. The x and y bounds of the grid are always `[0, width - 1]`
/// and `[0, height - 1]` respectively. The resolution of the grid is used to convert the
/// `(x, y)` coordinates to the location of a point on the grid.
///
/// # Examples
///
/// # Examples:
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
///
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
///
/// let point = painter.get_point(1.0, 0.0);
/// assert_eq!(point, Some((0, 7)));
///
/// let point = painter.get_point(1.5, 1.0);
/// assert_eq!(point, Some((1, 3)));
///
/// let point = painter.get_point(0.0, 0.0);
/// assert_eq!(point, None);
///
/// let point = painter.get_point(2.0, 2.0);
/// assert_eq!(point, Some((3, 0)));
///
/// let point = painter.get_point(1.0, 2.0);
/// assert_eq!(point, Some((0, 0)));
/// ```
@@ -396,13 +424,14 @@ impl<'a, 'b> Painter<'a, 'b> {
/// Paint a point of the grid
///
/// # Examples:
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
///
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
/// let cell = painter.paint(1, 3, Color::Red);
/// painter.paint(1, 3, Color::Red);
/// ```
pub fn paint(&mut self, x: usize, y: usize, color: Color) {
self.context.grid.paint(x, y, color);
@@ -419,7 +448,7 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
}
}
/// Holds the state of the Canvas when painting to it.
/// Holds the state of the [`Canvas`] when painting to it.
///
/// 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.
@@ -437,14 +466,14 @@ pub struct Context<'a> {
impl<'a> Context<'a> {
/// Create a new Context with the given width and height measured in terminal columns and rows
/// respectively. The x and y bounds define the specific area of some coordinate system that
/// respectively. The `x` and `y` bounds define the specific area of some coordinate system that
/// will be drawn on the canvas. The marker defines the type of points used to draw the shapes.
///
/// Applications should not use this directly but rather use the [`Canvas`] widget. This will be
/// created by the [`Canvas::paint`] moethod and passed to the closure that is used to draw on
/// created by the [`Canvas::paint`] method and passed to the closure that is used to draw on
/// the canvas.
///
/// The x and y bounds should be specified as left/right and bottom/top respectively. For
/// The `x` and `y` bounds should be specified as left/right and bottom/top respectively. For
/// example, if you want to draw a map of the world, you might want to use the following bounds:
///
/// ```
@@ -485,7 +514,7 @@ impl<'a> Context<'a> {
}
}
/// Draw any object that may implement the Shape trait
/// Draw the given [`Shape`] in this context
pub fn draw<S>(&mut self, shape: &S)
where
S: Shape,
@@ -495,16 +524,23 @@ impl<'a> Context<'a> {
shape.draw(&mut painter);
}
/// Save the existing state of the grid as a layer to be rendered and reset the grid to its
/// initial state for the next layer.
/// Save the existing state of the grid as a layer.
///
/// Save the existing state as a layer to be rendered and reset the grid to its initial
/// state for the next layer.
///
/// This allows the canvas to be drawn in multiple layers. This is useful if you want to
/// draw multiple shapes on the [`Canvas`] in specific order.
pub fn layer(&mut self) {
self.layers.push(self.grid.save());
self.grid.reset();
self.dirty = false;
}
/// Print a string on the canvas at the given position. Note that the text is always printed
/// on top of the canvas and is not affected by the layers.
/// Print a [`Text`] on the [`Canvas`] at the given position.
///
/// Note that the text is always printed on top of the canvas and is **not** affected by the
/// layers.
pub fn print<T>(&mut self, x: f64, y: f64, line: T)
where
T: Into<TextLine<'a>>,
@@ -516,7 +552,7 @@ impl<'a> Context<'a> {
});
}
/// Push the last layer if necessary
/// Save the last layer if necessary
fn finish(&mut self) {
if self.dirty {
self.layer();
@@ -619,15 +655,22 @@ impl<'a, F> Canvas<'a, F>
where
F: Fn(&mut Context),
{
/// Set the block that will be rendered around the canvas
/// Wraps the canvas with a custom [`Block`] widget.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Canvas<'a, F> {
self.block = Some(block);
self
}
/// Define the viewport of the canvas.
///
/// If you were to "zoom" to a certain part of the world you may want to choose different
/// bounds.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
self.x_bounds = bounds;
self
@@ -637,31 +680,48 @@ where
///
/// If you were to "zoom" to a certain part of the world you may want to choose different
/// bounds.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
self.y_bounds = bounds;
self
}
/// Store the closure that will be used to draw to the Canvas
/// Store the closure that will be used to draw to the [`Canvas`]
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn paint(mut self, f: F) -> Canvas<'a, F> {
self.paint_func = Some(f);
self
}
/// Change the background color of the canvas
/// Change the background [`Color`] of the entire canvas
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn background_color(mut self, color: Color) -> Canvas<'a, F> {
self.background_color = color;
self
}
/// Change the type of points used to draw the shapes. By default the braille patterns are used
/// as they provide a more fine grained result but you might want to use the simple dot or
/// block instead if the targeted terminal does not support those symbols.
/// Change the type of points used to draw the shapes.
///
/// The HalfBlock marker is useful when you want to draw shapes with a higher resolution than a
/// CharGrid but lower than a BrailleGrid. This grid type supports a foreground and background
/// color for each terminal cell. This allows for more flexibility than the BrailleGrid which
/// only supports a single foreground color for each 2x4 dots cell.
/// By default the [`Braille`] patterns are used as they provide a more fine grained result,
/// but you might want to use the simple [`Dot`] or [`Block`] instead if the targeted terminal
/// does not support those symbols.
///
/// The [`HalfBlock`] marker is useful when you want to draw shapes with a higher resolution
/// than with a grid of characters (e.g. with [`Block`] or [`Dot`]) but lower than with
/// [`Braille`]. This grid type supports a foreground and background color for each terminal
/// cell. This allows for more flexibility than the BrailleGrid which only supports a single
/// foreground color for each 2x4 dots cell.
///
/// [`Braille`]: crate::symbols::Marker::Braille
/// [`HalfBlock`]: crate::symbols::Marker::HalfBlock
/// [`Dot`]: crate::symbols::Marker::Dot
/// [`Block`]: crate::symbols::Marker::Block
///
/// # Examples
///
@@ -671,12 +731,15 @@ where
/// Canvas::default()
/// .marker(symbols::Marker::Braille)
/// .paint(|ctx| {});
///
/// Canvas::default()
/// .marker(symbols::Marker::HalfBlock)
/// .paint(|ctx| {});
///
/// Canvas::default()
/// .marker(symbols::Marker::Dot)
/// .paint(|ctx| {});
///
/// Canvas::default()
/// .marker(symbols::Marker::Block)
/// .paint(|ctx| {});

View File

@@ -3,12 +3,16 @@ use crate::{
widgets::canvas::{Painter, Shape},
};
/// Shape to draw a circle with a given center and radius and with the given color
/// A circle with a given center and radius and with a given color
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Circle {
/// `x` coordinate of the circle's center
pub x: f64,
/// `y` coordinate of the circle's center
pub y: f64,
/// Radius of the circle
pub radius: f64,
/// Color of the circle
pub color: Color,
}

View File

@@ -3,18 +3,23 @@ use crate::{
widgets::canvas::{Painter, Shape},
};
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
/// A line from `(x1, y1)` to `(x2, y2)` with the given color
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Line {
/// `x` of the starting point
pub x1: f64,
/// `y` of the starting point
pub y1: f64,
/// `x` of the ending point
pub x2: f64,
/// `y` of the ending point
pub y2: f64,
/// Color of the line
pub color: Color,
}
impl Line {
/// Create a new line from (x1, y1) to (x2, y2) with the given color
/// Create a new line from `(x1, y1)` to `(x2, y2)` with the given color
pub fn new(x1: f64, y1: f64, x2: f64, y2: f64, color: Color) -> Self {
Self {
x1,

View File

@@ -8,10 +8,21 @@ use crate::{
},
};
/// Defines how many points are going to be used to draw a [`Map`].
///
/// You generally want a [high](MapResolution::High) resolution map.
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum MapResolution {
/// A lesser resolution for the [`Map`] [`Shape`].
///
/// Contains about 1000 points.
#[default]
Low,
/// A higher resolution for the [`Map`] [`Shape`].
///
/// Contains about 5000 points, you likely want to use [`Marker::Braille`] with this.
///
/// [`Marker::Braille`]: (crate::symbols::Marker::Braille)
High,
}
@@ -24,10 +35,18 @@ impl MapResolution {
}
}
/// Shape to draw a world map with the given resolution and color
/// A world map
///
/// A world map can be rendered with different [resolutions](MapResolution) and [colors](Color).
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Map {
/// The resolution of the map.
///
/// This is the number of points used to draw the map.
pub resolution: MapResolution,
/// Map color
///
/// This is the color of the points of the map.
pub color: Color,
}

View File

@@ -3,10 +3,12 @@ use crate::{
widgets::canvas::{Painter, Shape},
};
/// A shape to draw a group of points with the given color
/// A group of points with a given color
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Points<'a> {
/// List of points to draw
pub coords: &'a [(f64, f64)],
/// Color of the points
pub color: Color,
}

View File

@@ -3,13 +3,25 @@ use crate::{
widgets::canvas::{Line, Painter, Shape},
};
/// Shape to draw a rectangle from a `Rect` with the given color
/// A rectangle to draw on a [`Canvas`](super::Canvas)
///
/// Sizes used here are **not** in terminal cell. This is much more similar to the
/// mathematic coordinate system.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Rectangle {
/// The `x` position of the rectangle.
///
/// The rectangle is positioned from its bottom left corner.
pub x: f64,
/// The `y` position of the rectangle.
///
/// The rectangle is positioned from its bottom left corner.
pub y: f64,
/// The width of the rectangle.
pub width: f64,
/// The height of the rectangle.
pub height: f64,
/// The color of the rectangle.
pub color: Color,
}

View File

@@ -1,4 +1,3 @@
#![warn(missing_docs)]
use std::cmp::max;
use strum::{Display, EnumString};

View File

@@ -1,5 +1,3 @@
#![deny(missing_docs)]
use crate::{prelude::*, widgets::Block};
/// A widget to display a progress bar.

View File

@@ -1,4 +1,3 @@
#![warn(missing_docs)]
use strum::{Display, EnumString};
use unicode_width::UnicodeWidthStr;
@@ -896,7 +895,7 @@ impl StatefulWidgetRef for List<'_> {
let is_selected = state.selected.map_or(false, |s| s == i);
let item_area = if selection_spacing {
let highlight_symbol_width = self.highlight_symbol.unwrap_or("").len() as u16;
let highlight_symbol_width = self.highlight_symbol.unwrap_or("").width() as u16;
Rect {
x: row_area.x + highlight_symbol_width,
width: row_area.width - highlight_symbol_width,

View File

@@ -340,9 +340,7 @@ impl Paragraph<'_> {
}
let styled = self.text.iter().map(|line| {
let graphemes = line
.iter()
.flat_map(|span| span.styled_graphemes(self.style));
let graphemes = line.styled_graphemes(self.style);
let alignment = line.alignment.unwrap_or(self.alignment);
(graphemes, alignment)
});
@@ -593,6 +591,40 @@ mod test {
);
}
#[test]
fn test_render_line_styled() {
let l0 = Line::raw("unformatted");
let l1 = Line::styled("bold text", Style::new().bold());
let l2 = Line::styled("cyan text", Style::new().cyan());
let l3 = Line::styled("dim text", Style::new().dim());
let paragraph = Paragraph::new(vec![l0, l1, l2, l3]);
let mut expected =
Buffer::with_lines(vec!["unformatted", "bold text", "cyan text", "dim text"]);
expected.set_style(Rect::new(0, 1, 9, 1), Style::new().bold());
expected.set_style(Rect::new(0, 2, 9, 1), Style::new().cyan());
expected.set_style(Rect::new(0, 3, 8, 1), Style::new().dim());
test_case(&paragraph, expected);
}
#[test]
fn test_render_line_spans_styled() {
let l0 = Line::default().spans(vec![
Span::styled("bold", Style::new().bold()),
Span::raw(" and "),
Span::styled("cyan", Style::new().cyan()),
]);
let l1 = Line::default().spans(vec![Span::raw("unformatted")]);
let paragraph = Paragraph::new(vec![l0, l1]);
let mut expected = Buffer::with_lines(vec!["bold and cyan", "unformatted"]);
expected.set_style(Rect::new(0, 0, 4, 1), Style::new().bold());
expected.set_style(Rect::new(9, 0, 4, 1), Style::new().cyan());
test_case(&paragraph, expected);
}
#[test]
fn test_render_paragraph_with_block_with_bottom_title_and_border() {
let block = Block::default()

View File

@@ -1,4 +1,3 @@
#![warn(missing_docs)]
use std::iter;
use strum::{Display, EnumString};
@@ -454,7 +453,7 @@ impl<'a> StatefulWidget for Scrollbar<'a> {
let area = self.scollbar_area(area);
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
if let Some((symbol, style)) = bar.next() {
if let Some(Some((symbol, style))) = bar.next() {
buf.set_string(x, y, symbol, style);
}
}
@@ -468,23 +467,23 @@ impl Scrollbar<'_> {
&self,
area: Rect,
state: &mut ScrollbarState,
) -> impl Iterator<Item = (&str, Style)> {
) -> impl Iterator<Item = Option<(&str, Style)>> {
let (track_start_len, thumb_len, track_end_len) = self.part_lengths(area, state);
let begin = self.begin_symbol.map(|s| (s, self.begin_style));
let track = self.track_symbol.map(|s| (s, self.track_style));
let thumb = Some((self.thumb_symbol, self.thumb_style));
let end = self.end_symbol.map(|s| (s, self.end_style));
let begin = self.begin_symbol.map(|s| Some((s, self.begin_style)));
let track = Some(self.track_symbol.map(|s| (s, self.track_style)));
let thumb = Some(Some((self.thumb_symbol, self.thumb_style)));
let end = self.end_symbol.map(|s| Some((s, self.end_style)));
// `<``
// `<`
iter::once(begin)
// `<═══`
.chain(iter::repeat(track).take(track_start_len))
// `<═══█████`
.chain(iter::repeat(thumb).take(thumb_len))
// `<═══█████═══════``
// `<═══█████═══════`
.chain(iter::repeat(track).take(track_end_len))
// `<═══█████═══════>``
// `<═══█████═══════>`
.chain(iter::once(end))
.flatten()
}
@@ -769,6 +768,83 @@ mod tests {
);
}
#[rstest]
#[case("█████ ", 0, 10, "position_0")]
#[case(" █████ ", 1, 10, "position_1")]
#[case(" █████ ", 2, 10, "position_2")]
#[case(" █████ ", 3, 10, "position_3")]
#[case(" █████ ", 4, 10, "position_4")]
#[case(" █████ ", 5, 10, "position_5")]
#[case(" █████ ", 6, 10, "position_6")]
#[case(" █████ ", 7, 10, "position_7")]
#[case(" █████ ", 8, 10, "position_8")]
#[case(" █████", 9, 10, "position_9")]
#[case(" █████", 100, 10, "position_out_of_bounds")]
fn render_scrollbar_without_track_symbols(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] assertion_message: &str,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.track_symbol(None)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{}",
assertion_message
);
}
#[rstest]
#[case("█████-----", 0, 10, "position_0")]
#[case("-█████----", 1, 10, "position_1")]
#[case("-█████----", 2, 10, "position_2")]
#[case("--█████---", 3, 10, "position_3")]
#[case("--█████---", 4, 10, "position_4")]
#[case("---█████--", 5, 10, "position_5")]
#[case("---█████--", 6, 10, "position_6")]
#[case("----█████-", 7, 10, "position_7")]
#[case("----█████-", 8, 10, "position_8")]
#[case("-----█████", 9, 10, "position_9")]
#[case("-----█████", 100, 10, "position_out_of_bounds")]
fn render_scrollbar_without_track_symbols_over_content(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] assertion_message: &str,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let width = buffer.area.width as usize;
let s = "";
Text::from(format!("{s:-^width$}")).render(buffer.area, &mut buffer);
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.track_symbol(None)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{}",
assertion_message
);
}
#[rstest]
#[case("<####---->", 0, 10, "position_0")]
#[case("<#####--->", 1, 10, "position_1")]

View File

@@ -1,4 +1,3 @@
#![warn(missing_docs)]
use std::cmp::min;
use strum::{Display, EnumString};

View File

@@ -1,87 +1,12 @@
#![warn(missing_docs)]
use strum::{Display, EnumString};
mod cell;
mod highlight_spacing;
mod row;
#[allow(clippy::module_inception)]
mod table;
mod table_state;
pub use cell::Cell;
pub use row::Row;
pub use table::Table;
pub use table_state::TableState;
/// This option allows the user to configure the "highlight symbol" column width spacing
#[derive(Debug, Display, EnumString, PartialEq, Eq, Clone, Default, Hash)]
pub enum HighlightSpacing {
/// Always add spacing for the selection symbol column
///
/// With this variant, the column for the selection symbol will always be allocated, and so the
/// table will never change size, regardless of if a row is selected or not
Always,
/// Only add spacing for the selection symbol column if a row is selected
///
/// With this variant, the column for the selection symbol will only be allocated if there is a
/// selection, causing the table to shift if selected / unselected
#[default]
WhenSelected,
/// Never add spacing to the selection symbol column, regardless of whether something is
/// selected or not
///
/// This means that the highlight symbol will never be drawn
Never,
}
impl HighlightSpacing {
/// Determine if a selection column should be displayed
///
/// has_selection: true if a row is selected in the table
///
/// Returns true if a selection column should be displayed
pub(crate) fn should_add(&self, has_selection: bool) -> bool {
match self {
HighlightSpacing::Always => true,
HighlightSpacing::WhenSelected => has_selection,
HighlightSpacing::Never => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn highlight_spacing_to_string() {
assert_eq!(HighlightSpacing::Always.to_string(), "Always".to_string());
assert_eq!(
HighlightSpacing::WhenSelected.to_string(),
"WhenSelected".to_string()
);
assert_eq!(HighlightSpacing::Never.to_string(), "Never".to_string());
}
#[test]
fn highlight_spacing_from_str() {
assert_eq!(
"Always".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::Always)
);
assert_eq!(
"WhenSelected".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::WhenSelected)
);
assert_eq!(
"Never".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::Never)
);
assert_eq!(
"".parse::<HighlightSpacing>(),
Err(strum::ParseError::VariantNotFound)
);
}
}
pub use cell::*;
pub use highlight_spacing::*;
pub use row::*;
pub use table::*;
pub use table_state::*;

View File

@@ -0,0 +1,74 @@
use strum::{Display, EnumString};
/// This option allows the user to configure the "highlight symbol" column width spacing
#[derive(Debug, Display, EnumString, PartialEq, Eq, Clone, Default, Hash)]
pub enum HighlightSpacing {
/// Always add spacing for the selection symbol column
///
/// With this variant, the column for the selection symbol will always be allocated, and so the
/// table will never change size, regardless of if a row is selected or not
Always,
/// Only add spacing for the selection symbol column if a row is selected
///
/// With this variant, the column for the selection symbol will only be allocated if there is a
/// selection, causing the table to shift if selected / unselected
#[default]
WhenSelected,
/// Never add spacing to the selection symbol column, regardless of whether something is
/// selected or not
///
/// This means that the highlight symbol will never be drawn
Never,
}
impl HighlightSpacing {
/// Determine if a selection column should be displayed
///
/// has_selection: true if a row is selected in the table
///
/// Returns true if a selection column should be displayed
pub(crate) fn should_add(&self, has_selection: bool) -> bool {
match self {
HighlightSpacing::Always => true,
HighlightSpacing::WhenSelected => has_selection,
HighlightSpacing::Never => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_string() {
assert_eq!(HighlightSpacing::Always.to_string(), "Always".to_string());
assert_eq!(
HighlightSpacing::WhenSelected.to_string(),
"WhenSelected".to_string()
);
assert_eq!(HighlightSpacing::Never.to_string(), "Never".to_string());
}
#[test]
fn from_str() {
assert_eq!(
"Always".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::Always)
);
assert_eq!(
"WhenSelected".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::WhenSelected)
);
assert_eq!(
"Never".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::Never)
);
assert_eq!(
"".parse::<HighlightSpacing>(),
Err(strum::ParseError::VariantNotFound)
);
}
}

View File

@@ -1,4 +1,3 @@
#![deny(missing_docs)]
use crate::{prelude::*, widgets::Block};
const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);

View File

@@ -15,21 +15,11 @@ use ratatui::{
fn widgets_block_renders() {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend).unwrap();
let block = Block::default()
.title(Span::styled("Title", Style::default().fg(Color::LightBlue)))
.borders(Borders::ALL);
terminal
.draw(|f| {
let block = Block::default()
.title(Span::styled("Title", Style::default().fg(Color::LightBlue)))
.borders(Borders::ALL);
f.render_widget(
block,
Rect {
x: 0,
y: 0,
width: 8,
height: 8,
},
);
})
.draw(|frame| frame.render_widget(block, Rect::new(0, 0, 8, 8)))
.unwrap();
let mut expected = Buffer::with_lines(vec![
"┌Title─┐ ",
@@ -51,16 +41,15 @@ fn widgets_block_renders() {
#[test]
fn widgets_block_titles_overlap() {
let test_case = |block, area: Rect, expected| {
#[track_caller]
fn test_case(block: Block, area: Rect, expected: Buffer) {
let backend = TestBackend::new(area.width, area.height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
f.render_widget(block, area);
})
.draw(|frame| frame.render_widget(block, area))
.unwrap();
terminal.backend().assert_buffer(&expected);
};
}
// Left overrides the center
test_case(
@@ -68,12 +57,7 @@ fn widgets_block_titles_overlap() {
.title(Title::from("aaaaa").alignment(Alignment::Left))
.title(Title::from("bbb").alignment(Alignment::Center))
.title(Title::from("ccc").alignment(Alignment::Right)),
Rect {
x: 0,
y: 0,
width: 10,
height: 1,
},
Rect::new(0, 0, 10, 1),
Buffer::with_lines(vec!["aaaaab ccc"]),
);
@@ -83,12 +67,7 @@ fn widgets_block_titles_overlap() {
.title(Title::from("aaaaa").alignment(Alignment::Left))
.title(Title::from("bbbbb").alignment(Alignment::Center))
.title(Title::from("ccccc").alignment(Alignment::Right)),
Rect {
x: 0,
y: 0,
width: 11,
height: 1,
},
Rect::new(0, 0, 11, 1),
Buffer::with_lines(vec!["aaaaabbbccc"]),
);
@@ -99,12 +78,7 @@ fn widgets_block_titles_overlap() {
.title(Title::from("aaaaa").alignment(Alignment::Left))
.title(Title::from("bbbbb").alignment(Alignment::Center))
.title(Title::from("ccccc").alignment(Alignment::Right)),
Rect {
x: 0,
y: 0,
width: 11,
height: 1,
},
Rect::new(0, 0, 11, 1),
Buffer::with_lines(vec!["aaaaabaaaaa"]),
);
@@ -113,28 +87,22 @@ fn widgets_block_titles_overlap() {
Block::default()
.title(Title::from("bbbbb").alignment(Alignment::Center))
.title(Title::from("ccccccccccc").alignment(Alignment::Right)),
Rect {
x: 0,
y: 0,
width: 11,
height: 1,
},
Rect::new(0, 0, 11, 1),
Buffer::with_lines(vec!["cccbbbbbccc"]),
);
}
#[test]
fn widgets_block_renders_on_small_areas() {
let test_case = |block, area: Rect, expected| {
#[track_caller]
fn test_case(block: Block, area: Rect, expected: Buffer) {
let backend = TestBackend::new(area.width, area.height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
f.render_widget(block, area);
})
.draw(|frame| frame.render_widget(block, area))
.unwrap();
terminal.backend().assert_buffer(&expected);
};
}
let one_cell_test_cases = [
(Borders::NONE, "T"),
@@ -147,152 +115,78 @@ fn widgets_block_renders_on_small_areas() {
for (borders, symbol) in one_cell_test_cases.iter().cloned() {
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
},
Buffer::empty(Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}),
Rect::new(0, 0, 0, 0),
Buffer::empty(Rect::new(0, 0, 0, 0)),
);
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 1,
height: 0,
},
Buffer::empty(Rect {
x: 0,
y: 0,
width: 1,
height: 0,
}),
Rect::new(0, 0, 1, 0),
Buffer::empty(Rect::new(0, 0, 1, 0)),
);
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 0,
height: 1,
},
Buffer::empty(Rect {
x: 0,
y: 0,
width: 0,
height: 1,
}),
Rect::new(0, 0, 0, 1),
Buffer::empty(Rect::new(0, 0, 0, 1)),
);
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 1,
height: 1,
},
Rect::new(0, 0, 1, 1),
Buffer::with_lines(vec![symbol]),
);
}
test_case(
Block::default().title("Test").borders(Borders::LEFT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Rect::new(0, 0, 4, 1),
Buffer::with_lines(vec!["│Tes"]),
);
test_case(
Block::default().title("Test").borders(Borders::RIGHT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Rect::new(0, 0, 4, 1),
Buffer::with_lines(vec!["Tes│"]),
);
test_case(
Block::default().title("Test").borders(Borders::RIGHT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Rect::new(0, 0, 4, 1),
Buffer::with_lines(vec!["Tes│"]),
);
test_case(
Block::default()
.title("Test")
.borders(Borders::LEFT | Borders::RIGHT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Rect::new(0, 0, 4, 1),
Buffer::with_lines(vec!["│Te│"]),
);
test_case(
Block::default().title("Test").borders(Borders::TOP),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Rect::new(0, 0, 4, 1),
Buffer::with_lines(vec!["Test"]),
);
test_case(
Block::default().title("Test").borders(Borders::TOP),
Rect {
x: 0,
y: 0,
width: 5,
height: 1,
},
Rect::new(0, 0, 5, 1),
Buffer::with_lines(vec!["Test─"]),
);
test_case(
Block::default()
.title("Test")
.borders(Borders::LEFT | Borders::TOP),
Rect {
x: 0,
y: 0,
width: 5,
height: 1,
},
Rect::new(0, 0, 5, 1),
Buffer::with_lines(vec!["┌Test"]),
);
test_case(
Block::default()
.title("Test")
.borders(Borders::LEFT | Borders::TOP),
Rect {
x: 0,
y: 0,
width: 6,
height: 1,
},
Rect::new(0, 0, 6, 1),
Buffer::with_lines(vec!["┌Test─"]),
);
}
#[test]
fn widgets_block_title_alignment() {
let test_case = |alignment, borders, expected| {
let backend = TestBackend::new(15, 2);
#[track_caller]
fn test_case(alignment: Alignment, borders: Borders, expected: Buffer) {
let backend = TestBackend::new(15, 3);
let mut terminal = Terminal::new(backend).unwrap();
let block1 = Block::default()
@@ -304,270 +198,371 @@ fn widgets_block_title_alignment() {
.title_alignment(alignment)
.borders(borders);
let area = Rect {
x: 1,
y: 0,
width: 13,
height: 2,
};
let area = Rect::new(1, 0, 13, 3);
for block in [block1, block2] {
terminal
.draw(|f| {
f.render_widget(block, area);
})
.draw(|frame| frame.render_widget(block, area))
.unwrap();
terminal.backend().assert_buffer(&expected);
}
};
}
// title top-left with all borders
test_case(
Alignment::Left,
Borders::ALL,
Buffer::with_lines(vec![" ┌Title──────┐ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" ┌Title──────┐ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title top-left without top border
test_case(
Alignment::Left,
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │Title │ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" │Title │ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title top-left with no left border
test_case(
Alignment::Left,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" Title───────┐ ", " ────────────┘ "]),
Buffer::with_lines(vec![
" Title───────┐ ",
"",
" ────────────┘ ",
]),
);
// title top-left without right border
test_case(
Alignment::Left,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌Title─────── ", " └──────────── "]),
Buffer::with_lines(vec![
" ┌Title─────── ",
"",
" └──────────── ",
]),
);
// title top-left without borders
test_case(
Alignment::Left,
Borders::NONE,
Buffer::with_lines(vec![" Title ", " "]),
Buffer::with_lines(vec![
" Title ",
" ",
" ",
]),
);
// title center with all borders
test_case(
Alignment::Center,
Borders::ALL,
Buffer::with_lines(vec![" ┌───Title───┐ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" ┌───Title───┐ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title center without top border
test_case(
Alignment::Center,
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │ Title │ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" │ Title │ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title center with no left border
test_case(
Alignment::Center,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ────Title───┐ ", " ────────────┘ "]),
Buffer::with_lines(vec![
" ───Title────┐ ",
"",
" ────────────┘ ",
]),
);
// title center without right border
test_case(
Alignment::Center,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌───Title──── ", " └──────────── "]),
Buffer::with_lines(vec![
" ┌───Title──── ",
"",
" └──────────── ",
]),
);
// title center without borders
test_case(
Alignment::Center,
Borders::NONE,
Buffer::with_lines(vec![" Title ", " "]),
Buffer::with_lines(vec![
" Title ",
" ",
" ",
]),
);
// title top-right with all borders
test_case(
Alignment::Right,
Borders::ALL,
Buffer::with_lines(vec![" ┌──────Title┐ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" ┌──────Title┐ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title top-right without top border
test_case(
Alignment::Right,
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │ Title│ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" │ Title│ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title top-right with no left border
test_case(
Alignment::Right,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ───────Title┐ ", " ────────────┘ "]),
Buffer::with_lines(vec![
" ───────Title┐ ",
"",
" ────────────┘ ",
]),
);
// title top-right without right border
test_case(
Alignment::Right,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌───────Title ", " └──────────── "]),
Buffer::with_lines(vec![
" ┌───────Title ",
"",
" └──────────── ",
]),
);
// title top-right without borders
test_case(
Alignment::Right,
Borders::NONE,
Buffer::with_lines(vec![" Title ", " "]),
Buffer::with_lines(vec![
" Title ",
" ",
" ",
]),
);
}
#[test]
fn widgets_block_title_alignment_bottom() {
let test_case = |alignment, borders, expected| {
let backend = TestBackend::new(15, 2);
#[track_caller]
fn test_case(alignment: Alignment, borders: Borders, expected: Buffer) {
let backend = TestBackend::new(15, 3);
let mut terminal = Terminal::new(backend).unwrap();
let block = Block::default()
.title(
Title::from(Span::styled("Title", Style::default()))
.alignment(alignment)
.position(Position::Bottom),
)
.borders(borders);
let area = Rect {
x: 1,
y: 0,
width: 13,
height: 2,
};
let title = Title::from(Span::styled("Title", Style::default()))
.alignment(alignment)
.position(Position::Bottom);
let block = Block::default().title(title).borders(borders);
let area = Rect::new(1, 0, 13, 3);
terminal
.draw(|f| {
f.render_widget(block, area);
})
.draw(|frame| frame.render_widget(block, area))
.unwrap();
terminal.backend().assert_buffer(&expected);
};
}
// title bottom-left with all borders
test_case(
Alignment::Left,
Borders::ALL,
Buffer::with_lines(vec![" ┌───────────┐ ", " └Title──────┘ "]),
Buffer::with_lines(vec![
" ┌───────────┐ ",
" │ │ ",
" └Title──────┘ ",
]),
);
// title bottom-left without bottom border
test_case(
Alignment::Left,
Borders::LEFT | Borders::TOP | Borders::RIGHT,
Buffer::with_lines(vec![" ┌───────────┐ ", " │Title │ "]),
Buffer::with_lines(vec![
" ┌───────────┐ ",
" │ │ ",
" │Title │ ",
]),
);
// title bottom-left with no left border
test_case(
Alignment::Left,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ────────────┐ ", " Title───────┘ "]),
Buffer::with_lines(vec![
" ────────────┐ ",
"",
" Title───────┘ ",
]),
);
// title bottom-left without right border
test_case(
Alignment::Left,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌──────────── ", " └Title─────── "]),
Buffer::with_lines(vec![
" ┌──────────── ",
"",
" └Title─────── ",
]),
);
// title bottom-left without borders
test_case(
Alignment::Left,
Borders::NONE,
Buffer::with_lines(vec![" ", " Title "]),
Buffer::with_lines(vec![
" ",
" ",
" Title ",
]),
);
// title center with all borders
test_case(
Alignment::Center,
Borders::ALL,
Buffer::with_lines(vec![" ┌───────────┐ ", " └───Title───┘ "]),
Buffer::with_lines(vec![
" ┌───────────┐ ",
" │ │ ",
" └───Title───┘ ",
]),
);
// title center without bottom border
test_case(
Alignment::Center,
Borders::LEFT | Borders::TOP | Borders::RIGHT,
Buffer::with_lines(vec![" ┌───────────┐ ", " │ Title │ "]),
Buffer::with_lines(vec![
" ┌───────────┐ ",
" │ │ ",
" │ Title │ ",
]),
);
// title center with no left border
test_case(
Alignment::Center,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ────────────┐ ", " ────Title───┘ "]),
Buffer::with_lines(vec![
" ────────────┐ ",
"",
" ───Title────┘ ",
]),
);
// title center without right border
test_case(
Alignment::Center,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌──────────── ", " └───Title──── "]),
Buffer::with_lines(vec![
" ┌──────────── ",
"",
" └───Title──── ",
]),
);
// title center without borders
test_case(
Alignment::Center,
Borders::NONE,
Buffer::with_lines(vec![" ", " Title "]),
Buffer::with_lines(vec![
" ",
" ",
" Title ",
]),
);
// title bottom-right with all borders
test_case(
Alignment::Right,
Borders::ALL,
Buffer::with_lines(vec![" ┌───────────┐ ", " └──────Title┘ "]),
Buffer::with_lines(vec![
" ┌───────────┐ ",
" │ │ ",
" └──────Title┘ ",
]),
);
// title bottom-right without bottom border
test_case(
Alignment::Right,
Borders::LEFT | Borders::TOP | Borders::RIGHT,
Buffer::with_lines(vec![" ┌───────────┐ ", " │ Title│ "]),
Buffer::with_lines(vec![
" ┌───────────┐ ",
" │ │ ",
" │ Title│ ",
]),
);
// title bottom-right with no left border
test_case(
Alignment::Right,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ────────────┐ ", " ───────Title┘ "]),
Buffer::with_lines(vec![
" ────────────┐ ",
"",
" ───────Title┘ ",
]),
);
// title bottom-right without right border
test_case(
Alignment::Right,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌──────────── ", " └───────Title "]),
Buffer::with_lines(vec![
" ┌──────────── ",
"",
" └───────Title ",
]),
);
// title bottom-right without borders
test_case(
Alignment::Right,
Borders::NONE,
Buffer::with_lines(vec![" ", " Title "]),
Buffer::with_lines(vec![
" ",
" ",
" Title ",
]),
);
}
#[test]
fn widgets_block_multiple_titles() {
let test_case = |title_a, title_b, borders, expected| {
let backend = TestBackend::new(15, 2);
#[track_caller]
fn test_case(title_a: Title, title_b: Title, borders: Borders, expected: Buffer) {
let backend = TestBackend::new(15, 3);
let mut terminal = Terminal::new(backend).unwrap();
let block = Block::default()
@@ -575,12 +570,7 @@ fn widgets_block_multiple_titles() {
.title(title_b)
.borders(borders);
let area = Rect {
x: 1,
y: 0,
width: 13,
height: 2,
};
let area = Rect::new(1, 0, 13, 3);
terminal
.draw(|f| {
@@ -589,14 +579,18 @@ fn widgets_block_multiple_titles() {
.unwrap();
terminal.backend().assert_buffer(&expected);
};
}
// title bottom-left with all borders
test_case(
Title::from("foo"),
Title::from("bar"),
Borders::ALL,
Buffer::with_lines(vec![" ┌foo─bar────┐ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" ┌foo─bar────┐ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title top-left without top border
@@ -604,7 +598,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo"),
Title::from("bar"),
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │foo bar │ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" │foo bar │ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title top-left with no left border
@@ -612,7 +610,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo"),
Title::from("bar"),
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" foo─bar─────┐ ", " ────────────┘ "]),
Buffer::with_lines(vec![
" foo─bar─────┐ ",
"",
" ────────────┘ ",
]),
);
// title top-left without right border
@@ -620,7 +622,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo"),
Title::from("bar"),
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌foo─bar───── ", " └──────────── "]),
Buffer::with_lines(vec![
" ┌foo─bar───── ",
"",
" └──────────── ",
]),
);
// title top-left without borders
@@ -628,7 +634,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo"),
Title::from("bar"),
Borders::NONE,
Buffer::with_lines(vec![" foo bar ", " "]),
Buffer::with_lines(vec![
" foo bar ",
" ",
" ",
]),
);
// title center with all borders
@@ -636,7 +646,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo").alignment(Alignment::Center),
Title::from("bar").alignment(Alignment::Center),
Borders::ALL,
Buffer::with_lines(vec![" ┌──foo─bar──┐ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" ┌──foo─bar──┐ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title center without top border
@@ -644,7 +658,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo").alignment(Alignment::Center),
Title::from("bar").alignment(Alignment::Center),
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │ foo bar │ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" │ foo bar │ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title center with no left border
@@ -652,7 +670,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo").alignment(Alignment::Center),
Title::from("bar").alignment(Alignment::Center),
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ───foo─bar──┐ ", " ────────────┘ "]),
Buffer::with_lines(vec![
" ──foo─bar───┐ ",
"",
" ────────────┘ ",
]),
);
// title center without right border
@@ -660,7 +682,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo").alignment(Alignment::Center),
Title::from("bar").alignment(Alignment::Center),
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌──foo─bar─── ", " └──────────── "]),
Buffer::with_lines(vec![
" ┌──foo─bar─── ",
"",
" └──────────── ",
]),
);
// title center without borders
@@ -668,7 +694,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo").alignment(Alignment::Center),
Title::from("bar").alignment(Alignment::Center),
Borders::NONE,
Buffer::with_lines(vec![" foo bar ", " "]),
Buffer::with_lines(vec![
" foo bar ",
" ",
" ",
]),
);
// title top-right with all borders
@@ -676,7 +706,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo").alignment(Alignment::Right),
Title::from("bar").alignment(Alignment::Right),
Borders::ALL,
Buffer::with_lines(vec![" ┌────foo─bar┐ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" ┌────foo─bar┐ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title top-right without top border
@@ -684,7 +718,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo").alignment(Alignment::Right),
Title::from("bar").alignment(Alignment::Right),
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │ foo bar│ ", " └───────────┘ "]),
Buffer::with_lines(vec![
" │ foo bar│ ",
" │ │ ",
" └───────────┘ ",
]),
);
// title top-right with no left border
@@ -692,7 +730,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo").alignment(Alignment::Right),
Title::from("bar").alignment(Alignment::Right),
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ─────foo─bar┐ ", " ────────────┘ "]),
Buffer::with_lines(vec![
" ─────foo─bar┐ ",
"",
" ────────────┘ ",
]),
);
// title top-right without right border
@@ -700,7 +742,11 @@ fn widgets_block_multiple_titles() {
Title::from("foo").alignment(Alignment::Right),
Title::from("bar").alignment(Alignment::Right),
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌─────foo─bar ", " └──────────── "]),
Buffer::with_lines(vec![
" ┌─────foo─bar ",
"",
" └──────────── ",
]),
);
// title top-right without borders
@@ -708,6 +754,10 @@ fn widgets_block_multiple_titles() {
Title::from("foo").alignment(Alignment::Right),
Title::from("bar").alignment(Alignment::Right),
Borders::NONE,
Buffer::with_lines(vec![" foo bar ", " "]),
Buffer::with_lines(vec![
" foo bar ",
" ",
" ",
]),
);
}

View File

@@ -52,6 +52,38 @@ fn widgets_list_should_highlight_the_selected_item() {
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_list_should_highlight_the_selected_item_wide_symbol() {
let backend = TestBackend::new(10, 3);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
let wide_symbol = "";
state.select(Some(1));
terminal
.draw(|f| {
let size = f.size();
let items = vec![
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
];
let list = List::new(items)
.highlight_style(Style::default().bg(Color::Yellow))
.highlight_symbol(wide_symbol);
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![" Item 1 ", "▶ Item 2 ", " Item 3 "]);
for x in 0..10 {
expected.get_mut(x, 1).set_bg(Color::Yellow);
}
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_list_should_truncate_items() {
let backend = TestBackend::new(10, 2);