Compare commits

..

31 Commits

Author SHA1 Message Date
dependabot[bot]
94f4547dcf chore(deps): bump orhun/git-cliff-action from 2 to 3 (#972) 2024-02-26 20:19:34 +01:00
dependabot[bot]
3a6b8808ed chore(deps): update derive_builder requirement from 0.13.0 to 0.20.0 (#960) 2024-02-26 01:23:20 -08:00
Josh McKinney
1cff511934 feat(line): impl Styled for Line (#968)
This adds `FromIterator` impls for `Line` and `Text` that allow creating
`Line` and `Text` instances from iterators of `Span` and `Line`
instances, respectively.

```rust
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
let line: Line = iter::once("Hello".blue())
    .chain(iter::once(" world!".green()))
    .collect();
let text = Text::from_iter(vec!["The first line", "The second line"]);
let text: Text = iter::once("The first line")
    .chain(iter::once("The second line"))
    .collect();
```
2024-02-25 05:14:19 -08:00
Josh McKinney
b5bdde079e feat(text): add FromIterator impls for Line and Text (#967)
This adds `FromIterator` impls for `Line` and `Text` that allow creating
`Line` and `Text` instances from iterators of `Span` and `Line`
instances, respectively.

```rust
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
let line: Line = iter::once("Hello".blue())
    .chain(iter::once(" world!".green()))
    .collect();
let text = Text::from_iter(vec!["The first line", "The second line"]);
let text: Text = iter::once("The first line")
    .chain(iter::once("The second line"))
    .collect();
```
2024-02-25 05:13:32 -08:00
Cameron Barnes
654949bb00 feat(list): Add Scroll Padding to Lists (#958)
Introduces scroll padding, which allows the api user to request that a certain number of ListItems be kept visible above and below the currently selected item while scrolling.

```rust
let list = List::new(items).scroll_padding(1);
```

Fixes: https://github.com/ratatui-org/ratatui/pull/955
2024-02-24 19:11:29 -08:00
EdJoPaTo
943c0431d9 fix(scrollbar): dont render on 0 length track (#964)
Fixes a panic when `track_length - 1` is used. (clamp panics on `-1.0`
being smaller than `0.0`)
2024-02-24 19:21:24 +01:00
EdJoPaTo
65e7923753 perf(scrollbar): const creation (#963)
A bunch of `const fn` allow for more performance and `Default` now uses the `const` new implementations.
2024-02-24 18:29:42 +01:00
Orhun Parmaksız
d0067c8815 docs(license): update copyright years (#962) 2024-02-21 11:24:38 +01:00
ThomasMiz
35e971f7eb fix: scrollbar thumb not visible on long lists (#959)
When displaying somewhat-long lists, the `Scrollbar` widget sometimes did not display a thumb character, and only the track will be visible.
2024-02-20 20:24:33 +01:00
Valentin271
b0314c5731 chore: remove conventional commit check for PR (#950)
This removes conventional commit check for PRs.

Since we use the PR title and description this is useless. It fails a
lot of time and we ignore it.

IMPORTANT NOTE: This does **not** mean Ratatui abandons conventional
commits. This only relates to commits in PRs.
2024-02-14 19:08:36 +01:00
Dheepak Krishnamurthy
12f67e810f feat: impl Widget for &str and String (#952)
Currently, `f.render_widget("hello world".bold(), area)` works but
`f.render_widget("hello world", area)` doesn't. This PR changes that my
implementing `Widget` for `&str` and `String`. This makes it easier to
render strings with no styles as widgets.

Example usage:

```rust
terminal.draw(|f| f.render_widget("Hello World!", f.size()))?;
```

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-02-14 06:26:08 -05:00
EdJoPaTo
11b452d56f feat(layout): mark various functions as const (#951) 2024-02-13 19:05:23 -08:00
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
45 changed files with 2235 additions and 937 deletions

View File

@@ -37,7 +37,7 @@ jobs:
args: --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
- name: Generate a changelog
uses: orhun/git-cliff-action@v2
uses: orhun/git-cliff-action@v3
with:
config: cliff.toml
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header

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

@@ -29,13 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
if: github.event_name != 'pull_request'
uses: actions/checkout@v4
- name: Checkout
if: github.event_name == 'pull_request'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
@@ -48,11 +42,6 @@ jobs:
run: cargo make lint-format
- name: Check documentation
run: cargo make lint-docs
- name: Check conventional commits
uses: crate-ci/committed@master
with:
args: "-vv"
commits: HEAD
- name: Check typos
uses: crate-ci/typos@master
- name: Lint dependencies
@@ -92,7 +81,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/"
@@ -51,7 +51,7 @@ cargo-husky = { version = "1.5.0", default-features = false, features = [
] }
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
derive_builder = "0.13.0"
derive_builder = "0.20.0"
fakeit = "1.1"
font8x8 = "0.3.1"
palette = "0.7.3"

View File

@@ -1,7 +1,7 @@
The MIT License (MIT)
Copyright (c) 2016-2022 Florian Dehau
Copyright (c) 2023 The Ratatui Developers
Copyright (c) 2023-2024 The Ratatui Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

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

@@ -133,10 +133,7 @@ fn ui(f: &mut Frame, app: &mut App) {
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
]),
Line::from("This is a line "),
Line::from("This is a line ".red()),
@@ -146,10 +143,7 @@ fn ui(f: &mut Frame, app: &mut App) {
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
]),
];
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
@@ -157,9 +151,9 @@ fn ui(f: &mut Frame, app: &mut App) {
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
let title = Block::default()
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold())
.title_alignment(Alignment::Center);
let title = Block::new()
.title_alignment(Alignment::Center)
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold());
f.render_widget(title, chunks[0]);
let paragraph = Paragraph::new(text.clone())
@@ -168,8 +162,7 @@ fn ui(f: &mut Frame, app: &mut App) {
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[1]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some("")),
chunks[1],
@@ -184,8 +177,7 @@ fn ui(f: &mut Frame, app: &mut App) {
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[2]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalLeft)
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
.symbols(scrollbar::VERTICAL)
.begin_symbol(None)
.track_symbol(None)
@@ -205,8 +197,7 @@ fn ui(f: &mut Frame, app: &mut App) {
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[3]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("🬋")
.end_symbol(None),
chunks[3].inner(&Margin {
@@ -224,8 +215,7 @@ fn ui(f: &mut Frame, app: &mut App) {
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[4]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("")
.track_symbol(Some("")),
chunks[4].inner(&Margin {

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

@@ -1,3 +1,5 @@
#![warn(clippy::missing_const_for_fn)]
mod alignment;
mod constraint;
mod corner;

View File

@@ -38,7 +38,7 @@ pub struct Position {
impl Position {
/// Create a new position
pub fn new(x: u16, y: u16) -> Self {
pub const fn new(x: u16, y: u16) -> Self {
Position { x, y }
}
}

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 const 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 const 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 const fn positions(self) -> Positions {
Positions::new(self)
}
/// Returns a [`Position`] with the same coordinates as this rect.
@@ -323,7 +292,7 @@ impl Rect {
/// let rect = Rect::new(1, 2, 3, 4);
/// let position = rect.as_position();
/// ````
pub fn as_position(self) -> Position {
pub const fn as_position(self) -> Position {
Position {
x: self.x,
y: self.y,
@@ -331,7 +300,7 @@ impl Rect {
}
/// Converts the rect into a size struct.
pub fn as_size(self) -> Size {
pub const fn as_size(self) -> Size {
Size {
width: self.width,
height: self.height,

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 const 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 const 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 const 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

@@ -15,7 +15,7 @@ pub struct Size {
impl Size {
/// Create a new `Size` struct
pub fn new(width: u16, height: u16) -> Self {
pub const fn new(width: u16, height: u16) -> Self {
Size { width, height }
}
}

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

@@ -2,7 +2,7 @@
use std::borrow::Cow;
use super::StyledGrapheme;
use crate::{prelude::*, widgets::Widget};
use crate::prelude::*;
/// A line of text, consisting of one or more [`Span`]s.
///
@@ -443,6 +443,15 @@ impl<'a> From<Line<'a>> for String {
}
}
impl<'a, T> FromIterator<T> for Line<'a>
where
T: Into<Span<'a>>,
{
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
Self::from(iter.into_iter().map(Into::into).collect::<Vec<_>>())
}
}
impl Widget for Line<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
@@ -486,8 +495,22 @@ impl std::fmt::Display for Line<'_> {
}
}
impl<'a> Styled for Line<'a> {
type Item = Line<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use std::iter;
use rstest::{fixture, rstest};
use super::*;
@@ -601,6 +624,16 @@ mod tests {
assert_eq!(Style::reset(), line.style);
}
#[test]
fn stylize() {
assert_eq!(Line::default().green().style, Color::Green.into());
assert_eq!(
Line::default().on_green().style,
Style::new().bg(Color::Green)
);
assert_eq!(Line::default().italic().style, Modifier::ITALIC.into());
}
#[test]
fn from_string() {
let s = String::from("Hello, world!");
@@ -625,6 +658,32 @@ mod tests {
assert_eq!(spans, line.spans);
}
#[test]
fn from_iter() {
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
assert_eq!(
line.spans,
vec![
Span::styled("Hello", Style::new().blue()),
Span::styled(" world!", Style::new().green()),
]
);
}
#[test]
fn collect() {
let line: Line = iter::once("Hello".blue())
.chain(iter::once(" world!".green()))
.collect();
assert_eq!(
line.spans,
vec![
Span::styled("Hello", Style::new().blue()),
Span::styled(" world!", Style::new().green()),
]
);
}
#[test]
fn from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));

View File

@@ -4,7 +4,7 @@ use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::StyledGrapheme;
use crate::{prelude::*, widgets::Widget};
use crate::prelude::*;
/// Represents a part of a line that is contiguous and where all characters share the same style.
///

View File

@@ -419,6 +419,19 @@ impl<'a> From<Vec<Line<'a>>> for Text<'a> {
}
}
impl<'a, T> FromIterator<T> for Text<'a>
where
T: Into<Line<'a>>,
{
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let lines = iter.into_iter().map(Into::into).collect();
Text {
lines,
..Default::default()
}
}
}
impl<'a, T> Extend<T> for Text<'a>
where
T: Into<Line<'a>>,
@@ -486,6 +499,8 @@ impl<'a> Styled for Text<'a> {
#[cfg(test)]
mod tests {
use std::iter;
use rstest::{fixture, rstest};
use super::*;
@@ -601,6 +616,26 @@ mod tests {
);
}
#[test]
fn from_iterator() {
let text = Text::from_iter(vec!["The first line", "The second line"]);
assert_eq!(
text.lines,
vec![Line::from("The first line"), Line::from("The second line")]
);
}
#[test]
fn collect() {
let text: Text = iter::once("The first line")
.chain(iter::once("The second line"))
.collect();
assert_eq!(
text.lines,
vec![Line::from("The first line"), Line::from("The second line")]
);
}
#[test]
fn into_iter() {
let text = Text::from("The first line\nThe second line");

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.
@@ -69,6 +70,7 @@ use crate::{buffer::Buffer, layout::Rect};
/// used where backwards compatibility is required (all the internal widgets use this approach).
///
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
/// Widget is also implemented for `&str` and `String` types.
///
/// # Examples
///
@@ -215,7 +217,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 +297,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 +394,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);
}
@@ -404,6 +418,51 @@ pub trait StatefulWidgetRef {
// }
// }
/// Renders a string slice as a widget.
///
/// This implementation allows a string slice (`&str`) to act as a widget, meaning it can be drawn
/// onto a [`Buffer`] in a specified [`Rect`]. The slice represents a static string which can be
/// rendered by reference, thereby avoiding the need for string cloning or ownership transfer when
/// drawing the text to the screen.
impl Widget for &str {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// Provides the ability to render a string slice by reference.
///
/// This trait implementation ensures that a string slice, which is an immutable view over a
/// `String`, can be drawn on demand without requiring ownership of the string itself. It utilizes
/// the default text style when rendering onto the provided [`Buffer`] at the position defined by
/// [`Rect`].
impl WidgetRef for &str {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.x, area.y, self, crate::style::Style::default())
}
}
/// Renders a `String` object as a widget.
///
/// This implementation enables an owned `String` to be treated as a widget, which can be rendered
/// on a [`Buffer`] within the bounds of a given [`Rect`].
impl Widget for String {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// Provides the ability to render a `String` by reference.
///
/// This trait allows for a `String` to be rendered onto the [`Buffer`], similarly using the default
/// style settings. It ensures that an owned `String` can be rendered efficiently by reference,
/// without the need to give up ownership of the underlying text.
impl WidgetRef for String {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.x, area.y, self, crate::style::Style::default())
}
}
#[cfg(test)]
mod tests {
use rstest::{fixture, rstest};
@@ -548,4 +607,52 @@ mod tests {
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([" "]));
}
#[rstest]
fn str_render(mut buf: Buffer) {
"hello world".render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn str_render_ref(mut buf: Buffer) {
"hello world".render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn str_option_render(mut buf: Buffer) {
Some("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn str_option_render_ref(mut buf: Buffer) {
Some("hello world").render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn string_render(mut buf: Buffer) {
String::from("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn string_render_ref(mut buf: Buffer) {
String::from("hello world").render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn string_option_render(mut buf: Buffer) {
Some(String::from("hello world")).render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn string_option_render_ref(mut buf: Buffer) {
Some(String::from("hello world")).render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]),);
}
}

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,19 +3,24 @@ 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
pub fn new(x1: f64, y1: f64, x2: f64, y2: f64, color: Color) -> Self {
/// Create a new line from `(x1, y1)` to `(x2, y2)` with the given color
pub const fn new(x1: f64, y1: f64, x2: f64, y2: f64, color: Color) -> Self {
Self {
x1,
y1,

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;
@@ -439,6 +438,8 @@ pub struct List<'a> {
repeat_highlight_symbol: bool,
/// Decides when to allocate spacing for the selection symbol
highlight_spacing: HighlightSpacing,
/// How many items to try to keep visible before and after the selected item
scroll_padding: usize,
}
/// Defines the direction in which the list will be rendered.
@@ -686,6 +687,25 @@ impl<'a> List<'a> {
self
}
/// Sets the number of items around the currently selected item that should be kept visible
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
///
/// A padding value of 1 will keep 1 item above and 1 item bellow visible if possible
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = vec!["Item 1"];
/// let list = List::new(items).scroll_padding(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn scroll_padding(mut self, padding: usize) -> List<'a> {
self.scroll_padding = padding;
self
}
/// Defines the list direction (up or down)
///
/// Defines if the `List` is displayed *top to bottom* (default) or *bottom to top*. Use
@@ -738,6 +758,52 @@ impl<'a> List<'a> {
self.items.is_empty()
}
/// Applies scroll padding to the selected index, reducing the padding value to keep the
/// selected item on screen even with items of inconsistent sizes
///
/// This function is sensitive to how the bounds checking function handles item height
fn apply_scroll_padding_to_selected_index(
&self,
selected: Option<usize>,
max_height: usize,
first_visible_index: usize,
last_visible_index: usize,
) -> Option<usize> {
let last_valid_index = self.items.len().saturating_sub(1);
let selected = selected?.min(last_valid_index);
// The bellow loop handles situations where the list item sizes may not be consistent,
// where the offset would have excluded some items that we want to include, or could
// cause the offset value to be set to an inconsistent value each time we render.
// The padding value will be reduced in case any of these issues would occur
let mut scroll_padding = self.scroll_padding;
while scroll_padding > 0 {
let mut height_around_selected = 0;
for index in selected.saturating_sub(scroll_padding)
..=selected
.saturating_add(scroll_padding)
.min(last_valid_index)
{
height_around_selected += self.items[index].height();
}
if height_around_selected <= max_height {
break;
}
scroll_padding -= 1;
}
Some(
if (selected + scroll_padding).min(last_valid_index) >= last_visible_index {
selected + scroll_padding
} else if selected.saturating_sub(scroll_padding) < first_visible_index {
selected.saturating_sub(scroll_padding)
} else {
selected
}
.min(last_valid_index),
)
}
/// Given an offset, calculate which items can fit in a given area
fn get_items_bounds(
&self,
@@ -766,9 +832,17 @@ impl<'a> List<'a> {
last_visible_index += 1;
}
// Get the selected index, but still honor the offset if nothing is selected
// This allows for the list to stay at a position after select()ing None.
let index_to_display = selected.unwrap_or(offset).min(self.items.len() - 1);
// Get the selected index and apply scroll_padding to it, but still honor the offset if
// nothing is selected. This allows for the list to stay at a position after select()ing
// None.
let index_to_display = self
.apply_scroll_padding_to_selected_index(
selected,
max_height,
first_visible_index,
last_visible_index,
)
.unwrap_or(offset);
// Recall that last_visible_index is the index of what we
// can render up to in the given space after the offset
@@ -896,7 +970,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,
@@ -971,6 +1045,9 @@ where
mod tests {
use std::borrow::Cow;
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
use crate::{
assert_buffer_eq,
@@ -1964,4 +2041,202 @@ mod tests {
let expected = Buffer::with_lines(vec!["Large", " ", " "]);
assert_buffer_eq!(buffer, expected);
}
#[rstest]
#[case::no_padding(
4,
2, // Offset
0, // Padding
Some(2), // Selected
Buffer::with_lines(vec![">> Item 2 ", " Item 3 ", " Item 4 ", " Item 5 "])
)]
#[case::one_before(
4,
2, // Offset
1, // Padding
Some(2), // Selected
Buffer::with_lines(vec![" Item 1 ", ">> Item 2 ", " Item 3 ", " Item 4 "])
)]
#[case::one_after(
4,
1, // Offset
1, // Padding
Some(4), // Selected
Buffer::with_lines(vec![" Item 2 ", " Item 3 ", ">> Item 4 ", " Item 5 "])
)]
#[case::check_padding_overflow(
4,
1, // Offset
2, // Padding
Some(4), // Selected
Buffer::with_lines(vec![" Item 2 ", " Item 3 ", ">> Item 4 ", " Item 5 "])
)]
#[case::no_padding_offset_behavior(
5, // Render Area Height
2, // Offset
0, // Padding
Some(3), // Selected
Buffer::with_lines(
vec![" Item 2 ", ">> Item 3 ", " Item 4 ", " Item 5 ", " "]
)
)]
#[case::two_before(
5, // Render Area Height
2, // Offset
2, // Padding
Some(3), // Selected
Buffer::with_lines(
vec![" Item 1 ", " Item 2 ", ">> Item 3 ", " Item 4 ", " Item 5 "]
)
)]
#[case::keep_selected_visible(
4,
0, // Offset
4, // Padding
Some(1), // Selected
Buffer::with_lines(vec![" Item 0 ", ">> Item 1 ", " Item 2 ", " Item 3 "])
)]
fn test_padding(
#[case] render_height: u16,
#[case] offset: usize,
#[case] padding: usize,
#[case] selected: Option<usize>,
#[case] expected: Buffer,
) {
let backend = backend::TestBackend::new(10, render_height);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
*state.offset_mut() = offset;
state.select(selected);
let items = vec![
ListItem::new("Item 0"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
ListItem::new("Item 4"),
ListItem::new("Item 5"),
];
let list = List::new(items)
.scroll_padding(padding)
.highlight_symbol(">> ");
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
}
/// If there isnt enough room for the selected item and the requested padding the list can jump
/// up and down every frame if something isnt done about it. This code tests to make sure that
/// isnt currently happening
#[test]
fn test_padding_flicker() {
let backend = backend::TestBackend::new(10, 5);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
*state.offset_mut() = 2;
state.select(Some(4));
let items = vec![
ListItem::new("Item 0"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
ListItem::new("Item 4"),
ListItem::new("Item 5"),
ListItem::new("Item 6"),
ListItem::new("Item 7"),
];
let list = List::new(items).scroll_padding(3).highlight_symbol(">> ");
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(&list, size, &mut state);
})
.unwrap();
let offset_after_render = state.offset();
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(&list, size, &mut state);
})
.unwrap();
// Offset after rendering twice should remain the same as after once
assert_eq!(offset_after_render, state.offset());
}
#[test]
fn test_padding_inconsistent_item_sizes() {
let backend = backend::TestBackend::new(10, 3);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default().with_offset(0).with_selected(Some(3));
let items = vec![
ListItem::new("Item 0"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
ListItem::new("Item 4\nTest\nTest"),
ListItem::new("Item 5"),
];
let list = List::new(items).scroll_padding(1).highlight_symbol(">> ");
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
terminal.backend().assert_buffer(&Buffer::with_lines(vec![
" Item 1 ",
" Item 2 ",
">> Item 3 ",
]));
}
// Tests to make sure when it's pushing back the first visible index value that it doesnt
// include an item that's too large
#[test]
fn test_padding_offset_pushback_break() {
let backend = backend::TestBackend::new(10, 4);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
*state.offset_mut() = 1;
state.select(Some(2));
let items = vec![
ListItem::new("Item 0\nTest\nTest"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
];
let list = List::new(items).scroll_padding(2).highlight_symbol(">> ");
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
terminal.backend().assert_buffer(&Buffer::with_lines(vec![
" Item 1 ",
">> Item 2 ",
" Item 3 ",
" ",
]));
}
}

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};
@@ -120,7 +119,7 @@ pub enum ScrollbarOrientation {
///
/// If you don't have multi-line content, you can leave the `viewport_content_length` set to the
/// default and it'll use the track size as a `viewport_content_length`.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScrollbarState {
/// The total length of the scrollable content.
@@ -149,27 +148,39 @@ pub enum ScrollDirection {
impl<'a> Default for Scrollbar<'a> {
fn default() -> Self {
Self {
orientation: ScrollbarOrientation::default(),
thumb_symbol: DOUBLE_VERTICAL.thumb,
thumb_style: Style::default(),
track_symbol: Some(DOUBLE_VERTICAL.track),
track_style: Style::default(),
begin_symbol: Some(DOUBLE_VERTICAL.begin),
begin_style: Style::default(),
end_symbol: Some(DOUBLE_VERTICAL.end),
end_style: Style::default(),
}
Self::new(ScrollbarOrientation::default())
}
}
impl<'a> Scrollbar<'a> {
/// Creates a new scrollbar with the given position.
/// Creates a new scrollbar with the given orientation.
///
/// Most of the time you'll want [`ScrollbarOrientation::VerticalLeft`] or
/// Most of the time you'll want [`ScrollbarOrientation::VerticalRight`] or
/// [`ScrollbarOrientation::HorizontalBottom`]. See [`ScrollbarOrientation`] for more options.
pub fn new(orientation: ScrollbarOrientation) -> Self {
Self::default().orientation(orientation)
#[must_use = "creates the Scrollbar"]
pub const fn new(orientation: ScrollbarOrientation) -> Self {
let symbols = if orientation.is_vertical() {
DOUBLE_VERTICAL
} else {
DOUBLE_HORIZONTAL
};
Self::new_with_symbols(orientation, &symbols)
}
/// Creates a new scrollbar with the given orientation and symbol set.
#[must_use = "creates the Scrollbar"]
const fn new_with_symbols(orientation: ScrollbarOrientation, symbols: &Set) -> Self {
Self {
orientation,
thumb_symbol: symbols.thumb,
thumb_style: Style::new(),
track_symbol: Some(symbols.track),
track_style: Style::new(),
begin_symbol: Some(symbols.begin),
begin_style: Style::new(),
end_symbol: Some(symbols.end),
end_style: Style::new(),
}
}
/// Sets the position of the scrollbar.
@@ -181,14 +192,14 @@ impl<'a> Scrollbar<'a> {
///
/// 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 orientation(mut self, orientation: ScrollbarOrientation) -> Self {
pub const fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
self.orientation = orientation;
let set = if self.orientation.is_vertical() {
let symbols = if self.orientation.is_vertical() {
DOUBLE_VERTICAL
} else {
DOUBLE_HORIZONTAL
};
self.symbols(set)
self.symbols(symbols)
}
/// Sets the orientation and symbols for the scrollbar from a [`Set`].
@@ -198,9 +209,13 @@ impl<'a> Scrollbar<'a> {
///
/// 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 orientation_and_symbol(mut self, orientation: ScrollbarOrientation, set: Set) -> Self {
pub const fn orientation_and_symbol(
mut self,
orientation: ScrollbarOrientation,
symbols: Set,
) -> Self {
self.orientation = orientation;
self.symbols(set)
self.symbols(symbols)
}
/// Sets the symbol that represents the thumb of the scrollbar.
@@ -210,7 +225,7 @@ impl<'a> Scrollbar<'a> {
///
/// 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 thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
pub const fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
self.thumb_symbol = thumb_symbol;
self
}
@@ -236,7 +251,7 @@ impl<'a> Scrollbar<'a> {
///
/// 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 track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
pub const fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
self.track_symbol = track_symbol;
self
}
@@ -261,7 +276,7 @@ impl<'a> Scrollbar<'a> {
///
/// 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 begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
pub const fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
self.begin_symbol = begin_symbol;
self
}
@@ -286,7 +301,7 @@ impl<'a> Scrollbar<'a> {
///
/// 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 end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
pub const fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
self.end_symbol = end_symbol;
self
}
@@ -316,22 +331,23 @@ impl<'a> Scrollbar<'a> {
/// └─────────── begin
/// ```
///
/// Only sets begin_symbol, end_symbol and track_symbol if they already contain a value.
/// Only sets `begin_symbol`, `end_symbol` and `track_symbol` if they already contain a value.
/// If they were set to `None` explicitly, this function will respect that choice. Use their
/// respective setters to change their value.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[allow(clippy::needless_pass_by_value)] // Breaking change
#[must_use = "method moves the value of self and returns the modified value"]
pub fn symbols(mut self, symbol: Set) -> Self {
self.thumb_symbol = symbol.thumb;
pub const fn symbols(mut self, symbols: Set) -> Self {
self.thumb_symbol = symbols.thumb;
if self.track_symbol.is_some() {
self.track_symbol = Some(symbol.track);
self.track_symbol = Some(symbols.track);
}
if self.begin_symbol.is_some() {
self.begin_symbol = Some(symbol.begin);
self.begin_symbol = Some(symbols.begin);
}
if self.end_symbol.is_some() {
self.end_symbol = Some(symbol.end);
self.end_symbol = Some(symbols.end);
}
self
}
@@ -362,15 +378,23 @@ impl<'a> Scrollbar<'a> {
}
}
impl Default for ScrollbarState {
fn default() -> Self {
Self::new(0)
}
}
impl ScrollbarState {
/// Constructs a new ScrollbarState with the specified content length.
/// Constructs a new [`ScrollbarState`] with the specified content length.
///
/// `content_length` is the total number of element, that can be scrolled. See
/// [`ScrollbarState`] for more details.
pub fn new(content_length: usize) -> Self {
#[must_use = "creates the ScrollbarState"]
pub const fn new(content_length: usize) -> Self {
Self {
content_length,
..Default::default()
position: 0,
viewport_content_length: 0,
}
}
@@ -380,7 +404,7 @@ impl ScrollbarState {
///
/// 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 position(mut self, position: usize) -> Self {
pub const fn position(mut self, position: usize) -> Self {
self.position = position;
self
}
@@ -392,7 +416,7 @@ impl ScrollbarState {
///
/// 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 content_length(mut self, content_length: usize) -> Self {
pub const fn content_length(mut self, content_length: usize) -> Self {
self.content_length = content_length;
self
}
@@ -401,7 +425,7 @@ impl ScrollbarState {
///
/// 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 viewport_content_length(mut self, viewport_content_length: usize) -> Self {
pub const fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
self.viewport_content_length = viewport_content_length;
self
}
@@ -416,7 +440,7 @@ impl ScrollbarState {
self.position = self
.position
.saturating_add(1)
.min(self.content_length.saturating_sub(1))
.min(self.content_length.saturating_sub(1));
}
/// Sets the scroll position to the start of the scrollable content.
@@ -426,7 +450,7 @@ impl ScrollbarState {
/// Sets the scroll position to the end of the scrollable content.
pub fn last(&mut self) {
self.position = self.content_length.saturating_sub(1)
self.position = self.content_length.saturating_sub(1);
}
/// Changes the scroll position based on the provided [`ScrollDirection`].
@@ -446,7 +470,7 @@ impl<'a> StatefulWidget for Scrollbar<'a> {
type State = ScrollbarState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if state.content_length == 0 || area.is_empty() {
if state.content_length == 0 || self.track_length_excluding_arrow_heads(area) == 0 {
return;
}
@@ -454,7 +478,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);
}
}
@@ -467,24 +491,24 @@ impl Scrollbar<'_> {
fn bar_symbols(
&self,
area: Rect,
state: &mut ScrollbarState,
) -> impl Iterator<Item = (&str, Style)> {
state: &ScrollbarState,
) -> 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()
}
@@ -493,33 +517,38 @@ impl Scrollbar<'_> {
///
/// The scrollbar has 3 parts of note:
/// - `<═══█████═══════>`: full scrollbar
/// - ` ═══ `: track start part
/// - ` █████ `: thumb part part
/// - ` ═══════ `: track end part
/// - ` ═══ `: track start
/// - ` █████ `: thumb
/// - ` ═══════ `: track end
///
/// This method returns the length of the start, thumb, and end as a tuple.
fn part_lengths(&self, area: Rect, state: &mut ScrollbarState) -> (usize, usize, usize) {
let track_len = self.track_length_excluding_arrow_heads(area) as f64;
let viewport_len = self.viewport_length(state, area) as f64;
fn part_lengths(&self, area: Rect, state: &ScrollbarState) -> (usize, usize, usize) {
let track_length = self.track_length_excluding_arrow_heads(area) as f64;
let viewport_length = self.viewport_length(state, area) as f64;
let content_length = state.content_length as f64;
// Clamp the position to show at least one line of the content, even if the content is
let position = state.position.min(state.content_length - 1) as f64;
// Ensure that the position of the thumb is within the bounds of the content taking into
// account the content and viewport length. When the last line of the content is at the top
// of the viewport, the thumb should be at the bottom of the track.
let max_position = state.content_length.saturating_sub(1) as f64;
let start_position = (state.position as f64).clamp(0.0, max_position);
let max_viewport_position = max_position + viewport_length;
let end_position = start_position + viewport_length;
// vscode style scrolling behavior (allow scrolling past end of content)
let scrollable_content_len = content_length + viewport_len - 1.0;
let thumb_start = position * track_len / scrollable_content_len;
let thumb_end = (position + viewport_len) * track_len / scrollable_content_len;
// Calculate the start and end positions of the thumb. The size will be proportional to the
// viewport length compared to the total amount of possible visible rows.
let thumb_start = start_position * track_length / max_viewport_position;
let thumb_end = end_position * track_length / max_viewport_position;
// We round just the positions (instead of floor / ceil), and then calculate the sizes from
// those positions. Rounding the sizes instead causes subtle off by 1 errors.
let track_start_len = thumb_start.round() as usize;
let thumb_end = thumb_end.round() as usize;
// Make sure that the thumb is at least 1 cell long by ensuring that the start of the thumb
// is less than the track_len. We use the positions instead of the sizes and use nearest
// integer instead of floor / ceil to avoid problems caused by rounding errors.
let thumb_start = thumb_start.round().clamp(0.0, track_length - 1.0) as usize;
let thumb_end = thumb_end.round().clamp(0.0, track_length) as usize;
let thumb_len = thumb_end.saturating_sub(track_start_len);
let track_end_len = track_len as usize - track_start_len - thumb_len;
let thumb_length = thumb_end.saturating_sub(thumb_start).max(1);
let track_end_length = (track_length as usize).saturating_sub(thumb_start + thumb_length);
(track_start_len, thumb_len, track_end_len)
(thumb_start, thumb_length, track_end_length)
}
fn scollbar_area(&self, area: Rect) -> Rect {
@@ -540,9 +569,9 @@ impl Scrollbar<'_> {
/// <═══█████═══════>
/// ```
fn track_length_excluding_arrow_heads(&self, area: Rect) -> u16 {
let start_len = self.begin_symbol.map(|s| s.width() as u16).unwrap_or(0);
let end_len = self.end_symbol.map(|s| s.width() as u16).unwrap_or(0);
let arrows_len = start_len + end_len;
let start_len = self.begin_symbol.map_or(0, |s| s.width() as u16);
let end_len = self.end_symbol.map_or(0, |s| s.width() as u16);
let arrows_len = start_len.saturating_add(end_len);
if self.orientation.is_vertical() {
area.height.saturating_sub(arrows_len)
} else {
@@ -550,26 +579,27 @@ impl Scrollbar<'_> {
}
}
fn viewport_length(&self, state: &ScrollbarState, area: Rect) -> u16 {
const fn viewport_length(&self, state: &ScrollbarState, area: Rect) -> usize {
if state.viewport_content_length != 0 {
return state.viewport_content_length as u16;
}
if self.orientation.is_vertical() {
area.height
state.viewport_content_length
} else if self.orientation.is_vertical() {
area.height as usize
} else {
area.width
area.width as usize
}
}
}
impl ScrollbarOrientation {
/// Returns `true` if the scrollbar is vertical.
pub fn is_vertical(&self) -> bool {
#[must_use = "returns the requested kind of the scrollbar"]
pub const fn is_vertical(&self) -> bool {
matches!(self, Self::VerticalRight | Self::VerticalLeft)
}
/// Returns `true` if the scrollbar is horizontal.
pub fn is_horizontal(&self) -> bool {
#[must_use = "returns the requested kind of the scrollbar"]
pub const fn is_horizontal(&self) -> bool {
matches!(self, Self::HorizontalBottom | Self::HorizontalTop)
}
}
@@ -624,8 +654,7 @@ mod tests {
#[fixture]
fn scrollbar_no_arrows() -> Scrollbar<'static> {
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalTop)
Scrollbar::new(ScrollbarOrientation::HorizontalTop)
.begin_symbol(None)
.end_symbol(None)
.track_symbol(Some("-"))
@@ -643,9 +672,7 @@ mod tests {
scrollbar_no_arrows: Scrollbar,
) {
let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
}
@@ -669,9 +696,7 @@ mod tests {
scrollbar_no_arrows: Scrollbar,
) {
let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
}
@@ -687,9 +712,7 @@ mod tests {
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
}
@@ -707,9 +730,7 @@ mod tests {
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
}
@@ -726,9 +747,7 @@ mod tests {
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
}
@@ -753,19 +772,84 @@ mod tests {
) {
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)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{}",
assertion_message
"{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(
#[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::new(content_length).position(position);
Scrollbar::new(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::new(content_length).position(position);
Scrollbar::new(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}",
);
}
@@ -791,11 +875,8 @@ mod tests {
) {
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::HorizontalTop)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalTop)
.begin_symbol(Some("<"))
.end_symbol(Some(">"))
.track_symbol(Some("-"))
@@ -804,8 +885,7 @@ mod tests {
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{}",
assertion_message,
"{assertion_message}",
);
}
@@ -829,15 +909,12 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let empty_string: String = " ".repeat(size as usize);
let empty_string = " ".repeat(size as usize);
assert_eq!(
buffer,
Buffer::with_lines(vec![&empty_string, expected]),
@@ -865,15 +942,12 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalTop)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalTop)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let empty_string: String = " ".repeat(size as usize);
let empty_string = " ".repeat(size as usize);
assert_eq!(
buffer,
Buffer::with_lines(vec![expected, &empty_string]),
@@ -901,11 +975,8 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalLeft)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
.begin_symbol(Some("<"))
.end_symbol(Some(">"))
.track_symbol(Some("-"))
@@ -935,11 +1006,8 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("<"))
.end_symbol(Some(">"))
.track_symbol(Some("-"))
@@ -970,9 +1038,38 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::default()
let mut state = ScrollbarState::new(content_length)
.position(position)
.viewport_content_length(2);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}");
}
/// Fixes <https://github.com/ratatui-org/ratatui/pull/959> which was a bug that would not
/// render a thumb when the viewport was very small in comparison to the content length.
#[rstest]
#[case("#----", 0, 100, "position_0")]
#[case("#----", 10, 100, "position_10")]
#[case("-#---", 20, 100, "position_20")]
#[case("-#---", 30, 100, "position_30")]
#[case("--#--", 40, 100, "position_40")]
#[case("--#--", 50, 100, "position_50")]
#[case("---#-", 60, 100, "position_60")]
#[case("---#-", 70, 100, "position_70")]
#[case("----#", 80, 100, "position_80")]
#[case("----#", 90, 100, "position_90")]
#[case("----#", 100, 100, "position_one_out_of_bounds")]
fn thumb_visible_on_very_small_track(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::new(content_length)
.position(position)
.content_length(content_length)
.viewport_content_length(2);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}");

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);