Compare commits

..

119 Commits

Author SHA1 Message Date
Josh McKinney
9ea840d87f feat(layout): accept (x, y) tuple for Rect::offset
This allows callers to call `Rect::offset((x, y))`` instead of the more
verbose `Rect::offset(Offset { x, y })`.
2024-03-28 16:22:43 -07:00
Josh McKinney
26af65043e feat(text): add push methods for text and line (#998)
Adds the following methods to the `Text` and `Line` structs:
- Text::push_line
- Text::push_span
- Line::push_span

This allows for adding lines and spans to a text object without having
to call methods on the fields directly, which is usefult for incremental
construction of text objects.
2024-03-28 15:30:21 -07:00
Benjamin Nickolls
07da90a718 chore(funding): add eth address for receiving funds from drips.network (#994) 2024-03-27 18:11:26 +03:00
Orhun Parmaksız
125ee929ee chore(docs): fix typos in crate documentation (#1002) 2024-03-27 16:28:23 +03:00
Josh McKinney
742a5ead06 fix(text): fix panic when rendering out of bounds (#997)
Previously it was possible to cause a panic when rendering to an area
outside of the buffer bounds. Instead this now correctly renders nothing
to the buffer.
2024-03-24 22:06:31 -07:00
EdJoPaTo
8719608bda refactor(span): rename to_aligned_line into into_aligned_line (#993)
With the Rust method naming conventions these methods are into methods
consuming the Span. Therefore, it's more consistent to use `into_`
instead of `to_`.

```rust
Span::to_centered_line
Span::to_left_aligned_line
Span::to_right_aligned_line
```

Are marked deprecated and replaced with the following

```rust
Span::into_centered_line
Span::into_left_aligned_line
Span::into_right_aligned_line
```
2024-03-22 04:29:29 -07:00
Josh McKinney
f6c4e447e6 fix: ensure that paragraph correctly renders styled text (#992)
Paragraph was ignoring the new `Text::style` field added in 0.26.0

Fixes: https://github.com/ratatui-org/ratatui/issues/990
2024-03-22 02:14:59 -07:00
Jack Wills
c56f49b9fb fix(list): saturating_sub to fix highlight_symbol overflow (#949)
An overflow (pedantically an underflow) can occur if the
highlight_symbol is a multi-byte char, and area is reduced to a size
less than that char length.
2024-03-16 15:58:39 -07:00
Orhun Parmaksız
078e97e4ff chore(github): add EdJoPaTo as a maintainer (#986) 2024-03-16 19:29:57 +01:00
EdJoPaTo
8e68db9e2f refactor: remove pointless default on internal structs (#980)
See #978

Also remove other derives. They are unused and just slow down
compilation.
2024-03-12 00:33:30 -07:00
EdJoPaTo
541f0f9953 perf(cell): use const CompactString::new_inline (#979)
Some minor find when messing around trying to `const` all the things.

While `reset()` and `default()` can not be `const` it's still a benefit
when their contents are.
2024-03-05 22:05:24 +01:00
Josh McKinney
88bfb5a430 docs(text): update Text and Line docs (#969) 2024-03-05 22:02:07 +01:00
EdJoPaTo
c4ce7e8ff6 build: enable more satisfied lints (#974)
These lints dont generate warnings and therefore dont need refactoring.
I think they are useful in the future.
2024-03-03 21:41:24 -08:00
EdJoPaTo
3be189e3c6 refactor: clippy::thread_local_initializer_can_be_made_const (#974)
enabled by default on nightly
2024-03-03 21:41:24 -08:00
EdJoPaTo
5c4efacd1d refactor: clippy::map_err_ignore (#974) 2024-03-03 21:41:24 -08:00
EdJoPaTo
bbb6d65e06 refactor: clippy::else_if_without_else (#974) 2024-03-03 21:41:24 -08:00
EdJoPaTo
fdb14dc7cd refactor: clippy::redundant_type_annotations (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
9b3b23ac14 refactor: remove literal suffix (#974)
its not needed and can just be assumed

related: clippy::(un)separated_literal_suffix
2024-03-03 21:41:23 -08:00
EdJoPaTo
58b6e0be0f refactor: clippy::should_panic_without_expect (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
6e6ba27a12 build(lint): warn on pedantic and allow the rest (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
c870a41057 refactor: clippy::many_single_char_names (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
a6036ad789 refactor: clippy::similar_names (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
060d26b6dc refactor: clippy::match_same_arms (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
a4e84a6a7f build!: increase msrv to 1.74.0 (#974)
configure lints in Cargo.toml requires 1.74.0

BREAKING CHANGE: rust 1.74 is required now
2024-03-03 21:41:22 -08:00
EdJoPaTo
fcbea9ee68 refactor: clippy::uninlined_format_args (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
14b24e7585 refactor: clippy::if_not_else (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
5ed1f43c62 refactor: clippy::redundant_closure_for_method_calls (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
c8c7924e0c refactor: clippy::too_many_lines (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
e3afe7c8a1 refactor: clippy::unreadable_literal (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
a1f54de7d6 refactor: clippy::bool_to_int_with_if (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
b8ea190bf2 refactor: clippy::cast_lossless (#974) 2024-03-03 21:41:21 -08:00
EdJoPaTo
0de5238ed3 refactor: dead_code (#974)
enabled by default, only detected by nightly yet
2024-03-03 21:41:21 -08:00
EdJoPaTo
df5dddfbc9 refactor: unused_imports (#974)
enabled by default, only detected on nightly yet
2024-03-03 21:41:21 -08:00
EdJoPaTo
f1398ae6cb refactor: clippy::useless_vec (#974)
Lint enabled by default but only nightly finds this yet
2024-03-03 21:41:21 -08:00
EdJoPaTo
525848ff4e refactor: manually apply clippy::use_self for impl with lifetimes (#974) 2024-03-03 21:41:21 -08:00
EdJoPaTo
660c7183c7 refactor: clippy::empty_line_after_doc_comments (#974) 2024-03-03 21:41:21 -08:00
EdJoPaTo
ab951fae81 refactor: clippy::return_self_not_must_use (#974) 2024-03-03 21:41:21 -08:00
EdJoPaTo
3cd4369176 refactor: clippy::doc_markdown (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
9bc014d7f1 refactor: clippy::items_after_statements (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
36a0cd56e5 refactor: clippy::deref_by_slicing (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
b831c5688c refactor(widget-ref): clippy::needless_pass_by_value (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
8195f526cb perf: clippy::needless_pass_by_value (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
f7f66928a8 refactor: clippy::equatable_if_let (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
01418eb7c2 refactor: clippy::default_trait_access (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
8536760e78 refactor: clippy::inefficient_to_string (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
a558b19c9a refactor: clippy::implicit_clone (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
183c07ef43 perf: clippy::trivially_copy_pass_by_ref (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
a13867ffce perf: clippy::cloned_instead_of_copied (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
5b00e3aae9 refactor: clippy::use_self (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
38c17e091c chore(editorconfig): set and apply some defaults (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
3834374652 perf: clippy::missing_const_for_fn (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
27680c05ce refactor: clippy::semicolon_if_nothing_returned (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
6fd5f631bb refactor(lint): prefer idiomatic for loops (#974) 2024-03-03 21:41:18 -08:00
EdJoPaTo
37b957c7e1 refactor(lints): add lints to scrollbar (#974) 2024-03-03 21:41:18 -08:00
EdJoPaTo
e02f4768ce perf(borders): allow border!() in const (#977)
This allows more compiler optimizations when the macro is used.
2024-03-03 21:17:28 -08:00
EdJoPaTo
c12bcfefa2 refactor(non-src): apply pedantic lints (#976)
Fixes many not yet enabled lints (mostly pedantic) on everything that is
not the lib (examples, benchs, tests). Therefore, this is not containing
anything that can be a breaking change.

Lints are not enabled as that should be the job of #974. I created this
as a separate PR as its mostly independent and would only clutter up the
diff of #974 even more.

Also see
https://github.com/ratatui-org/ratatui/pull/974#discussion_r1506458743

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-03-02 01:06:53 -08:00
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
Orhun Parmaksız
d2d91f754c docs(changelog): add sponsors section (#908)
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
2024-02-02 17:28:23 +01:00
Dheepak Krishnamurthy
bcf43688ec docs: Update BREAKING-CHANGES.md summary (#907)
Update summary section
2024-02-02 03:50:07 -05:00
Orhun Parmaksız
b7942ee252 chore(release): prepare for 0.26.0 (#905)
🐭

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-02-02 00:16:00 -08:00
Josh McKinney
c8dd87918d feat: add WidgetRef and StatefulWidgetRef traits (#903)
The Widget trait consumes self, which makes it impossible to use in a
boxed context. Previously we implemented the Widget trait for &T, but
this was not enough to render a boxed widget. We now have a new trait
called `WidgetRef` that allows rendering a widget by reference. This
trait is useful when you want to store a reference to one or more
widgets and render them later. Additionaly this makes it possible to
render boxed widgets where the type is not known at compile time (e.g.
in a composite layout with multiple panes of different types).

This change also adds a new trait called `StatefulWidgetRef` which is
the stateful equivalent of `WidgetRef`.

Both new traits are gated behind the `unstable-widget-ref` feature flag
as we may change the exact name / approach a little on this based on
further discussion.

Blanket implementation of `Widget` for `&W` where `W` implements
`WidgetRef` and `StatefulWidget` for `&W` where `W` implements
`StatefulWidgetRef` is provided. This allows you to render a widget by
reference and a stateful widget by reference.

A blanket implementation of `WidgetRef` for `Option<W>` where `W`
implements `WidgetRef` is provided. This makes it easier to render
child widgets that are optional without the boilerplate of unwrapping
the option. Previously several widgets implemented this manually. This
commits expands the pattern to apply to all widgets.

```rust
struct Parent {
    child: Option<Child>,
}

impl WidgetRef for Parent {
    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
        self.child.render_ref(area, buf);
    }
}
```

```rust
let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(Greeting), Box::new(Farewell)];
for widget in widgets {
    widget.render_ref(buf.area, &mut buf);
}
assert_eq!(buf, Buffer::with_lines(["Hello        Goodbye"]));
```
2024-02-02 00:02:16 -08:00
Dheepak Krishnamurthy
652dc469ea docs: Update BREAKING-CHANGES.md with flex section (#906)
Follow up to: https://github.com/ratatui-org/ratatui/pull/881
2024-02-02 00:40:11 -05:00
Josh McKinney
87bf1dd9df feat: replace Rect::split with Layout::areas and spacers (#904)
In a recent commit we added Rec::split, but this feels more ergonomic as
Layout::areas. This also adds Layout::spacers to get the spacers between
the areas.
2024-02-01 20:26:35 -08:00
Josh McKinney
f8367fdfdd chore: allow Buffer::with_lines to accept IntoIterator (#901)
This can make it easier to use `Buffer::with_lines` with iterators that
don't necessarily produce a `Vec`. For example, this allows using
`Buffer::with_lines` with `&[&str]` directly, without having to call
`collect` on it first.
2024-01-31 14:12:10 -08:00
Josh McKinney
9ba7354335 feat(text): implement iterators for Text (#900)
This allows iterating over the `Lines`s of a text using `for` loops and
other iterator methods.

- add `iter` and `iter_mut` methods to `Text`
- implement `IntoIterator` for `Text`, `&Text`, and `&mut Text` traits
- update call sites to iterate over `Text` rather than `Text::lines`
2024-01-31 13:44:39 -08:00
Dheepak Krishnamurthy
dab08b99b6 feat: show space constrained UIs conditionally (#895)
With this PR the constraint explorer demo only shows space constrained
UIs instead:

Smallest (15 row height):

<img width="759" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/37a4a027-6c6d-4feb-8104-d732aee298ac">

Small (20 row height):

<img width="759" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/f76e025f-0061-4f09-9c91-2f7b00fcfb9e">

Medium (30 row height):

<img width="758" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/81b070da-1bfb-40c5-9fbc-c1ab44ce422e">

Full (40 row height):

<img width="760" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/7bb8a8c4-1a77-4bbc-a346-c8b5c198c6d3">
2024-01-31 13:01:29 -08:00
Josh McKinney
2a12f7bddf feat: impl Widget for &BarChart (#897)
BarChart had some internal mutations that needed to be removed to
implement the Widget trait for &BarChart to bring it in line with the
other widgets.
2024-01-31 11:21:32 -08:00
Josh McKinney
4278b4088d feat(line): implement iterators for Line (#896)
This allows iterating over the `Span`s of a line using `for` loops and
other iterator methods.

- add `iter` and `iter_mut` methods to `Line`
- implement `IntoIterator` for `Line`, `&Line`, and `&mut Line` traits
- update call sites to iterate over `Line` rather than `Line::spans`
2024-01-31 17:33:30 +01:00
Dheepak Krishnamurthy
86168aa711 docs: Fix docstring for Max constraints (#898) 2024-01-31 17:31:05 +01:00
Josh McKinney
78f1c1446b chore: small fixes to constraint-explorer (#894) 2024-01-30 21:27:56 -08:00
Dheepak Krishnamurthy
9ec43eff1c feat: Constraint Explorer example (#893)
Here's a constraint explorer demo put together with @joshka 


https://github.com/ratatui-org/ratatui/assets/1813121/08d7d8f6-d013-44b4-8331-f4eee3589cce

It allows users to interactive explore how the constraints behave with
respect to each other and compare that across flex modes. It allows
users to swap constraints out for other constraints, increment or
decrement the values, add and remove constraints, and add spacing

It is also a good example for how to structure a simple TUI with several
Ratatui code patterns that are useful for refactoring.

Fixes: https://github.com/ratatui-org/ratatui/issues/792

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-01-31 00:12:29 -05:00
Dheepak Krishnamurthy
4ee4e6d78a feat: Make spacing work in Flex::SpaceAround and Flex::SpaceBetween (#892)
This PR implements user provided spacing gaps for `SpaceAround` and
`SpaceBetween`.


https://github.com/ratatui-org/ratatui/assets/1813121/2e260708-e8a7-48ef-aec7-9cf84b655e91

Now user provided spacing gaps always take priority in all `Flex` modes.
2024-01-30 23:34:59 -05:00
Josh McKinney
525479546a refactor: make layout tests a bit easier to understand (#890) 2024-01-29 23:07:18 -08:00
Dheepak Krishnamurthy
cf861232c7 refactor(Scrollbar): Rewrite scrollbar implementation (#847)
Implementation was simplified and calculates the size of the thumb a
bit more proportionally to the content that is visible.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-01-29 23:05:24 -08:00
Dheepak Krishnamurthy
dd5ca3a0c8 feat: Better weights for constraints (#889)
This PR is a split of reworking the weights from #888 

This keeps the same ranking of weights, just uses a different numerical
value so that the lowest weight is `WEAK` (`1.0`).

No tests are changed as a result of this change, and running the
following multiple times did not cause any errors for me:

```rust
for i in {0..100}
do
 cargo test --lib --
 if [ $? -ne 0 ]; then
 echo "Test failed. Exiting loop."
 break
 fi
done
```
2024-01-29 22:16:49 -08:00
Dheepak Krishnamurthy
aeec16369b feat: Change rounding to make tests stable (#888)
This fixes some unstable tests
2024-01-30 00:33:33 -05:00
dependabot[bot]
eb31617539 chore(deps): update termwiz requirement from 0.20.0 to 0.22.0 (#885)
Updates the requirements on [termwiz](https://github.com/wez/wezterm) to
permit the latest version.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/wez/wezterm/commits">compare view</a></li>
</ul>
</details>
<br />


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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 20:02:44 -08:00
dependabot[bot]
1ad629acd7 chore(deps): update derive_builder requirement from 0.12.0 to 0.13.0 (#887) 2024-01-29 17:12:43 +01:00
dependabot[bot]
04e6ccd448 chore(deps): update strum requirement from 0.25 to 0.26 (#886) 2024-01-29 16:59:42 +01:00
Dheepak Krishnamurthy
540fd2df03 feat(layout)!: Change Flex::default() (#881)
This PR makes a number of simplifications to the layout and constraint
features that were added after v0.25.0.

For users upgrading from v0.25.0, the net effect of this PR (along with
the other PRs) is the following:

- New `Flex` modes have been added.
  - `Flex::Start` (new default)
  - `Flex::Center`
  - `Flex::End`
  - `Flex::SpaceAround`
  - `Flex::SpaceBetween`
  - `Flex::Legacy` (old default)
- `Min(v)` grows to allocate excess space in all `Flex` modes instead of
shrinking (except in `Flex::Legacy` where it retains old behavior).
- `Fill(1)` grows to allocate excess space, growing equally with
`Min(v)`.

---

The following contains a summary of the changes in this PR and the
motivation behind them.

**`Flex`**

- Removes `Flex::Stretch`
- Renames `Flex::StretchLast` to `Flex::Legacy`

**`Constraint`**

- Removes `Fixed`
- Makes `Min(v)` grow as much as possible everywhere (except
`Flex::Legacy` where it retains the old behavior)
- Makes `Min(v)` grow equally as `Fill(1)` while respecting `Min` lower
bounds. When `Fill` and `Min` are used together, they both fill excess
space equally.

Allowing `Min(v)` to grow still allows users to build the same layouts
as before with `Flex::Start` with no breaking changes to the behavior.

This PR also removes the unstable feature `SegmentSize`.

This is a breaking change to the behavior of constraints. If users want
old behavior, they can use `Flex::Legacy`.

```rust
Layout::vertical([Length(25), Length(25)]).flex(Flex::Legacy)
```

Users that have constraint that exceed the available space will probably
not see any difference or see an improvement in their layouts. Any
layout with `Min` will be identical in `Flex::Start` and `Flex::Legacy`
so any layout with `Min` will not be breaking.

Previously, `Table` used `EvenDistribution` internally by default, but
with that gone the default is now `Flex::Start`. This changes the
behavior of `Table` (for the better in most cases). The only way for
users to get exactly the same as the old behavior is to change their
constraints. I imagine most users will be happier out of the box with
the new Table default.

Resolves https://github.com/ratatui-org/ratatui/issues/843

Thanks to @joshka for the direction
2024-01-29 09:37:50 -05:00
Josh McKinney
984afd580b chore: cache dependencies in the CI workflow to speed up builds (#883) 2024-01-29 14:43:48 +01:00
Josh McKinney
bbcfa55a88 feat(layout): add Rect::contains method (#882)
This is useful for performing hit tests (i.e. did the user click in an
area).
2024-01-29 04:44:22 -08:00
Dheepak Krishnamurthy
1cbe1f52ab feat(constraints): Rename Constraint::Proportional to Constraint::Fill (#880)
`Constraint::Fill` is a more intuitive name for the behavior, and it is
shorter.

Resolves #859
2024-01-28 05:41:01 -05:00
Dheepak Krishnamurthy
663bbde9c3 test(layout): Convert layout tests to use rstest (#879)
This PR makes all the letters test use `rstest`
2024-01-28 05:11:40 -05:00
Dheepak Krishnamurthy
27e9216cea feat(table): Remove allow deprecated attribute used previously for segment_size (#875) 2024-01-27 13:45:55 -08:00
Dheepak Krishnamurthy
be4fdaa0c7 feat: Change priority of constraints and add split_with_spacers (#788)
Follow up to https://github.com/ratatui-org/ratatui/pull/783

This PR introduces different priorities for each kind of constraint.
This PR also adds tests that specifies this behavior. This PR resolves a
number of broken tests.

Fixes https://github.com/ratatui-org/ratatui/issues/827

With this PR, the layout algorithm will do the following in order:

1. Ensure that all the segments are within the user provided area and
ensure that all segments and spacers are aligned next to each other
2. if a user provides a `layout.spacing`, it will enforce it.
3. ensure proportional elements are all proportional to each other
4. if a user provides a `Fixed(v)` constraint, it will enforce it. 
5. `Min` / `Max` binding inequality constraints
6. `Length`
7. `Percentage`
8. `Ratio`
9. collapse `Min` or collapse `Max`
10. grow `Proportional` as much as possible
11. grow spacers as much as possible

This PR also returns the spacer areas as `Rects` to the user. Users can
then draw into the spacers as they see fit (thanks @joshka for the
idea). Here's a screenshot with the modified flex example:

<img width="569" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/46c8901d-882c-43b0-ba87-b1d455099d8f">

This PR introduces a `strengths` module that has "default" weights that
give stable solutions as well as predictable behavior.
2024-01-27 15:35:42 -05:00
Eeelco
c1ed5c3637 feat(Span): add alignment functions (#873)
Implemented functions that convert Span into a
left-/center-/right-aligned Line. Implemented unit tests.

Closes #853 
---------

Signed-off-by: Eelco Empting <me@eelco.de>
2024-01-27 10:57:06 -08:00
Emirhan TALA
4b8e54e811 docs(examples): refactor Tabs example (#861)
- Used a few new techniques from the 0.26 features (ref widgets, text rendering,
  dividers / padding etc.)
- Updated the app to a simpler application approach
- Use color_eyre
- Make it look pretty (colors, new proportional borders)

![Made with VHS](https://vhs.charm.sh/vhs-4WW21XTtepDhUSq4ZShO56.gif)

---------
Fixes https://github.com/ratatui-org/ratatui/issues/819
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-01-27 02:39:40 -08:00
Emirhan TALA
5b7ad2ad82 docs(examples): update gauge example (#863)
- colored gauges
- removed box borders
- show the difference between ratio / percentage and unicode / no unicode better
- better application approach (consistent with newer examples)
- various changes for 0.26 featuers
- impl `Widget` for `&App`
- use color_eyre

for gauge.tape

- change to get better output from the new code

---------
Fixes: https://github.com/ratatui-org/ratatui/issues/846
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-01-27 01:47:17 -08:00
141 changed files with 10196 additions and 5064 deletions

View File

@@ -2,6 +2,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.rs]
indent_style = space
indent_size = 4

2
.github/CODEOWNERS vendored
View File

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

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:
@@ -102,4 +84,3 @@ jobs:
echo "Pull request is labeled as 'do not merge'"
echo "This workflow fails so that the pull request cannot be merged"
exit 1

View File

@@ -29,28 +29,19 @@ 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:
components: rustfmt
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo make lint-format
- name: Check documentation
run: cargo make lint-docs
- name: Check conventional commits
uses: crate-ci/committed@master
with:
args: "-vv"
commits: HEAD
- name: Check typos
uses: crate-ci/typos@master
- name: Lint dependencies
@@ -67,6 +58,8 @@ jobs:
components: clippy
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Run cargo make clippy-all
run: cargo make clippy
@@ -83,10 +76,12 @@ jobs:
uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov,cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Generate coverage
run: cargo make coverage
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
@@ -95,8 +90,8 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.70.0", "stable" ]
os: [ubuntu-latest, windows-latest, macos-latest]
toolchain: ["1.74.0", "stable"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
@@ -107,6 +102,8 @@ jobs:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Run cargo make check
run: cargo make check
env:
@@ -116,7 +113,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
@@ -125,6 +122,8 @@ jobs:
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Test docs
run: cargo make test-doc
env:
@@ -134,9 +133,9 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.70.0", "stable" ]
backend: [ crossterm, termion, termwiz ]
os: [ubuntu-latest, windows-latest, macos-latest]
toolchain: ["1.74.0", "stable"]
backend: [crossterm, termion, termwiz]
exclude:
# termion is not supported on windows
- os: windows-latest
@@ -153,6 +152,8 @@ jobs:
uses: taiki-e/install-action@cargo-make
- name: Install cargo-nextest
uses: taiki-e/install-action@nextest
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Test ${{ matrix.backend }}
run: cargo make test-backend ${{ matrix.backend }}
env:

View File

@@ -10,7 +10,8 @@ github with a [breaking change] label.
This is a quick summary of the sections below:
- [v0.26.0 (unreleased)](#v0260-unreleased)
- [v0.26.0](#v0260)
- `Flex::Start` is the new default flex mode for `Layout`
- `patch_style` & `reset_style` now consume and return `Self`
- Removed deprecated `Block::title_on_bottom`
- `Line` now has an extra `style` field which applies the style to the entire line
@@ -46,7 +47,28 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## v0.26.0 (unreleased)
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
### `Flex::Start` is the new default flex mode for `Layout`
[#881]: https://github.com/ratatui-org/ratatui/pull/881
Previously, constraints would stretch to fill all available space, violating constraints if
necessary.
With v0.26.0, `Flex` modes are introduced and the default is `Flex::Start`, which will align
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
more easily.
With v0.26.0, users will most likely not need to change what constraints they use to create
existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Legacy`.
```diff
- let rects = Layout::horizontal([Length(1), Length(2)]).split(area);
// becomes
+ let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::Legacy).split(area);
```
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
@@ -132,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()`).

File diff suppressed because it is too large Load Diff

View File

@@ -112,7 +112,8 @@ exist to show coverage directly in your editor. E.g.:
### Documentation
Here are some guidelines for writing documentation in Ratatui.
Here are some guidelines for writing documentation in Ratatui.
Every public API **must** be documented.
Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust
@@ -125,10 +126,9 @@ the concepts pointing to the various methods. Focus on interaction with various
enough information that helps understand why you might want something.
Examples should help users understand a particular usage, not test a feature. They should be as
simple as possible.
Prefer hiding imports and using wildcards to keep things concise. Some imports may still be shown
to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style methods).
Speaking of `Stylize`, you should use it over the more verbose style setters:
simple as possible. Prefer hiding imports and using wildcards to keep things concise. Some imports
may still be shown to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style
methods). Speaking of `Stylize`, you should use it over the more verbose style setters:
```rust
let style = Style::new().red().bold();
@@ -138,7 +138,7 @@ let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
#### Format
- First line is summary, second is blank, third onward is more detail
- First line is summary, second is blank, third onward is more detail
```rust
/// Summary
@@ -148,10 +148,10 @@ let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
fn foo() {}
```
- Max line length is 100 characters
- Max line length is 100 characters
See [vscode rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
- Doc comments are above macros
- Doc comments are above macros
i.e.
```rust
@@ -160,7 +160,7 @@ i.e.
struct Foo {}
```
- Code items should be between backticks
- Code items should be between backticks
i.e. ``[`Block`]``, **NOT** ``[Block]``
### Deprecation notice

View File

@@ -1,6 +1,6 @@
[package]
name = "ratatui"
version = "0.25.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/"
@@ -18,14 +18,14 @@ exclude = [
]
autoexamples = true
edition = "2021"
rust-version = "1.70.0"
rust-version = "1.74.0"
[badges]
[dependencies]
crossterm = { version = "0.27", optional = true }
termion = { version = "3.0", optional = true }
termwiz = { version = "0.20.0", optional = true }
termwiz = { version = "0.22.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
bitflags = "2.3"
@@ -33,7 +33,7 @@ cassowary = "0.3"
indoc = "2.0"
itertools = "0.12"
paste = "1.0.2"
strum = { version = "0.25", features = ["derive"] }
strum = { version = "0.26", features = ["derive"] }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-width = "0.1"
@@ -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.12.0"
derive_builder = "0.20.0"
fakeit = "1.1"
font8x8 = "0.3.1"
palette = "0.7.3"
@@ -61,6 +61,39 @@ rand_chacha = "0.3.1"
rstest = "0.18.2"
serde_json = "1.0.109"
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
module_name_repetitions = "allow"
must_use_candidate = "allow"
wildcard_imports = "allow"
# nursery or restricted
as_underscore = "warn"
deref_by_slicing = "warn"
else_if_without_else = "warn"
empty_line_after_doc_comments = "warn"
equatable_if_let = "warn"
fn_to_numeric_cast_any = "warn"
format_push_string = "warn"
map_err_ignore = "warn"
missing_const_for_fn = "warn"
mixed_read_write_in_expression = "warn"
mod_module_files = "warn"
needless_raw_strings = "warn"
redundant_type_annotations = "warn"
rest_pat_in_fully_bound_structs = "warn"
string_lit_chars_any = "warn"
string_to_string = "warn"
use_self = "warn"
[features]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
#!
@@ -100,11 +133,7 @@ underline-color = ["dep:crossterm"]
#! The following features are unstable and may change in the future:
## Enable all unstable features.
unstable = ["unstable-segment-size", "unstable-rendered-line-info"]
## Enables the [`Layout::segment_size`](crate::layout::Layout::segment_size) method which is experimental and may change in the
## future. See [Issue #536](https://github.com/ratatui-org/ratatui/issues/536) for more details.
unstable-segment-size = []
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"]
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
@@ -112,6 +141,10 @@ unstable-segment-size = []
## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
unstable-rendered-line-info = []
## Enables the `WidgetRef` and `StatefulWidgetRef` traits which are experimental and may change in
## the future.
unstable-widget-ref = []
[package.metadata.docs.rs]
all-features = true
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
@@ -223,6 +256,11 @@ name = "flex"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "constraint-explorer"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "list"
required-features = ["crossterm"]

7
FUNDING.json Normal file
View File

@@ -0,0 +1,7 @@
{
"drips": {
"ethereum": {
"ownedBy": "0x6053C8984f4F214Ad12c4653F28514E1E09213B5"
}
}
}

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",
@@ -74,7 +84,7 @@ on_success = "job:doc" # so that we don't open the browser at each change
command = [
"cargo", "llvm-cov",
"--lcov", "--output-path", "target/lcov.info",
"--all-features",
"--all-features",
"--color", "always",
]
@@ -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

@@ -8,7 +8,7 @@ use ratatui::{
};
/// Benchmark for rendering a barchart.
pub fn barchart(c: &mut Criterion) {
fn barchart(c: &mut Criterion) {
let mut group = c.benchmark_group("barchart");
let mut rng = rand::thread_rng();
@@ -66,7 +66,7 @@ fn render(bencher: &mut Bencher, barchart: &BarChart) {
bench_barchart.render(buffer.area, &mut buffer);
},
criterion::BatchSize::LargeInput,
)
);
}
criterion_group!(benches, barchart);

View File

@@ -10,10 +10,10 @@ use ratatui::{
};
/// Benchmark for rendering a block.
pub fn block(c: &mut Criterion) {
fn block(c: &mut Criterion) {
let mut group = c.benchmark_group("block");
for buffer_size in &[
for buffer_size in [
Rect::new(0, 0, 100, 50), // vertically split screen
Rect::new(0, 0, 200, 50), // 1080p fullscreen with medium font
Rect::new(0, 0, 256, 256), // Max sized area
@@ -47,8 +47,8 @@ pub fn block(c: &mut Criterion) {
}
/// render the block into a buffer of the given `size`
fn render(bencher: &mut Bencher, block: &Block, size: &Rect) {
let mut buffer = Buffer::empty(*size);
fn render(bencher: &mut Bencher, block: &Block, size: Rect) {
let mut buffer = Buffer::empty(size);
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
@@ -57,7 +57,7 @@ fn render(bencher: &mut Bencher, block: &Block, size: &Rect) {
bench_block.render(buffer.area, &mut buffer);
},
BatchSize::SmallInput,
)
);
}
criterion_group!(benches, block);

View File

@@ -7,7 +7,7 @@ use ratatui::{
/// Benchmark for rendering a list.
/// It only benchmarks the render with a different amount of items.
pub fn list(c: &mut Criterion) {
fn list(c: &mut Criterion) {
let mut group = c.benchmark_group("list");
for line_count in [64, 2048, 16384] {
@@ -33,7 +33,7 @@ pub fn list(c: &mut Criterion) {
ListState::default()
.with_offset(line_count / 2)
.with_selected(Some(line_count / 2)),
)
);
},
);
}
@@ -52,7 +52,7 @@ fn render(bencher: &mut Bencher, list: &List) {
Widget::render(bench_list, buffer.area, &mut buffer);
},
BatchSize::LargeInput,
)
);
}
/// render the list into a common size buffer with a state
@@ -66,7 +66,7 @@ fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
StatefulWidget::render(bench_list, buffer.area, &mut buffer, &mut state);
},
BatchSize::LargeInput,
)
);
}
criterion_group!(benches, list);

View File

@@ -17,15 +17,15 @@ const WRAP_WIDTH: u16 = 100;
/// Benchmark for rendering a paragraph with a given number of lines. The design of this benchmark
/// allows comparison of the performance of rendering a paragraph with different numbers of lines.
/// as well as comparing with the various settings on the scroll and wrap features.
pub fn paragraph(c: &mut Criterion) {
fn paragraph(c: &mut Criterion) {
let mut group = c.benchmark_group("paragraph");
for &line_count in [64, 2048, MAX_SCROLL_OFFSET].iter() {
for line_count in [64, 2048, MAX_SCROLL_OFFSET] {
let lines = random_lines(line_count);
let lines = lines.as_str();
// benchmark that measures the overhead of creating a paragraph separately from rendering
group.bench_with_input(BenchmarkId::new("new", line_count), lines, |b, lines| {
b.iter(|| Paragraph::new(black_box(lines)))
b.iter(|| Paragraph::new(black_box(lines)));
});
// render the paragraph with no scroll
@@ -38,14 +38,14 @@ pub fn paragraph(c: &mut Criterion) {
// scroll the paragraph by half the number of lines and render
group.bench_with_input(
BenchmarkId::new("render_scroll_half", line_count),
&Paragraph::new(lines).scroll((0u16, line_count / 2)),
&Paragraph::new(lines).scroll((0, line_count / 2)),
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
);
// scroll the paragraph by the full number of lines and render
group.bench_with_input(
BenchmarkId::new("render_scroll_full", line_count),
&Paragraph::new(lines).scroll((0u16, line_count)),
&Paragraph::new(lines).scroll((0, line_count)),
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
);
@@ -61,7 +61,7 @@ pub fn paragraph(c: &mut Criterion) {
BenchmarkId::new("render_wrap_scroll_full", line_count),
&Paragraph::new(lines)
.wrap(Wrap { trim: false })
.scroll((0u16, line_count)),
.scroll((0, line_count)),
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
);
}
@@ -79,7 +79,7 @@ fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
bench_paragraph.render(buffer.area, &mut buffer);
},
BatchSize::LargeInput,
)
);
}
/// Create a string with the given number of lines filled with nonsense words
@@ -87,7 +87,7 @@ fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
/// English language has about 5.1 average characters per word so including the space between words
/// this should emit around 200 characters per paragraph on average.
fn random_lines(count: u16) -> String {
let count = count as i64;
let count = i64::from(count);
let sentence_count = 3;
let word_count = 11;
fakeit::words::paragraph(count, sentence_count, word_count, "\n".into())

View File

@@ -7,7 +7,7 @@ use ratatui::{
};
/// Benchmark for rendering a sparkline.
pub fn sparkline(c: &mut Criterion) {
fn sparkline(c: &mut Criterion) {
let mut group = c.benchmark_group("sparkline");
let mut rng = rand::thread_rng();
@@ -38,7 +38,7 @@ fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
bench_sparkline.render(buffer.area, &mut buffer);
},
criterion::BatchSize::LargeInput,
)
);
}
criterion_group!(benches, sparkline);

View File

@@ -33,8 +33,7 @@ body = """
{%- for footer in commit.footers %}
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
{{ footer.token | indent(prefix=" ") }}{{ footer.separator }}
{{ footer.value | indent(prefix=" ") }}
{{ footer.token | indent(prefix=" ") }}{{ footer.separator }}{{ footer.value }}
{%- endif %}
{%- endfor %}
{% endmacro -%}

View File

@@ -1,4 +1,4 @@
//! # [Ratatui] BarChart example
//! # [Ratatui] `BarChart` example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
@@ -24,7 +24,10 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::*,
widgets::{Bar, BarChart, BarGroup, Block, Borders, Paragraph},
};
struct Company<'a> {
revenue: [u64; 4],
@@ -41,7 +44,7 @@ struct App<'a> {
const TOTAL_REVENUE: &str = "Total Revenue";
impl<'a> App<'a> {
fn new() -> App<'a> {
fn new() -> Self {
App {
data: vec![
("B1", 9),
@@ -137,7 +140,7 @@ fn run_app<B: Backend>(
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
@@ -152,8 +155,8 @@ fn run_app<B: Backend>(
fn ui(frame: &mut Frame, app: &App) {
let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [top, bottom] = frame.size().split(&vertical);
let [left, right] = bottom.split(&horizontal);
let [top, bottom] = vertical.areas(frame.size());
let [left, right] = horizontal.areas(bottom);
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
@@ -167,6 +170,7 @@ fn ui(frame: &mut Frame, app: &App) {
draw_horizontal_bars(frame, app, right);
}
#[allow(clippy::cast_precision_loss)]
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
app.months
.iter()
@@ -206,7 +210,10 @@ fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGr
.collect()
}
#[allow(clippy::cast_possible_truncation)]
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
const LEGEND_HEIGHT: u16 = 6;
let groups = create_groups(app, false);
let mut barchart = BarChart::default()
@@ -215,12 +222,11 @@ fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
.group_gap(3);
for group in groups {
barchart = barchart.data(group)
barchart = barchart.data(group);
}
f.render_widget(barchart, area);
const LEGEND_HEIGHT: u16 = 6;
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {
@@ -233,7 +239,10 @@ fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
}
}
#[allow(clippy::cast_possible_truncation)]
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
const LEGEND_HEIGHT: u16 = 6;
let groups = create_groups(app, true);
let mut barchart = BarChart::default()
@@ -244,12 +253,11 @@ fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
.direction(Direction::Horizontal);
for group in groups {
barchart = barchart.data(group)
barchart = barchart.data(group);
}
f.render_widget(barchart, area);
const LEGEND_HEIGHT: u16 = 6;
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {

View File

@@ -77,7 +77,7 @@ fn run(terminal: &mut Terminal) -> Result<()> {
fn handle_events() -> Result<ControlFlow<()>> {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
if key.code == KeyCode::Char('q') {
return Ok(ControlFlow::Break(()));
}
}
@@ -119,8 +119,8 @@ fn ui(frame: &mut Frame) {
/// Returns a tuple of the title area and the main areas.
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
let main_layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let block_layout = &Layout::vertical([Constraint::Max(4); 9]);
let [title_area, main_area] = area.split(&main_layout);
let block_layout = Layout::vertical([Constraint::Max(4); 9]);
let [title_area, main_area] = main_layout.areas(area);
let main_areas = block_layout
.split(main_area)
.iter()
@@ -150,7 +150,7 @@ fn placeholder_paragraph() -> Paragraph<'static> {
fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(border)
.title(format!("Borders::{border:#?}", border = border));
.title(format!("Borders::{border:#?}"));
frame.render_widget(paragraph.clone().block(block), area);
}

View File

@@ -13,6 +13,8 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(clippy::wildcard_imports)]
use std::{error::Error, io};
use crossterm::{
@@ -166,15 +168,13 @@ fn make_dates(current_year: i32) -> CalendarEventStore {
mod cals {
use super::*;
pub(super) fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
use Month::*;
pub fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
match m {
May => example1(m, y, es),
June => example2(m, y, es),
July => example3(m, y, es),
December => example3(m, y, es),
February => example4(m, y, es),
November => example5(m, y, es),
Month::May => example1(m, y, es),
Month::June => example2(m, y, es),
Month::July | Month::December => example3(m, y, es),
Month::February => example4(m, y, es),
Month::November => example5(m, y, es),
_ => default(m, y, es),
}
}

View File

@@ -13,6 +13,8 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(clippy::wildcard_imports)]
use std::{
io::{self, stdout, Stdout},
time::{Duration, Instant},
@@ -44,8 +46,8 @@ struct App {
}
impl App {
fn new() -> App {
App {
fn new() -> Self {
Self {
x: 0.0,
y: 0.0,
ball: Circle {
@@ -64,7 +66,7 @@ impl App {
pub fn run() -> io::Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::new();
let mut app = Self::new();
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(16);
loop {
@@ -106,13 +108,13 @@ impl App {
// bounce the ball by flipping the velocity vector
let ball = &self.ball;
let playground = self.playground;
if ball.x - ball.radius < playground.left() as f64
|| ball.x + ball.radius > playground.right() as f64
if ball.x - ball.radius < f64::from(playground.left())
|| ball.x + ball.radius > f64::from(playground.right())
{
self.vx = -self.vx;
}
if ball.y - ball.radius < playground.top() as f64
|| ball.y + ball.radius > playground.bottom() as f64
if ball.y - ball.radius < f64::from(playground.top())
|| ball.y + ball.radius > f64::from(playground.bottom())
{
self.vy = -self.vy;
}
@@ -125,8 +127,8 @@ impl App {
let horizontal =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [map, right] = frame.size().split(&horizontal);
let [pong, boxes] = right.split(&vertical);
let [map, right] = horizontal.areas(frame.size());
let [pong, boxes] = vertical.areas(right);
frame.render_widget(self.map_canvas(), map);
frame.render_widget(self.pong_canvas(), pong);
@@ -160,8 +162,10 @@ impl App {
}
fn boxes_canvas(&self, area: Rect) -> impl Widget {
let (left, right, bottom, top) =
(0.0, area.width as f64, 0.0, area.height as f64 * 2.0 - 4.0);
let left = 0.0;
let right = f64::from(area.width);
let bottom = 0.0;
let top = f64::from(area.height).mul_add(2.0, -4.0);
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Rects"))
.marker(self.marker)
@@ -170,26 +174,26 @@ impl App {
.paint(|ctx| {
for i in 0..=11 {
ctx.draw(&Rectangle {
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
y: 2.0,
width: i as f64,
height: i as f64,
width: f64::from(i),
height: f64::from(i),
color: Color::Red,
});
ctx.draw(&Rectangle {
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
y: 21.0,
width: i as f64,
height: i as f64,
width: f64::from(i),
height: f64::from(i),
color: Color::Blue,
});
}
for i in 0..100 {
if i % 10 != 0 {
ctx.print(i as f64 + 1.0, 0.0, format!("{i}", i = i % 10));
ctx.print(f64::from(i) + 1.0, 0.0, format!("{i}", i = i % 10));
}
if i % 2 == 0 && i % 10 != 0 {
ctx.print(0.0, i as f64, format!("{i}", i = i % 10));
ctx.print(0.0, f64::from(i), format!("{i}", i = i % 10));
}
}
})

View File

@@ -26,11 +26,11 @@ use crossterm::{
};
use ratatui::{
prelude::*,
widgets::{block::Title, *},
widgets::{block::Title, Axis, Block, Borders, Chart, Dataset, GraphType, LegendPosition},
};
#[derive(Clone)]
pub struct SinSignal {
struct SinSignal {
x: f64,
interval: f64,
period: f64,
@@ -38,8 +38,8 @@ pub struct SinSignal {
}
impl SinSignal {
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
SinSignal {
const fn new(interval: f64, period: f64, scale: f64) -> Self {
Self {
x: 0.0,
interval,
period,
@@ -66,12 +66,12 @@ struct App {
}
impl App {
fn new() -> App {
fn new() -> Self {
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
let mut signal2 = SinSignal::new(0.1, 2.0, 10.0);
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
App {
Self {
signal1,
data1,
signal2,
@@ -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;
}
@@ -135,7 +133,7 @@ fn run_app<B: Backend>(
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
@@ -152,8 +150,8 @@ fn ui(frame: &mut Frame, app: &App) {
let vertical = Layout::vertical([Constraint::Percentage(40), Constraint::Percentage(60)]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let [chart1, bottom] = area.split(&vertical);
let [line_chart, scatter] = bottom.split(&horizontal);
let [chart1, bottom] = vertical.areas(area);
let [line_chart, scatter] = horizontal.areas(bottom);
render_chart1(frame, chart1, app);
render_line_chart(frame, line_chart);
@@ -244,7 +242,7 @@ fn render_line_chart(f: &mut Frame, area: Rect) {
.legend_position(Some(LegendPosition::TopLeft))
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
f.render_widget(chart, area)
f.render_widget(chart, area);
}
fn render_scatter(f: &mut Frame, area: Rect) {
@@ -312,7 +310,7 @@ const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
(1963., 29500.),
(1964., 30600.),
(1965., 177900.),
(1965., 177_900.),
(1965., 21000.),
(1966., 17900.),
(1966., 8400.),
@@ -342,7 +340,7 @@ const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
];
const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
(1961., 118500.),
(1961., 118_500.),
(1962., 14900.),
(1975., 21400.),
(1980., 32800.),

View File

@@ -13,8 +13,9 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
/// This example shows all the colors supported by ratatui. It will render a grid of foreground
/// and background colors with their names and indexes.
// This example shows all the colors supported by ratatui. It will render a grid of foreground
// and background colors with their names and indexes.
use std::{
error::Error,
io::{self, Stdout},
@@ -28,7 +29,10 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph},
};
type Result<T> = result::Result<T, Box<dyn Error>>;
@@ -48,7 +52,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}

View File

@@ -1,4 +1,4 @@
//! # [Ratatui] Colors_RGB example
//! # [Ratatui] `Colors_RGB` example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
@@ -13,18 +13,19 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
/// This example shows the full range of RGB colors that can be displayed in the terminal.
///
/// Requires a terminal that supports 24-bit color (true color) and unicode.
///
/// This example also demonstrates how implementing the Widget trait on a mutable reference
/// allows the widget to update its state while it is being rendered. This allows the fps
/// widget to update the fps calculation and the colors widget to update a cached version of
/// the colors to render instead of recalculating them every frame.
///
/// This is an alternative to using the StatefulWidget trait and a separate state struct. It is
/// useful when the state is only used by the widget and doesn't need to be shared with other
/// widgets.
// This example shows the full range of RGB colors that can be displayed in the terminal.
//
// Requires a terminal that supports 24-bit color (true color) and unicode.
//
// This example also demonstrates how implementing the Widget trait on a mutable reference
// allows the widget to update its state while it is being rendered. This allows the fps
// widget to update the fps calculation and the colors widget to update a cached version of
// the colors to render instead of recalculating them every frame.
//
// This is an alternative to using the `StatefulWidget` trait and a separate state struct. It
// is useful when the state is only used by the widget and doesn't need to be shared with
// other widgets.
use std::{
io::stdout,
panic,
@@ -110,7 +111,7 @@ impl App {
Ok(())
}
fn is_running(&self) -> bool {
const fn is_running(&self) -> bool {
matches!(self.state, AppState::Running)
}
@@ -140,9 +141,10 @@ impl App {
/// to update the colors to render.
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
#[allow(clippy::enum_glob_use)]
use Constraint::*;
let [top, colors] = area.split(&Layout::vertical([Length(1), Min(0)]));
let [title, fps] = top.split(&Layout::horizontal([Min(0), Length(8)]));
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
Text::from("colors_rgb example. Press q to quit")
.centered()
.render(title, buf);
@@ -151,9 +153,9 @@ impl Widget for &mut App {
}
}
/// Default impl for FpsWidget
/// Default impl for `FpsWidget`
///
/// Manual impl is required because we need to initialize the last_instant field to the current
/// Manual impl is required because we need to initialize the `last_instant` field to the current
/// instant.
impl Default for FpsWidget {
fn default() -> Self {
@@ -165,7 +167,7 @@ impl Default for FpsWidget {
}
}
/// Widget impl for FpsWidget
/// Widget impl for `FpsWidget`
///
/// This is implemented on a mutable reference so that we can update the frame count and fps
/// calculation while rendering.
@@ -173,7 +175,7 @@ impl Widget for &mut FpsWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
self.calculate_fps();
if let Some(fps) = self.fps {
let text = format!("{:.1} fps", fps);
let text = format!("{fps:.1} fps");
Text::from(text).render(area, buf);
}
}
@@ -185,6 +187,7 @@ impl FpsWidget {
/// This updates the fps once a second, but only if the widget has rendered at least 2 frames
/// since the last calculation. This avoids noise in the fps calculation when rendering on slow
/// machines that can't render at least 2 frames per second.
#[allow(clippy::cast_precision_loss)]
fn calculate_fps(&mut self) {
self.frame_count += 1;
let elapsed = self.last_instant.elapsed();
@@ -196,7 +199,7 @@ impl FpsWidget {
}
}
/// Widget impl for ColorsWidget
/// Widget impl for `ColorsWidget`
///
/// This is implemented on a mutable reference so that we can update the frame count and store a
/// cached version of the colors to render instead of recalculating them every frame.
@@ -226,6 +229,7 @@ impl ColorsWidget {
///
/// This is called once per frame to setup the colors to render. It caches the colors so that
/// they don't need to be recalculated every frame.
#[allow(clippy::cast_precision_loss)]
fn setup_colors(&mut self, size: Rect) {
let Rect { width, height, .. } = size;
// double the height because each screen row has two rows of half block pixels
@@ -253,7 +257,7 @@ impl ColorsWidget {
}
}
/// Install color_eyre panic and error hooks
/// Install `color_eyre` panic and error hooks
///
/// The hooks restore the terminal to a usable state before printing the error message.
fn install_error_hooks() -> Result<()> {
@@ -266,7 +270,7 @@ fn install_error_hooks() -> Result<()> {
}))?;
panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
panic(info);
}));
Ok(())
}

View File

@@ -0,0 +1,646 @@
//! # [Ratatui] Constraint explorer example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
use std::io::{self, stdout};
use color_eyre::{config::HookBuilder, Result};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use itertools::Itertools;
use ratatui::{
layout::{Constraint::*, Flex},
prelude::*,
style::palette::tailwind::*,
symbols::line,
widgets::{Block, Paragraph, Wrap},
};
use strum::{Display, EnumIter, FromRepr};
#[derive(Default)]
struct App {
mode: AppMode,
spacing: u16,
constraints: Vec<Constraint>,
selected_index: usize,
value: u16,
}
#[derive(Debug, Default, PartialEq, Eq)]
enum AppMode {
#[default]
Running,
Quit,
}
/// A variant of [`Constraint`] that can be rendered as a tab.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, EnumIter, FromRepr, Display)]
enum ConstraintName {
#[default]
Length,
Percentage,
Ratio,
Min,
Max,
Fill,
}
/// A widget that renders a [`Constraint`] as a block. E.g.:
/// ```plain
/// ┌──────────────┐
/// │ Length(16) │
/// │ 16px │
/// └──────────────┘
/// ```
struct ConstraintBlock {
constraint: Constraint,
legend: bool,
selected: bool,
}
/// A widget that renders a spacer with a label indicating the width of the spacer. E.g.:
///
/// ```plain
/// ┌ ┐
/// 8 px
/// └ ┘
/// ```
struct SpacerBlock;
fn main() -> Result<()> {
init_error_hooks()?;
let terminal = init_terminal()?;
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
}
// App behaviour
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
self.insert_test_defaults();
while self.is_running() {
self.draw(&mut terminal)?;
self.handle_events()?;
}
Ok(())
}
// TODO remove these - these are just for testing
fn insert_test_defaults(&mut self) {
self.constraints = vec![
Constraint::Length(20),
Constraint::Length(20),
Constraint::Length(20),
];
self.value = 20;
}
fn is_running(&self) -> bool {
self.mode == AppMode::Running
}
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
use KeyCode::*;
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
Char('q') | Esc => self.exit(),
Char('1') => self.swap_constraint(ConstraintName::Min),
Char('2') => self.swap_constraint(ConstraintName::Max),
Char('3') => self.swap_constraint(ConstraintName::Length),
Char('4') => self.swap_constraint(ConstraintName::Percentage),
Char('5') => self.swap_constraint(ConstraintName::Ratio),
Char('6') => self.swap_constraint(ConstraintName::Fill),
Char('+') => self.increment_spacing(),
Char('-') => self.decrement_spacing(),
Char('x') => self.delete_block(),
Char('a') => self.insert_block(),
Char('k') | Up => self.increment_value(),
Char('j') | Down => self.decrement_value(),
Char('h') | Left => self.prev_block(),
Char('l') | Right => self.next_block(),
_ => {}
},
_ => {}
}
Ok(())
}
fn increment_value(&mut self) {
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
return;
};
match constraint {
Constraint::Length(v)
| Constraint::Min(v)
| Constraint::Max(v)
| Constraint::Fill(v)
| Constraint::Percentage(v) => *v = v.saturating_add(1),
Constraint::Ratio(_n, d) => *d = d.saturating_add(1),
};
}
fn decrement_value(&mut self) {
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
return;
};
match constraint {
Constraint::Length(v)
| Constraint::Min(v)
| Constraint::Max(v)
| Constraint::Fill(v)
| Constraint::Percentage(v) => *v = v.saturating_sub(1),
Constraint::Ratio(_n, d) => *d = d.saturating_sub(1),
};
}
/// select the next block with wrap around
fn next_block(&mut self) {
if self.constraints.is_empty() {
return;
}
let len = self.constraints.len();
self.selected_index = (self.selected_index + 1) % len;
}
/// select the previous block with wrap around
fn prev_block(&mut self) {
if self.constraints.is_empty() {
return;
}
let len = self.constraints.len();
self.selected_index = (self.selected_index + self.constraints.len() - 1) % len;
}
/// delete the selected block
fn delete_block(&mut self) {
if self.constraints.is_empty() {
return;
}
self.constraints.remove(self.selected_index);
self.selected_index = self.selected_index.saturating_sub(1);
}
/// insert a block after the selected block
fn insert_block(&mut self) {
let index = self
.selected_index
.saturating_add(1)
.min(self.constraints.len());
let constraint = Constraint::Length(self.value);
self.constraints.insert(index, constraint);
self.selected_index = index;
}
fn increment_spacing(&mut self) {
self.spacing = self.spacing.saturating_add(1);
}
fn decrement_spacing(&mut self) {
self.spacing = self.spacing.saturating_sub(1);
}
fn exit(&mut self) {
self.mode = AppMode::Quit;
}
fn swap_constraint(&mut self, name: ConstraintName) {
if self.constraints.is_empty() {
return;
}
let constraint = match name {
ConstraintName::Length => Length(self.value),
ConstraintName::Percentage => Percentage(self.value),
ConstraintName::Min => Min(self.value),
ConstraintName::Max => Max(self.value),
ConstraintName::Fill => Fill(self.value),
ConstraintName::Ratio => Ratio(1, u32::from(self.value) / 4), // for balance
};
self.constraints[self.selected_index] = constraint;
}
}
impl From<Constraint> for ConstraintName {
fn from(constraint: Constraint) -> Self {
match constraint {
Length(_) => Self::Length,
Percentage(_) => Self::Percentage,
Ratio(_, _) => Self::Ratio,
Min(_) => Self::Min,
Max(_) => Self::Max,
Fill(_) => Self::Fill,
}
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let [header_area, instructions_area, swap_legend_area, _, blocks_area] =
Layout::vertical([
Length(2), // header
Length(2), // instructions
Length(1), // swap key legend
Length(1), // gap
Fill(1), // blocks
])
.areas(area);
App::header().render(header_area, buf);
App::instructions().render(instructions_area, buf);
App::swap_legend().render(swap_legend_area, buf);
self.render_layout_blocks(blocks_area, buf);
}
}
// App rendering
impl App {
const HEADER_COLOR: Color = SLATE.c200;
const TEXT_COLOR: Color = SLATE.c400;
const AXIS_COLOR: Color = SLATE.c500;
fn header() -> impl Widget {
let text = "Constraint Explorer";
text.bold().fg(Self::HEADER_COLOR).into_centered_line()
}
fn instructions() -> impl Widget {
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, + -: spacing";
Paragraph::new(text)
.fg(Self::TEXT_COLOR)
.centered()
.wrap(Wrap { trim: false })
}
fn swap_legend() -> impl Widget {
#[allow(unstable_name_collisions)]
Paragraph::new(
Line::from(
[
ConstraintName::Min,
ConstraintName::Max,
ConstraintName::Length,
ConstraintName::Percentage,
ConstraintName::Ratio,
ConstraintName::Fill,
]
.iter()
.enumerate()
.map(|(i, name)| {
format!(" {i}: {name} ", i = i + 1)
.fg(SLATE.c200)
.bg(name.color())
})
.intersperse(Span::from(" "))
.collect_vec(),
)
.centered(),
)
.wrap(Wrap { trim: false })
}
/// A bar like `<----- 80 px (gap: 2 px) ----->`
///
/// Only shows the gap when spacing is not zero
fn axis(&self, width: u16) -> impl Widget {
let label = if self.spacing != 0 {
format!("{} px (gap: {} px)", width, self.spacing)
} else {
format!("{width} px")
};
let bar_width = width.saturating_sub(2) as usize; // we want to `<` and `>` at the ends
let width_bar = format!("<{label:-^bar_width$}>");
Paragraph::new(width_bar).fg(Self::AXIS_COLOR).centered()
}
fn render_layout_blocks(&self, area: Rect, buf: &mut Buffer) {
let [user_constraints, area] = Layout::vertical([Length(3), Fill(1)])
.spacing(1)
.areas(area);
self.render_user_constraints_legend(user_constraints, buf);
let [start, center, end, space_around, space_between] =
Layout::vertical([Length(7); 5]).areas(area);
self.render_layout_block(Flex::Start, start, buf);
self.render_layout_block(Flex::Center, center, buf);
self.render_layout_block(Flex::End, end, buf);
self.render_layout_block(Flex::SpaceAround, space_around, buf);
self.render_layout_block(Flex::SpaceBetween, space_between, buf);
}
fn render_user_constraints_legend(&self, area: Rect, buf: &mut Buffer) {
let blocks = Layout::horizontal(
self.constraints
.iter()
.map(|_| Constraint::Fill(1))
.collect_vec(),
)
.split(area);
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
let selected = self.selected_index == i;
ConstraintBlock::new(*constraint, selected, true).render(*area, buf);
}
}
fn render_layout_block(&self, flex: Flex, area: Rect, buf: &mut Buffer) {
let [label_area, axis_area, blocks_area] =
Layout::vertical([Length(1), Max(1), Length(4)]).areas(area);
if label_area.height > 0 {
format!("Flex::{flex:?}").bold().render(label_area, buf);
}
self.axis(area.width).render(axis_area, buf);
let (blocks, spacers) = Layout::horizontal(&self.constraints)
.flex(flex)
.spacing(self.spacing)
.split_with_spacers(blocks_area);
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
let selected = self.selected_index == i;
ConstraintBlock::new(*constraint, selected, false).render(*area, buf);
}
for area in spacers.iter() {
SpacerBlock.render(*area, buf);
}
}
}
impl Widget for ConstraintBlock {
fn render(self, area: Rect, buf: &mut Buffer) {
match area.height {
1 => self.render_1px(area, buf),
2 => self.render_2px(area, buf),
_ => self.render_4px(area, buf),
}
}
}
impl ConstraintBlock {
const TEXT_COLOR: Color = SLATE.c200;
const fn new(constraint: Constraint, selected: bool, legend: bool) -> Self {
Self {
constraint,
legend,
selected,
}
}
fn label(&self, width: u16) -> String {
let long_width = format!("{width} px");
let short_width = format!("{width}");
// border takes up 2 columns
let available_space = width.saturating_sub(2) as usize;
let width_label = if long_width.len() < available_space {
long_width
} else if short_width.len() < available_space {
short_width
} else {
String::new()
};
format!("{}\n{}", self.constraint, width_label)
}
fn render_1px(&self, area: Rect, buf: &mut Buffer) {
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
let main_color = ConstraintName::from(self.constraint).color();
let selected_color = if self.selected {
lighter_color
} else {
main_color
};
Block::default()
.fg(Self::TEXT_COLOR)
.bg(selected_color)
.render(area, buf);
}
fn render_2px(&self, area: Rect, buf: &mut Buffer) {
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
let main_color = ConstraintName::from(self.constraint).color();
let selected_color = if self.selected {
lighter_color
} else {
main_color
};
Block::bordered()
.border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(selected_color).reversed())
.render(area, buf);
}
fn render_4px(&self, area: Rect, buf: &mut Buffer) {
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
let main_color = ConstraintName::from(self.constraint).color();
let selected_color = if self.selected {
lighter_color
} else {
main_color
};
let color = if self.legend {
selected_color
} else {
main_color
};
let label = self.label(area.width);
let block = Block::bordered()
.border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(color).reversed())
.fg(Self::TEXT_COLOR)
.bg(color);
Paragraph::new(label)
.centered()
.fg(Self::TEXT_COLOR)
.bg(color)
.block(block)
.render(area, buf);
if !self.legend {
let border_color = if self.selected {
lighter_color
} else {
main_color
};
if let Some(last_row) = area.rows().last() {
buf.set_style(last_row, border_color);
}
}
}
}
impl Widget for SpacerBlock {
fn render(self, area: Rect, buf: &mut Buffer) {
match area.height {
1 => (),
2 => Self::render_2px(area, buf),
3 => Self::render_3px(area, buf),
_ => Self::render_4px(area, buf),
}
}
}
impl SpacerBlock {
const TEXT_COLOR: Color = SLATE.c500;
const BORDER_COLOR: Color = SLATE.c600;
/// A block with a corner borders
fn block() -> impl Widget {
let corners_only = symbols::border::Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: " ",
vertical_right: " ",
horizontal_top: " ",
horizontal_bottom: " ",
};
Block::bordered()
.border_set(corners_only)
.border_style(Self::BORDER_COLOR)
}
/// A vertical line used if there is not enough space to render the block
fn line() -> impl Widget {
Paragraph::new(Text::from(vec![
Line::from(""),
Line::from(""),
Line::from(""),
Line::from(""),
]))
.style(Self::BORDER_COLOR)
}
/// A label that says "Spacer" if there is enough space
fn spacer_label(width: u16) -> impl Widget {
let label = if width >= 6 { "Spacer" } else { "" };
label.fg(Self::TEXT_COLOR).into_centered_line()
}
/// A label that says "8 px" if there is enough space
fn label(width: u16) -> impl Widget {
let long_label = format!("{width} px");
let short_label = format!("{width}");
let label = if long_label.len() < width as usize {
long_label
} else if short_label.len() < width as usize {
short_label
} else {
String::new()
};
Line::styled(label, Self::TEXT_COLOR).centered()
}
fn render_2px(area: Rect, buf: &mut Buffer) {
if area.width > 1 {
Self::block().render(area, buf);
} else {
Self::line().render(area, buf);
}
}
fn render_3px(area: Rect, buf: &mut Buffer) {
if area.width > 1 {
Self::block().render(area, buf);
} else {
Self::line().render(area, buf);
}
let row = area.rows().nth(1).unwrap_or_default();
Self::spacer_label(area.width).render(row, buf);
}
fn render_4px(area: Rect, buf: &mut Buffer) {
if area.width > 1 {
Self::block().render(area, buf);
} else {
Self::line().render(area, buf);
}
let row = area.rows().nth(1).unwrap_or_default();
Self::spacer_label(area.width).render(row, buf);
let row = area.rows().nth(2).unwrap_or_default();
Self::label(area.width).render(row, buf);
}
}
impl ConstraintName {
const fn color(self) -> Color {
match self {
Self::Length => SLATE.c700,
Self::Percentage => SLATE.c800,
Self::Ratio => SLATE.c900,
Self::Fill => SLATE.c950,
Self::Min => BLUE.c800,
Self::Max => BLUE.c900,
}
}
const fn lighter_color(self) -> Color {
match self {
Self::Length => STONE.c500,
Self::Percentage => STONE.c600,
Self::Ratio => STONE.c700,
Self::Fill => STONE.c800,
Self::Min => SKY.c600,
Self::Max => SKY.c700,
}
}
}
fn init_error_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -13,6 +13,8 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
use std::io::{self, stdout};
use color_eyre::{config::HookBuilder, Result};
@@ -28,8 +30,6 @@ const SPACER_HEIGHT: u16 = 0;
const ILLUSTRATION_HEIGHT: u16 = 4;
const EXAMPLE_HEIGHT: u16 = ILLUSTRATION_HEIGHT + SPACER_HEIGHT;
// priority 1
const FIXED_COLOR: Color = tailwind::RED.c900;
// priority 2
const MIN_COLOR: Color = tailwind::BLUE.c900;
const MAX_COLOR: Color = tailwind::BLUE.c800;
@@ -38,7 +38,7 @@ const LENGTH_COLOR: Color = tailwind::SLATE.c700;
const PERCENTAGE_COLOR: Color = tailwind::SLATE.c800;
const RATIO_COLOR: Color = tailwind::SLATE.c900;
// priority 4
const PROPORTIONAL_COLOR: Color = tailwind::SLATE.c950;
const FILL_COLOR: Color = tailwind::SLATE.c950;
#[derive(Default, Clone, Copy)]
struct App {
@@ -54,13 +54,12 @@ struct App {
#[derive(Default, Debug, Copy, Clone, Display, FromRepr, EnumIter, PartialEq, Eq)]
enum SelectedTab {
#[default]
Fixed,
Min,
Max,
Length,
Percentage,
Ratio,
Proportional,
Fill,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
@@ -98,7 +97,7 @@ impl App {
self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT;
}
fn is_running(&self) -> bool {
fn is_running(self) -> bool {
self.state == AppState::Running
}
@@ -141,14 +140,14 @@ impl App {
}
fn up(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1)
self.scroll_offset = self.scroll_offset.saturating_sub(1);
}
fn down(&mut self) {
self.scroll_offset = self
.scroll_offset
.saturating_add(1)
.min(self.max_scroll_offset)
.min(self.max_scroll_offset);
}
fn top(&mut self) {
@@ -162,20 +161,16 @@ impl App {
impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) {
let [tabs, axis, demo] = area.split(&Layout::vertical([
Constraint::Fixed(3),
Constraint::Fixed(3),
Proportional(0),
]));
let [tabs, axis, demo] = Layout::vertical([Length(3), Length(3), Fill(0)]).areas(area);
self.render_tabs(tabs, buf);
self.render_axis(axis, buf);
Self::render_axis(axis, buf);
self.render_demo(demo, buf);
}
}
impl App {
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
fn render_tabs(self, area: Rect, buf: &mut Buffer) {
let titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
let block = Block::new()
.title("Constraints ".bold())
@@ -189,10 +184,10 @@ impl App {
.render(area, buf);
}
fn render_axis(&self, area: Rect, buf: &mut Buffer) {
fn render_axis(area: Rect, buf: &mut Buffer) {
let width = area.width as usize;
// a bar like `<----- 80 px ----->`
let width_label = format!("{} px", width);
let width_label = format!("{width} px");
let width_bar = format!(
"<{width_label:-^width$}>",
width = width - width_label.len() / 2
@@ -212,7 +207,8 @@ impl App {
///
/// This function renders the demo content into a separate buffer and then splices the buffer
/// into the main buffer. This is done to make it possible to handle scrolling easily.
fn render_demo(&self, area: Rect, buf: &mut Buffer) {
#[allow(clippy::cast_possible_truncation)]
fn render_demo(self, area: Rect, buf: &mut Buffer) {
// render demo content into a separate buffer so all examples fit we add an extra
// area.height to make sure the last example is fully visible even when the scroll offset is
// at the max
@@ -252,43 +248,40 @@ impl App {
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab.
fn previous(&self) -> Self {
let current_index: usize = *self as usize;
fn previous(self) -> Self {
let current_index: usize = self as usize;
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(*self)
Self::from_repr(previous_index).unwrap_or(self)
}
/// Get the next tab, if there is no next tab return the current tab.
fn next(&self) -> Self {
let current_index = *self as usize;
fn next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(*self)
Self::from_repr(next_index).unwrap_or(self)
}
fn get_example_count(&self) -> u16 {
use SelectedTab::*;
const fn get_example_count(self) -> u16 {
#[allow(clippy::match_same_arms)]
match self {
Fixed => 4,
Length => 4,
Percentage => 5,
Ratio => 4,
Proportional => 2,
Min => 5,
Max => 5,
Self::Length => 4,
Self::Percentage => 5,
Self::Ratio => 4,
Self::Fill => 2,
Self::Min => 5,
Self::Max => 5,
}
}
fn to_tab_title(value: SelectedTab) -> Line<'static> {
use SelectedTab::*;
fn to_tab_title(value: Self) -> Line<'static> {
let text = format!(" {value} ");
let color = match value {
Fixed => FIXED_COLOR,
Length => LENGTH_COLOR,
Percentage => PERCENTAGE_COLOR,
Ratio => RATIO_COLOR,
Proportional => PROPORTIONAL_COLOR,
Min => MIN_COLOR,
Max => MAX_COLOR,
Self::Length => LENGTH_COLOR,
Self::Percentage => PERCENTAGE_COLOR,
Self::Ratio => RATIO_COLOR,
Self::Fill => FILL_COLOR,
Self::Min => MIN_COLOR,
Self::Max => MAX_COLOR,
};
text.fg(tailwind::SLATE.c200).bg(color).into()
}
@@ -297,59 +290,40 @@ impl SelectedTab {
impl Widget for SelectedTab {
fn render(self, area: Rect, buf: &mut Buffer) {
match self {
SelectedTab::Fixed => self.render_fixed_example(area, buf),
SelectedTab::Length => self.render_length_example(area, buf),
SelectedTab::Percentage => self.render_percentage_example(area, buf),
SelectedTab::Ratio => self.render_ratio_example(area, buf),
SelectedTab::Proportional => self.render_proportional_example(area, buf),
SelectedTab::Min => self.render_min_example(area, buf),
SelectedTab::Max => self.render_max_example(area, buf),
Self::Length => Self::render_length_example(area, buf),
Self::Percentage => Self::render_percentage_example(area, buf),
Self::Ratio => Self::render_ratio_example(area, buf),
Self::Fill => Self::render_fill_example(area, buf),
Self::Min => Self::render_min_example(area, buf),
Self::Max => Self::render_max_example(area, buf),
}
}
}
impl SelectedTab {
fn render_fixed_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
fn render_length_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 4]).areas(area);
Example::new(&[Fixed(40), Proportional(0)]).render(example1, buf);
Example::new(&[Fixed(20), Fixed(20), Proportional(0)]).render(example2, buf);
Example::new(&[Fixed(20), Min(20), Max(20)]).render(example3, buf);
Example::new(&[
Length(20),
Percentage(20),
Ratio(1, 5),
Proportional(1),
Fixed(15),
])
.render(example4, buf);
Example::new(&[Length(20), Length(20)]).render(example1, buf);
Example::new(&[Length(20), Min(20)]).render(example2, buf);
Example::new(&[Length(20), Max(20)]).render(example3, buf);
}
fn render_length_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
Example::new(&[Length(20), Fixed(20)]).render(example1, buf);
Example::new(&[Length(20), Length(20)]).render(example2, buf);
Example::new(&[Length(20), Min(20)]).render(example3, buf);
Example::new(&[Length(20), Max(20)]).render(example4, buf);
}
fn render_percentage_example(&self, area: Rect, buf: &mut Buffer) {
fn render_percentage_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
Example::new(&[Percentage(75), Proportional(0)]).render(example1, buf);
Example::new(&[Percentage(25), Proportional(0)]).render(example2, buf);
Example::new(&[Percentage(75), Fill(0)]).render(example1, buf);
Example::new(&[Percentage(25), Fill(0)]).render(example2, buf);
Example::new(&[Percentage(50), Min(20)]).render(example3, buf);
Example::new(&[Percentage(0), Max(0)]).render(example4, buf);
Example::new(&[Percentage(0), Proportional(0)]).render(example5, buf);
Example::new(&[Percentage(0), Fill(0)]).render(example5, buf);
}
fn render_ratio_example(&self, area: Rect, buf: &mut Buffer) {
fn render_ratio_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
Layout::vertical([Length(EXAMPLE_HEIGHT); 5]).areas(area);
Example::new(&[Ratio(1, 2); 2]).render(example1, buf);
Example::new(&[Ratio(1, 4); 4]).render(example2, buf);
@@ -357,16 +331,16 @@ impl SelectedTab {
Example::new(&[Ratio(1, 2), Percentage(25), Length(10)]).render(example4, buf);
}
fn render_proportional_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, _] = area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 3]));
fn render_fill_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, _] = Layout::vertical([Length(EXAMPLE_HEIGHT); 3]).areas(area);
Example::new(&[Proportional(1), Proportional(2), Proportional(3)]).render(example1, buf);
Example::new(&[Proportional(1), Percentage(50), Proportional(1)]).render(example2, buf);
Example::new(&[Fill(1), Fill(2), Fill(3)]).render(example1, buf);
Example::new(&[Fill(1), Percentage(50), Fill(1)]).render(example2, buf);
}
fn render_min_example(&self, area: Rect, buf: &mut Buffer) {
fn render_min_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
Example::new(&[Percentage(100), Min(0)]).render(example1, buf);
Example::new(&[Percentage(100), Min(20)]).render(example2, buf);
@@ -375,9 +349,9 @@ impl SelectedTab {
Example::new(&[Percentage(100), Min(80)]).render(example5, buf);
}
fn render_max_example(&self, area: Rect, buf: &mut Buffer) {
fn render_max_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
Example::new(&[Percentage(0), Max(0)]).render(example1, buf);
Example::new(&[Percentage(0), Max(20)]).render(example2, buf);
@@ -401,27 +375,23 @@ impl Example {
impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) {
let [area, _] = area.split(&Layout::vertical([
Fixed(ILLUSTRATION_HEIGHT),
Fixed(SPACER_HEIGHT),
]));
let [area, _] =
Layout::vertical([Length(ILLUSTRATION_HEIGHT), Length(SPACER_HEIGHT)]).areas(area);
let blocks = Layout::horizontal(&self.constraints).split(area);
for (block, constraint) in blocks.iter().zip(&self.constraints) {
self.illustration(*constraint, block.width)
.render(*block, buf);
Self::illustration(*constraint, block.width).render(*block, buf);
}
}
}
impl Example {
fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph {
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
let color = match constraint {
Constraint::Fixed(_) => FIXED_COLOR,
Constraint::Length(_) => LENGTH_COLOR,
Constraint::Percentage(_) => PERCENTAGE_COLOR,
Constraint::Ratio(_, _) => RATIO_COLOR,
Constraint::Proportional(_) => PROPORTIONAL_COLOR,
Constraint::Fill(_) => FILL_COLOR,
Constraint::Min(_) => MIN_COLOR,
Constraint::Max(_) => MAX_COLOR,
};
@@ -447,7 +417,7 @@ fn init_error_hooks() -> Result<()> {
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
panic(info);
}));
Ok(())
}

View File

@@ -23,7 +23,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use ratatui::{prelude::*, widgets::Paragraph};
/// A custom widget that renders a button with a label, theme and state.
#[derive(Debug, Clone)]
@@ -71,7 +71,7 @@ const GREEN: Theme = Theme {
/// A button with a label that can be themed.
impl<'a> Button<'a> {
pub fn new<T: Into<Line<'a>>>(label: T) -> Button<'a> {
pub fn new<T: Into<Line<'a>>>(label: T) -> Self {
Button {
label: label.into(),
theme: BLUE,
@@ -79,18 +79,19 @@ impl<'a> Button<'a> {
}
}
pub fn theme(mut self, theme: Theme) -> Button<'a> {
pub const fn theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
pub fn state(mut self, state: State) -> Button<'a> {
pub const fn state(mut self, state: State) -> Self {
self.state = state;
self
}
}
impl<'a> Widget for Button<'a> {
#[allow(clippy::cast_possible_truncation)]
fn render(self, area: Rect, buf: &mut Buffer) {
let (background, text, shadow, highlight) = self.colors();
buf.set_style(area, Style::new().bg(background).fg(text));
@@ -124,7 +125,7 @@ impl<'a> Widget for Button<'a> {
}
impl Button<'_> {
fn colors(&self) -> (Color, Color, Color, Color) {
const fn colors(&self) -> (Color, Color, Color, Color) {
let theme = self.theme;
match self.state {
State::Normal => (theme.background, theme.text, theme.shadow, theme.highlight),
@@ -163,7 +164,7 @@ fn main() -> Result<(), Box<dyn Error>> {
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut selected_button: usize = 0;
let button_states = &mut [State::Selected, State::Normal, State::Normal];
let mut button_states = [State::Selected, State::Normal, State::Normal];
loop {
terminal.draw(|frame| ui(frame, button_states))?;
if !event::poll(Duration::from_millis(100))? {
@@ -174,25 +175,27 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
if key.kind != event::KeyEventKind::Press {
continue;
}
if handle_key_event(key, button_states, &mut selected_button).is_break() {
if handle_key_event(key, &mut button_states, &mut selected_button).is_break() {
break;
}
}
Event::Mouse(mouse) => handle_mouse_event(mouse, button_states, &mut selected_button),
Event::Mouse(mouse) => {
handle_mouse_event(mouse, &mut button_states, &mut selected_button);
}
_ => (),
}
}
Ok(())
}
fn ui(frame: &mut Frame, states: &[State; 3]) {
fn ui(frame: &mut Frame, states: [State; 3]) {
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Max(3),
Constraint::Length(1),
Constraint::Min(0), // ignore remaining space
]);
let [title, buttons, help, _] = frame.size().split(&vertical);
let [title, buttons, help, _] = vertical.areas(frame.size());
frame.render_widget(
Paragraph::new("Custom Widget Example (mouse enabled)"),
@@ -202,14 +205,14 @@ fn ui(frame: &mut Frame, states: &[State; 3]) {
frame.render_widget(Paragraph::new("←/→: select, Space: toggle, q: quit"), help);
}
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: &[State; 3]) {
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: [State; 3]) {
let horizontal = Layout::horizontal([
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(15),
Constraint::Min(0), // ignore remaining space
]);
let [red, green, blue, _] = area.split(&horizontal);
let [red, green, blue, _] = horizontal.areas(area);
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);

View File

@@ -2,7 +2,7 @@ use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::widgets::*;
use ratatui::widgets::ListState;
const TASKS: [&str; 24] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
@@ -73,8 +73,8 @@ pub struct RandomSignal {
}
impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> RandomSignal {
RandomSignal {
pub fn new(lower: u64, upper: u64) -> Self {
Self {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
@@ -97,8 +97,8 @@ pub struct SinSignal {
}
impl SinSignal {
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
SinSignal {
pub const fn new(interval: f64, period: f64, scale: f64) -> Self {
Self {
x: 0.0,
interval,
period,
@@ -144,8 +144,8 @@ pub struct StatefulList<T> {
}
impl<T> StatefulList<T> {
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
StatefulList {
pub fn with_items(items: Vec<T>) -> Self {
Self {
state: ListState::default(),
items,
}
@@ -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));
}
@@ -237,7 +235,7 @@ pub struct App<'a> {
}
impl<'a> App<'a> {
pub fn new(title: &'a str, enhanced_graphics: bool) -> App<'a> {
pub fn new(title: &'a str, enhanced_graphics: bool) -> Self {
let mut rand_signal = RandomSignal::new(0, 100);
let sparkline_points = rand_signal.by_ref().take(300).collect();
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);

View File

@@ -4,7 +4,10 @@ use std::{
};
use ratatui::prelude::*;
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
use termwiz::{
input::{InputEvent, KeyCode},
terminal::Terminal as TermwizTerminal,
};
use crate::{app::App, ui};

View File

@@ -1,3 +1,4 @@
#[allow(clippy::wildcard_imports)]
use ratatui::{
prelude::*,
widgets::{canvas::*, *},
@@ -85,6 +86,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
f.render_widget(line_gauge, chunks[2]);
}
#[allow(clippy::too_many_lines)]
fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
let constraints = if app.show_chart {
vec![Constraint::Percentage(50), Constraint::Percentage(50)]

View File

@@ -80,11 +80,12 @@ impl App {
let timeout = Duration::from_secs_f64(1.0 / 50.0);
match term::next_event(timeout)? {
Some(Event::Key(key)) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
_ => Ok(()),
_ => {}
}
Ok(())
}
fn handle_key_press(&mut self, key: KeyEvent) -> Result<()> {
fn handle_key_press(&mut self, key: KeyEvent) {
use KeyCode::*;
match key.code {
Char('q') | Esc => self.mode = Mode::Quit,
@@ -93,10 +94,8 @@ impl App {
Char('k') | Up => self.prev(),
Char('j') | Down => self.next(),
Char('d') | Delete => self.destroy(),
_ => {}
};
Ok(())
}
fn prev(&mut self) {
@@ -124,11 +123,11 @@ impl App {
}
fn next_tab(&mut self) {
self.tab = self.tab.next()
self.tab = self.tab.next();
}
fn destroy(&mut self) {
self.mode = Mode::Destroy
self.mode = Mode::Destroy;
}
}
@@ -142,22 +141,22 @@ impl Widget for &App {
Constraint::Min(0),
Constraint::Length(1),
]);
let [title_bar, tab, bottom_bar] = area.split(&vertical);
let [title_bar, tab, bottom_bar] = vertical.areas(area);
Block::new().style(THEME.root).render(area, buf);
self.render_title_bar(title_bar, buf);
self.render_selected_tab(tab, buf);
self.render_bottom_bar(bottom_bar, buf);
App::render_bottom_bar(bottom_bar, buf);
}
}
impl App {
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
let layout = Layout::horizontal([Constraint::Min(0), Constraint::Length(43)]);
let [title, tabs] = area.split(&layout);
let [title, tabs] = layout.areas(area);
Span::styled("Ratatui", THEME.app_title).render(title, buf);
let titles = Tab::iter().map(|tab| tab.title());
let titles = Tab::iter().map(Tab::title);
Tabs::new(titles)
.style(THEME.tabs)
.highlight_style(THEME.tabs_selected)
@@ -177,7 +176,7 @@ impl App {
};
}
fn render_bottom_bar(&self, area: Rect, buf: &mut Buffer) {
fn render_bottom_bar(area: Rect, buf: &mut Buffer) {
let keys = [
("H/←", "Left"),
("L/→", "Right"),
@@ -189,8 +188,8 @@ impl App {
let spans = keys
.iter()
.flat_map(|(key, desc)| {
let key = Span::styled(format!(" {} ", key), THEME.key_binding.key);
let desc = Span::styled(format!(" {} ", desc), THEME.key_binding.description);
let key = Span::styled(format!(" {key} "), THEME.key_binding.key);
let desc = Span::styled(format!(" {desc} "), THEME.key_binding.description);
[key, desc]
})
.collect_vec();
@@ -202,22 +201,22 @@ impl App {
}
impl Tab {
fn next(&self) -> Self {
let current_index = *self as usize;
fn next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(*self)
Self::from_repr(next_index).unwrap_or(self)
}
fn prev(&self) -> Self {
let current_index = *self as usize;
fn prev(self) -> Self {
let current_index = self as usize;
let prev_index = current_index.saturating_sub(1);
Self::from_repr(prev_index).unwrap_or(*self)
Self::from_repr(prev_index).unwrap_or(self)
}
fn title(&self) -> String {
fn title(self) -> String {
match self {
Tab::About => "".to_string(),
tab => format!(" {} ", tab),
Self::About => String::new(),
tab => format!(" {tab} "),
}
}
}

View File

@@ -50,7 +50,7 @@ use std::cmp::min;
use derive_builder::Builder;
use font8x8::UnicodeFonts;
use ratatui::{prelude::*, text::StyledGrapheme, widgets::Widget};
use ratatui::{prelude::*, text::StyledGrapheme};
#[allow(unused)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
@@ -129,24 +129,24 @@ pub struct BigText<'a> {
/// The size of single glyphs
///
/// Defaults to `BigTextSize::default()` (=> BigTextSize::Full)
/// Defaults to `BigTextSize::default()` (=> `BigTextSize::Full`)
#[builder(default)]
pixel_size: PixelSize,
}
impl Widget for BigText<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = layout(area, &self.pixel_size);
let layout = layout(area, self.pixel_size);
for (line, line_layout) in self.lines.iter().zip(layout) {
for (g, cell) in line.styled_graphemes(self.style).zip(line_layout) {
render_symbol(g, cell, buf, &self.pixel_size);
render_symbol(&g, cell, buf, self.pixel_size);
}
}
}
}
/// Returns how many cells are needed to display a full 8x8 glyphe using the given font size
fn cells_per_glyph(size: &PixelSize) -> (u16, u16) {
const fn cells_per_glyph(size: PixelSize) -> (u16, u16) {
match size {
PixelSize::Full => (8, 8),
PixelSize::HalfHeight => (8, 4),
@@ -160,7 +160,7 @@ fn cells_per_glyph(size: &PixelSize) -> (u16, u16) {
/// The size of each cell depends on given font size
fn layout(
area: Rect,
pixel_size: &PixelSize,
pixel_size: PixelSize,
) -> impl IntoIterator<Item = impl IntoIterator<Item = Rect>> {
let (width, height) = cells_per_glyph(pixel_size);
(area.top()..area.bottom())
@@ -178,7 +178,7 @@ fn layout(
/// Render a single grapheme into a cell by looking up the corresponding 8x8 bitmap in the
/// `BITMAPS` array and setting the corresponding cells in the buffer.
fn render_symbol(grapheme: StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_size: &PixelSize) {
fn render_symbol(grapheme: &StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_size: PixelSize) {
buf.set_style(area, grapheme.style);
let c = grapheme.symbol.chars().next().unwrap(); // TODO: handle multi-char graphemes
if let Some(glyph) = font8x8::BASIC_FONTS.get(c) {
@@ -187,7 +187,7 @@ fn render_symbol(grapheme: StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_s
}
/// Get the correct unicode symbol for two vertical "pixels"
fn get_symbol_half_height(top: u8, bottom: u8) -> char {
const fn get_symbol_half_height(top: u8, bottom: u8) -> char {
match top {
0 => match bottom {
0 => ' ',
@@ -201,7 +201,7 @@ fn get_symbol_half_height(top: u8, bottom: u8) -> char {
}
/// Get the correct unicode symbol for two horizontal "pixels"
fn get_symbol_half_width(left: u8, right: u8) -> char {
const fn get_symbol_half_width(left: u8, right: u8) -> char {
match left {
0 => match right {
0 => ' ',
@@ -215,20 +215,26 @@ fn get_symbol_half_width(left: u8, right: u8) -> char {
}
/// Get the correct unicode symbol for 2x2 "pixels"
fn get_symbol_half_size(top_left: u8, top_right: u8, bottom_left: u8, bottom_right: u8) -> char {
let top_left = if top_left > 0 { 1 } else { 0 };
let top_right = if top_right > 0 { 1 } else { 0 };
let bottom_left = if bottom_left > 0 { 1 } else { 0 };
let bottom_right = if bottom_right > 0 { 1 } else { 0 };
const fn get_symbol_half_size(
top_left: u8,
top_right: u8,
bottom_left: u8,
bottom_right: u8,
) -> char {
const QUADRANT_SYMBOLS: [char; 16] = [
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
];
QUADRANT_SYMBOLS[top_left + (top_right << 1) + (bottom_left << 2) + (bottom_right << 3)]
let top_left = if top_left > 0 { 1 } else { 0 };
let top_right = if top_right > 0 { 1 << 1 } else { 0 };
let bottom_left = if bottom_left > 0 { 1 << 2 } else { 0 };
let bottom_right = if bottom_right > 0 { 1 << 3 } else { 0 };
QUADRANT_SYMBOLS[top_left + top_right + bottom_left + bottom_right]
}
/// Render a single 8x8 glyph into a cell by setting the corresponding cells in the buffer.
fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: &PixelSize) {
fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: PixelSize) {
let (width, height) = cells_per_glyph(pixel_size);
let glyph_vertical_index = (0..glyph.len()).step_by(8 / height as usize);

View File

@@ -9,13 +9,14 @@ use ratatui::prelude::*;
pub struct RgbSwatch;
impl Widget for RgbSwatch {
#[allow(clippy::cast_precision_loss, clippy::similar_names)]
fn render(self, area: Rect, buf: &mut Buffer) {
for (yi, y) in (area.top()..area.bottom()).enumerate() {
let value = area.height as f32 - yi as f32;
let value_fg = value / (area.height as f32);
let value_bg = (value - 0.5) / (area.height as f32);
let value = f32::from(area.height) - yi as f32;
let value_fg = value / f32::from(area.height);
let value_bg = (value - 0.5) / f32::from(area.height);
for (xi, x) in (area.left()..area.right()).enumerate() {
let hue = xi as f32 * 360.0 / area.width as f32;
let hue = xi as f32 * 360.0 / f32::from(area.width);
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value_fg);
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value_bg);
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
@@ -24,7 +25,7 @@ impl Widget for RgbSwatch {
}
}
/// Convert a hue and value into an RGB color via the OkLab color space.
/// Convert a hue and value into an RGB color via the Oklab color space.
///
/// See <https://bottosson.github.io/posts/oklab/> for more details.
pub fn color_from_oklab(hue: f32, saturation: f32, value: f32) -> Color {

View File

@@ -30,11 +30,16 @@ pub fn destroy(frame: &mut Frame<'_>) {
///
/// Each pick some random pixels and move them each down one row. This is a very inefficient way to
/// do this, but it works well enough for this demo.
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
// a seeded rng as we have to move the same random pixels each frame
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
let ramp_frames = 450;
let fractional_speed = frame_count as f64 / ramp_frames as f64;
let fractional_speed = frame_count as f64 / f64::from(ramp_frames);
let variable_speed = DRIP_SPEED as f64 * fractional_speed * fractional_speed * fractional_speed;
let pixel_count = (frame_count as f64 * variable_speed).floor() as usize;
for _ in 0..pixel_count {
@@ -68,6 +73,7 @@ fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
}
/// draw some text fading in and out from black to red and back
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
if sub_frame == 0 {
@@ -114,10 +120,13 @@ fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
return mask_color;
};
let red = mask_red as f64 * percentage + cell_red as f64 * (1.0 - percentage);
let green = mask_green as f64 * percentage + cell_green as f64 * (1.0 - percentage);
let blue = mask_blue as f64 * percentage + cell_blue as f64 * (1.0 - percentage);
let remain = 1.0 - percentage;
let red = f64::from(mask_red).mul_add(percentage, f64::from(cell_red) * remain);
let green = f64::from(mask_green).mul_add(percentage, f64::from(cell_green) * remain);
let blue = f64::from(mask_blue).mul_add(percentage, f64::from(cell_blue) * remain);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Color::Rgb(red as u8, green as u8, blue as u8)
}
@@ -125,7 +134,7 @@ fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
let horizontal = Layout::horizontal([width]).flex(Flex::Center);
let vertical = Layout::vertical([height]).flex(Flex::Center);
let [area] = area.split(&vertical);
let [area] = area.split(&horizontal);
let [area] = vertical.areas(area);
let [area] = horizontal.areas(area);
area
}

View File

@@ -12,7 +12,7 @@ pub fn init_hooks() -> Result<()> {
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = term::restore();
panic(info)
panic(info);
}));
Ok(())
}

View File

@@ -13,6 +13,14 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(
clippy::enum_glob_use,
clippy::missing_errors_doc,
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::wildcard_imports
)]
mod app;
mod big_text;
mod colors;

View File

@@ -57,7 +57,7 @@ impl Widget for AboutTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
let [description, logo] = area.split(&horizontal);
let [description, logo] = horizontal.areas(area);
render_crate_description(description, buf);
render_logo(self.row_index, logo, buf);
}
@@ -97,10 +97,11 @@ fn render_crate_description(area: Rect, buf: &mut Buffer) {
.render(area, buf);
}
/// Use half block characters to render a logo based on the RATATUI_LOGO const.
/// Use half block characters to render a logo based on the `RATATUI_LOGO` const.
///
/// The logo is rendered in three colors, one for the rat, one for the terminal, and one for the
/// rat's eye. The eye color alternates between two colors based on the selected row.
#[allow(clippy::cast_possible_truncation)]
pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
let eye_color = if selected_row % 2 == 0 {
THEME.logo.rat_eye

View File

@@ -65,14 +65,14 @@ impl Widget for EmailTab {
});
Clear.render(area, buf);
let vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
let [inbox, email] = area.split(&vertical);
let [inbox, email] = vertical.areas(area);
render_inbox(self.row_index, inbox, buf);
render_email(self.row_index, email, buf);
}
}
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [tabs, inbox] = area.split(&vertical);
let [tabs, inbox] = vertical.areas(area);
let theme = THEME.email;
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
.style(theme.tabs)
@@ -127,7 +127,7 @@ fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
block.render(area, buf);
if let Some(email) = email {
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
let [headers_area, body_area] = inner.split(&vertical);
let [headers_area, body_area] = vertical.areas(inner);
let headers = vec![
Line::from(vec![
"From: ".set_style(theme.header),

View File

@@ -10,6 +10,7 @@ struct Ingredient {
}
impl Ingredient {
#[allow(clippy::cast_possible_truncation)]
fn height(&self) -> u16 {
self.name.lines().count() as u16
}
@@ -127,10 +128,8 @@ impl Widget for RecipeTab {
horizontal: 2,
vertical: 1,
});
let [recipe, ingredients] = area.split(&Layout::horizontal([
Constraint::Length(44),
Constraint::Min(0),
]));
let [recipe, ingredients] =
Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]).areas(area);
render_recipe(recipe, buf);
render_ingredients(self.row_index, ingredients, buf);
@@ -150,7 +149,7 @@ fn render_recipe(area: Rect, buf: &mut Buffer) {
fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row));
let rows = INGREDIENTS.iter().cloned();
let rows = INGREDIENTS.iter().copied();
let theme = THEME.recipe;
StatefulWidget::render(
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
@@ -173,5 +172,5 @@ fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) {
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(area, buf, &mut state)
.render(area, buf, &mut state);
}

View File

@@ -34,8 +34,8 @@ impl Widget for TracerouteTab {
Block::new().style(THEME.content).render(area, buf);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]);
let [left, map] = area.split(&horizontal);
let [hops, pings] = left.split(&vertical);
let [left, map] = horizontal.areas(area);
let [hops, pings] = vertical.areas(left);
render_hops(self.row_index, hops, buf);
render_ping(self.row_index, pings, buf);

View File

@@ -39,19 +39,16 @@ impl Widget for WeatherTab {
horizontal: 2,
vertical: 1,
});
let [main, _, gauges] = area.split(&Layout::vertical([
let [main, _, gauges] = Layout::vertical([
Constraint::Min(0),
Constraint::Length(1),
Constraint::Length(1),
]));
let [calendar, charts] = main.split(&Layout::horizontal([
Constraint::Length(23),
Constraint::Min(0),
]));
let [simple, horizontal] = charts.split(&Layout::vertical([
Constraint::Length(29),
Constraint::Min(0),
]));
])
.areas(area);
let [calendar, charts] =
Layout::horizontal([Constraint::Length(23), Constraint::Min(0)]).areas(main);
let [simple, horizontal] =
Layout::vertical([Constraint::Length(29), Constraint::Min(0)]).areas(charts);
render_calendar(calendar, buf);
render_simple_barchart(simple, buf);
@@ -87,7 +84,7 @@ fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
// This doesn't actually render correctly as the text is too wide for the bar
// See https://github.com/ratatui-org/ratatui/issues/513 for more info
// (the demo GIFs hack around this by hacking the calculation in bars.rs)
.text_value(format!("{}°", value))
.text_value(format!("{value}°"))
.style(if value > 70 {
Style::new().fg(Color::Red)
} else {
@@ -131,12 +128,14 @@ fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
.render(area, buf);
}
#[allow(clippy::cast_precision_loss)]
pub fn render_gauge(progress: usize, area: Rect, buf: &mut Buffer) {
let percent = (progress * 3).min(100) as f64;
render_line_gauge(percent, area, buf);
}
#[allow(clippy::cast_possible_truncation)]
fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
// cycle color hue based on the percent for a neat effect yellow -> red
let hue = 90.0 - (percent as f32 * 0.6);
@@ -144,7 +143,7 @@ fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value);
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
let label = if percent < 100.0 {
format!("Downloading: {}%", percent)
format!("Downloading: {percent}%")
} else {
"Download Complete!".into()
};

View File

@@ -19,9 +19,12 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph},
};
/// 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.
@@ -34,7 +37,6 @@ fn main() -> io::Result<()> {
let mut should_quit = false;
while !should_quit {
terminal.draw(match arg.as_str() {
"hello_world" => hello_world,
"layout" => layout,
"styling" => styling,
_ => hello_world,
@@ -74,8 +76,8 @@ fn layout(frame: &mut Frame) {
Constraint::Length(1),
]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2); 2]);
let [title_bar, main_area, status_bar] = frame.size().split(&vertical);
let [left, right] = main_area.split(&horizontal);
let [title_bar, main_area, status_bar] = vertical.areas(frame.size());
let [left, right] = horizontal.areas(main_area);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),

View File

@@ -13,6 +13,8 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
use std::io::{self, stdout};
use color_eyre::{config::HookBuilder, Result};
@@ -25,48 +27,82 @@ use ratatui::{
layout::{Constraint::*, Flex},
prelude::*,
style::palette::tailwind,
symbols::line,
widgets::{block::Title, *},
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
(
"Min(u16) takes any excess space when using `Stretch` or `StretchLast`",
&[Fixed(20), Min(20), Max(20)],
"Min(u16) takes any excess space always",
&[Length(10), Min(10), Max(10), Percentage(10), Ratio(1,10)],
),
(
"Proportional(u16) takes any excess space in all `Flex` layouts",
&[Length(20), Percentage(20), Ratio(1, 5), Proportional(1)],
"Fill(u16) takes any excess space always",
&[Length(20), Percentage(20), Ratio(1, 5), Fill(1)],
),
(
"In `StretchLast`, last constraint of lowest priority takes excess space",
&[Length(20), Fixed(20), Percentage(20)],
"Here's all constraints in one line",
&[Length(10), Min(10), Max(10), Percentage(10), Ratio(1,10), Fill(1)],
),
("", &[Fixed(20), Percentage(20), Length(20)]),
("", &[Percentage(20), Length(20), Fixed(20)]),
("", &[Length(20), Length(15)]),
("Spacing has no effect in `SpaceAround` and `SpaceBetween`", &[Proportional(1), Proportional(1)]),
("", &[Length(20), Fixed(20)]),
(
"When not using `Flex::Stretch` or `Flex::StretchLast`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values",
"",
&[Max(50), Min(50)],
),
(
"",
&[Max(20), Length(10)],
),
(
"",
&[Max(20), Length(10)],
),
(
"Min grows always but also allows Fill to grow",
&[Percentage(50), Fill(1), Fill(2), Min(50)],
),
(
"In `Legacy`, the last constraint of lowest priority takes excess space",
&[Length(20), Length(20), Percentage(20)],
),
("", &[Length(20), Percentage(20), Length(20)]),
("A lowest priority constraint will be broken before a high priority constraint", &[Ratio(1,4), Percentage(20)]),
("`Length` is higher priority than `Percentage`", &[Percentage(20), Length(10)]),
("`Min/Max` is higher priority than `Length`", &[Length(10), Max(20)]),
("", &[Length(100), Min(20)]),
("`Length` is higher priority than `Min/Max`", &[Max(20), Length(10)]),
("", &[Min(20), Length(90)]),
("Fill is the lowest priority and will fill any excess space", &[Fill(1), Ratio(1, 4)]),
("Fill can be used to scale proportionally with other Fill blocks", &[Fill(1), Percentage(20), Fill(2)]),
("", &[Ratio(1, 3), Percentage(20), Ratio(2, 3)]),
("Legacy will stretch the last lowest priority constraint\nStretch will only stretch equal weighted constraints", &[Length(20), Length(15)]),
("", &[Percentage(20), Length(15)]),
("`Fill(u16)` fills up excess space, but is lower priority to spacers.\ni.e. Fill will only have widths in Flex::Stretch and Flex::Legacy", &[Fill(1), Fill(1)]),
("", &[Length(20), Length(20)]),
(
"When not using `Flex::Stretch` or `Flex::Legacy`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values",
&[Min(20), Max(20)],
),
(
"`SpaceBetween` stretches when there's only one constraint",
"",
&[Max(20)],
),
("", &[Min(20), Max(20), Length(20), Fixed(20)]),
("`Proportional(u16)` always fills up space in every `Flex` layout", &[Proportional(0), Proportional(0)]),
("", &[Min(20), Max(20), Length(20), Length(20)]),
("", &[Fill(0), Fill(0)]),
(
"`Proportional(1)` can be to scale with respect to other `Proportional(2)`",
&[Proportional(1), Proportional(2)],
"`Fill(1)` can be to scale with respect to other `Fill(2)`",
&[Fill(1), Fill(2)],
),
(
"`Proportional(0)` collapses if there are other non-zero `Proportional(_)`\nconstraints. e.g. `[Proportional(0), Proportional(0), Proportional(1)]`:",
"",
&[Fill(1), Min(10), Max(10), Fill(2)],
),
(
"`Fill(0)` collapses if there are other non-zero `Fill(_)`\nconstraints. e.g. `[Fill(0), Fill(0), Fill(1)]`:",
&[
Proportional(0),
Proportional(0),
Proportional(1),
Fill(0),
Fill(0),
Fill(1),
],
),
];
@@ -102,8 +138,7 @@ struct Example {
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, FromRepr, Display, EnumIter)]
enum SelectedTab {
#[default]
StretchLast,
Stretch,
Legacy,
Start,
Center,
End,
@@ -116,12 +151,6 @@ fn main() -> Result<()> {
Layout::init_cache(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100);
init_error_hooks()?;
let terminal = init_terminal()?;
// Each line in the example is a layout
// so 13 examples * 7 = 91 currently
// Plus additional layout for tabs ...
Layout::init_cache(120);
App::default().run(terminal)?;
restore_terminal()?;
@@ -138,7 +167,7 @@ impl App {
Ok(())
}
fn is_running(&self) -> bool {
fn is_running(self) -> bool {
self.state == AppState::Running
}
@@ -176,14 +205,14 @@ impl App {
}
fn up(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1)
self.scroll_offset = self.scroll_offset.saturating_sub(1);
}
fn down(&mut self) {
self.scroll_offset = self
.scroll_offset
.saturating_add(1)
.min(max_scroll_offset())
.min(max_scroll_offset());
}
fn top(&mut self) {
@@ -212,8 +241,7 @@ fn max_scroll_offset() -> u16 {
example_height()
- EXAMPLE_DATA
.last()
.map(|(desc, _)| get_description_height(desc) + 4)
.unwrap_or(0)
.map_or(0, |(desc, _)| get_description_height(desc) + 4)
}
/// The height of all examples combined
@@ -228,21 +256,21 @@ fn example_height() -> u16 {
impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = Layout::vertical([Fixed(3), Fixed(1), Proportional(0)]);
let [tabs, axis, demo] = area.split(&layout);
let layout = Layout::vertical([Length(3), Length(1), Fill(0)]);
let [tabs, axis, demo] = layout.areas(area);
self.tabs().render(tabs, buf);
let scroll_needed = self.render_demo(demo, buf);
let axis_width = if scroll_needed {
axis.width - 1
axis.width.saturating_sub(1)
} else {
axis.width
};
self.axis(axis_width, self.spacing).render(axis, buf);
Self::axis(axis_width, self.spacing).render(axis, buf);
}
}
impl App {
fn tabs(&self) -> impl Widget {
fn tabs(self) -> impl Widget {
let tab_titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
let block = Block::new()
.title(Title::from("Flex Layouts ".bold()))
@@ -256,15 +284,15 @@ impl App {
}
/// a bar like `<----- 80 px (gap: 2 px)? ----->`
fn axis(&self, width: u16, spacing: u16) -> impl Widget {
fn axis(width: u16, spacing: u16) -> impl Widget {
let width = width as usize;
// only show gap when spacing is not zero
let label = if spacing != 0 {
format!("{} px (gap: {} px)", width, spacing)
format!("{width} px (gap: {spacing} px)")
} else {
format!("{} px", width)
format!("{width} px")
};
let bar_width = width - 2; // we want to `<` and `>` at the ends
let bar_width = width.saturating_sub(2); // we want to `<` and `>` at the ends
let width_bar = format!("<{label:-^bar_width$}>");
Paragraph::new(width_bar.dark_gray()).centered()
}
@@ -275,6 +303,7 @@ impl App {
/// into the main buffer. This is done to make it possible to handle scrolling easily.
///
/// Returns bool indicating whether scroll was needed
#[allow(clippy::cast_possible_truncation)]
fn render_demo(self, area: Rect, buf: &mut Buffer) -> bool {
// render demo content into a separate buffer so all examples fit we add an extra
// area.height to make sure the last example is fully visible even when the scroll offset is
@@ -320,32 +349,30 @@ impl App {
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab.
fn previous(&self) -> Self {
let current_index: usize = *self as usize;
fn previous(self) -> Self {
let current_index: usize = self as usize;
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(*self)
Self::from_repr(previous_index).unwrap_or(self)
}
/// Get the next tab, if there is no next tab return the current tab.
fn next(&self) -> Self {
let current_index = *self as usize;
fn next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(*self)
Self::from_repr(next_index).unwrap_or(self)
}
/// Convert a `SelectedTab` into a `Line` to display it by the `Tabs` widget.
fn to_tab_title(value: SelectedTab) -> Line<'static> {
fn to_tab_title(value: Self) -> Line<'static> {
use tailwind::*;
use SelectedTab::*;
let text = value.to_string();
let color = match value {
StretchLast => ORANGE.c400,
Stretch => ORANGE.c300,
Start => SKY.c400,
Center => SKY.c300,
End => SKY.c200,
SpaceAround => INDIGO.c400,
SpaceBetween => INDIGO.c300,
Self::Legacy => ORANGE.c400,
Self::Start => SKY.c400,
Self::Center => SKY.c300,
Self::End => SKY.c200,
Self::SpaceAround => INDIGO.c400,
Self::SpaceBetween => INDIGO.c300,
};
format!(" {text} ").fg(color).bg(Color::Black).into()
}
@@ -356,21 +383,18 @@ impl StatefulWidget for SelectedTab {
fn render(self, area: Rect, buf: &mut Buffer, spacing: &mut Self::State) {
let spacing = *spacing;
match self {
SelectedTab::StretchLast => self.render_examples(area, buf, Flex::StretchLast, spacing),
SelectedTab::Stretch => self.render_examples(area, buf, Flex::Stretch, spacing),
SelectedTab::Start => self.render_examples(area, buf, Flex::Start, spacing),
SelectedTab::Center => self.render_examples(area, buf, Flex::Center, spacing),
SelectedTab::End => self.render_examples(area, buf, Flex::End, spacing),
SelectedTab::SpaceAround => self.render_examples(area, buf, Flex::SpaceAround, spacing),
SelectedTab::SpaceBetween => {
self.render_examples(area, buf, Flex::SpaceBetween, spacing)
}
Self::Legacy => Self::render_examples(area, buf, Flex::Legacy, spacing),
Self::Start => Self::render_examples(area, buf, Flex::Start, spacing),
Self::Center => Self::render_examples(area, buf, Flex::Center, spacing),
Self::End => Self::render_examples(area, buf, Flex::End, spacing),
Self::SpaceAround => Self::render_examples(area, buf, Flex::SpaceAround, spacing),
Self::SpaceBetween => Self::render_examples(area, buf, Flex::SpaceBetween, spacing),
}
}
}
impl SelectedTab {
fn render_examples(&self, area: Rect, buf: &mut Buffer, flex: Flex, spacing: u16) {
fn render_examples(area: Rect, buf: &mut Buffer, flex: Flex, spacing: u16) {
let heights = EXAMPLE_DATA
.iter()
.map(|(desc, _)| get_description_height(desc) + 4);
@@ -395,18 +419,19 @@ impl Example {
impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) {
let title_height = get_description_height(&self.description);
let layout = Layout::vertical([Fixed(title_height), Proportional(0)]);
let [title, illustrations] = area.split(&layout);
let blocks = Layout::horizontal(&self.constraints)
let layout = Layout::vertical([Length(title_height), Fill(0)]);
let [title, illustrations] = layout.areas(area);
let (blocks, spacers) = Layout::horizontal(&self.constraints)
.flex(self.flex)
.spacing(self.spacing)
.split(illustrations);
.split_with_spacers(illustrations);
if !self.description.is_empty() {
Paragraph::new(
self.description
.split('\n')
.map(|s| format!("// {}", s).italic().fg(tailwind::SLATE.c400))
.map(|s| format!("// {s}").italic().fg(tailwind::SLATE.c400))
.map(Line::from)
.collect::<Vec<Line>>(),
)
@@ -414,14 +439,62 @@ impl Widget for Example {
}
for (block, constraint) in blocks.iter().zip(&self.constraints) {
self.illustration(*constraint, block.width)
.render(*block, buf);
Self::illustration(*constraint, block.width).render(*block, buf);
}
for spacer in spacers.iter() {
Self::render_spacer(*spacer, buf);
}
}
}
impl Example {
fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph {
fn render_spacer(spacer: Rect, buf: &mut Buffer) {
if spacer.width > 1 {
let corners_only = symbols::border::Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: " ",
vertical_right: " ",
horizontal_top: " ",
horizontal_bottom: " ",
};
Block::bordered()
.border_set(corners_only)
.border_style(Style::reset().dark_gray())
.render(spacer, buf);
} else {
Paragraph::new(Text::from(vec![
Line::from(""),
Line::from(""),
Line::from(""),
Line::from(""),
]))
.style(Style::reset().dark_gray())
.render(spacer, buf);
}
let width = spacer.width;
let label = if width > 4 {
format!("{width} px")
} else if width > 2 {
format!("{width}")
} else {
String::new()
};
let text = Text::from(vec![
Line::raw(""),
Line::raw(""),
Line::styled(label, Style::reset().dark_gray()),
]);
Paragraph::new(text)
.style(Style::reset().dark_gray())
.alignment(Alignment::Center)
.render(spacer, buf);
}
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
let main_color = color_for_constraint(constraint);
let fg_color = Color::White;
let title = format!("{constraint}");
@@ -435,16 +508,15 @@ impl Example {
}
}
fn color_for_constraint(constraint: Constraint) -> Color {
const fn color_for_constraint(constraint: Constraint) -> Color {
use tailwind::*;
match constraint {
Constraint::Fixed(_) => RED.c900,
Constraint::Min(_) => BLUE.c900,
Constraint::Max(_) => BLUE.c800,
Constraint::Length(_) => SLATE.c700,
Constraint::Percentage(_) => SLATE.c800,
Constraint::Ratio(_, _) => SLATE.c900,
Constraint::Proportional(_) => SLATE.c950,
Constraint::Fill(_) => SLATE.c950,
}
}
@@ -458,7 +530,7 @@ fn init_error_hooks() -> Result<()> {
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
panic(info);
}));
Ok(())
}
@@ -477,6 +549,7 @@ fn restore_terminal() -> Result<()> {
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
fn get_description_height(s: &str) -> u16 {
if s.is_empty() {
0

View File

@@ -13,12 +13,11 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
io::{self, stdout, Stdout},
time::Duration,
};
#![allow(clippy::enum_glob_use)]
use anyhow::Result;
use std::{io::stdout, time::Duration};
use color_eyre::{config::HookBuilder, Result};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
@@ -26,84 +25,85 @@ use crossterm::{
};
use ratatui::{
prelude::*,
widgets::{block::Title, *},
style::palette::tailwind,
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph},
};
fn main() -> Result<()> {
App::run()
}
struct App {
term: Term,
should_quit: bool,
state: AppState,
}
const GAUGE1_COLOR: Color = tailwind::RED.c800;
const GAUGE2_COLOR: Color = tailwind::GREEN.c800;
const GAUGE3_COLOR: Color = tailwind::BLUE.c800;
const GAUGE4_COLOR: Color = tailwind::ORANGE.c800;
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
#[derive(Debug, Default, Clone, Copy)]
struct AppState {
struct App {
state: AppState,
progress_columns: u16,
progress1: u16,
progress2: u16,
progress2: f64,
progress3: f64,
progress4: u16,
progress4: f64,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Started,
Quitting,
}
fn main() -> Result<()> {
init_error_hooks()?;
let terminal = init_terminal()?;
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
}
impl App {
fn run() -> Result<()> {
// run at ~10 fps minus the time it takes to draw
let timeout = Duration::from_secs_f32(1.0 / 10.0);
let mut app = Self::start()?;
while !app.should_quit {
app.update();
app.draw()?;
app.handle_events(timeout)?;
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
while self.state != AppState::Quitting {
self.draw(&mut terminal)?;
self.handle_events()?;
self.update(terminal.size()?.width);
}
app.stop()?;
Ok(())
}
fn start() -> Result<Self> {
Ok(App {
term: Term::start()?,
should_quit: false,
state: AppState {
progress1: 0,
progress2: 0,
progress3: 0.0,
progress4: 0,
},
})
}
fn stop(&mut self) -> Result<()> {
Term::stop()?;
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
terminal.draw(|f| f.render_widget(self, f.size()))?;
Ok(())
}
fn update(&mut self) {
self.state.progress1 = (self.state.progress1 + 4).min(100);
self.state.progress2 = (self.state.progress2 + 3).min(100);
self.state.progress3 = (self.state.progress3 + 0.02).min(1.0);
self.state.progress4 = (self.state.progress4 + 1).min(100);
fn update(&mut self, terminal_width: u16) {
if self.state != AppState::Started {
return;
}
// progress1 and progress2 help show the difference between ratio and percentage measuring
// the same thing, but converting to either a u16 or f64. Effectively, we're showing the
// difference between how a continuous gauge acts for floor and rounded values.
self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width);
self.progress1 = self.progress_columns * 100 / terminal_width;
self.progress2 = f64::from(self.progress_columns) * 100.0 / f64::from(terminal_width);
// progress3 and progress4 similarly show the difference between unicode and non-unicode
// gauges measuring the same thing.
self.progress3 = (self.progress3 + 0.1).clamp(40.0, 100.0);
self.progress4 = (self.progress4 + 0.1).clamp(40.0, 100.0);
}
fn draw(&mut self) -> Result<()> {
self.term.draw(|frame| {
let state = self.state;
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(frame.size());
Self::render_gauge1(state.progress1, frame, layout[0]);
Self::render_gauge2(state.progress2, frame, layout[1]);
Self::render_gauge3(state.progress3, frame, layout[2]);
Self::render_gauge4(state.progress4, frame, layout[3]);
})?;
Ok(())
}
fn handle_events(&mut self, timeout: Duration) -> io::Result<()> {
fn handle_events(&mut self) -> Result<()> {
let timeout = Duration::from_secs_f32(1.0 / 20.0);
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
if let KeyCode::Char('q') = key.code {
self.should_quit = true;
use KeyCode::*;
match key.code {
Char(' ') | Enter => self.start(),
Char('q') | Esc => self.quit(),
_ => {}
}
}
}
@@ -111,79 +111,133 @@ impl App {
Ok(())
}
fn render_gauge1(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress");
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::new().light_red())
.percent(progress);
frame.render_widget(gauge, area);
fn start(&mut self) {
self.state = AppState::Started;
}
fn render_gauge2(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress and custom label");
let label = format!("{}/100", progress);
let gauge = Gauge::default()
fn quit(&mut self) {
self.state = AppState::Quitting;
}
}
impl Widget for &App {
#[allow(clippy::similar_names)]
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::*;
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
let [header_area, gauge_area, footer_area] = layout.areas(area);
let layout = Layout::vertical([Ratio(1, 4); 4]);
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = layout.areas(gauge_area);
render_header(header_area, buf);
render_footer(footer_area, buf);
self.render_gauge1(gauge1_area, buf);
self.render_gauge2(gauge2_area, buf);
self.render_gauge3(gauge3_area, buf);
self.render_gauge4(gauge4_area, buf);
}
}
fn render_header(area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui Gauge Example")
.bold()
.alignment(Alignment::Center)
.fg(CUSTOM_LABEL_COLOR)
.render(area, buf);
}
fn render_footer(area: Rect, buf: &mut Buffer) {
Paragraph::new("Press ENTER to start")
.alignment(Alignment::Center)
.fg(CUSTOM_LABEL_COLOR)
.bold()
.render(area, buf);
}
impl App {
fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Gauge with percentage");
Gauge::default()
.block(title)
.gauge_style(Style::new().blue().on_light_blue())
.percent(progress)
.label(label);
frame.render_widget(gauge, area);
.gauge_style(GAUGE1_COLOR)
.percent(self.progress1)
.render(area, buf);
}
fn render_gauge3(progress: f64, frame: &mut Frame, area: Rect) {
let title =
Self::title_block("Gauge with ratio progress, custom label with style, and unicode");
fn render_gauge2(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Gauge with ratio and custom label");
let label = Span::styled(
format!("{:.2}%", progress * 100.0),
Style::new().red().italic().bold(),
format!("{:.1}/100", self.progress2),
Style::new().italic().bold().fg(CUSTOM_LABEL_COLOR),
);
let gauge = Gauge::default()
Gauge::default()
.block(title)
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(progress)
.gauge_style(GAUGE2_COLOR)
.ratio(self.progress2 / 100.0)
.label(label)
.use_unicode(true);
frame.render_widget(gauge, area);
.render(area, buf);
}
fn render_gauge4(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress and label");
let label = format!("{}/100", progress);
let gauge = Gauge::default()
fn render_gauge3(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Gauge with ratio (no unicode)");
let label = format!("{:.1}%", self.progress3);
Gauge::default()
.block(title)
.gauge_style(Style::new().green().italic())
.percent(progress)
.label(label);
frame.render_widget(gauge, area);
.gauge_style(GAUGE3_COLOR)
.ratio(self.progress3 / 100.0)
.label(label)
.render(area, buf);
}
fn title_block(title: &str) -> Block {
let title = Title::from(title).alignment(Alignment::Center);
Block::default().title(title).borders(Borders::TOP)
fn render_gauge4(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Gauge with ratio (unicode)");
let label = format!("{:.1}%", self.progress3);
Gauge::default()
.block(title)
.gauge_style(GAUGE4_COLOR)
.ratio(self.progress4 / 100.0)
.label(label)
.use_unicode(true)
.render(area, buf);
}
}
struct Term {
terminal: Terminal<CrosstermBackend<Stdout>>,
fn title_block(title: &str) -> Block {
let title = Title::from(title).alignment(Alignment::Center);
Block::default()
.title(title)
.borders(Borders::NONE)
.fg(CUSTOM_LABEL_COLOR)
.padding(Padding::vertical(1))
}
impl Term {
pub fn start() -> io::Result<Term> {
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
let terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
Ok(Self { terminal })
}
pub fn stop() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn draw(&mut self, frame: impl FnOnce(&mut Frame)) -> Result<()> {
self.terminal.draw(frame)?;
Ok(())
}
fn init_error_hooks() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> color_eyre::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -24,7 +24,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use ratatui::{prelude::*, widgets::Paragraph};
/// This is a bare minimum example. There are many approaches to running an application loop, so
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and

View File

@@ -13,6 +13,8 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(clippy::wildcard_imports)]
use std::{
collections::{BTreeMap, VecDeque},
error::Error,
@@ -129,6 +131,7 @@ fn input_handling(tx: mpsc::Sender<Event>) {
});
}
#[allow(clippy::cast_precision_loss, clippy::needless_pass_by_value)]
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
(0..4)
.map(|id| {
@@ -168,6 +171,7 @@ fn downloads() -> Downloads {
}
}
#[allow(clippy::needless_pass_by_value)]
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
workers: Vec<Worker>,
@@ -194,7 +198,7 @@ fn run_app<B: Backend>(
Event::DownloadUpdate(worker_id, _download_id, progress) => {
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
download.progress = progress;
redraw = false
redraw = false;
}
Event::DownloadDone(worker_id, download_id) => {
let download = downloads.in_progress.remove(&worker_id).unwrap();
@@ -237,11 +241,12 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [progress_area, main] = area.split(&vertical);
let [list_area, gauge_area] = main.split(&horizontal);
let [progress_area, main] = vertical.areas(area);
let [list_area, gauge_area] = horizontal.areas(main);
// total progress
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
#[allow(clippy::cast_precision_loss)]
let progress = LineGauge::default()
.gauge_style(Style::default().fg(Color::Blue))
.label(format!("{done}/{NUM_DOWNLOADS}"))
@@ -271,6 +276,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
let list = List::new(items);
f.render_widget(list, list_area);
#[allow(clippy::cast_possible_truncation)]
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
let gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Yellow))

View File

@@ -13,6 +13,8 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(clippy::enum_glob_use)]
use std::{error::Error, io};
use crossterm::{
@@ -21,7 +23,11 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{layout::Constraint::*, prelude::*, widgets::*};
use ratatui::{
layout::Constraint::*,
prelude::*,
widgets::{Block, Borders, Paragraph},
};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
@@ -55,20 +61,21 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
}
#[allow(clippy::too_many_lines)]
fn ui(frame: &mut Frame) {
let vertical = Layout::vertical([
Length(4), // text
Length(50), // examples
Min(0), // fills remaining space
]);
let [text_area, examples_area, _] = frame.size().split(&vertical);
let [text_area, examples_area, _] = vertical.areas(frame.size());
// title
frame.render_widget(
@@ -94,12 +101,12 @@ fn ui(frame: &mut Frame) {
.iter()
.flat_map(|area| {
Layout::horizontal([
Constraint::Length(14),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Min(0), // fills remaining space
Length(14),
Length(14),
Length(14),
Length(14),
Length(14),
Min(0), // fills remaining space
])
.split(*area)
.iter()
@@ -191,8 +198,8 @@ fn render_example_combination(
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::vertical(vec![Length(1); constraints.len() + 1]).split(inner);
for (i, (a, b)) in constraints.iter().enumerate() {
render_single_example(frame, layout[i], vec![*a, *b, Min(0)]);
for (i, (a, b)) in constraints.into_iter().enumerate() {
render_single_example(frame, layout[i], vec![a, b, Min(0)]);
}
// This is to make it easy to visually see the alignment of the examples
// with the constraints.
@@ -205,7 +212,7 @@ fn render_single_example(frame: &mut Frame, area: Rect, constraints: Vec<Constra
let blue = Paragraph::new(constraint_label(constraints[1])).on_blue();
let green = Paragraph::new("·".repeat(12)).on_green();
let horizontal = Layout::horizontal(constraints);
let [r, b, g] = area.split(&horizontal);
let [r, b, g] = horizontal.areas(area);
frame.render_widget(red, r);
frame.render_widget(blue, b);
frame.render_widget(green, g);
@@ -213,12 +220,11 @@ fn render_single_example(frame: &mut Frame, area: Rect, constraints: Vec<Constra
fn constraint_label(constraint: Constraint) -> String {
match constraint {
Length(n) => format!("{n}"),
Min(n) => format!("{n}"),
Max(n) => format!("{n}"),
Percentage(n) => format!("{n}"),
Proportional(n) => format!("{n}"),
Fixed(n) => format!("{n}"),
Ratio(a, b) => format!("{a}:{b}"),
Constraint::Ratio(a, b) => format!("{a}:{b}"),
Constraint::Length(n)
| Constraint::Min(n)
| Constraint::Max(n)
| Constraint::Percentage(n)
| Constraint::Fill(n) => format!("{n}"),
}
}

View File

@@ -13,6 +13,8 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
use std::{error::Error, io, io::stdout};
use color_eyre::config::HookBuilder;
@@ -81,7 +83,7 @@ fn init_error_hooks() -> color_eyre::Result<()> {
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
panic(info);
}));
Ok(())
}
@@ -100,9 +102,9 @@ fn restore_terminal() -> color_eyre::Result<()> {
Ok(())
}
impl App<'_> {
fn new<'a>() -> App<'a> {
App {
impl<'a> App<'a> {
fn new() -> Self {
Self {
items: StatefulList::with_items([
("Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust", Status::Todo),
("Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui.", Status::Completed),
@@ -125,11 +127,11 @@ impl App<'_> {
}
fn go_top(&mut self) {
self.items.state.select(Some(0))
self.items.state.select(Some(0));
}
fn go_bottom(&mut self) {
self.items.state.select(Some(self.items.items.len() - 1))
self.items.state.select(Some(self.items.items.len() - 1));
}
}
@@ -170,28 +172,21 @@ impl Widget for &mut App<'_> {
Constraint::Min(0),
Constraint::Length(2),
]);
let [header_area, rest_area, footer_area] = area.split(&vertical);
let [header_area, rest_area, footer_area] = vertical.areas(area);
// Create two chunks with equal vertical screen space. One for the list and the other for
// the info block.
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [upper_item_list_area, lower_item_list_area] = rest_area.split(&vertical);
let [upper_item_list_area, lower_item_list_area] = vertical.areas(rest_area);
self.render_title(header_area, buf);
render_title(header_area, buf);
self.render_todo(upper_item_list_area, buf);
self.render_info(lower_item_list_area, buf);
self.render_footer(footer_area, buf);
render_footer(footer_area, buf);
}
}
impl App<'_> {
fn render_title(&self, area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui List Example")
.bold()
.centered()
.render(area, buf);
}
fn render_todo(&mut self, area: Rect, buf: &mut Buffer) {
// We create two blocks, one is for the header (outer) and the other is for list (inner).
let outer_block = Block::default()
@@ -278,14 +273,19 @@ impl App<'_> {
// We can now render the item info
info_paragraph.render(inner_info_area, buf);
}
}
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
Paragraph::new(
"\nUse ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.",
)
fn render_title(area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui List Example")
.bold()
.centered()
.render(area, buf);
}
fn render_footer(area: Rect, buf: &mut Buffer) {
Paragraph::new("\nUse ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
.centered()
.render(area, buf);
}
}
impl StatefulList<'_> {

View File

@@ -13,9 +13,10 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
/// This example is useful for testing how your terminal emulator handles different modifiers.
/// It will render a grid of combinations of foreground and background colors with all
/// modifiers applied to them.
// This example is useful for testing how your terminal emulator handles different modifiers.
// It will render a grid of combinations of foreground and background colors with all
// modifiers applied to them.
use std::{
error::Error,
io::{self, Stdout},
@@ -30,7 +31,7 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use ratatui::{prelude::*, widgets::Paragraph};
type Result<T> = result::Result<T, Box<dyn Error>>;
@@ -50,7 +51,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
@@ -60,7 +61,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
fn ui(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [text_area, main_area] = frame.size().split(&vertical);
let [text_area, main_area] = vertical.areas(frame.size());
frame.render_widget(
Paragraph::new("Note: not all terminals support all modifiers")
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
@@ -87,8 +88,8 @@ fn ui(frame: &mut Frame) {
.chain(Modifier::all().iter())
.collect_vec();
let mut index = 0;
for bg in colors.iter() {
for fg in colors.iter() {
for bg in &colors {
for fg in &colors {
for modifier in &all_modifiers {
let modifier_name = format!("{modifier:11?}");
let padding = (" ").repeat(12 - modifier_name.len());

View File

@@ -35,7 +35,10 @@ use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph},
};
type Result<T> = std::result::Result<T, Box<dyn Error>>;

View File

@@ -24,15 +24,18 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph, Wrap},
};
struct App {
scroll: u16,
}
impl App {
fn new() -> App {
App { scroll: 0 }
const fn new() -> Self {
Self { scroll: 0 }
}
fn on_tick(&mut self) {
@@ -82,7 +85,7 @@ fn run_app<B: Backend>(
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}

View File

@@ -13,8 +13,9 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
/// See also https://github.com/joshka/tui-popup and
/// https://github.com/sephiroth74/tui-confirm-dialog
// See also https://github.com/joshka/tui-popup and
// https://github.com/sephiroth74/tui-confirm-dialog
use std::{error::Error, io};
use crossterm::{
@@ -22,15 +23,18 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph, Wrap},
};
struct App {
show_popup: bool,
}
impl App {
fn new() -> App {
App { show_popup: false }
const fn new() -> Self {
Self { show_popup: false }
}
}
@@ -82,7 +86,7 @@ fn ui(f: &mut Frame, app: &App) {
let area = f.size();
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [instructions, content] = area.split(&vertical);
let [instructions, content] = vertical.areas(area);
let text = if app.show_popup {
"Press p to close the popup"

View File

@@ -22,49 +22,46 @@ use std::{
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use indoc::indoc;
use itertools::izip;
use ratatui::{prelude::*, widgets::*};
use ratatui::{prelude::*, widgets::Paragraph};
/// A fun example of using half block characters to draw a logo
fn main() -> io::Result<()> {
#[allow(clippy::many_single_char_names)]
fn logo() -> String {
let r = indoc! {"
▄▄▄
█▄▄▀
█ █
"}
.lines();
"};
let a = indoc! {"
▄▄
█▄▄█
█ █
"}
.lines();
"};
let t = indoc! {"
▄▄▄
"}
.lines();
"};
let u = indoc! {"
▄ ▄
█ █
▀▄▄▀
"}
.lines();
"};
let i = indoc! {"
"}
.lines();
"};
izip!(r.lines(), a.lines(), t.lines(), u.lines(), i.lines())
.map(|(r, a, t, u, i)| format!("{r:5}{a:5}{t:4}{a:5}{t:4}{u:5}{i:5}"))
.collect::<Vec<_>>()
.join("\n")
}
fn main() -> io::Result<()> {
let mut terminal = init()?;
terminal.draw(|frame| {
let logo = izip!(r, a.clone(), t.clone(), a, t, u, i)
.map(|(r, a, t, a2, t2, u, i)| {
format!("{:5}{:5}{:4}{:5}{:4}{:5}{:5}", r, a, t, a2, t2, u, i)
})
.collect::<Vec<_>>()
.join("\n");
frame.render_widget(Paragraph::new(logo), frame.size());
frame.render_widget(Paragraph::new(logo()), frame.size());
})?;
sleep(Duration::from_secs(5));
restore()?;
@@ -72,7 +69,7 @@ fn main() -> io::Result<()> {
Ok(())
}
pub fn init() -> io::Result<Terminal<impl Backend>> {
fn init() -> io::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
let options = TerminalOptions {
viewport: Viewport::Inline(3),
@@ -80,7 +77,7 @@ pub fn init() -> io::Result<Terminal<impl Backend>> {
Terminal::with_options(CrosstermBackend::new(stdout()), options)
}
pub fn restore() -> io::Result<()> {
fn restore() -> io::Result<()> {
disable_raw_mode()?;
Ok(())
}

View File

@@ -13,6 +13,9 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![warn(clippy::pedantic)]
#![allow(clippy::wildcard_imports)]
use std::{
error::Error,
io,
@@ -107,6 +110,7 @@ fn run_app<B: Backend>(
}
}
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
fn ui(f: &mut Frame, app: &mut App) {
let size = f.size();
@@ -133,10 +137,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 +147,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 +155,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 +166,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 +181,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 +201,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 +219,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

@@ -28,17 +28,20 @@ use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Sparkline},
};
#[derive(Clone)]
pub struct RandomSignal {
struct RandomSignal {
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> RandomSignal {
RandomSignal {
fn new(lower: u64, upper: u64) -> Self {
Self {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
@@ -60,12 +63,12 @@ struct App {
}
impl App {
fn new() -> App {
fn new() -> Self {
let mut signal = RandomSignal::new(0, 100);
let data1 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
App {
Self {
signal,
data1,
data2,
@@ -127,7 +130,7 @@ fn run_app<B: Backend>(
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}

View File

@@ -13,6 +13,8 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
use std::{error::Error, io};
use crossterm::{
@@ -48,7 +50,7 @@ struct TableColors {
}
impl TableColors {
fn new(color: &tailwind::Palette) -> Self {
const fn new(color: &tailwind::Palette) -> Self {
Self {
buffer_bg: tailwind::SLATE.c950,
header_bg: color.c900,
@@ -69,7 +71,7 @@ struct Data {
}
impl Data {
fn ref_array(&self) -> [&String; 3] {
const fn ref_array(&self) -> [&String; 3] {
[&self.name, &self.address, &self.email]
}
@@ -96,9 +98,9 @@ struct App {
}
impl App {
fn new() -> App {
fn new() -> Self {
let data_vec = generate_fake_names();
App {
Self {
state: TableState::default().with_selected(0),
longest_item_lens: constraint_len_calculator(&data_vec),
scroll_state: ScrollbarState::new((data_vec.len() - 1) * ITEM_HEIGHT),
@@ -147,7 +149,7 @@ impl App {
}
pub fn set_colors(&mut self) {
self.colors = TableColors::new(&PALETTES[self.color_index])
self.colors = TableColors::new(&PALETTES[self.color_index]);
}
}
@@ -245,8 +247,7 @@ fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
.fg(app.colors.selected_style_fg);
let header = ["Name", "Address", "Email"]
.iter()
.cloned()
.into_iter()
.map(Cell::from)
.collect::<Row>()
.style(header_style)
@@ -257,9 +258,8 @@ fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
_ => app.colors.alt_row_color,
};
let item = data.ref_array();
item.iter()
.cloned()
.map(|content| Cell::from(Text::from(format!("\n{}\n", content))))
item.into_iter()
.map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
.collect::<Row>()
.style(Style::new().fg(app.colors.row_fg).bg(color))
.height(4)
@@ -308,6 +308,7 @@ fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) {
.max()
.unwrap_or(0);
#[allow(clippy::cast_possible_truncation)]
(name_len as u16, address_len as u16, email_len as u16)
}
@@ -325,7 +326,7 @@ fn render_scrollbar(f: &mut Frame, app: &mut App, area: Rect) {
);
}
fn render_footer(f: &mut Frame, app: &mut App, area: Rect) {
fn render_footer(f: &mut Frame, app: &App, area: Rect) {
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
.style(Style::new().fg(app.colors.row_fg).bg(app.colors.buffer_bg))
.centered()

View File

@@ -13,170 +13,240 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{error::Error, io};
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use std::io::stdout;
use color_eyre::{config::HookBuilder, Result};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
const PALETTES: &[tailwind::Palette] = &[
tailwind::BLUE,
tailwind::EMERALD,
tailwind::INDIGO,
tailwind::RED,
];
#[derive(Default)]
struct App {
state: AppState,
selected_tab: SelectedTab,
}
const BORDER_TYPES: &[BorderType] = &[
BorderType::Rounded,
BorderType::Plain,
BorderType::Double,
BorderType::Thick,
];
#[derive(Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Quitting,
}
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
enum SelectedTab {
#[default]
Tab0,
#[strum(to_string = "Tab 1")]
Tab1,
#[strum(to_string = "Tab 2")]
Tab2,
#[strum(to_string = "Tab 3")]
Tab3,
#[strum(to_string = "Tab 4")]
Tab4,
}
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab.
fn previous(&self) -> Self {
let current_index: usize = *self as usize;
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(*self)
}
/// Get the next tab, if there is no next tab return the current tab.
fn next(&self) -> Self {
let current_index = *self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(*self)
}
}
impl From<SelectedTab> for Line<'_> {
/// Return enum name as a styled `Line` with two spaces both left and right.
fn from(value: SelectedTab) -> Self {
format!(" {value} ")
.fg(tailwind::SLATE.c200)
.bg(PALETTES[value as usize].c900)
.into()
}
}
struct App {
pub selected_tab: SelectedTab,
}
impl App {
fn new() -> App {
App {
selected_tab: SelectedTab::default(),
}
}
pub fn next(&mut self) {
self.selected_tab = self.selected_tab.next();
}
pub fn previous(&mut self) {
self.selected_tab = self.selected_tab.previous();
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
fn main() -> Result<()> {
init_error_hooks()?;
let mut terminal = init_terminal()?;
App::default().run(&mut terminal)?;
restore_terminal()?;
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &app))?;
impl App {
fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
while self.state == AppState::Running {
self.draw(terminal)?;
self.handle_events()?;
}
Ok(())
}
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
fn handle_events(&mut self) -> std::io::Result<()> {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
use KeyCode::*;
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
KeyCode::Char('l') | KeyCode::Right => app.next(),
KeyCode::Char('h') | KeyCode::Left => app.previous(),
Char('l') | Right => self.next_tab(),
Char('h') | Left => self.previous_tab(),
Char('q') | Esc => self.quit(),
_ => {}
}
}
}
Ok(())
}
pub fn next_tab(&mut self) {
self.selected_tab = self.selected_tab.next();
}
pub fn previous_tab(&mut self) {
self.selected_tab = self.selected_tab.previous();
}
pub fn quit(&mut self) {
self.state = AppState::Quitting;
}
}
fn ui(f: &mut Frame, app: &App) {
let area = f.size();
let vertical = Layout::vertical([Constraint::Length(4), Constraint::Min(3)]);
let [tabs_area, inner_area] = area.split(&vertical);
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab.
fn previous(self) -> Self {
let current_index: usize = self as usize;
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(self)
}
render_tabs(f, app, tabs_area);
render_inner(f, app, inner_area);
/// Get the next tab, if there is no next tab return the current tab.
fn next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(self)
}
}
fn render_tabs(f: &mut Frame, app: &App, area: Rect) {
let block = Block::new()
.title("Tabs Example".bold())
.title("Use h l or ◄ ► to change tab")
.title_alignment(Alignment::Center)
.padding(Padding::top(1)); // padding to separate tabs from block title.
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::*;
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
let [header_area, inner_area, footer_area] = vertical.areas(area);
let selected_tab_index = app.selected_tab as usize;
// Gets tab titles from `SelectedTab::iter()`
let tabs = Tabs::new(SelectedTab::iter())
.block(block)
.highlight_style(
Style::new()
.bg(PALETTES[selected_tab_index].c600)
.underlined(),
)
.select(selected_tab_index)
.padding("", "")
.divider(" | ");
let horizontal = Layout::horizontal([Min(0), Length(20)]);
let [tabs_area, title_area] = horizontal.areas(header_area);
f.render_widget(tabs, area);
render_title(title_area, buf);
self.render_tabs(tabs_area, buf);
self.selected_tab.render(inner_area, buf);
render_footer(footer_area, buf);
}
}
fn render_inner(f: &mut Frame, app: &App, area: Rect) {
let index = app.selected_tab as usize;
let inner_block = Block::default()
.title(format!("Inner {index}"))
.borders(Borders::ALL)
.border_type(BORDER_TYPES[index])
.border_style(Style::new().fg(PALETTES[index].c600));
f.render_widget(inner_block, area);
impl App {
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
let titles = SelectedTab::iter().map(SelectedTab::title);
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
let selected_tab_index = self.selected_tab as usize;
Tabs::new(titles)
.highlight_style(highlight_style)
.select(selected_tab_index)
.padding("", "")
.divider(" ")
.render(area, buf);
}
}
fn render_title(area: Rect, buf: &mut Buffer) {
"Ratatui Tabs Example".bold().render(area, buf);
}
fn render_footer(area: Rect, buf: &mut Buffer) {
Line::raw("◄ ► to change tab | Press q to quit")
.centered()
.render(area, buf);
}
impl Widget for SelectedTab {
fn render(self, area: Rect, buf: &mut Buffer) {
// in a real app these might be separate widgets
match self {
Self::Tab1 => self.render_tab0(area, buf),
Self::Tab2 => self.render_tab1(area, buf),
Self::Tab3 => self.render_tab2(area, buf),
Self::Tab4 => self.render_tab3(area, buf),
}
}
}
impl SelectedTab {
/// Return tab's name as a styled `Line`
fn title(self) -> Line<'static> {
format!(" {self} ")
.fg(tailwind::SLATE.c200)
.bg(self.palette().c900)
.into()
}
fn render_tab0(self, area: Rect, buf: &mut Buffer) {
Paragraph::new("Hello, World!")
.block(self.block())
.render(area, buf);
}
fn render_tab1(self, area: Rect, buf: &mut Buffer) {
Paragraph::new("Welcome to the Ratatui tabs example!")
.block(self.block())
.render(area, buf);
}
fn render_tab2(self, area: Rect, buf: &mut Buffer) {
Paragraph::new("Look! I'm different than others!")
.block(self.block())
.render(area, buf);
}
fn render_tab3(self, area: Rect, buf: &mut Buffer) {
Paragraph::new("I know, these are some basic changes. But I think you got the main idea.")
.block(self.block())
.render(area, buf);
}
/// A block surrounding the tab's content
fn block(self) -> Block<'static> {
Block::default()
.borders(Borders::ALL)
.border_set(symbols::border::PROPORTIONAL_TALL)
.padding(Padding::horizontal(1))
.border_style(self.palette().c700)
}
const fn palette(self) -> tailwind::Palette {
match self {
Self::Tab1 => tailwind::BLUE,
Self::Tab2 => tailwind::EMERALD,
Self::Tab3 => tailwind::INDIGO,
Self::Tab4 => tailwind::RED,
}
}
}
fn init_error_hooks() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> color_eyre::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -13,28 +13,31 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
// A simple example demonstrating how to handle user input. This is a bit out of the scope of
// the library as it does not provide any input handling out of the box. However, it may helps
// some to get started.
//
// This is a very simple example:
// * An input box always focused. Every character you type is registered here.
// * An entered character is inserted at the cursor position.
// * Pressing Backspace erases the left character before the cursor position
// * Pressing Enter pushes the current input in the history of previous messages. **Note: ** as
// this is a relatively simple example unicode characters are unsupported and their use will
// result in undefined behaviour.
//
// See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/
use std::{error::Error, io};
/// A simple example demonstrating how to handle user input. This is a bit out of the scope of
/// the library as it does not provide any input handling out of the box. However, it may helps
/// some to get started.
///
/// This is a very simple example:
/// * An input box always focused. Every character you type is registered here.
/// * An entered character is inserted at the cursor position.
/// * Pressing Backspace erases the left character before the cursor position
/// * Pressing Enter pushes the current input in the history of previous messages. **Note: **
/// as
/// this is a relatively simple example unicode characters are unsupported and their use will
/// result in undefined behaviour.
///
/// See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::*,
widgets::{Block, Borders, List, ListItem, Paragraph},
};
enum InputMode {
Normal,
@@ -53,18 +56,16 @@ struct App {
messages: Vec<String>,
}
impl Default for App {
fn default() -> App {
App {
impl App {
const fn new() -> Self {
Self {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
cursor_position: 0,
}
}
}
impl App {
fn move_cursor_left(&mut self) {
let cursor_moved_left = self.cursor_position.saturating_sub(1);
self.cursor_position = self.clamp_cursor(cursor_moved_left);
@@ -127,7 +128,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::default();
let app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
@@ -180,7 +181,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
_ => {}
},
_ => {}
InputMode::Editing => {}
}
}
}
@@ -192,7 +193,7 @@ fn ui(f: &mut Frame, app: &App) {
Constraint::Length(3),
Constraint::Min(1),
]);
let [help_area, input_area, messages_area] = f.size().split(&vertical);
let [help_area, input_area, messages_area] = vertical.areas(f.size());
let (msg, style) = match app.input_mode {
InputMode::Normal => (
@@ -235,13 +236,14 @@ fn ui(f: &mut Frame, app: &App) {
InputMode::Editing => {
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
// rendering
#[allow(clippy::cast_possible_truncation)]
f.set_cursor(
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
input_area.x + app.cursor_position as u16 + 1,
// Move one line down, from the border to the input line
input_area.y + 1,
)
);
}
}

View File

@@ -18,4 +18,4 @@ Space
Left
Space
Left
Space
Space

View File

@@ -15,4 +15,4 @@ Enter
Sleep 2s
Show
Type "d"
Sleep 30s
Sleep 30s

View File

@@ -40,4 +40,4 @@ Tab
# Weather
Set TypingSpeed 100ms
Down 40
Sleep 2s
Sleep 2s

View File

@@ -46,4 +46,4 @@ Tab
Screenshot "target/demo2-weather.png"
Set TypingSpeed 100ms
Down 40
Sleep 2s
Sleep 2s

View File

@@ -3,10 +3,12 @@
Output "target/gauge.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 550
Set Height 850
Hide
Type "cargo run --example=gauge --features=crossterm"
Enter
Sleep 1s
Sleep 2s
Show
Sleep 20s
Sleep 2s
Enter 1
Sleep 15s

View File

@@ -3,13 +3,13 @@
Output "target/tabs.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 332
Set Height 368
Hide
Type "cargo run --example=tabs --features=crossterm"
Enter
Sleep 1s
Sleep 2s
Show
Sleep 1s
Right@1s 3
Left@1s 3
Sleep 1s
Right@2.5s 3
Left@2.5s 3
Sleep 2s

View File

@@ -97,8 +97,8 @@ where
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// ```
pub fn new(writer: W) -> CrosstermBackend<W> {
CrosstermBackend { writer }
pub const fn new(writer: W) -> Self {
Self { writer }
}
}
@@ -252,25 +252,25 @@ where
impl From<Color> for CColor {
fn from(color: Color) -> Self {
match color {
Color::Reset => CColor::Reset,
Color::Black => CColor::Black,
Color::Red => CColor::DarkRed,
Color::Green => CColor::DarkGreen,
Color::Yellow => CColor::DarkYellow,
Color::Blue => CColor::DarkBlue,
Color::Magenta => CColor::DarkMagenta,
Color::Cyan => CColor::DarkCyan,
Color::Gray => CColor::Grey,
Color::DarkGray => CColor::DarkGrey,
Color::LightRed => CColor::Red,
Color::LightGreen => CColor::Green,
Color::LightBlue => CColor::Blue,
Color::LightYellow => CColor::Yellow,
Color::LightMagenta => CColor::Magenta,
Color::LightCyan => CColor::Cyan,
Color::White => CColor::White,
Color::Indexed(i) => CColor::AnsiValue(i),
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
Color::Reset => Self::Reset,
Color::Black => Self::Black,
Color::Red => Self::DarkRed,
Color::Green => Self::DarkGreen,
Color::Yellow => Self::DarkYellow,
Color::Blue => Self::DarkBlue,
Color::Magenta => Self::DarkMagenta,
Color::Cyan => Self::DarkCyan,
Color::Gray => Self::Grey,
Color::DarkGray => Self::DarkGrey,
Color::LightRed => Self::Red,
Color::LightGreen => Self::Green,
Color::LightBlue => Self::Blue,
Color::LightYellow => Self::Yellow,
Color::LightMagenta => Self::Magenta,
Color::LightCyan => Self::Cyan,
Color::White => Self::White,
Color::Indexed(i) => Self::AnsiValue(i),
Color::Rgb(r, g, b) => Self::Rgb { r, g, b },
}
}
}
@@ -304,14 +304,13 @@ impl From<CColor> for Color {
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
struct ModifierDiff {
pub from: Modifier,
pub to: Modifier,
}
impl ModifierDiff {
fn queue<W>(&self, mut w: W) -> io::Result<()>
fn queue<W>(self, mut w: W) -> io::Result<()>
where
W: io::Write,
{
@@ -377,22 +376,22 @@ impl From<CAttribute> for Modifier {
// `Attribute*s*` (note the *s*) contains multiple `Attribute`
// We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
// the conversion again
Modifier::from(CAttributes::from(value))
Self::from(CAttributes::from(value))
}
}
impl From<CAttributes> for Modifier {
fn from(value: CAttributes) -> Self {
let mut res = Modifier::empty();
let mut res = Self::empty();
if value.has(CAttribute::Bold) {
res |= Modifier::BOLD;
res |= Self::BOLD;
}
if value.has(CAttribute::Dim) {
res |= Modifier::DIM;
res |= Self::DIM;
}
if value.has(CAttribute::Italic) {
res |= Modifier::ITALIC;
res |= Self::ITALIC;
}
if value.has(CAttribute::Underlined)
|| value.has(CAttribute::DoubleUnderlined)
@@ -400,22 +399,22 @@ impl From<CAttributes> for Modifier {
|| value.has(CAttribute::Underdotted)
|| value.has(CAttribute::Underdashed)
{
res |= Modifier::UNDERLINED;
res |= Self::UNDERLINED;
}
if value.has(CAttribute::SlowBlink) {
res |= Modifier::SLOW_BLINK;
res |= Self::SLOW_BLINK;
}
if value.has(CAttribute::RapidBlink) {
res |= Modifier::RAPID_BLINK;
res |= Self::RAPID_BLINK;
}
if value.has(CAttribute::Reverse) {
res |= Modifier::REVERSED;
res |= Self::REVERSED;
}
if value.has(CAttribute::Hidden) {
res |= Modifier::HIDDEN;
res |= Self::HIDDEN;
}
if value.has(CAttribute::CrossedOut) {
res |= Modifier::CROSSED_OUT;
res |= Self::CROSSED_OUT;
}
res
@@ -449,10 +448,10 @@ impl From<ContentStyle> for Style {
}
Self {
fg: value.foreground_color.map(|c| c.into()),
bg: value.background_color.map(|c| c.into()),
fg: value.foreground_color.map(Into::into),
bg: value.background_color.map(Into::into),
#[cfg(feature = "underline-color")]
underline_color: value.underline_color.map(|c| c.into()),
underline_color: value.underline_color.map(Into::into),
add_modifier: value.attributes.into(),
sub_modifier,
}
@@ -668,6 +667,6 @@ mod tests {
..Default::default()
}),
Style::default().underline_color(Color::Red)
)
);
}
}

View File

@@ -82,8 +82,8 @@ where
/// # use ratatui::prelude::*;
/// let backend = TermionBackend::new(stdout());
/// ```
pub fn new(writer: W) -> TermionBackend<W> {
TermionBackend { writer }
pub const fn new(writer: W) -> Self {
Self { writer }
}
}
@@ -209,16 +209,13 @@ where
self.writer.flush()
}
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
struct Fg(Color);
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
struct Bg(Color);
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
struct ModifierDiff {
from: Modifier,
to: Modifier,
@@ -319,37 +316,37 @@ from_termion_for_color!(LightWhite, White);
impl From<tcolor::AnsiValue> for Color {
fn from(value: tcolor::AnsiValue) -> Self {
Color::Indexed(value.0)
Self::Indexed(value.0)
}
}
impl From<tcolor::Bg<tcolor::AnsiValue>> for Style {
fn from(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
Style::default().bg(Color::Indexed(value.0 .0))
Self::default().bg(Color::Indexed(value.0 .0))
}
}
impl From<tcolor::Fg<tcolor::AnsiValue>> for Style {
fn from(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
Style::default().fg(Color::Indexed(value.0 .0))
Self::default().fg(Color::Indexed(value.0 .0))
}
}
impl From<tcolor::Rgb> for Color {
fn from(value: tcolor::Rgb) -> Self {
Color::Rgb(value.0, value.1, value.2)
Self::Rgb(value.0, value.1, value.2)
}
}
impl From<tcolor::Bg<tcolor::Rgb>> for Style {
fn from(value: tcolor::Bg<tcolor::Rgb>) -> Self {
Style::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
Self::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
}
}
impl From<tcolor::Fg<tcolor::Rgb>> for Style {
fn from(value: tcolor::Fg<tcolor::Rgb>) -> Self {
Style::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
Self::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
}
}
@@ -438,7 +435,7 @@ from_termion_for_modifier!(Blink, SLOW_BLINK);
impl From<termion::style::Reset> for Modifier {
fn from(_: termion::style::Reset) -> Self {
Modifier::empty()
Self::empty()
}
}

View File

@@ -84,23 +84,23 @@ impl TermwizBackend {
/// let backend = TermwizBackend::new()?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn new() -> Result<TermwizBackend, Box<dyn Error>> {
pub fn new() -> Result<Self, Box<dyn Error>> {
let mut buffered_terminal =
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
buffered_terminal.terminal().set_raw_mode()?;
buffered_terminal.terminal().enter_alternate_screen()?;
Ok(TermwizBackend { buffered_terminal })
Ok(Self { buffered_terminal })
}
/// Creates a new Termwiz backend instance with the given buffered terminal.
pub fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> TermwizBackend {
TermwizBackend {
pub const fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> Self {
Self {
buffered_terminal: instance,
}
}
/// Returns a reference to the buffered terminal used by the backend.
pub fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
pub const fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
&self.buffered_terminal
}
@@ -251,7 +251,7 @@ impl Backend for TermwizBackend {
impl From<CellAttributes> for Style {
fn from(value: CellAttributes) -> Self {
let mut style = Style::new()
let mut style = Self::new()
.add_modifier(value.intensity().into())
.add_modifier(value.underline().into())
.add_modifier(value.blink().into());
@@ -283,9 +283,9 @@ impl From<CellAttributes> for Style {
impl From<Intensity> for Modifier {
fn from(value: Intensity) -> Self {
match value {
Intensity::Normal => Modifier::empty(),
Intensity::Bold => Modifier::BOLD,
Intensity::Half => Modifier::DIM,
Intensity::Normal => Self::empty(),
Intensity::Bold => Self::BOLD,
Intensity::Half => Self::DIM,
}
}
}
@@ -293,8 +293,8 @@ impl From<Intensity> for Modifier {
impl From<Underline> for Modifier {
fn from(value: Underline) -> Self {
match value {
Underline::None => Modifier::empty(),
_ => Modifier::UNDERLINED,
Underline::None => Self::empty(),
_ => Self::UNDERLINED,
}
}
}
@@ -302,17 +302,17 @@ impl From<Underline> for Modifier {
impl From<Blink> for Modifier {
fn from(value: Blink) -> Self {
match value {
Blink::None => Modifier::empty(),
Blink::Slow => Modifier::SLOW_BLINK,
Blink::Rapid => Modifier::RAPID_BLINK,
Blink::None => Self::empty(),
Blink::Slow => Self::SLOW_BLINK,
Blink::Rapid => Self::RAPID_BLINK,
}
}
}
impl From<Color> for ColorAttribute {
fn from(color: Color) -> ColorAttribute {
fn from(color: Color) -> Self {
match color {
Color::Reset => ColorAttribute::Default,
Color::Reset => Self::Default,
Color::Black => AnsiColor::Black.into(),
Color::DarkGray => AnsiColor::Grey.into(),
Color::Gray => AnsiColor::Silver.into(),
@@ -329,10 +329,8 @@ impl From<Color> for ColorAttribute {
Color::White => AnsiColor::White.into(),
Color::Blue => AnsiColor::Navy.into(),
Color::LightBlue => AnsiColor::Blue.into(),
Color::Indexed(i) => ColorAttribute::PaletteIndex(i),
Color::Rgb(r, g, b) => {
ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b)))
}
Color::Indexed(i) => Self::PaletteIndex(i),
Color::Rgb(r, g, b) => Self::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b))),
}
}
}
@@ -340,22 +338,22 @@ impl From<Color> for ColorAttribute {
impl From<AnsiColor> for Color {
fn from(value: AnsiColor) -> Self {
match value {
AnsiColor::Black => Color::Black,
AnsiColor::Grey => Color::DarkGray,
AnsiColor::Silver => Color::Gray,
AnsiColor::Maroon => Color::Red,
AnsiColor::Red => Color::LightRed,
AnsiColor::Green => Color::Green,
AnsiColor::Lime => Color::LightGreen,
AnsiColor::Olive => Color::Yellow,
AnsiColor::Yellow => Color::LightYellow,
AnsiColor::Purple => Color::Magenta,
AnsiColor::Fuchsia => Color::LightMagenta,
AnsiColor::Teal => Color::Cyan,
AnsiColor::Aqua => Color::LightCyan,
AnsiColor::White => Color::White,
AnsiColor::Navy => Color::Blue,
AnsiColor::Blue => Color::LightBlue,
AnsiColor::Black => Self::Black,
AnsiColor::Grey => Self::DarkGray,
AnsiColor::Silver => Self::Gray,
AnsiColor::Maroon => Self::Red,
AnsiColor::Red => Self::LightRed,
AnsiColor::Green => Self::Green,
AnsiColor::Lime => Self::LightGreen,
AnsiColor::Olive => Self::Yellow,
AnsiColor::Yellow => Self::LightYellow,
AnsiColor::Purple => Self::Magenta,
AnsiColor::Fuchsia => Self::LightMagenta,
AnsiColor::Teal => Self::Cyan,
AnsiColor::Aqua => Self::LightCyan,
AnsiColor::White => Self::White,
AnsiColor::Navy => Self::Blue,
AnsiColor::Blue => Self::LightBlue,
}
}
}
@@ -365,8 +363,8 @@ impl From<ColorAttribute> for Color {
match value {
ColorAttribute::TrueColorWithDefaultFallback(srgba)
| ColorAttribute::TrueColorWithPaletteFallback(srgba, _) => srgba.into(),
ColorAttribute::PaletteIndex(i) => Color::Indexed(i),
ColorAttribute::Default => Color::Reset,
ColorAttribute::PaletteIndex(i) => Self::Indexed(i),
ColorAttribute::Default => Self::Reset,
}
}
}
@@ -374,8 +372,8 @@ impl From<ColorAttribute> for Color {
impl From<ColorSpec> for Color {
fn from(value: ColorSpec) -> Self {
match value {
ColorSpec::Default => Color::Reset,
ColorSpec::PaletteIndex(i) => Color::Indexed(i),
ColorSpec::Default => Self::Reset,
ColorSpec::PaletteIndex(i) => Self::Indexed(i),
ColorSpec::TrueColor(srgba) => srgba.into(),
}
}
@@ -384,14 +382,14 @@ impl From<ColorSpec> for Color {
impl From<SrgbaTuple> for Color {
fn from(value: SrgbaTuple) -> Self {
let (r, g, b, _) = value.to_srgb_u8();
Color::Rgb(r, g, b)
Self::Rgb(r, g, b)
}
}
impl From<RgbColor> for Color {
fn from(value: RgbColor) -> Self {
let (r, g, b) = value.to_tuple_rgb8();
Color::Rgb(r, g, b)
Self::Rgb(r, g, b)
}
}

View File

@@ -9,6 +9,7 @@ use std::{
use unicode_width::UnicodeWidthStr;
use crate::{
assert_buffer_eq,
backend::{Backend, ClearType, WindowSize},
buffer::{Buffer, Cell},
layout::{Rect, Size},
@@ -72,9 +73,9 @@ fn buffer_view(buffer: &Buffer) -> String {
}
impl TestBackend {
/// Creates a new TestBackend with the specified width and height.
pub fn new(width: u16, height: u16) -> TestBackend {
TestBackend {
/// Creates a new `TestBackend` with the specified width and height.
pub fn new(width: u16, height: u16) -> Self {
Self {
width,
height,
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
@@ -83,60 +84,29 @@ impl TestBackend {
}
}
/// Returns a reference to the internal buffer of the TestBackend.
pub fn buffer(&self) -> &Buffer {
/// Returns a reference to the internal buffer of the `TestBackend`.
pub const fn buffer(&self) -> &Buffer {
&self.buffer
}
/// Resizes the TestBackend to the specified width and height.
/// Resizes the `TestBackend` to the specified width and height.
pub fn resize(&mut self, width: u16, height: u16) {
self.buffer.resize(Rect::new(0, 0, width, height));
self.width = width;
self.height = height;
}
/// Asserts that the TestBackend's buffer is equal to the expected buffer.
/// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
/// If the buffers are not equal, a panic occurs with a detailed error message
/// showing the differences between the expected and actual buffers.
#[track_caller]
pub fn assert_buffer(&self, expected: &Buffer) {
assert_eq!(expected.area, self.buffer.area);
let diff = expected.diff(&self.buffer);
if diff.is_empty() {
return;
}
let mut debug_info = String::from("Buffers are not equal");
debug_info.push('\n');
debug_info.push_str("Expected:");
debug_info.push('\n');
let expected_view = buffer_view(expected);
debug_info.push_str(&expected_view);
debug_info.push('\n');
debug_info.push_str("Got:");
debug_info.push('\n');
let view = buffer_view(&self.buffer);
debug_info.push_str(&view);
debug_info.push('\n');
debug_info.push_str("Diff:");
debug_info.push('\n');
let nice_diff = diff
.iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = expected.get(*x, *y);
format!("{i}: at ({x}, {y}) expected {expected_cell:?} got {cell:?}")
})
.collect::<Vec<String>>()
.join("\n");
debug_info.push_str(&nice_diff);
panic!("{debug_info}");
assert_buffer_eq!(&self.buffer, expected);
}
}
impl Display for TestBackend {
/// Formats the TestBackend for display by calling the buffer_view function
/// Formats the `TestBackend` for display by calling the `buffer_view` function
/// on its internal buffer.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", buffer_view(&self.buffer))
@@ -297,7 +267,6 @@ mod tests {
format!(
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " "), (2, " "), (3, " "), (4, " "), (5, " "), (6, " "), (7, " ")]
"#,
multi_byte_char = multi_byte_char
)
);
}
@@ -323,7 +292,7 @@ mod tests {
}
#[test]
#[should_panic]
#[should_panic = "buffer contents not equal"]
fn assert_buffer_panics() {
let backend = TestBackend::new(10, 2);
let buffer = Buffer::with_lines(vec!["aaaaaaaaaa"; 2]);
@@ -333,7 +302,7 @@ mod tests {
#[test]
fn display() {
let backend = TestBackend::new(10, 2);
assert_eq!(format!("{}", backend), "\" \"\n\" \"\n");
assert_eq!(format!("{backend}"), "\" \"\n\" \"\n");
}
#[test]

View File

@@ -63,7 +63,7 @@ mod tests {
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic]
#[should_panic = "buffer areas not equal"]
#[test]
fn assert_buffer_eq_panics_on_unequal_area() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
@@ -71,7 +71,7 @@ mod tests {
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic]
#[should_panic = "buffer contents not equal"]
#[test]
fn assert_buffer_eq_panics_on_unequal_style() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));

View File

@@ -55,30 +55,31 @@ pub struct Buffer {
impl Buffer {
/// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer {
pub fn empty(area: Rect) -> Self {
let cell = Cell::default();
Buffer::filled(area, &cell)
Self::filled(area, &cell)
}
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
pub fn filled(area: Rect, cell: &Cell) -> Self {
let size = area.area() as usize;
let mut content = Vec::with_capacity(size);
for _ in 0..size {
content.push(cell.clone());
}
Buffer { area, content }
Self { area, content }
}
/// Returns a Buffer containing the given lines
pub fn with_lines<'a, S>(lines: Vec<S>) -> Buffer
pub fn with_lines<'a, Iter>(lines: Iter) -> Self
where
S: Into<Line<'a>>,
Iter: IntoIterator,
Iter::Item: Into<Line<'a>>,
{
let lines = lines.into_iter().map(Into::into).collect::<Vec<_>>();
let height = lines.len() as u16;
let width = lines.iter().map(Line::width).max().unwrap_or_default() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
let mut buffer = Self::empty(Rect::new(0, 0, width, height));
for (y, line) in lines.iter().enumerate() {
buffer.set_line(0, y as u16, line, width);
}
@@ -91,7 +92,7 @@ impl Buffer {
}
/// Returns the area covered by this buffer
pub fn area(&self) -> &Rect {
pub const fn area(&self) -> &Rect {
&self.area
}
@@ -243,7 +244,7 @@ impl Buffer {
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &line.spans {
for span in line {
if remaining_width == 0 {
break;
}
@@ -252,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;
@@ -300,7 +301,7 @@ impl Buffer {
}
/// Merge an other buffer into this one
pub fn merge(&mut self, other: &Buffer) {
pub fn merge(&mut self, other: &Self) {
let area = self.area.union(other.area);
let cell = Cell::default();
self.content.resize(area.area() as usize, cell.clone());
@@ -357,7 +358,7 @@ impl Buffer {
/// Next: `aコ`
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
/// ```
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
pub fn diff<'a>(&self, other: &'a Self) -> Vec<(u16, u16, &'a Cell)> {
let previous_buffer = &self.content;
let next_buffer = &other.content;
@@ -452,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;
@@ -615,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(Cell::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

@@ -42,26 +42,26 @@ impl Cell {
}
/// Sets the symbol of the cell.
pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol = CompactString::new(symbol);
self
}
/// Sets the symbol of the cell to a single character.
pub fn set_char(&mut self, ch: char) -> &mut Cell {
pub fn set_char(&mut self, ch: char) -> &mut Self {
let mut buf = [0; 4];
self.symbol = CompactString::new(ch.encode_utf8(&mut buf));
self
}
/// Sets the foreground color of the cell.
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
pub fn set_fg(&mut self, color: Color) -> &mut Self {
self.fg = color;
self
}
/// Sets the background color of the cell.
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
pub fn set_bg(&mut self, color: Color) -> &mut Self {
self.bg = color;
self
}
@@ -70,7 +70,7 @@ impl Cell {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Cell {
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Self {
let style = style.into();
if let Some(c) = style.fg {
self.fg = c;
@@ -107,14 +107,14 @@ impl Cell {
///
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
pub fn set_skip(&mut self, skip: bool) -> &mut Cell {
pub fn set_skip(&mut self, skip: bool) -> &mut Self {
self.skip = skip;
self
}
/// Resets the cell to the default state.
pub fn reset(&mut self) {
self.symbol = CompactString::new(" ");
self.symbol = CompactString::new_inline(" ");
self.fg = Color::Reset;
self.bg = Color::Reset;
#[cfg(feature = "underline-color")]
@@ -127,9 +127,9 @@ impl Cell {
}
impl Default for Cell {
fn default() -> Cell {
Cell {
symbol: CompactString::new(" "),
fn default() -> Self {
Self {
symbol: CompactString::new_inline(" "),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]

View File

@@ -1,3 +1,5 @@
#![warn(clippy::missing_const_for_fn)]
mod alignment;
mod constraint;
mod corner;
@@ -8,7 +10,6 @@ mod layout;
mod margin;
mod position;
mod rect;
mod segment_size;
mod size;
pub use alignment::Alignment;
@@ -20,8 +21,4 @@ pub use layout::Layout;
pub use margin::Margin;
pub use position::Position;
pub use rect::*;
#[cfg(feature = "unstable-segment-size")]
pub use segment_size::SegmentSize;
#[cfg(not(feature = "unstable-segment-size"))]
pub(crate) use segment_size::SegmentSize;
pub use size::Size;

View File

@@ -1,11 +1,13 @@
use std::fmt::{self, Display};
use itertools::Itertools;
use strum::EnumIs;
/// A constraint that defines the size of a layout element.
///
/// Constraints can be used to specify a fixed size, a percentage of the available space, a ratio of
/// the available space, a minimum or maximum size or a proportional value for a layout element.
/// the available space, a minimum or maximum size or a fill proportional value for a layout
/// element.
///
/// Relative constraints (percentage, ratio) are calculated relative to the entire space being
/// divided, rather than the space available after applying more fixed constraints (min, max,
@@ -13,10 +15,12 @@ use itertools::Itertools;
///
/// Constraints are prioritized in the following order:
///
/// 1. [`Constraint::Fixed`]
/// 2. [`Constraint::Min`] / [`Constraint::Max`]
/// 3. [`Constraint::Length`] / [`Constraint::Percentage`] / [`Constraint::Ratio`]
/// 4. [`Constraint::Proportional`]
/// 1. [`Constraint::Min`]
/// 2. [`Constraint::Max`]
/// 3. [`Constraint::Length`]
/// 4. [`Constraint::Percentage`]
/// 5. [`Constraint::Ratio`]
/// 6. [`Constraint::Fill`]
///
/// # Examples
///
@@ -27,9 +31,6 @@ use itertools::Itertools;
/// // Create a layout with specified lengths for each element
/// let constraints = Constraint::from_lengths([10, 20, 10]);
///
/// // Create a layout with specified fixed lengths for each element
/// let constraints = Constraint::from_fixed_lengths([10, 20, 10]);
///
/// // Create a centered layout using ratio or percentage constraints
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
/// let constraints = Constraint::from_percentages([25, 50, 25]);
@@ -40,11 +41,80 @@ use itertools::Itertools;
/// // Create a sidebar layout specifying maximum sizes for the columns
/// let constraints = Constraint::from_maxes([30, 170]);
///
/// // Create a layout with proportional sizes for each element
/// let constraints = Constraint::from_proportional_lengths([1, 2, 1]);
/// // Create a layout with fill proportional sizes for each element
/// let constraints = Constraint::from_fills([1, 2, 1]);
/// ```
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, EnumIs)]
pub enum Constraint {
/// Applies a minimum size constraint to the element
///
/// The element size is set to at least the specified amount.
///
/// # Examples
///
/// `[Percentage(100), Min(20)]`
///
/// ```plain
/// ┌────────────────────────────┐┌──────────────────┐
/// │ 30 px ││ 20 px │
/// └────────────────────────────┘└──────────────────┘
/// ```
///
/// `[Percentage(100), Min(10)]`
///
/// ```plain
/// ┌──────────────────────────────────────┐┌────────┐
/// │ 40 px ││ 10 px │
/// └──────────────────────────────────────┘└────────┘
/// ```
Min(u16),
/// Applies a maximum size constraint to the element
///
/// The element size is set to at most the specified amount.
///
/// # Examples
///
/// `[Percentage(0), Max(20)]`
///
/// ```plain
/// ┌────────────────────────────┐┌──────────────────┐
/// │ 30 px ││ 20 px │
/// └────────────────────────────┘└──────────────────┘
/// ```
///
/// `[Percentage(0), Max(10)]`
///
/// ```plain
/// ┌──────────────────────────────────────┐┌────────┐
/// │ 40 px ││ 10 px │
/// └──────────────────────────────────────┘└────────┘
/// ```
Max(u16),
/// Applies a length constraint to the element
///
/// The element size is set to the specified amount.
///
/// # Examples
///
/// `[Length(20), Length(20)]`
///
/// ```plain
/// ┌──────────────────┐┌──────────────────┐
/// │ 20 px ││ 20 px │
/// └──────────────────┘└──────────────────┘
/// ```
///
/// `[Length(20), Length(30)]`
///
/// ```plain
/// ┌──────────────────┐┌────────────────────────────┐
/// │ 20 px ││ 30 px │
/// └──────────────────┘└────────────────────────────┘
/// ```
Length(u16),
/// Applies a percentage of the available space to the element
///
/// Converts the given percentage to a floating-point value and multiplies that with area.
@@ -52,7 +122,7 @@ pub enum Constraint {
///
/// # Examples
///
/// `[Percentage(75), Proportional(1)]`
/// `[Percentage(75), Fill(1)]`
///
/// ```plain
/// ┌────────────────────────────────────┐┌──────────┐
@@ -60,7 +130,7 @@ pub enum Constraint {
/// └────────────────────────────────────┘└──────────┘
/// ```
///
/// `[Percentage(50), Proportional(1)]`
/// `[Percentage(50), Fill(1)]`
///
/// ```plain
/// ┌───────────────────────┐┌───────────────────────┐
@@ -68,6 +138,7 @@ pub enum Constraint {
/// └───────────────────────┘└───────────────────────┘
/// ```
Percentage(u16),
/// Applies a ratio of the available space to the element
///
/// Converts the given ratio to a floating-point value and multiplies that with area.
@@ -91,61 +162,17 @@ pub enum Constraint {
/// └───────────┘└──────────┘└───────────┘└──────────┘
/// ```
Ratio(u32, u32),
/// Applies a fixed size to the element
///
/// The element size is set to the specified amount.
/// [`Constraint::Fixed`] will take precedence over all other constraints.
///
/// # Examples
///
/// `[Fixed(40), Proportional(1)]`
///
/// ```plain
/// ┌──────────────────────────────────────┐┌────────┐
/// │ 40 px ││ 10 px │
/// └──────────────────────────────────────┘└────────┘
/// ```
///
/// `[Fixed(20), Fixed(20), Proportional(1)]`
///
/// ```plain
/// ┌──────────────────┐┌──────────────────┐┌────────┐
/// │ 20 px ││ 20 px ││ 10 px │
/// └──────────────────┘└──────────────────┘└────────┘
/// ```
Fixed(u16),
/// Applies a length constraint to the element
///
/// The element size is set to the specified amount.
///
/// # Examples
///
/// `[Length(20), Fixed(20)]`
///
/// ```plain
/// ┌────────────────────────────┐┌──────────────────┐
/// │ 30 px ││ 20 px │
/// └────────────────────────────┘└──────────────────┘
/// ```
///
/// `[Length(20), Length(20)]`
///
/// ```plain
/// ┌──────────────────┐┌────────────────────────────┐
/// │ 20 px ││ 30 px │
/// └──────────────────┘└────────────────────────────┘
/// ```
Length(u16),
/// Applies the scaling factor proportional to all other [`Constraint::Proportional`] elements
/// Applies the scaling factor proportional to all other [`Constraint::Fill`] elements
/// to fill excess space
///
/// The element will only expand into excess available space, proportionally matching other
/// [`Constraint::Proportional`] elements while satisfying all other constraints.
/// The element will only expand or fill into excess available space, proportionally matching
/// other [`Constraint::Fill`] elements while satisfying all other constraints.
///
/// # Examples
///
///
/// `[Proportional(1), Proportional(2), Proportional(3)]`
/// `[Fill(1), Fill(2), Fill(3)]`
///
/// ```plain
/// ┌──────┐┌───────────────┐┌───────────────────────┐
@@ -153,58 +180,14 @@ pub enum Constraint {
/// └──────┘└───────────────┘└───────────────────────┘
/// ```
///
/// `[Proportional(1), Percentage(50), Proportional(1)]`
/// `[Fill(1), Percentage(50), Fill(1)]`
///
/// ```plain
/// ┌───────────┐┌───────────────────────┐┌──────────┐
/// │ 13 px ││ 25 px ││ 12 px │
/// └───────────┘└───────────────────────┘└──────────┘
/// ```
Proportional(u16),
/// Applies a maximum size constraint to the element
///
/// The element size is set to at most the specified amount.
///
/// # Examples
///
/// `[Percentage(100), Min(20)]`
///
/// ```plain
/// ┌────────────────────────────┐┌──────────────────┐
/// │ 30 px ││ 20 px │
/// └────────────────────────────┘└──────────────────┘
/// ```
///
/// `[Percentage(100), Min(10)]`
///
/// ```plain
/// ┌──────────────────────────────────────┐┌────────┐
/// │ 40 px ││ 10 px │
/// └──────────────────────────────────────┘└────────┘
/// ```
Max(u16),
/// Applies a minimum size constraint to the element
///
/// The element size is set to at least the specified amount.
///
/// # Examples
///
/// `[Percentage(100), Min(20)]`
///
/// ```plain
/// ┌────────────────────────────┐┌──────────────────┐
/// │ 30 px ││ 20 px │
/// └────────────────────────────┘└──────────────────┘
/// ```
///
/// `[Percentage(100), Min(10)]`
///
/// ```plain
/// ┌──────────────────────────────────────┐┌────────┐
/// │ 40 px ││ 10 px │
/// └──────────────────────────────────────┘└────────┘
/// ```
Min(u16),
Fill(u16),
}
impl Constraint {
@@ -214,23 +197,21 @@ impl Constraint {
)]
pub fn apply(&self, length: u16) -> u16 {
match *self {
Constraint::Percentage(p) => {
let p = p as f32 / 100.0;
let length = length as f32;
Self::Percentage(p) => {
let p = f32::from(p) / 100.0;
let length = f32::from(length);
(p * length).min(length) as u16
}
Constraint::Ratio(numerator, denominator) => {
Self::Ratio(numerator, denominator) => {
// avoid division by zero by using 1 when denominator is 0
// this results in 0/0 -> 0 and x/0 -> x for x != 0
let percentage = numerator as f32 / denominator.max(1) as f32;
let length = length as f32;
let length = f32::from(length);
(percentage * length).min(length) as u16
}
Constraint::Length(l) => length.min(l),
Constraint::Fixed(l) => length.min(l),
Constraint::Proportional(l) => length.min(l),
Constraint::Max(m) => length.min(m),
Constraint::Min(m) => length.max(m),
Self::Length(l) | Self::Fill(l) => length.min(l),
Self::Max(m) => length.min(m),
Self::Min(m) => length.max(m),
}
}
@@ -244,31 +225,11 @@ impl Constraint {
/// let constraints = Constraint::from_lengths([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_lengths<T>(lengths: T) -> Vec<Constraint>
pub fn from_lengths<T>(lengths: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
lengths.into_iter().map(Constraint::Length).collect_vec()
}
/// Convert an iterator of fixed lengths into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_fixed_lengths([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_fixed_lengths<T>(fixed_lengths: T) -> Vec<Constraint>
where
T: IntoIterator<Item = u16>,
{
fixed_lengths
.into_iter()
.map(Constraint::Fixed)
.collect_vec()
lengths.into_iter().map(Self::Length).collect_vec()
}
/// Convert an iterator of ratios into a vector of constraints
@@ -281,13 +242,13 @@ impl Constraint {
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_ratios<T>(ratios: T) -> Vec<Constraint>
pub fn from_ratios<T>(ratios: T) -> Vec<Self>
where
T: IntoIterator<Item = (u32, u32)>,
{
ratios
.into_iter()
.map(|(n, d)| Constraint::Ratio(n, d))
.map(|(n, d)| Self::Ratio(n, d))
.collect_vec()
}
@@ -301,14 +262,11 @@ impl Constraint {
/// let constraints = Constraint::from_percentages([25, 50, 25]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_percentages<T>(percentages: T) -> Vec<Constraint>
pub fn from_percentages<T>(percentages: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
percentages
.into_iter()
.map(Constraint::Percentage)
.collect_vec()
percentages.into_iter().map(Self::Percentage).collect_vec()
}
/// Convert an iterator of maxes into a vector of constraints
@@ -321,11 +279,11 @@ impl Constraint {
/// let constraints = Constraint::from_maxes([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_maxes<T>(maxes: T) -> Vec<Constraint>
pub fn from_maxes<T>(maxes: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
maxes.into_iter().map(Constraint::Max).collect_vec()
maxes.into_iter().map(Self::Max).collect_vec()
}
/// Convert an iterator of mins into a vector of constraints
@@ -338,11 +296,11 @@ impl Constraint {
/// let constraints = Constraint::from_mins([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_mins<T>(mins: T) -> Vec<Constraint>
pub fn from_mins<T>(mins: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
mins.into_iter().map(Constraint::Min).collect_vec()
mins.into_iter().map(Self::Min).collect_vec()
}
/// Convert an iterator of proportional factors into a vector of constraints
@@ -355,22 +313,22 @@ impl Constraint {
/// let constraints = Constraint::from_mins([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_proportional_lengths<T>(proportional_lengths: T) -> Vec<Constraint>
pub fn from_fills<T>(proportional_factors: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
proportional_lengths
proportional_factors
.into_iter()
.map(Constraint::Proportional)
.map(Self::Fill)
.collect_vec()
}
}
impl From<u16> for Constraint {
/// Convert a u16 into a [Constraint::Length]
/// Convert a `u16` into a [`Constraint::Length`]
///
/// This is useful when you want to specify a fixed size for a layout, but don't want to
/// explicitly create a [Constraint::Length] yourself.
/// explicitly create a [`Constraint::Length`] yourself.
///
/// # Examples
///
@@ -381,39 +339,38 @@ impl From<u16> for Constraint {
/// let layout = Layout::horizontal([1, 2, 3]).split(area);
/// let layout = Layout::vertical([1, 2, 3]).split(area);
/// ````
fn from(length: u16) -> Constraint {
Constraint::Length(length)
fn from(length: u16) -> Self {
Self::Length(length)
}
}
impl From<&Constraint> for Constraint {
fn from(constraint: &Constraint) -> Self {
impl From<&Self> for Constraint {
fn from(constraint: &Self) -> Self {
*constraint
}
}
impl AsRef<Constraint> for Constraint {
fn as_ref(&self) -> &Constraint {
impl AsRef<Self> for Constraint {
fn as_ref(&self) -> &Self {
self
}
}
impl Default for Constraint {
fn default() -> Self {
Constraint::Percentage(100)
Self::Percentage(100)
}
}
impl Display for Constraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Constraint::Percentage(p) => write!(f, "Percentage({})", p),
Constraint::Ratio(n, d) => write!(f, "Ratio({}, {})", n, d),
Constraint::Length(l) => write!(f, "Length({})", l),
Constraint::Fixed(l) => write!(f, "Fixed({})", l),
Constraint::Proportional(l) => write!(f, "Proportional({})", l),
Constraint::Max(m) => write!(f, "Max({})", m),
Constraint::Min(m) => write!(f, "Min({})", m),
Self::Percentage(p) => write!(f, "Percentage({p})"),
Self::Ratio(n, d) => write!(f, "Ratio({n}, {d})"),
Self::Length(l) => write!(f, "Length({l})"),
Self::Fill(l) => write!(f, "Fill({l})"),
Self::Max(m) => write!(f, "Max({m})"),
Self::Min(m) => write!(f, "Min({m})"),
}
}
}
@@ -447,17 +404,6 @@ mod tests {
assert_eq!(Constraint::from_lengths(vec![1, 2, 3]), expected);
}
#[test]
fn from_fixed_lengths() {
let expected = [
Constraint::Fixed(1),
Constraint::Fixed(2),
Constraint::Fixed(3),
];
assert_eq!(Constraint::from_fixed_lengths([1, 2, 3]), expected);
assert_eq!(Constraint::from_fixed_lengths(vec![1, 2, 3]), expected);
}
#[test]
fn from_ratios() {
let expected = [
@@ -498,17 +444,14 @@ mod tests {
}
#[test]
fn from_proportional_lengths() {
fn from_fills() {
let expected = [
Constraint::Proportional(1),
Constraint::Proportional(2),
Constraint::Proportional(3),
Constraint::Fill(1),
Constraint::Fill(2),
Constraint::Fill(3),
];
assert_eq!(Constraint::from_proportional_lengths([1, 2, 3]), expected);
assert_eq!(
Constraint::from_proportional_lengths(vec![1, 2, 3]),
expected
);
assert_eq!(Constraint::from_fills([1, 2, 3]), expected);
assert_eq!(Constraint::from_fills(vec![1, 2, 3]), expected);
}
#[test]

View File

@@ -1,4 +1,4 @@
use strum::{Display, EnumString};
use strum::{Display, EnumIs, EnumString};
#[allow(unused_imports)]
use super::constraint::Constraint;
@@ -7,15 +7,14 @@ use super::constraint::Constraint;
///
/// This enumeration controls the distribution of space when layout constraints are met.
///
/// - `StretchLast`: Fills the available space within the container, putting excess space into the
/// last element.
/// - `Stretch`: Always fills the available space within the container.
/// - `Legacy`: Fills the available space within the container, putting excess space into the last
/// element.
/// - `Start`: Aligns items to the start of the container.
/// - `End`: Aligns items to the end of the container.
/// - `Center`: Centers items within the container.
/// - `SpaceBetween`: Adds excess space between each element.
/// - `SpaceAround`: Adds excess space around each element.
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash, EnumIs)]
pub enum Flex {
/// Fills the available space within the container, putting excess space into the last
/// constraint of the lowest priority. This matches the default behavior of ratatui and tui
@@ -24,10 +23,12 @@ pub enum Flex {
/// The following examples illustrate the allocation of excess in various combinations of
/// constraints. As a refresher, the priorities of constraints are as follows:
///
/// 1. [`Constraint::Fixed`]
/// 2. [`Constraint::Min`] / [`Constraint::Max`]
/// 3. [`Constraint::Length`] / [`Constraint::Percentage`] / [`Constraint::Ratio`]
/// 4. [`Constraint::Proportional`]
/// 1. [`Constraint::Min`]
/// 2. [`Constraint::Max`]
/// 3. [`Constraint::Length`]
/// 4. [`Constraint::Percentage`]
/// 5. [`Constraint::Ratio`]
/// 6. [`Constraint::Fill`]
///
/// When every constraint is `Length`, the last element gets the excess.
///
@@ -39,55 +40,13 @@ pub enum Flex {
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
/// ```
///
/// If we replace the constraint at the end with a `Fixed`, because it has a
/// higher priority, the last constraint with the lowest priority, i.e. the last
/// `Length` gets the excess.
///
/// ```plain
/// <----------------------------------- 80 px ------------------------------------>
/// ┌──────20 px───────┐┌────────────────40 px─────────────────┐┌──────20 px───────┐
/// │ Length(20) ││ Length(20) ││ Fixed(20) │
/// └──────────────────┘└──────────────────────────────────────┘└──────────────────┘
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
/// ```
///
/// Violating a `Max` is lower priority than `Fixed` but higher
/// than `Length`.
///
/// ```plain
/// <----------------------------------- 80 px ------------------------------------>
/// ┌────────────────40 px─────────────────┐┌──────20 px───────┐┌──────20 px───────┐
/// │ Length(20) ││ Max(20) ││ Fixed(20) │
/// └──────────────────────────────────────┘└──────────────────┘└──────────────────┘
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
/// ```
///
/// It's important to note that while not violating a `Min` or `Max` constraint is
/// prioritized higher than a `Length`, `Min` and `Max` constraints allow for a range
/// of values and excess can (and will) be dumped into these ranges first, if possible,
/// even if it not the last constraint.
///
/// ```plain
/// <----------------------------------- 80 px ------------------------------------>
/// ┌──────20 px───────┐┌────────────────40 px─────────────────┐┌──────20 px───────┐
/// │ Length(20) ││ Min(20) ││ Fixed(20) │
/// └──────────────────┘└──────────────────────────────────────┘└──────────────────┘
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
///
/// <----------------------------------- 80 px ------------------------------------>
/// ┌────────────────40 px─────────────────┐┌──────20 px───────┐┌──────20 px───────┐
/// │ Min(20) ││ Length(20) ││ Fixed(20) │
/// └──────────────────────────────────────┘└──────────────────┘└──────────────────┘
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
/// ```
///
/// Proportional constraints have the lowest priority amongst all the constraints and hence
/// Fill constraints have the lowest priority amongst all the constraints and hence
/// will always take up any excess space available.
///
/// ```plain
/// <----------------------------------- 80 px ------------------------------------>
/// ┌──────20 px───────┐┌──────20 px───────┐┌──────20 px───────┐┌──────20 px───────┐
/// │ Proportional(0) ││ Min(20) ││ Length(20) ││ Fixed(20)
/// │ Fill(0) ││ Max(20) ││ Length(20) ││ Length(20) │
/// └──────────────────┘└──────────────────┘└──────────────────┘└──────────────────┘
/// ^^^^^^ EXCESS ^^^^^^
/// ```
@@ -96,11 +55,6 @@ pub enum Flex {
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌───────────30 px────────────┐┌───────────30 px────────────┐┌──────20 px───────┐
/// │ Percentage(20) ││ Length(20) ││ Fixed(20) │
/// └────────────────────────────┘└────────────────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────────────────────────60 px───────────────────────────┐┌──────20 px───────┐
/// │ Min(20) ││ Max(20) │
/// └──────────────────────────────────────────────────────────┘└──────────────────┘
@@ -110,30 +64,7 @@ pub enum Flex {
/// │ Max(20) │
/// └──────────────────────────────────────────────────────────────────────────────┘
/// ```
#[default]
StretchLast,
/// Always fills the available space within the container.
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐┌──────────────────44 px───────────────────┐┌──────20 px───────┐
/// │Percentage(20)││ Length(20) ││ Fixed(20) │
/// └──────────────┘└──────────────────────────────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────────────────────────60 px───────────────────────────┐┌──────20 px───────┐
/// │ Min(20) ││ Max(20) │
/// └──────────────────────────────────────────────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌────────────────────────────────────80 px─────────────────────────────────────┐
/// │ Max(20) │
/// └──────────────────────────────────────────────────────────────────────────────┘
/// ```
Stretch,
Legacy,
/// Aligns items to the start of the container.
///
@@ -147,7 +78,7 @@ pub enum Flex {
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐┌──────20 px───────┐
/// │ Min(20) ││ Max(20) │
/// │ Max(20) ││ Max(20) │
/// └──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
@@ -155,6 +86,7 @@ pub enum Flex {
/// │ Max(20) │
/// └──────────────────┘
/// ```
#[default]
Start,
/// Aligns items to the end of the container.
@@ -164,12 +96,12 @@ pub enum Flex {
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
/// │Percentage(20)││ Length(20) ││ Fixed(20)
/// │Percentage(20)││ Length(20) ││ Length(20) │
/// └──────────────┘└──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐┌──────20 px───────┐
/// │ Min(20) ││ Max(20) │
/// │ Max(20) ││ Max(20) │
/// └──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
@@ -186,12 +118,12 @@ pub enum Flex {
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
/// │Percentage(20)││ Length(20) ││ Fixed(20)
/// │Percentage(20)││ Length(20) ││ Length(20) │
/// └──────────────┘└──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐┌──────20 px───────┐
/// │ Min(20) ││ Max(20) │
/// │ Max(20) ││ Max(20) │
/// └──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
@@ -206,15 +138,14 @@ pub enum Flex {
/// # Examples
///
/// ```plain
///
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐ ┌──────20 px───────┐ ┌──────20 px───────┐
/// │Percentage(20)│ │ Length(20) │ │ Fixed(20)
/// │Percentage(20)│ │ Length(20) │ │ Length(20) │
/// └──────────────┘ └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐ ┌──────20 px───────┐
/// │ Min(20) │ │ Max(20) │
/// │ Max(20) │ │ Max(20) │
/// └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
@@ -231,12 +162,12 @@ pub enum Flex {
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐ ┌──────20 px───────┐ ┌──────20 px───────┐
/// │Percentage(20)│ │ Length(20) │ │ Fixed(20)
/// │Percentage(20)│ │ Length(20) │ │ Length(20) │
/// └──────────────┘ └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐ ┌──────20 px───────┐
/// │ Min(20) │ │ Max(20) │
/// │ Max(20) │ │ Max(20) │
/// └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ pub struct Margin {
}
impl Margin {
pub const fn new(horizontal: u16, vertical: u16) -> Margin {
Margin {
pub const fn new(horizontal: u16, vertical: u16) -> Self {
Self {
horizontal,
vertical,
}

View File

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

View File

@@ -4,77 +4,49 @@ 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)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rect {
/// The x coordinate of the top left corner of the rect.
/// The x coordinate of the top left corner of the `Rect`.
pub x: u16,
/// The y coordinate of the top left corner of the rect.
/// The y coordinate of the top left corner of the `Rect`.
pub y: u16,
/// The width of the rect.
/// The width of the `Rect`.
pub width: u16,
/// The height of the rect.
/// The height of the `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,
/// 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 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;
impl<X: Into<i32>, Y: Into<i32>> From<(X, Y)> for Offset {
/// Creates a new `Offset` from a tuple of (x, y).
fn from((x, y): (X, Y)) -> Self {
Self {
x: x.into(),
y: y.into(),
}
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
self.current_column += 1;
Some(column)
}
}
@@ -85,9 +57,9 @@ impl fmt::Display for Rect {
}
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16. If
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
/// clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
let max_area = u16::max_value();
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
@@ -99,7 +71,7 @@ impl Rect {
} else {
(width, height)
};
Rect {
Self {
x,
y,
width: clipped_width,
@@ -107,54 +79,57 @@ impl Rect {
}
}
/// The area of the rect. If the area is larger than the maximum value of u16, it will be
/// clamped to u16::MAX.
/// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
/// clamped to `u16::MAX`.
pub const fn area(self) -> u16 {
self.width.saturating_mul(self.height)
}
/// Returns true if the rect has no area.
/// Returns true if the `Rect` has no area.
pub const fn is_empty(self) -> bool {
self.width == 0 || self.height == 0
}
/// Returns the left coordinate of the rect.
/// Returns the left coordinate of the `Rect`.
pub const fn left(self) -> u16 {
self.x
}
/// Returns the right coordinate of the rect. This is the first coordinate outside of the rect.
/// Returns the right coordinate of the `Rect`. This is the first coordinate outside of the
/// `Rect`.
///
/// If the right coordinate is larger than the maximum value of u16, it will be clamped to
/// u16::MAX.
/// `u16::MAX`.
pub const fn right(self) -> u16 {
self.x.saturating_add(self.width)
}
/// Returns the top coordinate of the rect.
/// Returns the top coordinate of the `Rect`.
pub const fn top(self) -> u16 {
self.y
}
/// Returns the bottom coordinate of the rect. This is the first coordinate outside of the rect.
/// Returns the bottom coordinate of the `Rect`. This is the first coordinate outside of the
/// `Rect`.
///
/// If the bottom coordinate is larger than the maximum value of u16, it will be clamped to
/// u16::MAX.
/// `u16::MAX`.
pub const fn bottom(self) -> u16 {
self.y.saturating_add(self.height)
}
/// Returns a new rect inside the current one, with the given margin on each side.
/// Returns a new `Rect` inside the current one, with the given margin on each side.
///
/// If the margin is larger than the rect, the returned rect will have no area.
pub fn inner(self, margin: &Margin) -> Rect {
/// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
#[must_use = "method returns the modified value"]
pub fn inner(self, margin: &Margin) -> Self {
let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
let doubled_margin_vertical = margin.vertical.saturating_mul(2);
if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
Rect::default()
Self::default()
} else {
Rect {
Self {
x: self.x.saturating_add(margin.horizontal),
y: self.y.saturating_add(margin.vertical),
width: self.width.saturating_sub(doubled_margin_horizontal),
@@ -170,26 +145,39 @@ impl Rect {
/// - Positive `x` moves the whole `Rect` to the right, negative to the left.
/// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
///
/// See [`Offset`] for details.
pub fn offset(self, offset: Offset) -> Rect {
Rect {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, layout::Offset};
/// let rect = Rect::new(1, 2, 3, 4);
/// let rect = rect.offset(Offset { x: 10, y: 20 });
/// assert_eq!(rect, Rect::new(11, 22, 3, 4));
///
/// // offset can also be called with a tuple of (x, y)
/// let rect = rect.offset((10, 20));
/// ```
#[must_use = "method returns the modified value"]
pub fn offset<T: Into<Offset>>(self, offset: T) -> Self {
let offset = offset.into();
Self {
x: i32::from(self.x)
.saturating_add(offset.x)
.clamp(0, (u16::MAX - self.width) as i32) as u16,
.clamp(0, i32::from(u16::MAX - self.width)) as u16,
y: i32::from(self.y)
.saturating_add(offset.y)
.clamp(0, (u16::MAX - self.height) as i32) as u16,
.clamp(0, i32::from(u16::MAX - self.height)) as u16,
..self
}
}
/// Returns a new rect that contains both the current one and the given one.
pub fn union(self, other: Rect) -> Rect {
/// Returns a new `Rect` that contains both the current one and the given one.
#[must_use = "method returns the modified value"]
pub fn union(self, other: Self) -> Self {
let x1 = min(self.x, other.x);
let y1 = min(self.y, other.y);
let x2 = max(self.right(), other.right());
let y2 = max(self.bottom(), other.bottom());
Rect {
Self {
x: x1,
y: y1,
width: x2.saturating_sub(x1),
@@ -197,15 +185,16 @@ impl Rect {
}
}
/// Returns a new rect that is the intersection of the current one and the given one.
/// Returns a new `Rect` that is the intersection of the current one and the given one.
///
/// If the two rects do not intersect, the returned rect will have no area.
pub fn intersection(self, other: Rect) -> Rect {
/// If the two `Rect`s do not intersect, the returned `Rect` will have no area.
#[must_use = "method returns the modified value"]
pub fn intersection(self, other: Self) -> Self {
let x1 = max(self.x, other.x);
let y1 = max(self.y, other.y);
let x2 = min(self.right(), other.right());
let y2 = min(self.bottom(), other.bottom());
Rect {
Self {
x: x1,
y: y1,
width: x2.saturating_sub(x1),
@@ -213,59 +202,46 @@ impl Rect {
}
}
/// Returns true if the two rects intersect.
pub const fn intersects(self, other: Rect) -> bool {
/// Returns true if the two `Rect`s intersect.
pub const fn intersects(self, other: Self) -> bool {
self.x < other.right()
&& self.right() > other.x
&& self.y < other.bottom()
&& self.bottom() > other.y
}
/// Split the rect into a number of sub-rects according to the given [`Layout`]`.
/// Returns true if the given position is inside the `Rect`.
///
/// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
/// `Rc<[Rect]>`.
///
/// This method requires the number of constraints to be known at compile time. If you don't
/// know the number of constraints at compile time, use [`Layout::split`] instead.
///
/// # Panics
///
/// Panics if the number of constraints is not equal to the length of the returned array.
/// The position is considered inside the `Rect` if it is on the `Rect`'s border.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = area.split(&layout);
/// // or explicitly specify the number of constraints:
/// let rects = area.split::<2>(&layout);
/// # }
pub fn split<const N: usize>(self, layout: &Layout) -> [Rect; N] {
layout
.split(self)
.to_vec()
.try_into()
.expect("invalid number of rects")
/// # use ratatui::{prelude::*, layout::Position};
/// let rect = Rect::new(1, 2, 3, 4);
/// assert!(rect.contains(Position { x: 1, y: 2 }));
/// ````
pub const fn contains(self, position: Position) -> bool {
position.x >= self.x
&& position.x < self.right()
&& position.y >= self.y
&& position.y < self.bottom()
}
/// Clamp this rect to fit inside the other rect.
/// Clamp this `Rect` to fit inside the other `Rect`.
///
/// If the width or height of this rect is larger than the other rect, it will be clamped to the
/// other rect's width or height.
/// If the width or height of this `Rect` is larger than the other `Rect`, it will be clamped to
/// the other `Rect`'s width or height.
///
/// If the left or top coordinate of this rect is smaller than the other rect, it will be
/// clamped to the other rect's left or top coordinate.
/// If the left or top coordinate of this `Rect` is smaller than the other `Rect`, it will be
/// clamped to the other `Rect`'s left or top coordinate.
///
/// If the right or bottom coordinate of this rect is larger than the other rect, it will be
/// clamped to the other rect's right or bottom coordinate.
/// If the right or bottom coordinate of this `Rect` is larger than the other `Rect`, it will be
/// clamped to the other `Rect`'s right or bottom coordinate.
///
/// This is different from [`Rect::intersection`] because it will move this rect to fit inside
/// the other rect, while [`Rect::intersection`] instead would keep this rect's position and
/// truncate its size to only that which is inside the other rect.
/// This is different from [`Rect::intersection`] because it will move this `Rect` to fit inside
/// the other `Rect`, while [`Rect::intersection`] instead would keep this `Rect`'s position and
/// truncate its size to only that which is inside the other `Rect`.
///
/// # Examples
///
@@ -276,58 +252,66 @@ impl Rect {
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
/// # }
/// ```
pub fn clamp(self, other: Rect) -> Rect {
#[must_use = "method returns the modified value"]
pub fn clamp(self, other: Self) -> Self {
let width = self.width.min(other.width);
let height = self.height.min(other.height);
let x = self.x.clamp(other.x, other.right().saturating_sub(width));
let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
Rect::new(x, y, width, height)
Self::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)
}
/// Returns a [`Position`] with the same coordinates as this rect.
/// 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`.
///
/// # Examples
///
@@ -336,15 +320,15 @@ 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,
}
}
/// Converts the rect into a size struct.
pub fn as_size(self) -> Size {
/// Converts the `Rect` into a size struct.
pub const fn as_size(self) -> Size {
Size {
width: self.width,
height: self.height,
@@ -354,7 +338,7 @@ impl Rect {
impl From<(Position, Size)> for Rect {
fn from((position, size): (Position, Size)) -> Self {
Rect {
Self {
x: position.x,
y: position.y,
width: size.width,
@@ -460,6 +444,11 @@ mod tests {
);
}
#[test]
fn offset_from_tuple() {
assert_eq!(Rect::new(1, 2, 3, 4).offset((5, 6)), Rect::new(6, 8, 3, 4));
}
#[test]
fn union() {
assert_eq!(
@@ -490,6 +479,26 @@ mod tests {
assert!(!Rect::new(1, 2, 3, 4).intersects(Rect::new(5, 6, 7, 8)));
}
// the bounds of this rect are x: [1..=3], y: [2..=5]
#[rstest]
#[case::inside_top_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 2 }, true)]
#[case::inside_top_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 2 }, true)]
#[case::inside_bottom_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 5 }, true)]
#[case::inside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 5 }, true)]
#[case::outside_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 2 }, false)]
#[case::outside_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 2 }, false)]
#[case::outside_top(Rect::new(1, 2, 3, 4), Position { x: 1, y: 1 }, false)]
#[case::outside_bottom(Rect::new(1, 2, 3, 4), Position { x: 1, y: 6 }, false)]
#[case::outside_top_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 1 }, false)]
#[case::outside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 6 }, false)]
fn contains(#[case] rect: Rect, #[case] position: Position, #[case] expected: bool) {
assert_eq!(
rect.contains(position),
expected,
"rect: {rect:?}, position: {position:?}",
);
}
#[test]
fn size_truncation() {
for width in 256u16..300u16 {
@@ -552,8 +561,8 @@ mod tests {
#[test]
fn split() {
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [a, b] = Rect::new(0, 0, 2, 1).split(&layout);
let [a, b] = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(Rect::new(0, 0, 2, 1));
assert_eq!(a, Rect::new(0, 0, 1, 1));
assert_eq!(b, Rect::new(1, 0, 1, 1));
}
@@ -562,7 +571,7 @@ mod tests {
#[should_panic(expected = "invalid number of rects")]
fn split_invalid_number_of_recs() {
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [_a, _b, _c] = Rect::new(0, 0, 2, 1).split(&layout);
let [_a, _b, _c] = layout.areas(Rect::new(0, 0, 2, 1));
}
#[rstest]

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

@@ -0,0 +1,144 @@
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::*;
#[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

@@ -1,147 +0,0 @@
use strum::{Display, EnumString};
/// Option for segment size preferences
///
/// This controls how the space is distributed when the constraints are satisfied. By default, the
/// last chunk is expanded to fill the remaining space, but this can be changed to prefer equal
/// chunks or to not distribute extra space at all (which is the default used for laying out the
/// columns for [`Table`] widgets).
///
/// Note: If you're using this feature please help us come up with a good name. See [Issue
/// #536](https://github.com/ratatui-org/ratatui/issues/536) for more information.
///
/// [`Table`]: crate::widgets::Table
#[stability::unstable(
feature = "segment-size",
reason = "The name for this feature is not final and may change in the future",
issue = "https://github.com/ratatui-org/ratatui/issues/536"
)]
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
pub enum SegmentSize {
/// prefer equal chunks if other constraints are all satisfied
EvenDistribution,
/// the last chunk is expanded to fill the remaining space
#[default]
LastTakesRemainder,
/// extra space is not distributed
None,
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::{SegmentSize::*, *};
use crate::prelude::{Constraint::*, *};
#[test]
fn segment_size_to_string() {
assert_eq!(EvenDistribution.to_string(), "EvenDistribution");
assert_eq!(LastTakesRemainder.to_string(), "LastTakesRemainder");
assert_eq!(None.to_string(), "None");
}
#[test]
fn segment_size_from_string() {
assert_eq!(
"EvenDistribution".parse::<SegmentSize>(),
Ok(EvenDistribution)
);
assert_eq!(
"LastTakesRemainder".parse::<SegmentSize>(),
Ok(LastTakesRemainder)
);
assert_eq!("None".parse::<SegmentSize>(), Ok(None));
assert_eq!("".parse::<SegmentSize>(), Err(ParseError::VariantNotFound));
}
fn get_x_width_with_segment_size(
segment_size: SegmentSize,
constraints: Vec<Constraint>,
target: Rect,
) -> Vec<(u16, u16)> {
#[allow(deprecated)]
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.segment_size(segment_size);
let chunks = layout.split(target);
chunks.iter().map(|r| (r.x, r.width)).collect()
}
#[test]
fn test_split_equally_in_underspecified_case() {
let target = Rect::new(100, 200, 10, 10);
assert_eq!(
get_x_width_with_segment_size(LastTakesRemainder, vec![Min(2), Min(2), Min(0)], target),
[(100, 2), (102, 2), (104, 6)]
);
assert_eq!(
get_x_width_with_segment_size(EvenDistribution, vec![Min(2), Min(2), Min(0)], target),
[(100, 3), (103, 4), (107, 3)]
);
}
#[test]
fn test_split_equally_in_overconstrained_case_for_min() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(50), Min(10), Percentage(50)],
target
),
[(100, 50), (150, 10), (160, 40)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(50), Min(10), Percentage(50)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}
#[test]
fn test_split_equally_in_overconstrained_case_for_max() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(30), Max(10), Percentage(30)],
target
),
[(100, 30), (130, 10), (140, 60)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(30), Max(10), Percentage(30)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}
#[test]
fn test_split_equally_in_overconstrained_case_for_length() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(50), Length(10), Percentage(50)],
target
),
[(100, 50), (150, 10), (160, 40)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(50), Length(10), Percentage(50)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}
}

View File

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

View File

@@ -1,11 +1,9 @@
#![forbid(unsafe_code)]
//! ![Demo](https://github.com/ratatui-org/ratatui/blob/1d39444e3dea6f309cf9035be2417ac711c1abc9/examples/demo2-destroy.gif?raw=true)
//!
//! <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 +38,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 +300,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 +322,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 +340,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

@@ -23,6 +23,7 @@ pub use crate::backend::CrosstermBackend;
pub use crate::backend::TermionBackend;
#[cfg(feature = "termwiz")]
pub use crate::backend::TermwizBackend;
pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef};
pub use crate::{
backend::{self, Backend},
buffer::{self, Buffer},

View File

@@ -33,7 +33,7 @@
//! that implements [`Styled`]. E.g.:
//! - Strings and string slices when styled return a [`Span`]
//! - [`Span`]s can be styled again, which will merge the styles.
//! - Many widget types can be styled directly rather than calling their style() method.
//! - Many widget types can be styled directly rather than calling their `style()` method.
//!
//! See the [`Stylize`] and [`Styled`] traits for more information. These traits are re-exported in
//! the [`prelude`] module for convenience.
@@ -234,26 +234,26 @@ pub struct Style {
}
impl Default for Style {
fn default() -> Style {
Style::new()
fn default() -> Self {
Self::new()
}
}
impl Styled for Style {
type Item = Style;
type Item = Self;
fn style(&self) -> Style {
*self
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
fn set_style<S: Into<Self>>(self, style: S) -> Self::Item {
self.patch(style)
}
}
impl Style {
pub const fn new() -> Style {
Style {
pub const fn new() -> Self {
Self {
fg: None,
bg: None,
#[cfg(feature = "underline-color")]
@@ -264,8 +264,8 @@ impl Style {
}
/// Returns a `Style` resetting all properties.
pub const fn reset() -> Style {
Style {
pub const fn reset() -> Self {
Self {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
#[cfg(feature = "underline-color")]
@@ -286,7 +286,7 @@ impl Style {
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
/// ```
#[must_use = "`fg` returns the modified style without modifying the original"]
pub const fn fg(mut self, color: Color) -> Style {
pub const fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
@@ -302,7 +302,7 @@ impl Style {
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
/// ```
#[must_use = "`bg` returns the modified style without modifying the original"]
pub const fn bg(mut self, color: Color) -> Style {
pub const fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
@@ -336,7 +336,7 @@ impl Style {
/// ```
#[cfg(feature = "underline-color")]
#[must_use = "`underline_color` returns the modified style without modifying the original"]
pub const fn underline_color(mut self, color: Color) -> Style {
pub const fn underline_color(mut self, color: Color) -> Self {
self.underline_color = Some(color);
self
}
@@ -356,7 +356,7 @@ impl Style {
/// assert_eq!(patched.sub_modifier, Modifier::empty());
/// ```
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
pub const fn add_modifier(mut self, modifier: Modifier) -> Style {
pub const fn add_modifier(mut self, modifier: Modifier) -> Self {
self.sub_modifier = self.sub_modifier.difference(modifier);
self.add_modifier = self.add_modifier.union(modifier);
self
@@ -377,7 +377,7 @@ impl Style {
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
/// ```
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
pub const fn remove_modifier(mut self, modifier: Modifier) -> Style {
pub const fn remove_modifier(mut self, modifier: Modifier) -> Self {
self.add_modifier = self.add_modifier.difference(modifier);
self.sub_modifier = self.sub_modifier.union(modifier);
self
@@ -401,7 +401,7 @@ impl Style {
/// );
/// ```
#[must_use = "`patch` returns the modified style without modifying the original"]
pub fn patch<S: Into<Style>>(mut self, other: S) -> Style {
pub fn patch<S: Into<Self>>(mut self, other: S) -> Self {
let other = other.into();
self.fg = other.fg.or(self.fg);
self.bg = other.bg.or(self.bg);
@@ -660,9 +660,10 @@ mod tests {
.bg(Color::Black)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
)
);
}
#[allow(clippy::too_many_lines)]
#[test]
fn style_can_be_stylized() {
// foreground colors

View File

@@ -1,3 +1,5 @@
#![allow(clippy::unreadable_literal)]
use std::{
fmt::{self, Debug, Display},
str::FromStr,
@@ -131,11 +133,11 @@ impl Color {
/// Convert a u32 to a Color
///
/// The u32 should be in the format 0x00RRGGBB.
pub const fn from_u32(u: u32) -> Color {
pub const fn from_u32(u: u32) -> Self {
let r = (u >> 16) as u8;
let g = (u >> 8) as u8;
let b = u as u8;
Color::Rgb(r, g, b)
Self::Rgb(r, g, b)
}
}
@@ -250,25 +252,25 @@ impl FromStr for Color {
impl Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Color::Reset => write!(f, "Reset"),
Color::Black => write!(f, "Black"),
Color::Red => write!(f, "Red"),
Color::Green => write!(f, "Green"),
Color::Yellow => write!(f, "Yellow"),
Color::Blue => write!(f, "Blue"),
Color::Magenta => write!(f, "Magenta"),
Color::Cyan => write!(f, "Cyan"),
Color::Gray => write!(f, "Gray"),
Color::DarkGray => write!(f, "DarkGray"),
Color::LightRed => write!(f, "LightRed"),
Color::LightGreen => write!(f, "LightGreen"),
Color::LightYellow => write!(f, "LightYellow"),
Color::LightBlue => write!(f, "LightBlue"),
Color::LightMagenta => write!(f, "LightMagenta"),
Color::LightCyan => write!(f, "LightCyan"),
Color::White => write!(f, "White"),
Color::Rgb(r, g, b) => write!(f, "#{:02X}{:02X}{:02X}", r, g, b),
Color::Indexed(i) => write!(f, "{}", i),
Self::Reset => write!(f, "Reset"),
Self::Black => write!(f, "Black"),
Self::Red => write!(f, "Red"),
Self::Green => write!(f, "Green"),
Self::Yellow => write!(f, "Yellow"),
Self::Blue => write!(f, "Blue"),
Self::Magenta => write!(f, "Magenta"),
Self::Cyan => write!(f, "Cyan"),
Self::Gray => write!(f, "Gray"),
Self::DarkGray => write!(f, "DarkGray"),
Self::LightRed => write!(f, "LightRed"),
Self::LightGreen => write!(f, "LightGreen"),
Self::LightYellow => write!(f, "LightYellow"),
Self::LightBlue => write!(f, "LightBlue"),
Self::LightMagenta => write!(f, "LightMagenta"),
Self::LightCyan => write!(f, "LightCyan"),
Self::White => write!(f, "White"),
Self::Rgb(r, g, b) => write!(f, "#{r:02X}{g:02X}{b:02X}"),
Self::Indexed(i) => write!(f, "{i}"),
}
}
}
@@ -308,8 +310,8 @@ impl Color {
/// Converts normalized HSL (Hue, Saturation, Lightness) values to RGB (Red, Green, Blue) color
/// representation. H, S, and L values should be in the range [0, 1].
///
/// Based on https://github.com/killercup/hsl-rs/blob/b8a30e11afd75f262e0550725333293805f4ead0/src/lib.rs
fn normalized_hsl_to_rgb(h: f64, s: f64, l: f64) -> Color {
/// Based on <https://github.com/killercup/hsl-rs/blob/b8a30e11afd75f262e0550725333293805f4ead0/src/lib.rs>
fn normalized_hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) -> Color {
// This function can be made into `const` in the future.
// This comment contains the relevant information for making it `const`.
//
@@ -329,33 +331,33 @@ fn normalized_hsl_to_rgb(h: f64, s: f64, l: f64) -> Color {
// ```
// Initialize RGB components
let r: f64;
let g: f64;
let b: f64;
let red: f64;
let green: f64;
let blue: f64;
// Check if the color is achromatic (grayscale)
if s == 0.0 {
r = l;
g = l;
b = l;
if saturation == 0.0 {
red = lightness;
green = lightness;
blue = lightness;
} else {
// Calculate RGB components for colored cases
let q = if l < 0.5 {
l * (1.0 + s)
let q = if lightness < 0.5 {
lightness * (1.0 + saturation)
} else {
l + s - l * s
lightness + saturation - lightness * saturation
};
let p = 2.0 * l - q;
r = hue_to_rgb(p, q, h + 1.0 / 3.0);
g = hue_to_rgb(p, q, h);
b = hue_to_rgb(p, q, h - 1.0 / 3.0);
let p = 2.0 * lightness - q;
red = hue_to_rgb(p, q, hue + 1.0 / 3.0);
green = hue_to_rgb(p, q, hue);
blue = hue_to_rgb(p, q, hue - 1.0 / 3.0);
}
// Scale RGB components to the range [0, 255] and create a Color::Rgb instance
Color::Rgb(
(r * 255.0).round() as u8,
(g * 255.0).round() as u8,
(b * 255.0).round() as u8,
(red * 255.0).round() as u8,
(green * 255.0).round() as u8,
(blue * 255.0).round() as u8,
)
}

View File

@@ -1,3 +1,5 @@
#![allow(clippy::unreadable_literal)]
//! A module for defining color palettes.
pub mod material;

View File

@@ -455,11 +455,11 @@ pub struct NonAccentedPalette {
}
impl AccentedPalette {
/// Create a new AccentedPalette from the given variants
/// Create a new `AccentedPalette` from the given variants
///
/// The variants should be in the format [0x00RRGGBB, ...]
pub const fn from_variants(variants: [u32; 14]) -> AccentedPalette {
AccentedPalette {
pub const fn from_variants(variants: [u32; 14]) -> Self {
Self {
c50: Color::from_u32(variants[0]),
c100: Color::from_u32(variants[1]),
c200: Color::from_u32(variants[2]),
@@ -479,11 +479,11 @@ impl AccentedPalette {
}
impl NonAccentedPalette {
/// Create a new NonAccented from the given variants
/// Create a new `NonAccented` from the given variants
///
/// The variants should be in the format [0x00RRGGBB, ...]
pub const fn from_variants(variants: [u32; 10]) -> NonAccentedPalette {
NonAccentedPalette {
pub const fn from_variants(variants: [u32; 10]) -> Self {
Self {
c50: Color::from_u32(variants[0]),
c100: Color::from_u32(variants[1]),
c200: Color::from_u32(variants[2]),

View File

@@ -348,7 +348,7 @@ mod tests {
// format!() is used to create a temporary String inside a closure, which suffers the same
// issue as above without the `Styled` trait impl for `String`
let items = vec![String::from("a"), String::from("b")];
let items = [String::from("a"), String::from("b")];
let sss = items.iter().map(|s| format!("{s}{s}").red()).collect_vec();
assert_eq!(sss, vec![Span::from("aa").red(), Span::from("bb").red()]);
}
@@ -393,12 +393,12 @@ mod tests {
#[test]
fn repeated_attributes() {
let cyan_bg = Style::default().bg(Color::Cyan);
let cyan_fg = Style::default().fg(Color::Cyan);
let bg = Style::default().bg(Color::Cyan);
let fg = Style::default().fg(Color::Cyan);
// Behavior: the last one set is the definitive one
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", cyan_bg));
assert_eq!("hello".red().cyan(), Span::styled("hello", cyan_fg));
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", bg));
assert_eq!("hello".red().cyan(), Span::styled("hello", fg));
}
#[test]

View File

@@ -395,6 +395,7 @@ pub mod border {
/// ▏xxxxx▕
/// ▔▔▔▔▔▔▔
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_WIDE: Set = Set {
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
@@ -414,6 +415,7 @@ pub mod border {
/// ▕xx▏
/// ▕▁▁▏
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_TALL: Set = Set {
top_right: ONE_EIGHTH_LEFT_EIGHT,
top_left: ONE_EIGHTH_RIGHT_EIGHT,

View File

@@ -49,7 +49,7 @@ impl Frame<'_> {
/// If your app listens for a resize event from the backend, it should ignore the values from
/// the event for any calculations that are used to render the current frame and use this value
/// instead as this is the size of the buffer that is used to render the current frame.
pub fn size(&self) -> Rect {
pub const fn size(&self) -> Rect {
self.viewport_area
}
@@ -75,6 +75,28 @@ impl Frame<'_> {
widget.render(area, self.buffer);
}
/// Render a [`WidgetRef`] to the current buffer using [`WidgetRef::render_ref`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// # Example
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let block = Block::default();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget_ref(block, area);
/// ```
#[allow(clippy::needless_pass_by_value)]
#[stability::unstable(feature = "widget-ref")]
pub fn render_widget_ref<W: WidgetRef>(&mut self, widget: W, area: Rect) {
widget.render_ref(area, self.buffer);
}
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
@@ -83,7 +105,7 @@ impl Frame<'_> {
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
/// given [`StatefulWidget`].
///
/// # Examples
/// # Example
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
@@ -104,6 +126,36 @@ impl Frame<'_> {
widget.render(area, self.buffer, state);
}
/// Render a [`StatefulWidgetRef`] to the current buffer using
/// [`StatefulWidgetRef::render_ref`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// The last argument should be an instance of the [`StatefulWidgetRef::State`] associated to
/// the given [`StatefulWidgetRef`].
///
/// # Example
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let mut state = ListState::default().with_selected(Some(1));
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_stateful_widget_ref(list, area, &mut state);
/// ```
#[allow(clippy::needless_pass_by_value)]
#[stability::unstable(feature = "widget-ref")]
pub fn render_stateful_widget_ref<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidgetRef,
{
widget.render_ref(area, self.buffer, state);
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
@@ -127,7 +179,7 @@ impl Frame<'_> {
///
/// Each time a frame has been rendered, this count is incremented,
/// providing a consistent way to reference the order and number of frames processed by the
/// terminal. When count reaches its maximum value (usize::MAX), it wraps around to zero.
/// terminal. When count reaches its maximum value (`usize::MAX`), it wraps around to zero.
///
/// This count is particularly useful when dealing with dynamic content or animations where the
/// state of the display changes over time. By tracking the frame count, developers can
@@ -143,7 +195,7 @@ impl Frame<'_> {
/// let current_count = frame.count();
/// println!("Current frame count: {}", current_count);
/// ```
pub fn count(&self) -> usize {
pub const fn count(&self) -> usize {
self.count
}
}

View File

@@ -111,8 +111,8 @@ where
/// let terminal = Terminal::new(backend)?;
/// # std::io::Result::Ok(())
/// ```
pub fn new(backend: B) -> io::Result<Terminal<B>> {
Terminal::with_options(
pub fn new(backend: B) -> io::Result<Self> {
Self::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
@@ -132,7 +132,7 @@ where
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Self> {
let size = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
Viewport::Fixed(area) => area,
@@ -142,7 +142,7 @@ where
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
Viewport::Fixed(area) => (area, (area.left(), area.top())),
};
Ok(Terminal {
Ok(Self {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
@@ -172,7 +172,7 @@ where
}
/// Gets the backend
pub fn backend(&self) -> &B {
pub const fn backend(&self) -> &B {
&self.backend
}

View File

@@ -31,9 +31,9 @@ pub enum Viewport {
impl fmt::Display for Viewport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Viewport::Fullscreen => write!(f, "Fullscreen"),
Viewport::Inline(height) => write!(f, "Inline({})", height),
Viewport::Fixed(area) => write!(f, "Fixed({})", area),
Self::Fullscreen => write!(f, "Fullscreen"),
Self::Inline(height) => write!(f, "Inline({height})"),
Self::Fixed(area) => write!(f, "Fixed({area})"),
}
}
}

View File

@@ -16,8 +16,8 @@ impl<'a> StyledGrapheme<'a> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> StyledGrapheme<'a> {
StyledGrapheme {
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> Self {
Self {
symbol,
style: style.into(),
}
@@ -25,7 +25,7 @@ impl<'a> StyledGrapheme<'a> {
}
impl<'a> Styled for StyledGrapheme<'a> {
type Item = StyledGrapheme<'a>;
type Item = Self;
fn style(&self) -> Style {
self.style

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.
///
@@ -10,28 +10,21 @@ use crate::{prelude::*, widgets::Widget};
/// text. When a [`Line`] is rendered, it is rendered as a single line of text, with each [`Span`]
/// being rendered in order (left to right).
///
/// [`Line`]s can be created from [`Span`]s, [`String`]s, and [`&str`]s. They can be styled with a
/// [`Style`], and have an [`Alignment`].
///
/// The line's [`Alignment`] is used by the rendering widget to determine how to align the line
/// within the available space. If the line is longer than the available space, the alignment is
/// ignored and the line is truncated.
///
/// The line's [`Style`] is used by the rendering widget to determine how to style the line. If the
/// line is longer than the available space, the style is applied to the entire line, and the line
/// is truncated. Each [`Span`] in the line will be styled with the [`Style`] of the line, and then
/// with its own [`Style`].
///
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`]. Usually
/// apps will use the [`Paragraph`] widget instead of rendering a [`Line`] directly as it provides
/// more functionality.
///
/// # Constructor Methods
///
/// - [`Line::default`] creates a line with empty content and the default style.
/// - [`Line::raw`] creates a line with the given content and the default style.
/// - [`Line::styled`] creates a line with the given content and style.
///
/// # Conversion Methods
///
/// - [`Line::from`] creates a `Line` from a [`String`].
/// - [`Line::from`] creates a `Line` from a [`&str`].
/// - [`Line::from`] creates a `Line` from a [`Vec`] of [`Span`]s.
/// - [`Line::from`] creates a `Line` from single [`Span`].
/// - [`String::from`] converts a line into a [`String`].
/// - [`Line::from_iter`] creates a line from an iterator of items that are convertible to [`Span`].
///
/// # Setter Methods
///
/// These methods are fluent setters. They return a `Line` with the property set.
@@ -39,6 +32,15 @@ use crate::{prelude::*, widgets::Widget};
/// - [`Line::spans`] sets the content of the line.
/// - [`Line::style`] sets the style of the line.
/// - [`Line::alignment`] sets the alignment of the line.
/// - [`Line::left_aligned`] sets the alignment of the line to [`Alignment::Left`].
/// - [`Line::centered`] sets the alignment of the line to [`Alignment::Center`].
/// - [`Line::right_aligned`] sets the alignment of the line to [`Alignment::Right`].
///
/// # Iteration Methods
///
/// - [`Line::iter`] returns an iterator over the spans of this line.
/// - [`Line::iter_mut`] returns a mutable iterator over the spans of this line.
/// - [`Line::into_iter`] returns an iterator over the spans of this line.
///
/// # Other Methods
///
@@ -46,6 +48,7 @@ use crate::{prelude::*, widgets::Widget};
/// - [`Line::reset_style`] resets the style of the line.
/// - [`Line::width`] returns the unicode width of the content held by this line.
/// - [`Line::styled_graphemes`] returns an iterator over the graphemes held by this line.
/// - [`Line::push_span`] adds a span to the line.
///
/// # Compatibility Notes
///
@@ -56,19 +59,90 @@ use crate::{prelude::*, widgets::Widget};
///
/// # Examples
///
/// ## Creating Lines
/// [`Line`]s can be created from [`Span`]s, [`String`]s, and [`&str`]s. They can be styled with a
/// [`Style`].
///
/// ```rust
/// use ratatui::prelude::*;
///
/// Line::raw("unstyled");
/// Line::styled("yellow text", Style::new().yellow());
/// Line::from("red text").style(Style::new().red());
/// Line::from(String::from("unstyled"));
/// Line::from(vec![
/// let style = Style::new().yellow();
/// let line = Line::raw("Hello, world!").style(style);
/// let line = Line::styled("Hello, world!", style);
/// let line = Line::styled("Hello, world!", (Color::Yellow, Modifier::BOLD));
///
/// let line = Line::from("Hello, world!");
/// let line = Line::from(String::from("Hello, world!"));
/// let line = Line::from(vec![
/// Span::styled("Hello", Style::new().blue()),
/// Span::raw(" world!"),
/// ]);
/// ```
///
/// ## Styling Lines
///
/// The line's [`Style`] is used by the rendering widget to determine how to style the line. Each
/// [`Span`] in the line will be styled with the [`Style`] of the line, and then with its own
/// [`Style`]. If the line is longer than the available space, the style is applied to the entire
/// line, and the line is truncated. `Line` also implements [`Styled`] which means you can use the
/// methods of the [`Stylize`] trait.
///
/// ```rust
/// # use ratatui::prelude::*;
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
/// let line = Line::from("Hello world!").style(Color::Yellow);
/// let line = Line::from("Hello world!").style((Color::Yellow, Color::Black));
/// let line = Line::from("Hello world!").style((Color::Yellow, Modifier::ITALIC));
/// let line = Line::from("Hello world!").yellow().italic();
/// ```
///
/// ## Aligning Lines
///
/// The line's [`Alignment`] is used by the rendering widget to determine how to align the line
/// within the available space. If the line is longer than the available space, the alignment is
/// ignored and the line is truncated.
///
/// ```rust
/// # use ratatui::prelude::*;
/// let line = Line::from("Hello world!").alignment(Alignment::Right);
/// let line = Line::from("Hello world!").centered();
/// let line = Line::from("Hello world!").left_aligned();
/// let line = Line::from("Hello world!").right_aligned();
/// ```
///
/// ## Rendering Lines
///
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`].
///
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(area: Rect, buf: &mut Buffer) {
/// // in another widget's render method
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
/// line.render(area, buf);
/// # }
///
/// # fn draw(frame: &mut Frame, area: Rect) {
/// // in a terminal.draw closure
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
/// frame.render_widget(line, area);
/// # }
/// ```
/// ## Rendering Lines with a Paragraph widget
///
/// Usually apps will use the [`Paragraph`] widget instead of rendering a [`Line`] directly as it
/// provides more functionality.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let line = Line::from("Hello world!").yellow().italic();
/// Paragraph::new(line)
/// .wrap(Wrap { trim: true })
/// .render(area, buf);
/// # }
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Line<'a> {
@@ -102,11 +176,11 @@ impl<'a> Line<'a> {
/// Line::raw(String::from("test content"));
/// Line::raw(Cow::from("test content"));
/// ```
pub fn raw<T>(content: T) -> Line<'a>
pub fn raw<T>(content: T) -> Self
where
T: Into<Cow<'a, str>>,
{
Line {
Self {
spans: content
.into()
.lines()
@@ -135,12 +209,12 @@ impl<'a> Line<'a> {
/// Line::styled(String::from("My text"), style);
/// Line::styled(Cow::from("test content"), style);
/// ```
pub fn styled<T, S>(content: T, style: S) -> Line<'a>
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
S: Into<Style>,
{
Line {
Self {
spans: content
.into()
.lines()
@@ -368,6 +442,60 @@ impl<'a> Line<'a> {
pub fn reset_style(self) -> Self {
self.patch_style(Style::reset())
}
/// Returns an iterator over the spans of this line.
pub fn iter(&self) -> std::slice::Iter<Span<'a>> {
self.spans.iter()
}
/// Returns a mutable iterator over the spans of this line.
pub fn iter_mut(&mut self) -> std::slice::IterMut<Span<'a>> {
self.spans.iter_mut()
}
/// Adds a span to the line.
///
/// `span` can be any type that is convertible into a `Span`. For example, you can pass a
/// `&str`, a `String`, or a `Span`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut line = Line::from("Hello, ");
/// line.push_span(Span::raw("world!"));
/// line.push_span(" How are you?");
/// ```
pub fn push_span<T: Into<Span<'a>>>(&mut self, span: T) {
self.spans.push(span.into());
}
}
impl<'a> IntoIterator for Line<'a> {
type Item = Span<'a>;
type IntoIter = std::vec::IntoIter<Span<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.spans.into_iter()
}
}
impl<'a> IntoIterator for &'a Line<'a> {
type Item = &'a Span<'a>;
type IntoIter = std::slice::Iter<'a, Span<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a> IntoIterator for &'a mut Line<'a> {
type Item = &'a mut Span<'a>;
type IntoIter = std::slice::IterMut<'a, Span<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
impl<'a> From<String> for Line<'a> {
@@ -398,43 +526,41 @@ impl<'a> From<Span<'a>> for Line<'a> {
}
impl<'a> From<Line<'a>> for String {
fn from(line: Line<'a>) -> String {
line.spans.iter().fold(String::new(), |mut acc, s| {
fn from(line: Line<'a>) -> Self {
line.iter().fold(Self::new(), |mut acc, s| {
acc.push_str(s.content.as_ref());
acc
})
}
}
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) {
Widget::render(&self, area, buf);
self.render_ref(area, buf);
}
}
/// Implement [`Widget`] for [`Option<Line>`] to simplify the common case of having an optional
/// [`Line`] field in a widget.
impl Widget for &Option<Line<'_>> {
fn render(self, area: Rect, buf: &mut Buffer) {
if let Some(line) = self {
line.render(area, buf);
}
}
}
impl Widget for &Line<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
impl WidgetRef for Line<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
buf.set_style(area, self.style);
let width = self.width() as u16;
let offset = match self.alignment {
Some(Alignment::Left) => 0,
Some(Alignment::Center) => (area.width.saturating_sub(width)) / 2,
Some(Alignment::Right) => area.width.saturating_sub(width),
None => 0,
Some(Alignment::Left) | None => 0,
};
let mut x = area.left().saturating_add(offset);
for span in self.spans.iter() {
for span in &self.spans {
let span_width = span.width() as u16;
let span_area = Rect {
x,
@@ -459,10 +585,31 @@ impl std::fmt::Display for Line<'_> {
}
}
impl<'a> Styled for Line<'a> {
type Item = Self;
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::*;
#[fixture]
fn small_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
#[test]
fn raw_str() {
let line = Line::raw("test content");
@@ -572,6 +719,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!");
@@ -596,6 +753,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));
@@ -665,6 +848,35 @@ mod tests {
assert_eq!(format!("{line_from_styled_span}"), "Hello, world!");
}
#[test]
fn left_aligned() {
let line = Line::from("Hello, world!").left_aligned();
assert_eq!(line.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let line = Line::from("Hello, world!").centered();
assert_eq!(line.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let line = Line::from("Hello, world!").right_aligned();
assert_eq!(line.alignment, Some(Alignment::Right));
}
#[test]
pub fn push_span() {
let mut line = Line::from("A");
line.push_span(Span::raw("B"));
line.push_span("C");
assert_eq!(
line.spans,
vec![Span::raw("A"), Span::raw("B"), Span::raw("C")]
);
}
mod widget {
use super::*;
use crate::assert_buffer_eq;
@@ -672,6 +884,7 @@ mod tests {
const GREEN: Style = Style::new().fg(Color::Green);
const ITALIC: Style = Style::new().add_modifier(Modifier::ITALIC);
#[fixture]
fn hello_world() -> Line<'static> {
Line::from(vec![
Span::styled("Hello ", BLUE),
@@ -691,6 +904,13 @@ mod tests {
assert_buffer_eq!(buf, expected);
}
#[rstest]
fn render_out_of_bounds(hello_world: Line<'static>, mut small_buf: Buffer) {
let out_of_bounds = Rect::new(20, 20, 10, 1);
hello_world.render(out_of_bounds, &mut small_buf);
assert_buffer_eq!(small_buf, Buffer::empty(small_buf.area));
}
#[test]
fn render_only_styles_line_area() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
@@ -735,21 +955,91 @@ mod tests {
}
}
#[test]
fn left_aligned() {
let line = Line::from("Hello, world!").left_aligned();
assert_eq!(line.alignment, Some(Alignment::Left));
}
mod iterators {
use super::*;
#[test]
fn centered() {
let line = Line::from("Hello, world!").centered();
assert_eq!(line.alignment, Some(Alignment::Center));
}
/// a fixture used in the tests below to avoid repeating the same setup
#[fixture]
fn hello_world() -> Line<'static> {
Line::from(vec![
Span::styled("Hello ", Color::Blue),
Span::styled("world!", Color::Green),
])
}
#[test]
fn right_aligned() {
let line = Line::from("Hello, world!").right_aligned();
assert_eq!(line.alignment, Some(Alignment::Right));
#[rstest]
fn iter(hello_world: Line<'_>) {
let mut iter = hello_world.iter();
assert_eq!(iter.next(), Some(&Span::styled("Hello ", Color::Blue)));
assert_eq!(iter.next(), Some(&Span::styled("world!", Color::Green)));
assert_eq!(iter.next(), None);
}
#[rstest]
fn iter_mut(mut hello_world: Line<'_>) {
let mut iter = hello_world.iter_mut();
assert_eq!(iter.next(), Some(&mut Span::styled("Hello ", Color::Blue)));
assert_eq!(iter.next(), Some(&mut Span::styled("world!", Color::Green)));
assert_eq!(iter.next(), None);
}
#[rstest]
fn into_iter(hello_world: Line<'_>) {
let mut iter = hello_world.into_iter();
assert_eq!(iter.next(), Some(Span::styled("Hello ", Color::Blue)));
assert_eq!(iter.next(), Some(Span::styled("world!", Color::Green)));
assert_eq!(iter.next(), None);
}
#[rstest]
fn into_iter_ref(hello_world: Line<'_>) {
let mut iter = (&hello_world).into_iter();
assert_eq!(iter.next(), Some(&Span::styled("Hello ", Color::Blue)));
assert_eq!(iter.next(), Some(&Span::styled("world!", Color::Green)));
assert_eq!(iter.next(), None);
}
#[test]
fn into_iter_mut_ref() {
let mut hello_world = Line::from(vec![
Span::styled("Hello ", Color::Blue),
Span::styled("world!", Color::Green),
]);
let mut iter = (&mut hello_world).into_iter();
assert_eq!(iter.next(), Some(&mut Span::styled("Hello ", Color::Blue)));
assert_eq!(iter.next(), Some(&mut Span::styled("world!", Color::Green)));
assert_eq!(iter.next(), None);
}
#[rstest]
fn for_loop_ref(hello_world: Line<'_>) {
let mut result = String::new();
for span in &hello_world {
result.push_str(span.content.as_ref());
}
assert_eq!(result, "Hello world!");
}
#[rstest]
fn for_loop_mut_ref() {
let mut hello_world = Line::from(vec![
Span::styled("Hello ", Color::Blue),
Span::styled("world!", Color::Green),
]);
let mut result = String::new();
for span in &mut hello_world {
result.push_str(span.content.as_ref());
}
assert_eq!(result, "Hello world!");
}
#[rstest]
fn for_loop_into(hello_world: Line<'_>) {
let mut result = String::new();
for span in hello_world {
result.push_str(span.content.as_ref());
}
assert_eq!(result, "Hello world!");
}
}
}

View File

@@ -36,7 +36,7 @@ impl<'a> Masked<'a> {
}
/// The character to use for masking.
pub fn mask_char(&self) -> char {
pub const fn mask_char(&self) -> char {
self.mask_char
}
@@ -49,37 +49,37 @@ impl<'a> Masked<'a> {
impl Debug for Masked<'_> {
/// Debug representation of a masked string is the underlying string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.inner).map_err(|_| fmt::Error)
Display::fmt(&self.inner, f)
}
}
impl Display for Masked<'_> {
/// Display representation of a masked string is the masked string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.value()).map_err(|_| fmt::Error)
Display::fmt(&self.value(), f)
}
}
impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
fn from(masked: &'a Masked) -> Cow<'a, str> {
fn from(masked: &'a Masked) -> Self {
masked.value()
}
}
impl<'a> From<Masked<'a>> for Cow<'a, str> {
fn from(masked: Masked<'a>) -> Cow<'a, str> {
fn from(masked: Masked<'a>) -> Self {
masked.value()
}
}
impl<'a> From<&'a Masked<'_>> for Text<'a> {
fn from(masked: &'a Masked) -> Text<'a> {
fn from(masked: &'a Masked) -> Self {
Text::raw(masked.value())
}
}
impl<'a> From<Masked<'a>> for Text<'a> {
fn from(masked: Masked<'a>) -> Text<'a> {
fn from(masked: Masked<'a>) -> Self {
Text::raw(masked.value())
}
}

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.
///
@@ -107,11 +107,11 @@ impl<'a> Span<'a> {
/// Span::raw("test content");
/// Span::raw(String::from("test content"));
/// ```
pub fn raw<T>(content: T) -> Span<'a>
pub fn raw<T>(content: T) -> Self
where
T: Into<Cow<'a, str>>,
{
Span {
Self {
content: content.into(),
style: Style::default(),
}
@@ -133,12 +133,12 @@ impl<'a> Span<'a> {
/// Span::styled("test content", style);
/// Span::styled(String::from("test content"), style);
/// ```
pub fn styled<T, S>(content: T, style: S) -> Span<'a>
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
S: Into<Style>,
{
Span {
Self {
content: content.into(),
style: style.into(),
}
@@ -275,6 +275,63 @@ impl<'a> Span<'a> {
.filter(|g| *g != "\n")
.map(move |g| StyledGrapheme { symbol: g, style })
}
/// Converts this Span into a left-aligned [`Line`]
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let line = "Test Content".green().italic().into_left_aligned_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn into_left_aligned_line(self) -> Line<'a> {
Line::from(self).left_aligned()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_left_aligned_line"]
pub fn to_left_aligned_line(self) -> Line<'a> {
self.into_left_aligned_line()
}
/// Converts this Span into a center-aligned [`Line`]
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let line = "Test Content".green().italic().into_centered_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn into_centered_line(self) -> Line<'a> {
Line::from(self).centered()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_centered_line"]
pub fn to_centered_line(self) -> Line<'a> {
self.into_centered_line()
}
/// Converts this Span into a right-aligned [`Line`]
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let line = "Test Content".green().italic().into_right_aligned_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn into_right_aligned_line(self) -> Line<'a> {
Line::from(self).right_aligned()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_right_aligned_line"]
pub fn to_right_aligned_line(self) -> Line<'a> {
self.into_right_aligned_line()
}
}
impl<'a, T> From<T> for Span<'a>
@@ -287,7 +344,7 @@ where
}
impl<'a> Styled for Span<'a> {
type Item = Span<'a>;
type Item = Self;
fn style(&self) -> Style {
self.style
@@ -300,22 +357,13 @@ impl<'a> Styled for Span<'a> {
impl Widget for Span<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Widget::render(&self, area, buf);
self.render_ref(area, buf);
}
}
/// Implement [`Widget`] for [`Option<Span>`] to simplify the common case of having an optional
/// [`Span`] field in a widget.
impl Widget for &Option<Span<'_>> {
fn render(self, area: Rect, buf: &mut Buffer) {
if let Some(span) = self {
span.render(area, buf);
}
}
}
impl Widget for &Span<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
impl WidgetRef for Span<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
let Rect {
x: mut current_x,
y,
@@ -355,8 +403,15 @@ impl std::fmt::Display for Span<'_> {
#[cfg(test)]
mod tests {
use rstest::fixture;
use super::*;
#[fixture]
fn small_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
#[test]
fn default() {
let span = Span::default();
@@ -485,9 +540,32 @@ mod tests {
assert_eq!(format!("{stylized_span}"), "stylized test content");
}
#[test]
fn left_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_left_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_centered_line();
assert_eq!(line.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_right_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Right));
}
mod widget {
use rstest::rstest;
use super::*;
use crate::{assert_buffer_eq, style::Stylize};
use crate::assert_buffer_eq;
#[test]
fn render() {
@@ -503,6 +581,13 @@ mod tests {
assert_buffer_eq!(buf, expected);
}
#[rstest]
fn render_out_of_bounds(mut small_buf: Buffer) {
let out_of_bounds = Rect::new(20, 20, 10, 1);
Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
assert_eq!(small_buf, Buffer::empty(small_buf.area));
}
/// When the content of the span is longer than the area passed to render, the content
/// should be truncated
#[test]

View File

@@ -3,32 +3,30 @@ use std::borrow::Cow;
use itertools::{Itertools, Position};
use crate::{prelude::*, widgets::Widget};
use crate::prelude::*;
/// A string split over multiple lines where each line is composed of several clusters, each with
/// their own style.
/// A string split over one or more lines.
///
/// A [`Text`], like a [`Line`], can be constructed using one of the many `From` implementations
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
///
/// The text's [`Style`] is used by the rendering widget to determine how to style the text. Each
/// [`Line`] in the text will be styled with the [`Style`] of the text, and then with its own
/// [`Style`]. `Text` also implements [`Styled`] which means you can use the methods of the
/// [`Stylize`] trait.
///
/// The text's [`Alignment`] can be set using [`Text::alignment`]. Lines composing the text can
/// also be individually aligned with [`Line::alignment`].
///
/// `Text` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`].
/// Usually apps will use the [`Paragraph`] widget instead of rendering a `Text` directly as it
/// provides more functionality.
/// [`Text`] is used wherever text is displayed in the terminal and represents one or more [`Line`]s
/// of text. When a [`Text`] is rendered, each line is rendered as a single line of text from top to
/// bottom of the area. The text can be styled and aligned.
///
/// # Constructor Methods
///
/// - [`Text::default`] creates a `Text` with empty content and the default style.
/// - [`Text::raw`] creates a `Text` (potentially multiple lines) with no style.
/// - [`Text::styled`] creates a `Text` (potentially multiple lines) with a style.
/// - [`Text::default`] creates a `Text` with empty content and the default style.
///
/// # Conversion Methods
///
/// - [`Text::from`] creates a `Text` from a `String`.
/// - [`Text::from`] creates a `Text` from a `&str`.
/// - [`Text::from`] creates a `Text` from a `Cow<str>`.
/// - [`Text::from`] creates a `Text` from a [`Span`].
/// - [`Text::from`] creates a `Text` from a [`Line`].
/// - [`Text::from`] creates a `Text` from a `Vec<Line>`.
/// - [`Text::from_iter`] creates a `Text` from an iterator of items that can be converted into
/// `Line`.
///
/// # Setter Methods
///
@@ -36,6 +34,15 @@ use crate::{prelude::*, widgets::Widget};
///
/// - [`Text::style`] sets the style of this `Text`.
/// - [`Text::alignment`] sets the alignment for this `Text`.
/// - [`Text::left_aligned`] sets the alignment to [`Alignment::Left`].
/// - [`Text::centered`] sets the alignment to [`Alignment::Center`].
/// - [`Text::right_aligned`] sets the alignment to [`Alignment::Right`].
///
/// # Iteration Methods
///
/// - [`Text::iter`] returns an iterator over the lines of the text.
/// - [`Text::iter_mut`] returns an iterator that allows modifying each line.
/// - [`Text::into_iter`] returns an iterator over the lines of the text.
///
/// # Other Methods
///
@@ -43,29 +50,121 @@ use crate::{prelude::*, widgets::Widget};
/// - [`Text::height`] returns the height.
/// - [`Text::patch_style`] patches the style of this `Text`, adding modifiers from the given style.
/// - [`Text::reset_style`] resets the style of the `Text`.
/// - [`Text::push_line`] adds a line to the text.
/// - [`Text::push_span`] adds a span to the last line of the text.
///
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Widget`]: crate::widgets::Widget
/// # Examples
///
/// ## Creating Text
///
/// A [`Text`], like a [`Line`], can be constructed using one of the many `From` implementations or
/// via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
///
/// ```rust
/// use std::{borrow::Cow, iter};
///
/// use ratatui::prelude::*;
///
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
/// let style = Style::new().yellow().italic();
/// let text = Text::raw("The first line\nThe second line").style(style);
/// let text = Text::styled("The first line\nThe second line", style);
/// let text = Text::styled(
/// "The first line\nThe second line",
/// (Color::Yellow, Modifier::ITALIC),
/// );
///
/// // An initial two lines of `Text` built from a `&str`
/// let mut text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
/// let text = Text::from("The first line\nThe second line");
/// let text = Text::from(String::from("The first line\nThe second line"));
/// let text = Text::from(Cow::Borrowed("The first line\nThe second line"));
/// let text = Text::from(Span::styled("The first line\nThe second line", style));
/// let text = Text::from(Line::from("The first line"));
/// let text = Text::from(vec![
/// Line::from("The first line"),
/// Line::from("The second line"),
/// ]);
/// let text = Text::from_iter(iter::once("The first line").chain(iter::once("The second line")));
///
/// // Adding two more unstyled lines
/// text.extend(Text::raw("These are two\nmore lines!"));
/// assert_eq!(4, text.height());
///
/// // Adding a final two styled lines
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
/// assert_eq!(6, text.height());
/// let mut text = Text::default();
/// text.extend(vec![
/// Line::from("The first line"),
/// Line::from("The second line"),
/// ]);
/// text.extend(Text::from("The third line\nThe fourth line"));
/// ```
///
/// ## Styling Text
///
/// The text's [`Style`] is used by the rendering widget to determine how to style the text. Each
/// [`Line`] in the text will be styled with the [`Style`] of the text, and then with its own
/// [`Style`]. `Text` also implements [`Styled`] which means you can use the methods of the
/// [`Stylize`] trait.
///
/// ```rust
/// # use ratatui::prelude::*;
/// let text = Text::from("The first line\nThe second line").style(Style::new().yellow().italic());
/// let text = Text::from("The first line\nThe second line")
/// .yellow()
/// .italic();
/// let text = Text::from(vec![
/// Line::from("The first line").yellow(),
/// Line::from("The second line").yellow(),
/// ])
/// .italic();
/// ```
///
/// ## Aligning Text
/// The text's [`Alignment`] can be set using [`Text::alignment`] or the related helper methods.
/// Lines composing the text can also be individually aligned with [`Line::alignment`].
///
/// ```rust
/// # use ratatui::prelude::*;
/// let text = Text::from("The first line\nThe second line").alignment(Alignment::Right);
/// let text = Text::from("The first line\nThe second line").right_aligned();
/// let text = Text::from(vec![
/// Line::from("The first line").left_aligned(),
/// Line::from("The second line").right_aligned(),
/// Line::from("The third line"),
/// ])
/// .centered();
/// ```
///
/// ## Rendering Text
/// `Text` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`] or to a
/// [`Frame`].
///
/// ```rust
/// # use ratatui::prelude::*;
/// // within another widget's `render` method:
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let text = Text::from("The first line\nThe second line");
/// text.render(area, buf);
/// # }
///
/// // within a terminal.draw closure:
/// # fn draw(frame: &mut Frame, area: Rect) {
/// let text = Text::from("The first line\nThe second line");
/// frame.render_widget(text, area);
/// # }
/// ```
///
/// ## Rendering Text with a Paragraph Widget
///
/// Usually apps will use the [`Paragraph`] widget instead of rendering a `Text` directly as it
/// provides more functionality.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let text = Text::from("The first line\nThe second line");
/// let paragraph = Paragraph::new(text)
/// .wrap(Wrap { trim: true })
/// .scroll((1, 1))
/// .render(area, buf);
/// # }
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Text<'a> {
/// The lines that make up this piece of text.
@@ -79,14 +178,14 @@ pub struct Text<'a> {
impl<'a> Text<'a> {
/// Create some text (potentially multiple lines) with no style.
///
/// ## Examples
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// Text::raw("The first line\nThe second line");
/// Text::raw(String::from("The first line\nThe second line"));
/// ```
pub fn raw<T>(content: T) -> Text<'a>
pub fn raw<T>(content: T) -> Self
where
T: Into<Cow<'a, str>>,
{
@@ -96,8 +195,7 @@ impl<'a> Text<'a> {
Cow::Owned(s) if s.is_empty() => vec![Line::from("")],
Cow::Owned(s) => s.lines().map(|l| Line::from(l.to_owned())).collect(),
};
Text::from(lines)
Self::from(lines)
}
/// Create some text (potentially multiple lines) with a style.
@@ -115,17 +213,17 @@ impl<'a> Text<'a> {
/// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style);
/// ```
pub fn styled<T, S>(content: T, style: S) -> Text<'a>
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
S: Into<Style>,
{
Text::raw(content).patch_style(style)
Self::raw(content).patch_style(style)
}
/// Returns the max width of all the lines.
///
/// ## Examples
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
@@ -133,12 +231,12 @@ impl<'a> Text<'a> {
/// assert_eq!(15, text.width());
/// ```
pub fn width(&self) -> usize {
self.lines.iter().map(Line::width).max().unwrap_or_default()
self.iter().map(Line::width).max().unwrap_or_default()
}
/// Returns the height.
///
/// ## Examples
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
@@ -211,7 +309,7 @@ impl<'a> Text<'a> {
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// ## Examples
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
@@ -335,49 +433,54 @@ impl<'a> Text<'a> {
pub fn right_aligned(self) -> Self {
self.alignment(Alignment::Right)
}
}
impl<'a> From<String> for Text<'a> {
fn from(s: String) -> Text<'a> {
Text::raw(s)
/// Returns an iterator over the lines of the text.
pub fn iter(&self) -> std::slice::Iter<Line<'a>> {
self.lines.iter()
}
}
impl<'a> From<&'a str> for Text<'a> {
fn from(s: &'a str) -> Text<'a> {
Text::raw(s)
/// Returns an iterator that allows modifying each line.
pub fn iter_mut(&mut self) -> std::slice::IterMut<Line<'a>> {
self.lines.iter_mut()
}
}
impl<'a> From<Cow<'a, str>> for Text<'a> {
fn from(s: Cow<'a, str>) -> Text<'a> {
Text::raw(s)
/// Adds a line to the text.
///
/// `line` can be any type that can be converted into a `Line`. For example, you can pass a
/// `&str`, a `String`, a `Span`, or a `Line`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut text = Text::from("Hello, world!");
/// text.push_line(Line::from("How are you?"));
/// text.push_line(Span::from("How are you?"));
/// text.push_line("How are you?");
/// ```
pub fn push_line<T: Into<Line<'a>>>(&mut self, line: T) {
self.lines.push(line.into());
}
}
impl<'a> From<Span<'a>> for Text<'a> {
fn from(span: Span<'a>) -> Text<'a> {
Text {
lines: vec![Line::from(span)],
..Default::default()
}
}
}
impl<'a> From<Line<'a>> for Text<'a> {
fn from(line: Line<'a>) -> Text<'a> {
Text {
lines: vec![line],
..Default::default()
}
}
}
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
Text {
lines,
..Default::default()
/// Adds a span to the last line of the text.
///
/// `span` can be any type that is convertible into a `Span`. For example, you can pass a
/// `&str`, a `String`, or a `Span`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut text = Text::from("Hello, world!");
/// text.push_span(Span::from("How are you?"));
/// text.push_span("How are you?");
/// ```
pub fn push_span<T: Into<Span<'a>>>(&mut self, span: T) {
let span = span.into();
if let Some(last) = self.lines.last_mut() {
last.push_span(span);
} else {
self.lines.push(Line::from(span));
}
}
}
@@ -391,6 +494,82 @@ impl<'a> IntoIterator for Text<'a> {
}
}
impl<'a> IntoIterator for &'a Text<'a> {
type Item = &'a Line<'a>;
type IntoIter = std::slice::Iter<'a, Line<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a> IntoIterator for &'a mut Text<'a> {
type Item = &'a mut Line<'a>;
type IntoIter = std::slice::IterMut<'a, Line<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
impl<'a> From<String> for Text<'a> {
fn from(s: String) -> Self {
Self::raw(s)
}
}
impl<'a> From<&'a str> for Text<'a> {
fn from(s: &'a str) -> Self {
Self::raw(s)
}
}
impl<'a> From<Cow<'a, str>> for Text<'a> {
fn from(s: Cow<'a, str>) -> Self {
Self::raw(s)
}
}
impl<'a> From<Span<'a>> for Text<'a> {
fn from(span: Span<'a>) -> Self {
Self {
lines: vec![Line::from(span)],
..Default::default()
}
}
}
impl<'a> From<Line<'a>> for Text<'a> {
fn from(line: Line<'a>) -> Self {
Self {
lines: vec![line],
..Default::default()
}
}
}
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
fn from(lines: Vec<Line<'a>>) -> Self {
Self {
lines,
..Default::default()
}
}
}
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();
Self {
lines,
..Default::default()
}
}
}
impl<'a, T> Extend<T> for Text<'a>
where
T: Into<Line<'a>>,
@@ -403,7 +582,7 @@ where
impl std::fmt::Display for Text<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (position, line) in self.lines.iter().with_position() {
for (position, line) in self.iter().with_position() {
if position == Position::Last {
write!(f, "{line}")?;
} else {
@@ -416,24 +595,15 @@ impl std::fmt::Display for Text<'_> {
impl Widget for Text<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Widget::render(&self, area, buf);
self.render_ref(area, buf);
}
}
/// Implement [`Widget`] for [`Option<Text>`] to simplify the common case of having an optional
/// [`Text`] field in a widget.
impl Widget for &Option<Text<'_>> {
fn render(self, area: Rect, buf: &mut Buffer) {
if let Some(text) = self {
text.render(area, buf);
}
}
}
impl Widget for &Text<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
impl WidgetRef for Text<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
buf.set_style(area, self.style);
for (line, row) in self.lines.iter().zip(area.rows()) {
for (line, row) in self.iter().zip(area.rows()) {
let line_width = line.width() as u16;
let x_offset = match (self.alignment, line.alignment) {
@@ -455,7 +625,7 @@ impl Widget for &Text<'_> {
}
impl<'a> Styled for Text<'a> {
type Item = Text<'a>;
type Item = Self;
fn style(&self) -> Style {
self.style
@@ -468,8 +638,16 @@ impl<'a> Styled for Text<'a> {
#[cfg(test)]
mod tests {
use std::iter;
use rstest::{fixture, rstest};
use super::*;
use crate::style::Stylize;
#[fixture]
fn small_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
#[test]
fn raw() {
@@ -581,6 +759,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");
@@ -698,9 +896,73 @@ mod tests {
assert_eq!(Text::default().italic().style, Modifier::ITALIC.into());
}
#[test]
fn left_aligned() {
let text = Text::from("Hello, world!").left_aligned();
assert_eq!(text.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let text = Text::from("Hello, world!").centered();
assert_eq!(text.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let text = Text::from("Hello, world!").right_aligned();
assert_eq!(text.alignment, Some(Alignment::Right));
}
#[test]
fn push_line() {
let mut text = Text::from("A");
text.push_line(Line::from("B"));
text.push_line(Span::from("C"));
text.push_line("D");
assert_eq!(
text.lines,
vec![
Line::raw("A"),
Line::raw("B"),
Line::raw("C"),
Line::raw("D")
]
);
}
#[test]
fn push_line_empty() {
let mut text = Text::default();
text.push_line(Line::from("Hello, world!"));
assert_eq!(text.lines, vec![Line::from("Hello, world!")]);
}
#[test]
fn push_span() {
let mut text = Text::from("A");
text.push_span(Span::raw("B"));
text.push_span("C");
assert_eq!(
text.lines,
vec![Line::from(vec![
Span::raw("A"),
Span::raw("B"),
Span::raw("C")
])],
);
}
#[test]
fn push_span_empty() {
let mut text = Text::default();
text.push_span(Span::raw("Hello, world!"));
assert_eq!(text.lines, vec![Line::from(Span::raw("Hello, world!"))],);
}
mod widget {
use super::*;
use crate::{assert_buffer_eq, style::Color};
use crate::assert_buffer_eq;
#[test]
fn render() {
@@ -715,6 +977,13 @@ mod tests {
assert_buffer_eq!(buf, expected_buf);
}
#[rstest]
fn render_out_of_bounds(mut small_buf: Buffer) {
let out_of_bounds_area = Rect::new(20, 20, 10, 1);
Text::from("Hello, world!").render(out_of_bounds_area, &mut small_buf);
assert_eq!(small_buf, Buffer::empty(small_buf.area));
}
#[test]
fn render_right_aligned() {
let text = Text::from("foo").alignment(Alignment::Right);
@@ -795,21 +1064,91 @@ mod tests {
}
}
#[test]
fn left_aligned() {
let text = Text::from("Hello, world!").left_aligned();
assert_eq!(text.alignment, Some(Alignment::Left));
}
mod iterators {
use super::*;
#[test]
fn centered() {
let text = Text::from("Hello, world!").centered();
assert_eq!(text.alignment, Some(Alignment::Center));
}
/// a fixture used in the tests below to avoid repeating the same setup
#[fixture]
fn hello_world() -> Text<'static> {
Text::from(vec![
Line::styled("Hello ", Color::Blue),
Line::styled("world!", Color::Green),
])
}
#[test]
fn right_aligned() {
let text = Text::from("Hello, world!").right_aligned();
assert_eq!(text.alignment, Some(Alignment::Right));
#[rstest]
fn iter(hello_world: Text<'_>) {
let mut iter = hello_world.iter();
assert_eq!(iter.next(), Some(&Line::styled("Hello ", Color::Blue)));
assert_eq!(iter.next(), Some(&Line::styled("world!", Color::Green)));
assert_eq!(iter.next(), None);
}
#[rstest]
fn iter_mut(mut hello_world: Text<'_>) {
let mut iter = hello_world.iter_mut();
assert_eq!(iter.next(), Some(&mut Line::styled("Hello ", Color::Blue)));
assert_eq!(iter.next(), Some(&mut Line::styled("world!", Color::Green)));
assert_eq!(iter.next(), None);
}
#[rstest]
fn into_iter(hello_world: Text<'_>) {
let mut iter = hello_world.into_iter();
assert_eq!(iter.next(), Some(Line::styled("Hello ", Color::Blue)));
assert_eq!(iter.next(), Some(Line::styled("world!", Color::Green)));
assert_eq!(iter.next(), None);
}
#[rstest]
fn into_iter_ref(hello_world: Text<'_>) {
let mut iter = (&hello_world).into_iter();
assert_eq!(iter.next(), Some(&Line::styled("Hello ", Color::Blue)));
assert_eq!(iter.next(), Some(&Line::styled("world!", Color::Green)));
assert_eq!(iter.next(), None);
}
#[test]
fn into_iter_mut_ref() {
let mut hello_world = Text::from(vec![
Line::styled("Hello ", Color::Blue),
Line::styled("world!", Color::Green),
]);
let mut iter = (&mut hello_world).into_iter();
assert_eq!(iter.next(), Some(&mut Line::styled("Hello ", Color::Blue)));
assert_eq!(iter.next(), Some(&mut Line::styled("world!", Color::Green)));
assert_eq!(iter.next(), None);
}
#[rstest]
fn for_loop_ref(hello_world: Text<'_>) {
let mut result = String::new();
for line in &hello_world {
result.push_str(line.to_string().as_ref());
}
assert_eq!(result, "Hello world!");
}
#[rstest]
fn for_loop_mut_ref() {
let mut hello_world = Text::from(vec![
Line::styled("Hello ", Color::Blue),
Line::styled("world!", Color::Green),
]);
let mut result = String::new();
for line in &mut hello_world {
result.push_str(line.to_string().as_ref());
}
assert_eq!(result, "Hello world!");
}
#[rstest]
fn for_loop_into(hello_world: Text<'_>) {
let mut result = String::new();
for line in hello_world {
result.push_str(line.to_string().as_ref());
}
assert_eq!(result, "Hello world!");
}
}
}

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