Compare commits

..

5 Commits

Author SHA1 Message Date
Josh McKinney
1ff292742c fix: change Cell::EMPTY to Cell::empty() and fix underline_color bug 2024-10-14 18:25:06 -07:00
Josh McKinney
e084c9f013 add breaking change doc 2024-10-14 18:25:06 -07:00
Josh McKinney
f08640c53b feat: cache the symbol width in the cell
This leads to more than a 50% speedup
2024-10-14 18:25:06 -07:00
Josh McKinney
2b391ac15d perf: only calculate current symbol width once
This is a 20% performance improvement on buffer diff on my M2 MBP.

```
buffer/diff             time:   [100.26 µs 100.69 µs 101.15 µs]
                        change: [-18.007% -17.489% -16.929%] (p = 0.00 < 0.05)
                        Performance has improved.
```
2024-10-14 18:25:06 -07:00
Josh McKinney
99ef8651aa perf: add buffer diff benchmark 2024-10-14 18:23:55 -07:00
31 changed files with 1009 additions and 5662 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
target
Cargo.lock
*.log
*.rs.rustfmt
.gdb_history

View File

@@ -10,13 +10,12 @@ GitHub with a [breaking change] label.
This is a quick summary of the sections below:
- [v0.29.0](#v0290)
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
- [v0.29.0](#unreleased)
- `Terminal`, `Buffer`, `Cell`, `Frame` are no longer `Sync` / `RefUnwindSafe`
- Removed public fields from `Rect` iterators
- `Line` now implements `From<Cow<str>`
- `Table::highlight_style` is now `Table::row_highlight_style`
- `Tabs::select` now accepts `Into<Option<usize>>`
- `Color::from_hsl` is now behind the `palette` feature
- [v0.28.0](#v0280)
- `Backend::size` returns `Size` instead of `Rect`
- `Backend` trait migrates to `get/set_cursor_position`
@@ -72,56 +71,26 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0)
## Unreleased
### `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const ([#1326])
### `Terminal`, `Buffer`, `Cell`, `Frame` are no longer `Sync` / `RefUnwindSafe` [#1339]
[#1326]: https://github.com/ratatui/ratatui/pull/1326
[#1339]: https://github.com/ratatui/ratatui/pull/1339
The `Sparkline::data` method has been modified to accept `IntoIterator<Item = SparklineBar>`
instead of `&[u64]`.
In #1339, we added a cache of the Cell width which uses a std::cell::Cell. This causes `Cell` and
all types that contain this (`Terminal`, `Buffer`, `Frame`, `CompletedFrame`, `TestBackend`) to no
longer be `Sync`
`SparklineBar` is a struct that contains an `Option<u64>` value, which represents an possible
_absent_ value, as distinct from a `0` value. This change allows the `Sparkline` to style
data points differently, depending on whether they are present or absent.
This change is unlikely to cause problems as these types likely should not be sent between threads
regardless due to their interaction with various things which mutated externally (e.g. stdio).
`SparklineBar` also contains an `Option<Style>` that will be used to apply a style the bar in
addition to any other styling applied to the `Sparkline`.
Several `From` implementations have been added to `SparklineBar` to support existing callers who
provide `&[u64]` and other types that can be converted to `SparklineBar`, such as `Option<u64>`.
If you encounter any type inference issues, you may need to provide an explicit type for the data
passed to `Sparkline::data`. For example, if you are passing a single value, you may need to use
`into()` to convert it to form that can be used as a `SparklineBar`:
```diff
let value = 1u8;
- Sparkline::default().data(&[value.into()]);
+ Sparkline::default().data(&[u64::from(value)]);
```
As a consequence of this change, the `data` method is no longer a `const fn`.
### `Color::from_hsl` is now behind the `palette` feature and accepts `palette::Hsl` ([#1418])
[#1418]: https://github.com/ratatui/ratatui/pull/1418
Previously `Color::from_hsl` accepted components as individual f64 parameters. It now accepts a
single `palette::Hsl` value and is gated behind a `palette` feature flag.
```diff
- Color::from_hsl(360.0, 100.0, 100.0)
+ Color::from_hsl(Hsl::new(360.0, 100.0, 100.0))
```
### Removed public fields from `Rect` iterators ([#1358], [#1424])
### Removed public fields from `Rect` iterators ([#1358])
[#1358]: https://github.com/ratatui/ratatui/pull/1358
[#1424]: https://github.com/ratatui/ratatui/pull/1424
The `pub` modifier has been removed from fields on the `Columns`,`Rows`, and `Positions` iterators.
These fields were not intended to be public and should not have been accessed directly.
The `pub` modifier has been removed from fields on the `layout::rect::Columns` and
`layout::rect::Rows`. These fields were not intended to be public and should not have been accessed
directly.
### `Rect::area()` now returns u32 instead of u16 ([#1378])
@@ -173,7 +142,7 @@ implementation on `TableState`) may have to be refactored if the "selected_colum
accounted for. This does not affect users who rely on the `Deserialize`, or `Serialize`
implementation on the state.
## [v0.28.0](https://github.com/ratatui/ratatui/releases/tag/v0.28.0)
## v0.28.0
### `Backend::size` returns `Size` instead of `Rect` ([#1254])

View File

@@ -2,556 +2,6 @@
All notable changes to this project will be documented in this file.
_"Food will come, Remy. Food always comes to those who love to cook." Gusteau_
We are excited to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 👨‍🍳🐀
**Release highlights**: <https://ratatui.rs/highlights/v029/>
⚠️ List of breaking changes can be found [here](https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md).
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0) - 2024-10-21
### Features
- [3a43274](https://github.com/ratatui/ratatui/commit/3a43274881a79b4e593536c2ca915b509e557215) *(color)* Add hsluv support by @du-ob in [#1333](https://github.com/ratatui/ratatui/pull/1333)
- [4c4851c](https://github.com/ratatui/ratatui/commit/4c4851ca3d1437a50ed1f146c0849b58716b89a2) *(example)* Add drawing feature to the canvas example by @orhun in [#1429](https://github.com/ratatui/ratatui/pull/1429)
> ![rec_20241018T235208](https://github.com/user-attachments/assets/cfb2f9f8-773b-4599-9312-29625ff2ca60)
>
>
> fun fact: I had to do [35
> pushups](https://www.youtube.com/watch?v=eS92stzBYXA) for this...
>
> ---------
- [e5a7609](https://github.com/ratatui/ratatui/commit/e5a76095884a4ce792846289f56d04a4acaaa6fa) *(line)* Impl From<Cow<str>> for Line by @joshka in [#1373](https://github.com/ratatui/ratatui/pull/1373) [**breaking**]
>
> BREAKING-CHANGES:`Line` now implements `From<Cow<str>`
>
> As this adds an extra conversion, ambiguous inferred values may no longer
> compile.
>
> ```rust
> // given:
> struct Foo { ... }
> impl From<Foo> for String { ... }
> impl From<Foo> for Cow<str> { ... }
>
> let foo = Foo { ... };
> let line = Line::from(foo); // now fails due to ambiguous type inference
> // replace with
> let line = Line::from(String::from(foo));
> ```
>
> Fixes:https://github.com/ratatui/ratatui/issues/1367
>
> ---------
- [2805ddd](https://github.com/ratatui/ratatui/commit/2805dddf0527584da9c7865ff6a78a9c74731187) *(logo)* Add a Ratatui logo widget by @joshka in [#1307](https://github.com/ratatui/ratatui/pull/1307)
> This is a simple logo widget that can be used to render the Ratatui logo
> in the terminal. It is used in the `examples/ratatui-logo.rs` example,
> and may be used in your applications' help or about screens.
>
> ```rust
> use ratatui::{Frame, widgets::RatatuiLogo};
>
> fn draw(frame: &mut Frame) {
> frame.render_widget(RatatuiLogo::tiny(), frame.area());
> }
> ```
- [d72968d](https://github.com/ratatui/ratatui/commit/d72968d86b94100579feba80c5cd207c2e7e13e7) *(scrolling-regions)* Use terminal scrolling regions to stop Terminal::insert_before from flickering by @nfachan in [#1341](https://github.com/ratatui/ratatui/pull/1341) [**breaking**]
> The current implementation of Terminal::insert_before causes the
> viewport to flicker. This is described in #584 .
>
> This PR removes that flickering by using terminal scrolling regions
> (sometimes called "scroll regions"). A terminal can have its scrolling
> region set to something other than the whole screen. When a scroll ANSI
> sequence is sent to the terminal and it has a non-default scrolling
> region, the terminal will scroll just inside of that region.
>
> We use scrolling regions to implement insert_before. We create a region
> on the screen above the viewport, scroll that up to make room for the
> newly inserted lines, and then draw the new lines. We may need to repeat
> this process depending on how much space there is and how many lines we
> need to draw.
>
> When the viewport takes up the entire screen, we take a modified
> approach. We create a scrolling region of just the top line (could be
> more) of the viewport, then use that to draw the lines we want to
> output. When we're done, we scroll it up by one line, into the
> scrollback history, and then redraw the top line from the viewport.
>
> A final edge case is when the viewport hasn't yet reached the bottom of
> the screen. This case, we set up a different scrolling region, where the
> top is the top of the viewport, and the bottom is the viewport's bottom
> plus the number of lines we want to scroll by. We then scroll this
> region down to open up space above the viewport for drawing the inserted
> lines.
>
> Regardless of what we do, we need to reset the scrolling region. This PR
> takes the approach of always resetting the scrolling region after every
> operation. So the Backend gets new scroll_region_up and
> scroll_region_down methods instead of set_scrolling_region, scroll_up,
> scroll_down, and reset_scrolling_region methods. We chose that approach
> for two reasons. First, we don't want Ratatui to have to remember that
> state and then reset the scrolling region when tearing down. Second, the
> pre-Windows-10 console code doesn't support scrolling region
>
> This PR:
> - Adds a new scrolling-regions feature.
> - Adds two new Backend methods: scroll_region_up and scroll_region_down.
> - Implements those Backend methods on all backends in the codebase.
> - The crossterm and termion implementations use raw ANSI escape
> sequences. I'm trying to merge changes into those two projects
> separately to support these functions.
> - Adds code to Terminal::insert_before to choose between
> insert_before_scrolling_regions and insert_before_no_scrolling_regions.
> The latter is the old implementation.
> - Adds lots of tests to the TestBackend to for the
> scrolling-region-related Backend methods.
> - Adds versions of terminal tests that show that insert_before doesn't
> clobber the viewport. This is a change in behavior from before.
- [dc8d058](https://github.com/ratatui/ratatui/commit/dc8d0587ecfd46cde86c9e33a6fd385e2d4810a9) *(table)* Add support for selecting column and cell by @airblast-dev in [#1331](https://github.com/ratatui/ratatui/pull/1331) [**breaking**]
> Fixes https://github.com/ratatui-org/ratatui/issues/1250
>
> Adds support for selecting a column and cell in `TableState`. The
> selected column, and cells style can be set by
> `Table::column_highlight_style` and `Table::cell_highlight_style`
> respectively.
>
> The table example has also been updated to display the new
> functionality:
>
> https://github.com/user-attachments/assets/e5fd2858-4931-4ce1-a2f6-a5ea1eacbecc
>
> BREAKING CHANGE:The Serialized output of the state will now include the
> "selected_column" field. Software that manually parse the serialized the
> output (with anything other than the `Serialize` implementation on
> `TableState`) may have to be refactored if the "selected_column" field
> is not accounted for. This does not affect users who rely on the
> `Deserialize`, or `Serialize` implementation on the state.
>
> BREAKING CHANGE:The `Table::highlight_style` is now deprecated in favor
> of `Table::row_highlight_style`.
>
> ---------
- [ab6b1fe](https://github.com/ratatui/ratatui/commit/ab6b1feaec3ef0cf23bcfac219b95ec946180fa8) *(tabs)* Allow tabs to be deselected by @joshka in [#1413](https://github.com/ratatui/ratatui/pull/1413) [**breaking**]
>
> `Tabs::select()` now accepts `Into<Option<usize>>` instead of `usize`.
> This allows tabs to be deselected by passing `None`.
>
> `Tabs::default()` is now also implemented manually instead of deriving
> `Default`, and a new method `Tabs::titles()` is added to set the titles
> of the tabs.
>
> Fixes:<https://github.com/ratatui/ratatui/pull/1412>
>
> BREAKING CHANGE:`Tabs::select()` now accepts `Into<Option<usize>>`
> which breaks any code already using parameter type inference:
>
> ```diff
> let selected = 1u8;
> - let tabs = Tabs::new(["A", "B"]).select(selected.into())
> + let tabs = Tabs::new(["A", "B"]).select(selected as usize)
> ```
- [23c0d52](https://github.com/ratatui/ratatui/commit/23c0d52c29f27547d94448be44aa46e85f49fbb0) *(text)* Improve concise debug view for Span,Line,Text,Style by @joshka in [#1410](https://github.com/ratatui/ratatui/pull/1410)
> Improves https://github.com/ratatui/ratatui/pull/1383
>
> The following now round trips when formatted for debug.
> This will make it easier to use insta when testing text related views of
> widgets.
>
> ```rust
> Text::from_iter([
> Line::from("Hello, world!"),
> Line::from("How are you?").bold().left_aligned(),
> Line::from_iter([
> Span::from("I'm "),
> Span::from("doing ").italic(),
> Span::from("great!").bold(),
> ]),
> ]).on_blue().italic().centered()
> ```
- [60cc15b](https://github.com/ratatui/ratatui/commit/60cc15bbb064faa704f78ca51ae60584b5f7ca31) *(uncategorized)* Add support for empty bar style to `Sparkline` by @fujiapple852 in [#1326](https://github.com/ratatui/ratatui/pull/1326) [**breaking**]
> - distinguish between empty bars and bars with a value of 0
> - provide custom styling for empty bars
> - provide custom styling for individual bars
> - inverts the rendering algorithm to be item first
>
> Closes:#1325
>
> BREAKING CHANGE:`Sparkline::data` takes `IntoIterator<Item = SparklineBar>`
> instead of `&[u64]` and is no longer const
- [453a308](https://github.com/ratatui/ratatui/commit/453a308b46bbacba2ee7cba849cf0c19c88a1a27) *(uncategorized)* Add overlap to layout by @kdheepak in [#1398](https://github.com/ratatui/ratatui/pull/1398) [**breaking**]
> This PR adds a new feature for the existing `Layout::spacing` method,
> and introducing a `Spacing` enum.
>
> Now `Layout::spacing` is generic and can take
>
> - zero or positive numbers, e.g. `Layout::spacing(1)` (current
> functionality)
> - negative number, e.g. `Layout::spacing(-1)` (new)
> - variant of the `Spacing` (new)
>
> This allows creating layouts with a shared pixel for segments. When
> `spacing(negative_value)` is used, spacing is ignored and all segments
> will be adjacent and have pixels overlapping.
> `spacing(zero_or_positive_value)` behaves the same as before. These are
> internally converted to `Spacing::Overlap` or `Spacing::Space`.
>
> Here's an example output to illustrate the layout solve from this PR:
>
> ```rust
> #[test]
> fn test_layout() {
> use crate::layout::Constraint::*;
> let mut terminal = crate::Terminal::new(crate::backend::TestBackend::new(50, 4)).unwrap();
> terminal
> .draw(|frame| {
> let [upper, lower] = Layout::vertical([Fill(1), Fill(1)]).areas(frame.area());
>
> let (segments, spacers) = Layout::horizontal([Length(10), Length(10), Length(10)])
> .flex(Flex::Center)
> .split_with_spacers(upper);
>
> for segment in segments.iter() {
> frame.render_widget(
> crate::widgets::Block::bordered()
> .border_set(crate::symbols::border::DOUBLE),
> *segment,
> );
> }
> for spacer in spacers.iter() {
> frame.render_widget(crate::widgets::Block::bordered(), *spacer);
> }
>
> let (segments, spacers) = Layout::horizontal([Length(10), Length(10), Length(10)])
> .flex(Flex::Center)
> .spacing(-1) // new feature
> .split_with_spacers(lower);
>
> for segment in segments.iter() {
> frame.render_widget(
> crate::widgets::Block::bordered()
> .border_set(crate::symbols::border::DOUBLE),
> *segment,
> );
> }
> for spacer in spacers.iter() {
> frame.render_widget(crate::widgets::Block::bordered(), *spacer);
> }
> })
> .unwrap();
> dbg!(terminal.backend());
> }
> ```
>
>
> ```plain
> ┌────────┐╔════════╗╔════════╗╔════════╗┌────────┐
> └────────┘╚════════╝╚════════╝╚════════╝└────────┘
> ┌─────────┐╔════════╔════════╔════════╗┌─────────┐
> └─────────┘╚════════╚════════╚════════╝└─────────┘
> ```
>
> Currently drawing a border on top of an existing border overwrites it.
> Future PRs will allow for making the border drawing handle overlaps
> better.
>
> ---------
- [7bdccce](https://github.com/ratatui/ratatui/commit/7bdccce3d56052306eb4121afe6b1ff56b198796) *(uncategorized)* Add an impl of `DoubleEndedIterator` for `Columns` and `Rows` by @fujiapple852 [**breaking**]
>
> BREAKING-CHANGE:The `pub` modifier has been removed from fields on the
>
> `layout::rect::Columns` and `layout::rect::Rows` iterators. These fields
> were not intended to be public and should not have been accessed
> directly.
>
> Fixes:#1357
### Bug Fixes
- [4f5503d](https://github.com/ratatui/ratatui/commit/4f5503dbf610290904a759a3f169a15111f11392) *(color)* Hsl and hsluv are now clamped before conversion by @joshka in [#1436](https://github.com/ratatui/ratatui/pull/1436) [**breaking**]
> The `from_hsl` and `from_hsluv` functions now clamp the HSL and HSLuv
> values before converting them to RGB. This ensures that the input values
> are within the expected range before conversion.
>
> Also note that the ranges of Saturation and Lightness values have been
> aligned to be consistent with the palette crate. Saturation and Lightness
> for `from_hsl` are now in the range [0.0..1.0] while `from_hsluv` are
> in the range [0.0..100.0].
>
> Refs:- <https://github.com/Ogeon/palette/discussions/253>
> - <https://docs.rs/palette/latest/palette/struct.Hsl.html>
> - <https://docs.rs/palette/latest/palette/struct.Hsluv.html>
>
> Fixes:<https://github.com/ratatui/ratatui/issues/1433>
- [b7e4885](https://github.com/ratatui/ratatui/commit/b7e488507d23cbc91ac63d5249088ad0f4852205) *(color)* Fix doc test for from_hsl by @joshka in [#1421](https://github.com/ratatui/ratatui/pull/1421)
- [3df685e](https://github.com/ratatui/ratatui/commit/3df685e1144340935db2b1d929e2546f83c5e65f) *(rect)* Rect::area now returns u32 and Rect::new() no longer clamps area to u16::MAX by @joshka in [#1378](https://github.com/ratatui/ratatui/pull/1378) [**breaking**]
> This change fixes the unexpected behavior of the Rect::new() function to
> be more intuitive. The Rect::new() function now clamps the width and
> height of the rectangle to keep each bound within u16::MAX. The
> Rect::area() function now returns a u32 instead of a u16 to allow for
> larger areas to be calculated.
>
> Previously, the Rect::new() function would clamp the total area of the
> rectangle to u16::MAX, by preserving the aspect ratio of the rectangle.
>
> BREAKING CHANGE:Rect::area() now returns a u32 instead of a u16.
>
> Fixes:<https://github.com/ratatui/ratatui/issues/1375>
- [514d273](https://github.com/ratatui/ratatui/commit/514d2738750d792a75fde6cc7666f9220bcf6b3a) *(terminal)* Use the latest, resized area when clearing by @roberth in [#1427](https://github.com/ratatui/ratatui/pull/1427)
- [0f48239](https://github.com/ratatui/ratatui/commit/0f4823977894cef51d5ffafe6ae35ca7ad56e1ac) *(terminal)* Resize() now resizes fixed viewports by @Patryk27 in [#1353](https://github.com/ratatui/ratatui/pull/1353)
>
> `Terminal::resize()` on a fixed viewport used to do nothing due to
> an accidentally shadowed variable. This now works as intended.
- [a52ee82](https://github.com/ratatui/ratatui/commit/a52ee82fc716fafb2652b83a331c36f844104dda) *(text)* Truncate based on alignment by @Lunderberg in [#1432](https://github.com/ratatui/ratatui/pull/1432)
> This is a follow-up PR to https://github.com/ratatui/ratatui/pull/987,
> which implemented alignment-aware truncation for the `Line` widget.
> However, the truncation only checked the `Line::alignment` field, and
> any alignment inherited from a parent's `Text::alignment` field would
> not be used.
>
> This commit updates the truncation of `Line` to depend both on the
> individual `Line::alignment`, and on any alignment inherited from the
> parent's `Text::alignment`.
- [611086e](https://github.com/ratatui/ratatui/commit/611086eba4dc07dcef89502a3bedfc28015b879f) *(uncategorized)* Sparkline docs / doc tests by @joshka in [#1437](https://github.com/ratatui/ratatui/pull/1437)
- [b9653ba](https://github.com/ratatui/ratatui/commit/b9653ba05a468d3843499d8abd243158df823f82) *(uncategorized)* Prevent calender render panic when terminal height is small by @adrodgers in [#1380](https://github.com/ratatui/ratatui/pull/1380)
>
> Fixes:#1379
- [da821b4](https://github.com/ratatui/ratatui/commit/da821b431edd656973b4480d3d4f22e7eea6d369) *(uncategorized)* Clippy lints from rust 1.81.0 by @fujiapple852 in [#1356](https://github.com/ratatui/ratatui/pull/1356)
- [68886d1](https://github.com/ratatui/ratatui/commit/68886d1787b8e07d307dda4f36342d51d650345b) *(uncategorized)* Add `unstable-backend-writer` feature by @Patryk27 in [#1352](https://github.com/ratatui/ratatui/pull/1352)
>
> https://github.com/ratatui/ratatui/pull/991 created a new unstable
> feature, but forgot to add it to Cargo.toml, making it impossible to use
> on newer versions of rustc - this commit fixes it.
### Refactor
- [6db16d6](https://github.com/ratatui/ratatui/commit/6db16d67fc3cc97f1e5bd4b7df02ce9f00756a55) *(color)* Use palette types for Hsl/Hsluv conversions by @orhun in [#1418](https://github.com/ratatui/ratatui/pull/1418) [**breaking**]
>
> BREAKING-CHANGE:Previously `Color::from_hsl` accepted components
> as individual f64 parameters. It now accepts a single `palette::Hsl`
> value
> and is gated behind a `palette` feature flag.
>
> ```diff
> - Color::from_hsl(360.0, 100.0, 100.0)
> + Color::from_hsl(Hsl::new(360.0, 100.0, 100.0))
> ```
>
> Fixes:<https://github.com/ratatui/ratatui/issues/1414>
>
> ---------
- [edcdc8a](https://github.com/ratatui/ratatui/commit/edcdc8a8147a2f450d2c871b19da6d6383fd5497) *(layout)* Rename element to segment in layout by @kdheepak in [#1397](https://github.com/ratatui/ratatui/pull/1397)
> This PR renames `element` to `segment` in a couple of functions in the
> layout calculations for clarity. `element` can refer to `segment`s or
> `spacer`s and functions that take only `segment`s should use `segment`
> as the variable names.
- [1153a9e](https://github.com/ratatui/ratatui/commit/1153a9ebaf0b98c45982002a659cb718e3c1d137) *(uncategorized)* Consistent result expected in layout tests by @farmeroy in [#1406](https://github.com/ratatui/ratatui/pull/1406)
>
> Fixes #1399
> I've looked through all the `assert_eq` and made sure that they follow
> the `expected, result` pattern. I wasn't sure if it was desired to
> actually pass result and expected as variables to the assert_eq
> statements, so I've left everything that seems to have followed the
> pattern as is.
- [20c88aa](https://github.com/ratatui/ratatui/commit/20c88aaa5b9eb011a52240eab5edc1a8db23157a) *(uncategorized)* Avoid unneeded allocations by @mo8it in [#1345](https://github.com/ratatui/ratatui/pull/1345)
### Documentation
- [b13e2f9](https://github.com/ratatui/ratatui/commit/b13e2f94733afccfe02275fca263bde1dc532d2f) *(backend)* Added link to stdio FAQ by @Valentin271 in [#1349](https://github.com/ratatui/ratatui/pull/1349)
- [b88717b](https://github.com/ratatui/ratatui/commit/b88717b65f7f89276edd855c4a3f9da2eda44361) *(constraint)* Add note about percentages by @joshka in [#1368](https://github.com/ratatui/ratatui/pull/1368)
- [381ec75](https://github.com/ratatui/ratatui/commit/381ec75329866b3c1256113d1cb7716206b79fb7) *(readme)* Reduce the length by @joshka in [#1431](https://github.com/ratatui/ratatui/pull/1431)
> Motivation for this is that there's a bunch of stuff at the bottom of the Readme that we don't really keep up to date. Instead it's better to link to the places that we do keep this info.
- [4728f0e](https://github.com/ratatui/ratatui/commit/4728f0e68b41eabb7d4ebd041fd5a85a0e794287) *(uncategorized)* Tweak readme by @joshka in [#1419](https://github.com/ratatui/ratatui/pull/1419)
>
> Fixes:<https://github.com/ratatui/ratatui/issues/1417>
- [4069aa8](https://github.com/ratatui/ratatui/commit/4069aa82745585f53b4b3376af589bb1b6108427) *(uncategorized)* Fix missing breaking changes link by @joshka in [#1416](https://github.com/ratatui/ratatui/pull/1416)
- [870bc6a](https://github.com/ratatui/ratatui/commit/870bc6a64a680e9209d30e67e2e1f4e50a10a4bb) *(uncategorized)* Use `Frame::area()` instead of `size()` in examples by @hosseinnedaee in [#1361](https://github.com/ratatui/ratatui/pull/1361)
>
> `Frame::size()` is deprecated
### Performance
- [8db7a9a](https://github.com/ratatui/ratatui/commit/8db7a9a44a2358315dedaee3e7a2cb1a44ae1e58) *(uncategorized)* Implement size hints for `Rect` iterators by @airblast-dev in [#1420](https://github.com/ratatui/ratatui/pull/1420)
### Styling
- [e02947b](https://github.com/ratatui/ratatui/commit/e02947be6185643f906a97c453540676eade3f38) *(example)* Update panic message in minimal template by @orhun in [#1344](https://github.com/ratatui/ratatui/pull/1344)
### Miscellaneous Tasks
- [67c0ea2](https://github.com/ratatui/ratatui/commit/67c0ea243b5eb08159e41f922067247984902c1a) *(block)* Deprecate block::Title by @joshka in [#1372](https://github.com/ratatui/ratatui/pull/1372)
>
> `ratatui::widgets::block::Title` is deprecated in favor of using `Line`
> to represent titles.
> This removes an unnecessary layer of wrapping (string -> Span -> Line ->
> Title).
>
> This struct will be removed in a future release of Ratatui (likely
> 0.31).
> For more information see:
>
> <https://github.com/ratatui/ratatui/issues/738>
>
> To update your code:
> ```rust
>
> Block::new().title(Title::from("foo"));
> // becomes any of
>
> Block::new().title("foo");
>
> Block::new().title(Line::from("foo"));
>
> Block::new().title(Title::from("foo").position(Position::TOP));
> // becomes any of
>
> Block::new().title_top("foo");
>
> Block::new().title_top(Line::from("foo"));
>
> Block::new().title(Title::from("foo").position(Position::BOTTOM));
> // becomes any of
>
> Block::new().title_bottom("foo");
>
> Block::new().title_bottom(Line::from("foo"));
> ```
- [6515097](https://github.com/ratatui/ratatui/commit/6515097434a10c08276b58f0cd10b9301b44e9fe) *(cargo)* Check in Cargo.lock by @joshka in [#1434](https://github.com/ratatui/ratatui/pull/1434)
> When kept up to date, this makes it possible to build any git version
> with the same versions of crates that were used for any version, without
> it, you can only use the current versions. This makes bugs in semver
> compatible code difficult to detect.
>
> The Cargo.lock file is not used by downstream consumers of the crate, so
> it is safe to include it in the repository (and recommended by the Rust
> docs).
>
> See:- https://doc.rust-lang.org/cargo/faq.html#why-have-cargolock-in-version-control
> - https://blog.rust-lang.org/2023/08/29/committing-lockfiles.html
> - https://github.com/rust-lang/cargo/issues/8728
- [c777beb](https://github.com/ratatui/ratatui/commit/c777beb658ebab26890b52cbda8df5d945525221) *(ci)* Bump git-cliff-action to v4 by @orhun in [#1350](https://github.com/ratatui/ratatui/pull/1350)
>
> See:https://github.com/orhun/git-cliff-action/releases/tag/v4.0.0
- [69e0cd2](https://github.com/ratatui/ratatui/commit/69e0cd2fc4b126870b3381704260271904996c8f) *(deny)* Allow Zlib license in cargo-deny configuration by @orhun in [#1411](https://github.com/ratatui/ratatui/pull/1411)
- [bc10af5](https://github.com/ratatui/ratatui/commit/bc10af5931d1c1ec58a4181c01807ed3c52051c6) *(style)* Make Debug output for Text/Line/Span/Style more concise by @joshka in [#1383](https://github.com/ratatui/ratatui/pull/1383)
>
> Given:```rust
>
> Text::from_iter([
> Line::from("without line fields"),
> Line::from("with line fields").bold().centered(),
> Line::from_iter([
> Span::from("without span fields"),
> Span::from("with span fields")
> .green()
> .on_black()
> .italic()
> .not_dim(),
> ]),
> ])
> ```
>
> Debug:```
> Text [Line [Span("without line fields")], Line { style: Style::new().add_modifier(Modifier::BOLD), alignment: Some(Center), spans: [Span("with line fields")] }, Line [Span("without span fields"), Span { style: Style::new().green().on_black().add_modifier(Modifier::ITALIC).remove_modifier(Modifier::DIM), content: "with span fields" }]]
> ```
>
> Fixes: https://github.com/ratatui/ratatui/issues/1382
>
> ---------
- [f6f7794](https://github.com/ratatui/ratatui/commit/f6f7794dd782d20cd41875c0578ffc4331692c1e) *(uncategorized)* Remove leftover prelude refs / glob imports from example code by @joshka in [#1430](https://github.com/ratatui/ratatui/pull/1430)
>
> Fixes:<https://github.com/ratatui/ratatui/issues/1150>
- [9fd1bee](https://github.com/ratatui/ratatui/commit/9fd1beedb25938bcc9565a52f1104ed45636c2dd) *(uncategorized)* Make Positions iterator fields private by @joshka in [#1424](https://github.com/ratatui/ratatui/pull/1424) [**breaking**]
>
> BREAKING CHANGE:The Rect Positions iterator no longer has public
> fields. The `rect` and `current_position` fields have been made private
> as they were not intended to be accessed directly.
- [c32baa7](https://github.com/ratatui/ratatui/commit/c32baa7cd8a29a370a71da07ee02cf32125c9bcf) *(uncategorized)* Add benchmark for `Table` by @airblast-dev in [#1408](https://github.com/ratatui/ratatui/pull/1408)
- [5ad623c](https://github.com/ratatui/ratatui/commit/5ad623c29b8f0b50fad742448902245f353ef19e) *(uncategorized)* Remove usage of prelude by @joshka in [#1390](https://github.com/ratatui/ratatui/pull/1390)
> This helps make the doc examples more explicit about what is being used.
> It will also makes it a bit easier to do future refactoring of Ratatui,
> into several crates, as the ambiguity of where types are coming from
> will be reduced.
>
> Additionally, several doc examples have been simplified to use Stylize,
> and necessary imports are no longer hidden.
>
> This doesn't remove the prelude. Only the internal usages.
- [f4880b4](https://github.com/ratatui/ratatui/commit/cc7497532ac50e7e15e8ee8ff506f4689c396f50) *(deps)* Pin unicode-width to 0.2.0 by @orhun in [#1403](https://github.com/ratatui/ratatui/pull/1403) [**breaking**]
> We pin unicode-width to avoid breaking applications when there are breaking changes in the library.
>
> Discussion in [#1271](https://github.com/ratatui/ratatui/pull/1271)
### Continuous Integration
- [5635b93](https://github.com/ratatui/ratatui/commit/5635b930c7196ef8f12824341a7bd8b7323aabcd) *(uncategorized)* Add cargo-machete and remove unused deps by @Veetaha in [#1362](https://github.com/ratatui/ratatui/pull/1362)
>
> https://github.com/bnjbvr/cargo-machete
### New Contributors
* @roberth made their first contribution in [#1427](https://github.com/ratatui/ratatui/pull/1427)
* @du-ob made their first contribution in [#1333](https://github.com/ratatui/ratatui/pull/1333)
* @farmeroy made their first contribution in [#1406](https://github.com/ratatui/ratatui/pull/1406)
* @adrodgers made their first contribution in [#1380](https://github.com/ratatui/ratatui/pull/1380)
* @Veetaha made their first contribution in [#1362](https://github.com/ratatui/ratatui/pull/1362)
* @hosseinnedaee made their first contribution in [#1361](https://github.com/ratatui/ratatui/pull/1361)
* @Patryk27 made their first contribution in [#1352](https://github.com/ratatui/ratatui/pull/1352)
**Full Changelog**: https://github.com/ratatui/ratatui/compare/v0.28.1...v0.29.0
## [v0.28.1](https://github.com/ratatui/ratatui/releases/tag/v0.28.1) - 2024-08-25
### Features

3398
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "ratatui"
version = "0.29.0" # crate version
version = "0.28.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/"
@@ -39,8 +39,7 @@ termwiz = { version = "0.22.0", optional = true }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-truncate = "1"
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
unicode-width = "=0.2.0"
unicode-width = "=0.1.13"
[target.'cfg(not(windows))'.dependencies]
# termion is not supported on Windows

316
README.md
View File

@@ -2,13 +2,14 @@
<summary>Table of Contents</summary>
- [Ratatui](#ratatui)
- [Quick Start](#quickstart)
- [Other documentation](#other-documentation)
- [Installation](#installation)
- [Introduction](#introduction)
- [Other documentation](#other-documentation)
- [Quickstart](#quickstart)
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
- [Drawing the UI](#drawing-the-ui)
- [Handling events](#handling-events)
- [Example](#example)
- [Layout](#layout)
- [Text and styling](#text-and-styling)
- [Status of this fork](#status-of-this-fork)
@@ -44,42 +45,28 @@ Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]
lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
## Quickstart
## Installation
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
Add `ratatui` as a dependency to your cargo.toml:
```shell
cargo add ratatui crossterm
cargo add ratatui
```
Then you can create a simple "Hello World" application:
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
[Termwiz]).
```rust
use crossterm::event::{self, Event};
use ratatui::{text::Text, Frame};
## Introduction
fn main() {
let mut terminal = ratatui::init();
loop {
terminal.draw(draw).expect("failed to draw frame");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
that for each frame, your app must render all widgets that are supposed to be part of the UI.
This is in contrast to the retained mode style of rendering where widgets are updated and then
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
for more info.
fn draw(frame: &mut Frame) {
let text = Text::raw("Hello World!");
frame.render_widget(text, frame.area());
}
```
The full code for this example which contains a little more detail is in the [Examples]
directory. For more guidance on different ways to structure your application see the
[Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
various [Examples]. There are also several starter templates available in the [templates]
repository.
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
@@ -91,82 +78,46 @@ repository.
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
- [Breaking Changes] - a list of breaking changes in the library.
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.
## Quickstart
## Introduction
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
that for each frame, your app must render all widgets that are supposed to be part of the UI.
This is in contrast to the retained mode style of rendering where widgets are updated and then
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
for more info.
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
[Termwiz]).
The following example demonstrates the minimal amount of code necessary to setup a terminal and
render "Hello World!". The full code for this example which contains a little more detail is in
the [Examples] directory. For more guidance on different ways to structure your application see
the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
various [Examples]. There are also several starter templates available in the [templates]
repository.
Every application built with `ratatui` needs to implement the following steps:
- Initialize the terminal
- A main loop that:
- Draws the UI
- Handles input events
- A main loop to:
- Handle input events
- Draw the UI
- Restore the terminal state
The library contains a [`prelude`] module that re-exports the most commonly used traits and
types for convenience. Most examples in the documentation will use this instead of showing the
full path of each type.
### Initialize and restore the terminal
The [`Terminal`] type is the main entry point for any Ratatui application. It is generic over a
a choice of [`Backend`] implementations that each provide functionality to draw frames, clear
the screen, hide the cursor, etc. There are backend implementations for [Crossterm], [Termion]
and [Termwiz].
The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
abstraction over a choice of [`Backend`] implementations that provides functionality to draw
each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
[Termwiz].
The simplest way to initialize the terminal is to use the [`init`] function which returns a
[`DefaultTerminal`] instance with the default options, enters the Alternate Screen and Raw mode
and sets up a panic hook that restores the terminal in case of panic. This instance can then be
used to draw frames and interact with the terminal state. (The [`DefaultTerminal`] instance is a
type alias for a terminal with the [`crossterm`] backend.) The [`restore`] function restores the
terminal to its original state.
```rust
fn main() -> std::io::Result<()> {
let mut terminal = ratatui::init();
let result = run(&mut terminal);
ratatui::restore();
result
}
```
See the [`backend` module] and the [Backends] section of the [Ratatui Website] for more info on
the alternate screen and raw mode.
Most applications should enter the Alternate Screen when starting and leave it when exiting and
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
module] and the [Backends] section of the [Ratatui Website] for more info.
### Drawing the UI
Drawing the UI is done by calling the [`Terminal::draw`] method on the terminal instance. This
method takes a closure that is called with a [`Frame`] instance. The [`Frame`] provides the size
of the area to draw to and allows the app to render any [`Widget`] using the provided
[`render_widget`] method. After this closure returns, a diff is performed and only the changes
are drawn to the terminal. See the [Widgets] section of the [Ratatui Website] for more info.
The closure passed to the [`Terminal::draw`] method should handle the rendering of a full frame.
```rust
use ratatui::{Frame, widgets::Paragraph};
fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
loop {
terminal.draw(|frame| draw(frame))?;
if handle_events()? {
break Ok(());
}
}
}
fn draw(frame: &mut Frame) {
let text = Paragraph::new("Hello World!");
frame.render_widget(text, frame.area());
}
```
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
using the provided [`render_widget`] method. After this closure returns, a diff is performed and
only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
for more info.
### Handling events
@@ -175,23 +126,63 @@ calling backend library methods directly. See the [Handling Events] section of t
Website] for more info. For example, if you are using [Crossterm], you can use the
[`crossterm::event`] module to handle events.
```rust
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
### Example
fn handle_events() -> std::io::Result<bool> {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Char('q') => return Ok(true),
// handle other key events
_ => {}
```rust
use std::io::{self, stdout};
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
// handle other events
_ => {}
ExecutableCommand,
},
widgets::{Block, Paragraph},
Frame, Terminal,
};
fn main() -> io::Result<()> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut should_quit = false;
while !should_quit {
terminal.draw(ui)?;
should_quit = handle_events()?;
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn handle_events() -> io::Result<bool> {
if event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
}
Ok(false)
}
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
frame.area(),
);
}
```
Running this example produces the following output:
![docsrs-hello]
## Layout
The library comes with a basic yet useful layout management object called [`Layout`] which
@@ -206,13 +197,16 @@ use ratatui::{
Frame,
};
fn draw(frame: &mut Frame) {
use Constraint::{Fill, Length, Min};
let vertical = Layout::vertical([Length(1), Min(0),Length(1)]);
let [title_area, main_area, status_area] = vertical.areas(frame.area());
let horizontal = Layout::horizontal([Fill(1); 2]);
let [left_area, right_area] = horizontal.areas(main_area);
fn ui(frame: &mut Frame) {
let [title_area, main_area, status_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.areas(frame.area());
let [left_area, right_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(main_area);
frame.render_widget(Block::bordered().title("Title Bar"), title_area);
frame.render_widget(Block::bordered().title("Status Bar"), status_area);
@@ -223,13 +217,7 @@ fn draw(frame: &mut Frame) {
Running this example produces the following output:
```text
Title Bar───────────────────────────────────
┌Left────────────────┐┌Right───────────────┐
│ ││ │
└────────────────────┘└────────────────────┘
Status Bar──────────────────────────────────
```
![docsrs-layout]
## Text and styling
@@ -252,7 +240,7 @@ use ratatui::{
Frame,
};
fn draw(frame: &mut Frame) {
fn ui(frame: &mut Frame) {
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
let line = Line::from(vec![
@@ -282,6 +270,10 @@ fn draw(frame: &mut Frame) {
}
```
Running this example produces the following output:
![docsrs-styling]
[Ratatui Website]: https://ratatui.rs/
[Installation]: https://ratatui.rs/installation/
[Rendering]: https://ratatui.rs/concepts/rendering/
@@ -304,6 +296,9 @@ fn draw(frame: &mut Frame) {
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
[docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
[`Frame`]: terminal::Frame
[`render_widget`]: terminal::Frame::render_widget
[`Widget`]: widgets::Widget
@@ -342,24 +337,86 @@ fn draw(frame: &mut Frame) {
<!-- cargo-rdme end -->
## Contributing
## Status of this fork
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
regarding the [future of `tui-rs`](https://github.com/fdehau/tui-rs/issues/654), several members of
the community forked the project and created this crate. We look forward to continuing the work
started by Florian 🚀
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
We have also recently launched the [Ratatui Forum][Forum], For bugs and features, we rely on GitHub.
Please [Report a bug], [Request a Feature] or [Create a Pull Request].
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub.
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a
Pull Request].
Please make sure you read the [contributing](./CONTRIBUTING.md) guidelines, especially if you are
interested in working on a PR or issue opened in the previous repository.
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
you are interested in working on a PR or issue opened in the previous repository.
## Built with Ratatui
## Widgets
Ratatui has a number of built-in [widgets](https://docs.rs/ratatui/latest/ratatui/widgets/), as well
as many contributed by external contributors. Check out the [Showcase](https://ratatui.rs/showcase/)
section of the website, or the [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) repo
for a curated list of awesome apps/libraries built with `ratatui`!
### Built in
The library comes with the following
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
- [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
- [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
- [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
- [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
rendering [points, lines, shapes and a world
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
- [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
- [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
- [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
- [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
- [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
- [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
- [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
- [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
- [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
Each widget has an associated example which can be found in the [Examples] folder. Run each example
with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
be installed with `cargo install cargo-make`).
### Third-party libraries, bootstrapping templates and widgets
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
`ratatui::text::Text`
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`ratatui::style::Color`
- [templates](https://github.com/ratatui/templates) — Starter templates for
bootstrapping a Rust TUI application with Ratatui & crossterm
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
Tui-rs + Crossterm apps
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
- [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
- [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
- [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications
with a React/Elm inspired approach
- [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
Tui-realm
- [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget) — Widget for tree data
structures.
- [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
windows and their rendering
- [tui-textarea](https://github.com/rhysd/tui-textarea) — Simple yet powerful multi-line text editor
widget supporting several key shortcuts, undo/redo, text search, etc.
- [tui-input](https://github.com/sayanarijit/tui-input) — TUI input library supporting multiple
backends and tui-rs.
- [tui-term](https://github.com/a-kenji/tui-term) — A pseudoterminal widget library
that enables the rendering of terminal applications as ratatui widgets.
## Apps
Check out [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) for a curated list of
awesome apps/libraries built with `ratatui`!
## Alternatives
@@ -368,9 +425,6 @@ to build text user interfaces in Rust.
## Acknowledgments
None of this could be possible without [**Florian Dehau**](https://github.com/fdehau) who originally
created [tui-rs] which inspired many Rust TUIs.
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
awesome logo** for the ratatui project and ratatui organization.

View File

@@ -1,11 +1,14 @@
use std::iter::zip;
use criterion::{black_box, BenchmarkId, Criterion};
use ratatui::{
buffer::{Buffer, Cell},
layout::Rect,
text::Line,
widgets::Widget,
};
criterion::criterion_group!(benches, empty, filled, with_lines);
criterion::criterion_group!(benches, empty, filled, with_lines, diff);
const fn rect(size: u16) -> Rect {
Rect::new(0, 0, size, size)
@@ -58,3 +61,37 @@ fn with_lines(c: &mut Criterion) {
}
group.finish();
}
fn diff(c: &mut Criterion) {
const AREA: Rect = Rect {
x: 0,
y: 0,
width: 200,
height: 50,
};
c.bench_function("buffer/diff", |b| {
let buffer_1 = create_random_buffer(AREA);
let buffer_2 = create_random_buffer(AREA);
b.iter(|| {
let _ = black_box(&buffer_1).diff(black_box(&buffer_2));
});
});
}
fn create_random_buffer(area: Rect) -> Buffer {
const PARAGRAPH_COUNT: i64 = 15;
const SENTENCE_COUNT: i64 = 5;
const WORD_COUNT: i64 = 20;
const SEPARATOR: &str = "\n\n";
let paragraphs = fakeit::words::paragraph(
PARAGRAPH_COUNT,
SENTENCE_COUNT,
WORD_COUNT,
SEPARATOR.to_string(),
);
let mut buffer = Buffer::empty(area);
for (line, row) in zip(paragraphs.lines(), area.rows()) {
Line::from(line).render(row, &mut buffer);
}
buffer
}

View File

@@ -1,112 +1,24 @@
use criterion::{black_box, criterion_group, BatchSize, Bencher, BenchmarkId, Criterion};
use criterion::{black_box, criterion_group, BenchmarkId, Criterion};
use ratatui::layout::Rect;
fn rect_iters_benchmark(c: &mut Criterion) {
let rect_sizes = vec![[16, 16], [64, 64], [255, 255]];
let mut group = c.benchmark_group("rect");
for rect in rect_sizes.into_iter().map(|[width, height]| Rect {
width,
height,
..Default::default()
}) {
group.bench_with_input(
BenchmarkId::new("rect_rows_iter", rect.height),
&rect,
|b, rect| rect_rows_iter(b, *rect),
);
group.bench_with_input(
BenchmarkId::new("rect_rows_collect", rect.height),
&rect,
|b, rect| rect_rows_collect(b, *rect),
);
group.bench_with_input(
BenchmarkId::new("rect_columns_iter", rect.width),
&rect,
|b, rect| rect_columns_iter(b, *rect),
);
group.bench_with_input(
BenchmarkId::new("rect_columns_collect", rect.width),
&rect,
|b, rect| rect_columns_collect(b, *rect),
);
group.bench_with_input(
BenchmarkId::new(
"rect_positions_iter",
format!("{}x{}", rect.width, rect.height),
),
&rect,
|b, rect| rect_positions_iter(b, *rect),
);
group.bench_with_input(
BenchmarkId::new(
"rect_positions_collect",
format!("{}x{}", rect.width, rect.height),
),
&rect,
|b, rect| rect_positions_collect(b, *rect),
);
fn rect_rows_benchmark(c: &mut Criterion) {
let rect_sizes = vec![
Rect::new(0, 0, 1, 16),
Rect::new(0, 0, 1, 1024),
Rect::new(0, 0, 1, 65535),
];
let mut group = c.benchmark_group("rect_rows");
for rect in rect_sizes {
group.bench_with_input(BenchmarkId::new("rows", rect.height), &rect, |b, rect| {
b.iter(|| {
for row in rect.rows() {
// Perform any necessary operations on each row
black_box(row);
}
});
});
}
group.finish();
}
fn rect_rows_iter(c: &mut Bencher, rect: Rect) {
c.iter_batched(
|| black_box(rect),
|rect| {
for row in black_box(rect.rows()) {
black_box(row);
}
},
BatchSize::LargeInput,
);
}
fn rect_rows_collect(c: &mut Bencher, rect: Rect) {
c.iter_batched(
|| black_box(rect),
|rect| black_box(rect.rows()).collect::<Vec<_>>(),
BatchSize::LargeInput,
);
}
fn rect_columns_iter(c: &mut Bencher, rect: Rect) {
c.iter_batched(
|| black_box(rect),
|rect| {
for col in black_box(rect.columns()) {
black_box(col);
}
},
BatchSize::LargeInput,
);
}
fn rect_columns_collect(c: &mut Bencher, rect: Rect) {
c.iter_batched(
|| black_box(rect),
|rect| black_box(rect.columns()).collect::<Vec<_>>(),
BatchSize::LargeInput,
);
}
fn rect_positions_iter(c: &mut Bencher, rect: Rect) {
c.iter_batched(
|| black_box(rect),
|rect| {
for pos in black_box(rect.positions()) {
black_box(pos);
}
},
BatchSize::LargeInput,
);
}
fn rect_positions_collect(b: &mut Bencher, rect: Rect) {
b.iter_batched(
|| black_box(rect),
|rect| black_box(rect.positions()).collect::<Vec<_>>(),
BatchSize::LargeInput,
);
}
criterion_group!(benches, rect_iters_benchmark);
criterion_group!(benches, rect_rows_benchmark);

View File

@@ -30,9 +30,19 @@ body = """
{% macro commit(commit) -%}
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}\
{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}){%- endif %}\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
{%- if commit.breaking %} [**breaking**]{% endif %}
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
{%- endif %}
{%- for footer in commit.footers %}\n
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
>
{{ footer.token | indent(prefix=" > ", first=true, blank=true) }}
{{- footer.separator }}
{{- footer.value| indent(prefix=" > ", first=false, blank=true) }}
{%- endif %}
{%- endfor %}
{% endmacro -%}
{% for group, commits in commits | group_by(attribute="group") %}

View File

@@ -14,5 +14,4 @@ allowed-duplicate-crates = [
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"unicode-width",
]

View File

@@ -13,24 +13,16 @@
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{
io::stdout,
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use color_eyre::Result;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, KeyEventKind},
ExecutableCommand,
};
use itertools::Itertools;
use ratatui::{
crossterm::event::{self, Event, KeyCode, MouseEventKind},
layout::{Constraint, Layout, Position, Rect},
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Stylize},
symbols::Marker,
widgets::{
canvas::{Canvas, Circle, Map, MapResolution, Points, Rectangle},
canvas::{Canvas, Circle, Map, MapResolution, Rectangle},
Block, Widget,
},
DefaultTerminal, Frame,
@@ -38,16 +30,13 @@ use ratatui::{
fn main() -> Result<()> {
color_eyre::install()?;
stdout().execute(EnableMouseCapture)?;
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
ratatui::restore();
stdout().execute(DisableMouseCapture)?;
app_result
}
struct App {
exit: bool,
x: f64,
y: f64,
ball: Circle,
@@ -56,14 +45,11 @@ struct App {
vy: f64,
tick_count: u64,
marker: Marker,
points: Vec<Position>,
is_drawing: bool,
}
impl App {
const fn new() -> Self {
Self {
exit: false,
x: 0.0,
y: 0.0,
ball: Circle {
@@ -77,22 +63,25 @@ impl App {
vy: 1.0,
tick_count: 0,
marker: Marker::Dot,
points: vec![],
is_drawing: false,
}
}
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
let tick_rate = Duration::from_millis(16);
let mut last_tick = Instant::now();
while !self.exit {
loop {
terminal.draw(|frame| self.draw(frame))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
match event::read()? {
Event::Key(key) => self.handle_key_press(key),
Event::Mouse(event) => self.handle_mouse_event(event),
_ => (),
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => break Ok(()),
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
_ => {}
}
}
}
@@ -101,32 +90,6 @@ impl App {
last_tick = Instant::now();
}
}
Ok(())
}
fn handle_key_press(&mut self, key: event::KeyEvent) {
if key.kind != KeyEventKind::Press {
return;
}
match key.code {
KeyCode::Char('q') => self.exit = true,
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
_ => {}
}
}
fn handle_mouse_event(&mut self, event: event::MouseEvent) {
match event.kind {
MouseEventKind::Down(_) => self.is_drawing = true,
MouseEventKind::Up(_) => self.is_drawing = false,
MouseEventKind::Drag(_) => {
self.points.push(Position::new(event.column, event.row));
}
_ => {}
}
}
fn on_tick(&mut self) {
@@ -163,12 +126,10 @@ impl App {
let horizontal =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [left, right] = horizontal.areas(frame.area());
let [draw, map] = vertical.areas(left);
let [map, right] = horizontal.areas(frame.area());
let [pong, boxes] = vertical.areas(right);
frame.render_widget(self.map_canvas(), map);
frame.render_widget(self.draw_canvas(draw), draw);
frame.render_widget(self.pong_canvas(), pong);
frame.render_widget(self.boxes_canvas(boxes), boxes);
}
@@ -188,30 +149,6 @@ impl App {
.y_bounds([-90.0, 90.0])
}
fn draw_canvas(&self, area: Rect) -> impl Widget + '_ {
Canvas::default()
.block(Block::bordered().title("Draw here"))
.marker(self.marker)
.x_bounds([0.0, f64::from(area.width)])
.y_bounds([0.0, f64::from(area.height)])
.paint(move |ctx| {
let points = self
.points
.iter()
.map(|p| {
(
f64::from(p.x) - f64::from(area.left()),
f64::from(area.bottom()) - f64::from(p.y),
)
})
.collect_vec();
ctx.draw(&Points {
coords: &points,
color: Color::White,
});
})
}
fn pong_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::bordered().title("Pong"))

View File

@@ -99,7 +99,7 @@ pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) {
.title_alignment(Alignment::Center)
.border_type(BorderType::Thick),
)
.data(data)
.data(&data)
.style(THEME.traceroute.ping)
.render(area, buf);
}

View File

@@ -20,21 +20,21 @@
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crossterm::event::{self, Event};
use ratatui::{text::Text, Frame};
use ratatui::{
crossterm::event::{self, Event},
text::Text,
Frame,
};
fn main() {
let mut terminal = ratatui::init();
loop {
terminal.draw(draw).expect("failed to draw frame");
terminal
.draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
.expect("failed to draw frame");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
fn draw(frame: &mut Frame) {
let text = Text::raw("Hello World!");
frame.render_widget(text, frame.area());
}

View File

@@ -6,8 +6,6 @@ use std::{
io, iter,
};
use unicode_width::UnicodeWidthStr;
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::{Buffer, Cell},
@@ -52,13 +50,13 @@ fn buffer_view(buffer: &Buffer) -> String {
let mut overwritten = vec![];
let mut skip: usize = 0;
view.push('"');
for (x, c) in cells.iter().enumerate() {
for (x, cell) in cells.iter().enumerate() {
if skip == 0 {
view.push_str(c.symbol());
view.push_str(cell.symbol());
} else {
overwritten.push((x, c.symbol()));
overwritten.push((x, cell.symbol()));
}
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
skip = std::cmp::max(skip, cell.width()).saturating_sub(1);
}
view.push('"');
if !overwritten.is_empty() {
@@ -479,12 +477,12 @@ mod tests {
#[test]
fn buffer_view_with_overwrites() {
let multi_byte_char = "👨‍👩‍👧‍👦"; // renders 2 wide
let multi_byte_char = "👨‍👩‍👧‍👦"; // renders 8 wide
let buffer = Buffer::with_lines([multi_byte_char]);
assert_eq!(
buffer_view(&buffer),
format!(
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " ")]
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " "), (2, " "), (3, " "), (4, " "), (5, " "), (6, " "), (7, " ")]
"#,
)
);

View File

@@ -79,7 +79,7 @@ impl Buffer {
/// Returns a Buffer with all cells set to the default one
#[must_use]
pub fn empty(area: Rect) -> Self {
Self::filled(area, Cell::EMPTY)
Self::filled(area, Cell::empty())
}
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
@@ -303,17 +303,15 @@ impl Buffer {
/// buffer.pos_of(100); // Panics
/// ```
#[must_use]
pub fn pos_of(&self, index: usize) -> (u16, u16) {
pub fn pos_of(&self, i: usize) -> (u16, u16) {
debug_assert!(
index < self.content.len(),
"Trying to get the coords of a cell outside the buffer: i={index} len={}",
i < self.content.len(),
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
self.content.len()
);
let x = index % self.area.width as usize + self.area.x as usize;
let y = index / self.area.width as usize + self.area.y as usize;
(
u16::try_from(x).expect("x overflow. This should never happen as area.width is u16"),
u16::try_from(y).expect("y overflow. This should never happen as area.height is u16"),
self.area.x + (i as u16) % self.area.width,
self.area.y + (i as u16) / self.area.width,
)
}
@@ -416,7 +414,7 @@ impl Buffer {
if self.content.len() > length {
self.content.truncate(length);
} else {
self.content.resize(length, Cell::EMPTY);
self.content.resize(length, Cell::empty());
}
self.area = area;
}
@@ -431,7 +429,7 @@ impl Buffer {
/// Merge an other buffer into this one
pub fn merge(&mut self, other: &Self) {
let area = self.area.union(other.area);
self.content.resize(area.area() as usize, Cell::EMPTY);
self.content.resize(area.area() as usize, Cell::empty());
// Move original content to the appropriate space
let size = self.area.area() as usize;
@@ -501,9 +499,8 @@ impl Buffer {
updates.push((x, y, &next_buffer[i]));
}
to_skip = current.symbol().width().saturating_sub(1);
let affected_width = std::cmp::max(current.symbol().width(), previous.symbol().width());
to_skip = current.width().saturating_sub(1);
let affected_width = std::cmp::max(current.width(), previous.width());
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
}
updates
@@ -600,7 +597,7 @@ impl fmt::Debug for Buffer {
} else {
overwritten.push((x, c.symbol()));
}
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
skip = std::cmp::max(skip, c.width()).saturating_sub(1);
#[cfg(feature = "underline-color")]
{
let style = (c.fg, c.bg, c.underline_color, c.modifier);
@@ -1245,12 +1242,11 @@ mod tests {
#[case::shrug("🤷", "🤷xxxxx")]
// Technically this is a (brown) bear, a zero-width joiner and a snowflake
// As it is joined its a single emoji and should therefore have a width of 2.
// Prior to unicode-width 0.2, this was incorrectly detected as width 4 for some reason
#[case::polarbear("🐻‍❄️", "🐻xxxxx")]
// It's correctly detected as a single grapheme but it's width is 4 for some reason
#[case::polarbear("🐻‍❄️", "🐻xxx")]
// Technically this is an eye, a zero-width joiner and a speech bubble
// Both eye and speech bubble include a 'display as emoji' variation selector
// Prior to unicode-width 0.2, this was incorrectly detected as width 4 for some reason
#[case::eye_speechbubble("👁️‍🗨️", "👁🗨xxxxx")]
#[case::eye_speechbubble("👁️‍🗨️", "👁🗨xxx")]
fn renders_emoji(#[case] input: &str, #[case] expected: &str) {
use unicode_width::UnicodeWidthChar;
@@ -1276,24 +1272,4 @@ mod tests {
let expected = Buffer::with_lines([expected]);
assert_eq!(buffer, expected);
}
/// Regression test for <https://github.com/ratatui/ratatui/issues/1441>
///
/// Previously the `pos_of` function would incorrectly cast the index to a u16 value instead of
/// using the index as is. This caused incorrect rendering of any buffer with an length > 65535.
#[test]
fn index_pos_of_u16_max() {
let buffer = Buffer::empty(Rect::new(0, 0, 256, 256 + 1));
assert_eq!(buffer.index_of(255, 255), 65535);
assert_eq!(buffer.pos_of(65535), (255, 255));
assert_eq!(buffer.index_of(0, 256), 65536);
assert_eq!(buffer.pos_of(65536), (0, 256)); // previously (0, 0)
assert_eq!(buffer.index_of(1, 256), 65537);
assert_eq!(buffer.pos_of(65537), (1, 256)); // previously (1, 0)
assert_eq!(buffer.index_of(255, 256), 65791);
assert_eq!(buffer.pos_of(65791), (255, 256)); // previously (255, 0)
}
}

View File

@@ -1,9 +1,12 @@
use std::hash::{Hash, Hasher};
use compact_str::CompactString;
use unicode_width::UnicodeWidthStr;
use crate::style::{Color, Modifier, Style};
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cell {
/// The string to be drawn in the cell.
@@ -31,11 +34,57 @@ pub struct Cell {
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
pub skip: bool,
/// Cache the width of the cell.
width: std::cell::Cell<Option<usize>>,
}
impl PartialEq for Cell {
fn eq(&self, other: &Self) -> bool {
let eq = self.symbol == other.symbol
&& self.fg == other.fg
&& self.bg == other.bg
&& self.modifier == other.modifier
&& self.skip == other.skip;
// explicitly not comparing width, as it is a cache and may be not set
// && self.width == other.width
#[cfg(feature = "underline-color")]
return eq && self.underline_color == other.underline_color;
#[cfg(not(feature = "underline-color"))]
return eq;
}
}
impl Hash for Cell {
fn hash<H: Hasher>(&self, state: &mut H) {
self.symbol.hash(state);
self.fg.hash(state);
self.bg.hash(state);
#[cfg(feature = "underline-color")]
self.underline_color.hash(state);
self.modifier.hash(state);
self.skip.hash(state);
// explicitly not hashing width, as it is a cache and not part of the cell's identity
// self.width.hash(state);
}
}
impl Cell {
/// An empty `Cell`
pub const EMPTY: Self = Self::new(" ");
pub const fn empty() -> Self {
Self {
symbol: CompactString::const_new(" "),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
width: std::cell::Cell::new(Some(1)),
}
}
/// Creates a new `Cell` with the given symbol.
///
@@ -52,6 +101,7 @@ impl Cell {
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
width: std::cell::Cell::new(None),
}
}
@@ -64,6 +114,7 @@ impl Cell {
/// Sets the symbol of the cell.
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol = CompactString::new(symbol);
self.width.set(None);
self
}
@@ -72,6 +123,7 @@ impl Cell {
/// This is particularly useful for adding zero-width characters to the cell.
pub(crate) fn append_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol.push_str(symbol);
self.width.set(None);
self
}
@@ -79,6 +131,7 @@ impl 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.width.set(None);
self
}
@@ -148,18 +201,33 @@ impl Cell {
}
self.modifier = Modifier::empty();
self.skip = false;
self.width.set(Some(1));
}
/// Returns the width of the cell.
///
/// This value is cached and will only be recomputed when the cell is modified.
#[must_use]
pub fn width(&self) -> usize {
if let Some(width) = self.width.get() {
width
} else {
let width = self.symbol().width();
self.width.set(Some(width));
width
}
}
}
impl Default for Cell {
fn default() -> Self {
Self::EMPTY
Self::empty()
}
}
impl From<char> for Cell {
fn from(ch: char) -> Self {
let mut cell = Self::EMPTY;
let mut cell = Self::empty();
cell.set_char(ch);
cell
}
@@ -182,19 +250,20 @@ mod tests {
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
width: std::cell::Cell::new(None),
}
);
}
#[test]
fn empty() {
let cell = Cell::EMPTY;
let cell = Cell::empty();
assert_eq!(cell.symbol(), " ");
}
#[test]
fn set_symbol() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_symbol(""); // Multi-byte character
assert_eq!(cell.symbol(), "");
cell.set_symbol("👨‍👩‍👧‍👦"); // Multiple code units combined with ZWJ
@@ -203,7 +272,7 @@ mod tests {
#[test]
fn append_symbol() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_symbol(""); // Multi-byte character
cell.append_symbol("\u{200B}"); // zero-width space
assert_eq!(cell.symbol(), "\u{200B}");
@@ -211,28 +280,28 @@ mod tests {
#[test]
fn set_char() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_char('あ'); // Multi-byte character
assert_eq!(cell.symbol(), "");
}
#[test]
fn set_fg() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_fg(Color::Red);
assert_eq!(cell.fg, Color::Red);
}
#[test]
fn set_bg() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_bg(Color::Red);
assert_eq!(cell.bg, Color::Red);
}
#[test]
fn set_style() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_style(Style::new().fg(Color::Red).bg(Color::Blue));
assert_eq!(cell.fg, Color::Red);
assert_eq!(cell.bg, Color::Blue);
@@ -240,14 +309,14 @@ mod tests {
#[test]
fn set_skip() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_skip(true);
assert!(cell.skip);
}
#[test]
fn reset() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_symbol("");
cell.set_fg(Color::Red);
cell.set_bg(Color::Blue);
@@ -261,7 +330,7 @@ mod tests {
#[test]
fn style() {
let cell = Cell::EMPTY;
let cell = Cell::empty();
assert_eq!(
cell.style(),
Style {
@@ -294,4 +363,12 @@ mod tests {
let cell2 = Cell::new("");
assert_ne!(cell1, cell2);
}
#[test]
fn width() {
let cell = Cell::new("");
assert_eq!(cell.width, std::cell::Cell::new(None)); // not yet cached
assert_eq!(cell.width(), 2);
assert_eq!(cell.width, std::cell::Cell::new(Some(2))); // cached
}
}

View File

@@ -14,7 +14,7 @@ pub use alignment::Alignment;
pub use constraint::Constraint;
pub use direction::Direction;
pub use flex::Flex;
pub use layout::{Layout, Spacing};
pub use layout::Layout;
pub use margin::Margin;
pub use position::Position;
pub use rect::{Columns, Offset, Positions, Rect, Rows};

View File

@@ -41,68 +41,6 @@ thread_local! {
));
}
/// Represents the spacing between segments in a layout.
///
/// The `Spacing` enum is used to define the spacing between segments in a layout. It can represent
/// either positive spacing (space between segments) or negative spacing (overlap between segments).
///
/// # Variants
///
/// - `Space(u16)`: Represents positive spacing between segments. The value indicates the number of
/// cells.
/// - `Overlap(u16)`: Represents negative spacing, causing overlap between segments. The value
/// indicates the number of overlapping cells.
///
/// # Default
///
/// The default value for `Spacing` is `Space(0)`, which means no spacing or no overlap between
/// segments.
///
/// # Conversions
///
/// The `Spacing` enum can be created from different integer types:
///
/// - From `u16`: Directly converts the value to `Spacing::Space`.
/// - From `i16`: Converts negative values to `Spacing::Overlap` and non-negative values to
/// `Spacing::Space`.
/// - From `i32`: Clamps the value to the range of `i16` and converts negative values to
/// `Spacing::Overlap` and non-negative values to `Spacing::Space`.
///
/// See the [`Layout::spacing`] method for details on how to use this enum.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Spacing {
Space(u16),
Overlap(u16),
}
impl Default for Spacing {
fn default() -> Self {
Self::Space(0)
}
}
impl From<i32> for Spacing {
fn from(value: i32) -> Self {
Self::from(value.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16)
}
}
impl From<u16> for Spacing {
fn from(value: u16) -> Self {
Self::Space(value)
}
}
impl From<i16> for Spacing {
fn from(value: i16) -> Self {
if value < 0 {
Self::Overlap(value.unsigned_abs())
} else {
Self::Space(value.unsigned_abs())
}
}
}
/// A layout is a set of constraints that can be applied to a given area to split it into smaller
/// ones.
///
@@ -114,7 +52,7 @@ impl From<i16> for Spacing {
/// - a flex option
/// - a spacing option
///
/// The algorithm used to compute the layout is based on the [`cassowary`] solver. It is a simple
/// The algorithm used to compute the layout is based on the [`cassowary-rs`] solver. It is a simple
/// linear solver that can be used to solve linear equations and inequalities. In our case, we
/// define a set of constraints that are applied to split the provided area into Rects aligned in a
/// single direction, and the solver computes the values of the position and sizes that satisfy as
@@ -171,7 +109,7 @@ impl From<i16> for Spacing {
/// ![layout
/// example](https://camo.githubusercontent.com/77d22f3313b782a81e5e033ef82814bb48d786d2598699c27f8e757ccee62021/68747470733a2f2f7668732e636861726d2e73682f7668732d315a4e6f4e4c4e6c4c746b4a58706767396e435635652e676966)
///
/// [`cassowary`]: https://crates.io/crates/cassowary
/// [`cassowary-rs`]: https://crates.io/crates/cassowary
/// [Examples]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Layout {
@@ -179,7 +117,7 @@ pub struct Layout {
constraints: Vec<Constraint>,
margin: Margin,
flex: Flex,
spacing: Spacing,
spacing: u16,
}
impl Layout {
@@ -464,16 +402,9 @@ impl Layout {
/// Sets the spacing between items in the layout.
///
/// The `spacing` method sets the spacing between items in the layout. The spacing is applied
/// evenly between all segments. The spacing value represents the number of cells between each
/// evenly between all items. The spacing value represents the number of cells between each
/// item.
///
/// Spacing can be positive integers, representing gaps between segments; or negative integers
/// representing overlaps. Additionally, one of the variants of the [`Spacing`] enum can be
/// passed to this function. See the documentation of the [`Spacing`] enum for more information.
///
/// Note that if the layout has only one segment, the spacing will not be applied.
/// Also, spacing will not be applied for [`Flex::SpaceAround`] and [`Flex::SpaceBetween`]
///
/// # Examples
///
/// In this example, the spacing between each item in the layout is set to 2 cells.
@@ -484,19 +415,13 @@ impl Layout {
/// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).spacing(2);
/// ```
///
/// In this example, the spacing between each item in the layout is set to -1 cells, i.e. the
/// three segments will have an overlapping border.
/// # Notes
///
/// ```rust
/// use ratatui::layout::{Constraint::*, Layout};
/// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).spacing(-1);
/// ```
/// - If the layout has only one item, the spacing will not be applied.
/// - Spacing will not be applied for [`Flex::SpaceAround`] and [`Flex::SpaceBetween`]
#[must_use = "method moves the value of self and returns the modified value"]
pub fn spacing<T>(mut self, spacing: T) -> Self
where
T: Into<Spacing>,
{
self.spacing = spacing.into();
pub const fn spacing(mut self, spacing: u16) -> Self {
self.spacing = spacing;
self
}
@@ -567,7 +492,7 @@ impl Layout {
.expect("invalid number of rects")
}
/// Wrapper function around the cassowary solver to be able to split a given area into
/// Wrapper function around the cassowary-rs solver to be able to split a given area into
/// smaller ones based on the preferred widths or heights and the direction.
///
/// Note that the constraints are applied to the whole area that is to be split, so using
@@ -605,7 +530,7 @@ impl Layout {
self.split_with_spacers(area).0
}
/// Wrapper function around the cassowary solver that splits the given area into smaller ones
/// Wrapper function around the cassowary-r solver that splits the given area into smaller ones
/// based on the preferred widths or heights and the direction, with the ability to include
/// spacers between the areas.
///
@@ -695,7 +620,7 @@ impl Layout {
};
// ```plain
// <───────────────────────────────────area_size──────────────────────────────────>
// <───────────────────────────────────area_width─────────────────────────────────>
// ┌─area_start area_end─┐
// V V
// ┌────┬───────────────────┬────┬─────variables─────┬────┬───────────────────┬────┐
@@ -730,18 +655,12 @@ impl Layout {
.collect_vec();
let flex = self.flex;
let spacing = match self.spacing {
Spacing::Space(x) => x as i16,
Spacing::Overlap(x) => -(x as i16),
};
let spacing = self.spacing;
let constraints = &self.constraints;
let area_size = Element::from((*variables.first().unwrap(), *variables.last().unwrap()));
configure_area(&mut solver, area_size, area_start, area_end)?;
configure_variable_in_area_constraints(&mut solver, &variables, area_size)?;
configure_variable_constraints(&mut solver, &variables)?;
configure_variable_constraints(&mut solver, &variables, area_size)?;
configure_flex_constraints(&mut solver, area_size, &spacers, flex, spacing)?;
configure_constraints(&mut solver, area_size, &segments, constraints, flex)?;
configure_fill_constraints(&mut solver, &segments, constraints, flex)?;
@@ -754,8 +673,7 @@ impl Layout {
// `solver.fetch_changes()` can only be called once per solve
let changes: HashMap<Variable, f64> = solver.fetch_changes().iter().copied().collect();
// debug_elements(&segments, &changes);
// debug_elements(&spacers, &changes);
// debug_segments(&segments, &changes);
let segment_rects = changes_to_rects(&changes, &segments, inner_area, self.direction);
let spacer_rects = changes_to_rects(&changes, &spacers, inner_area, self.direction);
@@ -775,7 +693,7 @@ fn configure_area(
Ok(())
}
fn configure_variable_in_area_constraints(
fn configure_variable_constraints(
solver: &mut Solver,
variables: &[Variable],
area: Element,
@@ -786,25 +704,11 @@ fn configure_variable_in_area_constraints(
solver.add_constraint(variable | LE(REQUIRED) | area.end)?;
}
Ok(())
}
fn configure_variable_constraints(
solver: &mut Solver,
variables: &[Variable],
) -> Result<(), AddConstraintError> {
// ┌────┬───────────────────┬────┬─────variables─────┬────┬───────────────────┬────┐
// │ │ │ │ │ │ │ │
// v v v v v v v v
// ┌ ┐┌──────────────────┐┌ ┐┌──────────────────┐┌ ┐┌──────────────────┐┌ ┐
// │ Max(20) │ │ Max(20) │ │ Max(20) │
// └ ┘└──────────────────┘└ ┘└──────────────────┘└ ┘└──────────────────┘└ ┘
// ^ ^ ^ ^ ^ ^ ^ ^
// └v0 └v1 └v2 └v3 └v4 └v5 └v6 └v7
for (&left, &right) in variables.iter().skip(1).tuples() {
// all variables are in ascending order
for (&left, &right) in variables.iter().tuple_windows() {
solver.add_constraint(left | LE(REQUIRED) | right)?;
}
Ok(())
}
@@ -822,7 +726,7 @@ fn configure_constraints(
solver.add_constraint(segment.has_int_size(max, MAX_SIZE_EQ))?;
}
Constraint::Min(min) => {
solver.add_constraint(segment.has_min_size(min as i16, MIN_SIZE_GE))?;
solver.add_constraint(segment.has_min_size(min, MIN_SIZE_GE))?;
if flex.is_legacy() {
solver.add_constraint(segment.has_int_size(min, MIN_SIZE_EQ))?;
} else {
@@ -855,7 +759,7 @@ fn configure_flex_constraints(
area: Element,
spacers: &[Element],
flex: Flex,
spacing: i16,
spacing: u16,
) -> Result<(), AddConstraintError> {
let spacers_except_first_and_last = spacers.get(1..spacers.len() - 1).unwrap_or(&[]);
let spacing_f64 = f64::from(spacing) * FLOAT_PRECISION_MULTIPLIER;
@@ -889,6 +793,8 @@ fn configure_flex_constraints(
}
for spacer in spacers_except_first_and_last {
solver.add_constraint(spacer.has_min_size(spacing, SPACER_SIZE_EQ))?;
}
for spacer in spacers_except_first_and_last {
solver.add_constraint(spacer.has_size(area, SPACE_GROW))?;
}
if let (Some(first), Some(last)) = (spacers.first(), spacers.last()) {
@@ -1009,18 +915,15 @@ fn changes_to_rects(
/// please leave this here as it's useful for debugging unit tests when we make any changes to
/// layout code - we should replace this with tracing in the future.
#[allow(dead_code)]
fn debug_elements(elements: &[Element], changes: &HashMap<Variable, f64>) {
let variables = format!(
fn debug_segments(segments: &[Element], changes: &HashMap<Variable, f64>) {
let ends = format!(
"{:?}",
elements
segments
.iter()
.map(|e| (
changes.get(&e.start).unwrap_or(&0.0) / FLOAT_PRECISION_MULTIPLIER,
changes.get(&e.end).unwrap_or(&0.0) / FLOAT_PRECISION_MULTIPLIER,
))
.collect::<Vec<(f64, f64)>>()
.map(|e| changes.get(&e.end).unwrap_or(&0.0))
.collect::<Vec<&f64>>()
);
dbg!(variables);
dbg!(ends);
}
/// A container used by the solver inside split
@@ -1053,7 +956,7 @@ impl Element {
self.size() | LE(strength) | (f64::from(size) * FLOAT_PRECISION_MULTIPLIER)
}
fn has_min_size(&self, size: i16, strength: f64) -> cassowary::Constraint {
fn has_min_size(&self, size: u16, strength: f64) -> cassowary::Constraint {
self.size() | GE(strength) | (f64::from(size) * FLOAT_PRECISION_MULTIPLIER)
}
@@ -1086,13 +989,12 @@ impl From<&Element> for Expression {
mod strengths {
use cassowary::strength::{MEDIUM, REQUIRED, STRONG, WEAK};
/// The strength to apply to Spacers to ensure that their sizes are equal.
///
/// ┌ ┐┌───┐┌ ┐┌───┐┌ ┐
/// ==x │ │ ==x │ │ ==x
/// └ ┘└───┘└ ┘└───┘└ ┘
pub const SPACER_SIZE_EQ: f64 = REQUIRED / 10.0;
pub const SPACER_SIZE_EQ: f64 = REQUIRED - 1.0;
/// The strength to apply to Min inequality constraints.
///
@@ -1216,7 +1118,7 @@ mod tests {
margin: Margin::new(0, 0),
constraints: vec![],
flex: Flex::default(),
spacing: Spacing::default(),
spacing: 0,
}
);
}
@@ -1261,7 +1163,7 @@ mod tests {
margin: Margin::new(0, 0),
constraints: vec![Constraint::Min(0)],
flex: Flex::default(),
spacing: Spacing::default(),
spacing: 0,
}
);
}
@@ -1275,7 +1177,7 @@ mod tests {
margin: Margin::new(0, 0),
constraints: vec![Constraint::Min(0)],
flex: Flex::default(),
spacing: Spacing::default(),
spacing: 0,
}
);
}
@@ -1382,9 +1284,8 @@ mod tests {
#[test]
fn spacing() {
assert_eq!(Layout::default().spacing(10).spacing, Spacing::Space(10));
assert_eq!(Layout::default().spacing(0).spacing, Spacing::Space(0));
assert_eq!(Layout::default().spacing(-10).spacing, Spacing::Overlap(10));
assert_eq!(Layout::default().spacing(10).spacing, 10);
assert_eq!(Layout::default().spacing(0).spacing, 0);
}
/// Tests for the `Layout::split()` function.
@@ -1405,9 +1306,6 @@ mod tests {
/// - underflow: constraint is for less than the full space
/// - overflow: constraint is for more than the full space
mod split {
use std::ops::Range;
use itertools::Itertools;
use pretty_assertions::assert_eq;
use rstest::rstest;
@@ -2039,305 +1937,303 @@ mod tests {
}
#[rstest]
#[case::len_min1(vec![Length(25), Min(100)], vec![0..0, 0..100])]
#[case::len_min2(vec![Length(25), Min(0)], vec![0..25, 25..100])]
#[case::len_max1(vec![Length(25), Max(0)], vec![0..100, 100..100])]
#[case::len_max2(vec![Length(25), Max(100)], vec![0..25, 25..100])]
#[case::len_perc(vec![Length(25), Percentage(25)], vec![0..25, 25..100])]
#[case::perc_len(vec![Percentage(25), Length(25)], vec![0..75, 75..100])]
#[case::len_ratio(vec![Length(25), Ratio(1, 4)], vec![0..25, 25..100])]
#[case::ratio_len(vec![Ratio(1, 4), Length(25)], vec![0..75, 75..100])]
#[case::len_len(vec![Length(25), Length(25)], vec![0..25, 25..100])]
#[case::len1(vec![Length(25), Length(25), Length(25)], vec![0..25, 25..50, 50..100])]
#[case::len2(vec![Length(15), Length(35), Length(25)], vec![0..15, 15..50, 50..100])]
#[case::len3(vec![Length(25), Length(25), Length(25)], vec![0..25, 25..50, 50..100])]
fn constraint_length(
#[case] constraints: Vec<Constraint>,
#[case] expected: Vec<Range<u16>>,
) {
#[case::length_priority(vec![0, 100], vec![Length(25), Min(100)])]
#[case::length_priority(vec![25, 75], vec![Length(25), Min(0)])]
#[case::length_priority(vec![100, 0], vec![Length(25), Max(0)])]
#[case::length_priority(vec![25, 75], vec![Length(25), Max(100)])]
#[case::length_priority(vec![25, 75], vec![Length(25), Percentage(25)])]
#[case::length_priority(vec![75, 25], vec![Percentage(25), Length(25)])]
#[case::length_priority(vec![25, 75], vec![Length(25), Ratio(1, 4)])]
#[case::length_priority(vec![75, 25], vec![Ratio(1, 4), Length(25)])]
#[case::length_priority(vec![25, 75], vec![Length(25), Length(25)])]
#[case::excess_in_last_variable(vec![25, 25, 50], vec![Length(25), Length(25), Length(25)])]
#[case::excess_in_last_variable(vec![15, 35, 50], vec![Length(15), Length(35), Length(25)])]
#[case::three_lengths(vec![25, 25, 50], vec![Length(25), Length(25), Length(25)])]
fn constraint_length(#[case] expected: Vec<u16>, #[case] constraints: Vec<Constraint>) {
let rect = Rect::new(0, 0, 100, 1);
let ranges = Layout::horizontal(constraints)
let r = Layout::horizontal(constraints)
.flex(Flex::Legacy)
.split(rect)
.iter()
.map(|r| r.left()..r.right())
.collect_vec();
assert_eq!(ranges, expected);
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(r, expected);
}
#[rstest]
#[case(7, vec![Length(4), Length(4)], vec![0..3, 4..7])]
#[case(4, vec![Length(4), Length(4)], vec![0..2, 3..4])]
#[case::table_length_test(vec![Length(4), Length(4)], vec![(0, 3), (4, 3)], 7)]
#[case::table_length_test(vec![Length(4), Length(4)], vec![(0, 2), (3, 1)], 4)]
fn table_length(
#[case] width: u16,
#[case] constraints: Vec<Constraint>,
#[case] expected: Vec<Range<u16>>,
#[case] expected: Vec<(u16, u16)>,
#[case] width: u16,
) {
let rect = Rect::new(0, 0, width, 1);
let ranges = Layout::horizontal(constraints)
let r = Layout::horizontal(constraints)
.spacing(1)
.flex(Flex::Start)
.split(rect)
.iter()
.map(|r| r.left()..r.right())
.collect::<Vec<Range<u16>>>();
assert_eq!(ranges, expected);
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(r, expected);
}
#[rstest]
#[case::min_len_max(vec![Min(25), Length(25), Max(25)], vec![0..50, 50..75, 75..100])]
#[case::max_len_min(vec![Max(25), Length(25), Min(25)], vec![0..25, 25..50, 50..100])]
#[case::len_len_len(vec![Length(33), Length(33), Length(33)], vec![0..33, 33..66, 66..100])]
#[case::len_len_len_25(vec![Length(25), Length(25), Length(25)], vec![0..25, 25..50, 50..100])]
#[case::perc_len_ratio(vec![Percentage(25), Length(25), Ratio(1, 4)], vec![0..25, 25..50, 50..100])]
#[case::len_ratio_perc(vec![Length(25), Ratio(1, 4), Percentage(25)], vec![0..25, 25..75, 75..100])]
#[case::ratio_len_perc(vec![Ratio(1, 4), Length(25), Percentage(25)], vec![0..50, 50..75, 75..100])]
#[case::ratio_perc_len(vec![Ratio(1, 4), Percentage(25), Length(25)], vec![0..50, 50..75, 75..100])]
#[case::len_len_min(vec![Length(100), Length(1), Min(20)], vec![0..80, 80..80, 80..100])]
#[case::min_len_len(vec![Min(20), Length(1), Length(100)], vec![0..20, 20..21, 21..100])]
#[case::fill_len_fill(vec![Fill(1), Length(10), Fill(1)], vec![0..45, 45..55, 55..100])]
#[case::fill_len_fill_2(vec![Fill(1), Length(10), Fill(2)], vec![0..30, 30..40, 40..100])]
#[case::fill_len_fill_4(vec![Fill(1), Length(10), Fill(4)], vec![0..18, 18..28, 28..100])]
#[case::fill_len_fill_5(vec![Fill(1), Length(10), Fill(5)], vec![0..15, 15..25, 25..100])]
#[case::len_len_len_25(vec![Length(25), Length(25), Length(25)], vec![0..25, 25..50, 50..100])]
#[case::unstable_test(vec![Length(25), Length(25), Length(25)], vec![0..25, 25..50, 50..100])]
#[case::length_is_higher_priority_than_min_max(vec![50, 25, 25], vec![Min(25), Length(25), Max(25)])]
#[case::length_is_higher_priority_than_min_max(vec![25, 25, 50], vec![Max(25), Length(25), Min(25)])]
#[case::excess_in_lowest_priority(vec![33, 33, 34], vec![Length(33), Length(33), Length(33)])]
#[case::excess_in_lowest_priority(vec![25, 25, 50], vec![Length(25), Length(25), Length(25)])]
#[case::length_higher_priority(vec![25, 25, 50], vec![Percentage(25), Length(25), Ratio(1, 4)])]
#[case::length_higher_priority(vec![25, 50, 25], vec![Length(25), Ratio(1, 4), Percentage(25)])]
#[case::length_higher_priority(vec![50, 25, 25], vec![Ratio(1, 4), Length(25), Percentage(25)])]
#[case::length_higher_priority(vec![50, 25, 25], vec![Ratio(1, 4), Percentage(25), Length(25)])]
#[case::length_higher_priority(vec![80, 0, 20], vec![Length(100), Length(1), Min(20)])]
#[case::length_higher_priority(vec![20, 1, 79], vec![Min(20), Length(1), Length(100)])]
#[case::length_higher_priority(vec![45, 10, 45], vec![Fill(1), Length(10), Fill(1)])]
#[case::length_higher_priority(vec![30, 10, 60], vec![Fill(1), Length(10), Fill(2)])]
#[case::length_higher_priority(vec![18, 10, 72], vec![Fill(1), Length(10), Fill(4)])]
#[case::length_higher_priority(vec![15, 10, 75], vec![Fill(1), Length(10), Fill(5)])]
#[case::three_lengths_reference(vec![25, 25, 50], vec![Length(25), Length(25), Length(25)])]
#[case::previously_unstable_test(vec![25, 25, 50], vec![Length(25), Length(25), Length(25)])]
fn length_is_higher_priority(
#[case] constraints: Vec<Constraint>,
#[case] expected: Vec<Range<u16>>,
) {
let rect = Rect::new(0, 0, 100, 1);
let ranges = Layout::horizontal(constraints)
.flex(Flex::Legacy)
.split(rect)
.iter()
.map(|r| r.left()..r.right())
.collect_vec();
assert_eq!(ranges, expected);
}
#[rstest]
#[case::min_len_max(vec![Min(25), Length(25), Max(25)], vec![50, 25, 25])]
#[case::max_len_min(vec![Max(25), Length(25), Min(25)], vec![25, 25, 50])]
#[case::len_len_len1(vec![Length(33), Length(33), Length(33)], vec![33, 33, 33])]
#[case::len_len_len2(vec![Length(25), Length(25), Length(25)], vec![25, 25, 25])]
#[case::perc_len_ratio(vec![Percentage(25), Length(25), Ratio(1, 4)], vec![25, 25, 25])]
#[case::len_ratio_perc(vec![Length(25), Ratio(1, 4), Percentage(25)], vec![25, 25, 25])]
#[case::ratio_len_perc(vec![Ratio(1, 4), Length(25), Percentage(25)], vec![25, 25, 25])]
#[case::ratio_perc_len(vec![Ratio(1, 4), Percentage(25), Length(25)], vec![25, 25, 25])]
#[case::len_len_min(vec![Length(100), Length(1), Min(20)], vec![79, 1, 20])]
#[case::min_len_len(vec![Min(20), Length(1), Length(100)], vec![20, 1, 79])]
#[case::fill_len_fill1(vec![Fill(1), Length(10), Fill(1)], vec![45, 10, 45])]
#[case::fill_len_fill2(vec![Fill(1), Length(10), Fill(2)], vec![30, 10, 60])]
#[case::fill_len_fill4(vec![Fill(1), Length(10), Fill(4)], vec![18, 10, 72])]
#[case::fill_len_fill5(vec![Fill(1), Length(10), Fill(5)], vec![15, 10, 75])]
#[case::len_len_len3(vec![Length(25), Length(25), Length(25)], vec![25, 25, 25])]
fn length_is_higher_priority_in_flex(
#[case] constraints: Vec<Constraint>,
#[case] expected: Vec<u16>,
#[case] constraints: Vec<Constraint>,
) {
let rect = Rect::new(0, 0, 100, 1);
for flex in [
Flex::Start,
Flex::End,
Flex::Center,
Flex::SpaceAround,
Flex::SpaceBetween,
] {
let widths = Layout::horizontal(&constraints)
.flex(flex)
.split(rect)
.iter()
.map(|r| r.width)
.collect_vec();
assert_eq!(widths, expected);
}
let r = Layout::horizontal(constraints)
.flex(Flex::Legacy)
.split(rect)
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(r, expected);
}
#[rstest]
#[case::fill_len_fill(vec![Fill(1), Length(10), Fill(2)], vec![0..13, 13..23, 23..50])]
#[case::len_fill_fill(vec![Length(10), Fill(2), Fill(1)], vec![0..10, 10..37, 37..50])] // might be unstable?
fn fixed_with_50_width(
#[case::length_is_higher_priority_than_min_max(vec![50, 25, 25], vec![Min(25), Length(25), Max(25)])]
#[case::length_is_higher_priority_than_min_max(vec![25, 25, 50], vec![Max(25), Length(25), Min(25)])]
#[case::excess_in_lowest_priority(vec![33, 33, 33], vec![Length(33), Length(33), Length(33)])]
#[case::excess_in_lowest_priority(vec![25, 25, 25], vec![Length(25), Length(25), Length(25)])]
#[case::length_higher_priority(vec![25, 25, 25], vec![Percentage(25), Length(25), Ratio(1, 4)])]
#[case::length_higher_priority(vec![25, 25, 25], vec![Length(25), Ratio(1, 4), Percentage(25)])]
#[case::length_higher_priority(vec![25, 25, 25], vec![Ratio(1, 4), Length(25), Percentage(25)])]
#[case::length_higher_priority(vec![25, 25, 25], vec![Ratio(1, 4), Percentage(25), Length(25)])]
#[case::length_higher_priority(vec![79, 1, 20], vec![Length(100), Length(1), Min(20)])]
#[case::length_higher_priority(vec![20, 1, 79], vec![Min(20), Length(1), Length(100)])]
#[case::length_higher_priority(vec![45, 10, 45], vec![Fill(1), Length(10), Fill(1)])]
#[case::length_higher_priority(vec![30, 10, 60], vec![Fill(1), Length(10), Fill(2)])]
#[case::length_higher_priority(vec![18, 10, 72], vec![Fill(1), Length(10), Fill(4)])]
#[case::length_higher_priority(vec![15, 10, 75], vec![Fill(1), Length(10), Fill(5)])]
#[case::previously_unstable_test(vec![25, 25, 25], vec![Length(25), Length(25), Length(25)])]
fn length_is_higher_priority_in_flex(
#[case] expected: Vec<u16>,
#[case] constraints: Vec<Constraint>,
#[case] expected: Vec<Range<u16>>,
) {
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(&constraints)
.flex(Flex::Start)
.split(rect)
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(r, expected);
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(&constraints)
.flex(Flex::Center)
.split(rect)
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(r, expected);
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(&constraints)
.flex(Flex::End)
.split(rect)
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(r, expected);
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(&constraints)
.flex(Flex::SpaceAround)
.split(rect)
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(r, expected);
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(&constraints)
.flex(Flex::SpaceBetween)
.split(rect)
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(r, expected);
}
#[rstest]
#[case::excess_in_last_variable(vec![13, 10, 27], vec![Fill(1), Length(10), Fill(2)])]
#[case::excess_in_last_variable(vec![10, 27, 13], vec![Length(10), Fill(2), Fill(1)])] // might be unstable?
fn fixed_with_50_width(#[case] expected: Vec<u16>, #[case] constraints: Vec<Constraint>) {
let rect = Rect::new(0, 0, 50, 1);
let ranges = Layout::horizontal(constraints)
let r = Layout::horizontal(constraints)
.flex(Flex::Legacy)
.split(rect)
.iter()
.map(|r| r.left()..r.right())
.collect_vec();
assert_eq!(ranges, expected);
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(r, expected);
}
#[rstest]
#[case::same_fill(vec![Fill(1), Fill(2), Fill(1), Fill(1)], vec![0..20, 20..60, 60..80, 80..100])]
#[case::inc_fill(vec![Fill(1), Fill(2), Fill(3), Fill(4)], vec![0..10, 10..30, 30..60, 60..100])]
#[case::dec_fill(vec![Fill(4), Fill(3), Fill(2), Fill(1)], vec![0..40, 40..70, 70..90, 90..100])]
#[case::rand_fill1(vec![Fill(1), Fill(3), Fill(2), Fill(4)], vec![0..10, 10..40, 40..60, 60..100])]
#[case::rand_fill2(vec![Fill(1), Fill(3), Length(50), Fill(2), Fill(4)], vec![0..5, 5..20, 20..70, 70..80, 80..100])]
#[case::rand_fill3(vec![Fill(1), Fill(3), Percentage(50), Fill(2), Fill(4)], vec![0..5, 5..20, 20..70, 70..80, 80..100])]
#[case::rand_fill4(vec![Fill(1), Fill(3), Min(50), Fill(2), Fill(4)], vec![0..5, 5..20, 20..70, 70..80, 80..100])]
#[case::rand_fill5(vec![Fill(1), Fill(3), Max(50), Fill(2), Fill(4)], vec![0..5, 5..20, 20..70, 70..80, 80..100])]
#[case::zero_fill1(vec![Fill(0), Fill(1), Fill(0)], vec![0..0, 0..100, 100..100])]
#[case::zero_fill2(vec![Fill(0), Length(1), Fill(0)], vec![0..50, 50..51, 51..100])]
#[case::zero_fill3(vec![Fill(0), Percentage(1), Fill(0)], vec![0..50, 50..51, 51..100])]
#[case::zero_fill4(vec![Fill(0), Min(1), Fill(0)], vec![0..50, 50..51, 51..100])]
#[case::zero_fill5(vec![Fill(0), Max(1), Fill(0)], vec![0..50, 50..51, 51..100])]
#[case::zero_fill6(vec![Fill(0), Fill(2), Fill(0), Fill(1)], vec![0..0, 0..67, 67..67, 67..100])]
#[case::space_fill1(vec![Fill(0), Fill(2), Percentage(20)], vec![0..0, 0..80, 80..100])]
#[case::space_fill2(vec![Fill(0), Fill(0), Percentage(20)], vec![0..40, 40..80, 80..100])]
#[case::space_fill3(vec![Fill(0), Ratio(1, 5)], vec![0..80, 80..100])]
#[case::space_fill4(vec![Fill(0), Fill(u16::MAX)], vec![0..0, 0..100])]
#[case::space_fill5(vec![Fill(u16::MAX), Fill(0)], vec![0..100, 100..100])]
#[case::space_fill6(vec![Fill(0), Percentage(20)], vec![0..80, 80..100])]
#[case::space_fill7(vec![Fill(1), Percentage(20)], vec![0..80, 80..100])]
#[case::space_fill8(vec![Fill(u16::MAX), Percentage(20)], vec![0..80, 80..100])]
#[case::space_fill9(vec![Fill(u16::MAX), Fill(0), Percentage(20)], vec![0..80, 80..80, 80..100])]
#[case::space_fill10(vec![Fill(0), Length(20)], vec![0..80, 80..100])]
#[case::space_fill11(vec![Fill(0), Min(20)], vec![0..80, 80..100])]
#[case::space_fill12(vec![Fill(0), Max(20)], vec![0..80, 80..100])]
#[case::fill_collapse1(vec![Fill(1), Fill(1), Fill(1), Min(30), Length(50)], vec![0..7, 7..13, 13..20, 20..50, 50..100])]
#[case::fill_collapse2(vec![Fill(1), Fill(1), Fill(1), Length(50), Length(50)], vec![0..0, 0..0, 0..0, 0..50, 50..100])]
#[case::fill_collapse3(vec![Fill(1), Fill(1), Fill(1), Length(75), Length(50)], vec![0..0, 0..0, 0..0, 0..75, 75..100])]
#[case::fill_collapse4(vec![Fill(1), Fill(1), Fill(1), Min(50), Max(50)], vec![0..0, 0..0, 0..0, 0..50, 50..100])]
#[case::fill_collapse5(vec![Fill(1), Fill(1), Fill(1), Ratio(1, 1)], vec![0..0, 0..0, 0..0, 0..100])]
#[case::fill_collapse6(vec![Fill(1), Fill(1), Fill(1), Percentage(100)], vec![0..0, 0..0, 0..0, 0..100])]
fn fill(#[case] constraints: Vec<Constraint>, #[case] expected: Vec<Range<u16>>) {
#[case::multiple_same_fill_are_same(vec![20, 40, 20, 20], vec![Fill(1), Fill(2), Fill(1), Fill(1)])]
#[case::incremental(vec![10, 20, 30, 40], vec![Fill(1), Fill(2), Fill(3), Fill(4)])]
#[case::decremental(vec![40, 30, 20, 10], vec![Fill(4), Fill(3), Fill(2), Fill(1)])]
#[case::randomly_ordered(vec![10, 30, 20, 40], vec![Fill(1), Fill(3), Fill(2), Fill(4)])]
#[case::randomly_ordered(vec![5, 15, 50, 10, 20], vec![Fill(1), Fill(3), Length(50), Fill(2), Fill(4)])]
#[case::randomly_ordered(vec![5, 15, 50, 10, 20], vec![Fill(1), Fill(3), Length(50), Fill(2), Fill(4)])]
#[case::randomly_ordered(vec![5, 15, 50, 10, 20], vec![Fill(1), Fill(3), Percentage(50), Fill(2), Fill(4)])]
#[case::randomly_ordered(vec![5, 15, 50, 10, 20], vec![Fill(1), Fill(3), Min(50), Fill(2), Fill(4)])]
#[case::randomly_ordered(vec![5, 15, 50, 10, 20], vec![Fill(1), Fill(3), Max(50), Fill(2), Fill(4)])]
#[case::zero_width(vec![0, 100, 0], vec![Fill(0), Fill(1), Fill(0)])]
#[case::zero_width(vec![50, 1, 49], vec![Fill(0), Length(1), Fill(0)])]
#[case::zero_width(vec![50, 1, 49], vec![Fill(0), Length(1), Fill(0)])]
#[case::zero_width(vec![50, 1, 49], vec![Fill(0), Percentage(1), Fill(0)])]
#[case::zero_width(vec![50, 1, 49], vec![Fill(0), Min(1), Fill(0)])]
#[case::zero_width(vec![50, 1, 49], vec![Fill(0), Max(1), Fill(0)])]
#[case::zero_width(vec![0, 67, 0, 33], vec![Fill(0), Fill(2), Fill(0), Fill(1)])]
#[case::space_filler(vec![0, 80, 20], vec![Fill(0), Fill(2), Percentage(20)])]
#[case::space_filler(vec![40, 40, 20], vec![Fill(0), Fill(0), Percentage(20)])]
#[case::space_filler(vec![80, 20], vec![Fill(0), Ratio(1, 5)])]
#[case::space_filler(vec![0, 100], vec![Fill(0), Fill(u16::MAX)])]
#[case::space_filler(vec![100, 0], vec![Fill(u16::MAX), Fill(0)])]
#[case::space_filler(vec![80, 20], vec![Fill(0), Percentage(20)])]
#[case::space_filler(vec![80, 20], vec![Fill(1), Percentage(20)])]
#[case::space_filler(vec![80, 20], vec![Fill(u16::MAX), Percentage(20)])]
#[case::space_filler(vec![80, 0, 20], vec![Fill(u16::MAX), Fill(0), Percentage(20)])]
#[case::space_filler(vec![80, 20], vec![Fill(0), Length(20)])]
#[case::space_filler(vec![80, 20], vec![Fill(0), Length(20)])]
#[case::space_filler(vec![80, 20], vec![Fill(0), Min(20)])]
#[case::space_filler(vec![80, 20], vec![Fill(0), Max(20)])]
#[case::fill_collapses_first(vec![7, 6, 7, 30, 50], vec![Fill(1), Fill(1), Fill(1), Min(30), Length(50)])]
#[case::fill_collapses_first(vec![0, 0, 0, 50, 50], vec![Fill(1), Fill(1), Fill(1), Length(50), Length(50)])]
#[case::fill_collapses_first(vec![0, 0, 0, 75, 25], vec![Fill(1), Fill(1), Fill(1), Length(75), Length(50)])]
#[case::fill_collapses_first(vec![0, 0, 0, 50, 50], vec![Fill(1), Fill(1), Fill(1), Min(50), Max(50)])]
#[case::fill_collapses_first(vec![0, 0, 0, 100], vec![Fill(1), Fill(1), Fill(1), Ratio(1, 1)])]
#[case::fill_collapses_first(vec![0, 0, 0, 100], vec![Fill(1), Fill(1), Fill(1), Percentage(100)])]
fn fill(#[case] expected: Vec<u16>, #[case] constraints: Vec<Constraint>) {
let rect = Rect::new(0, 0, 100, 1);
let ranges = Layout::horizontal(constraints)
let r = Layout::horizontal(constraints)
.flex(Flex::Legacy)
.split(rect)
.iter()
.map(|r| r.left()..r.right())
.collect_vec();
assert_eq!(ranges, expected);
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(r, expected);
}
#[rstest]
#[case::min_percentage(vec![Min(0), Percentage(20)], vec![0..80, 80..100])]
#[case::max_percentage(vec![Max(0), Percentage(20)], vec![0..0, 0..100])]
#[case::min_percentage(vec![80, 20], vec![Min(0), Percentage(20)])]
#[case::max_percentage(vec![0, 100], vec![Max(0), Percentage(20)])]
fn percentage_parameterized(
#[case] expected: Vec<u16>,
#[case] constraints: Vec<Constraint>,
#[case] expected: Vec<Range<u16>>,
) {
let rect = Rect::new(0, 0, 100, 1);
let ranges = Layout::horizontal(constraints)
let r = Layout::horizontal(constraints)
.flex(Flex::Legacy)
.split(rect)
.iter()
.map(|r| r.left()..r.right())
.collect_vec();
assert_eq!(ranges, expected);
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(r, expected);
}
#[rstest]
#[case::max_min(vec![Max(100), Min(0)], vec![0..100, 100..100])]
#[case::min_max(vec![Min(0), Max(100)], vec![0..0, 0..100])]
#[case::length_min(vec![Length(u16::MAX), Min(10)], vec![0..90, 90..100])]
#[case::min_length(vec![Min(10), Length(u16::MAX)], vec![0..10, 10..100])]
#[case::length_max(vec![Length(0), Max(10)], vec![0..90, 90..100])]
#[case::max_length(vec![Max(10), Length(0)], vec![0..10, 10..100])]
fn min_max(#[case] constraints: Vec<Constraint>, #[case] expected: Vec<Range<u16>>) {
#[case::min_max_priority(vec![100, 0], vec![Max(100), Min(0)])]
#[case::min_max_priority(vec![0, 100], vec![Min(0), Max(100)])]
#[case::min_max_priority(vec![90, 10], vec![Length(u16::MAX), Min(10)])]
#[case::min_max_priority(vec![10, 90], vec![Min(10), Length(u16::MAX)])]
#[case::min_max_priority(vec![90, 10], vec![Length(0), Max(10)])]
#[case::min_max_priority(vec![10, 90], vec![Max(10), Length(0)])]
fn min_max(#[case] expected: Vec<u16>, #[case] constraints: Vec<Constraint>) {
let rect = Rect::new(0, 0, 100, 1);
let ranges = Layout::horizontal(constraints)
let r = Layout::horizontal(constraints)
.flex(Flex::Legacy)
.split(rect)
.iter()
.map(|r| r.left()..r.right())
.collect_vec();
assert_eq!(ranges, expected);
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(r, expected);
}
#[rstest]
#[case::length_legacy(vec![Length(50)], vec![0..100], Flex::Legacy)]
#[case::length_start(vec![Length(50)], vec![0..50], Flex::Start)]
#[case::length_end(vec![Length(50)], vec![50..100], Flex::End)]
#[case::length_center(vec![Length(50)], vec![25..75], Flex::Center)]
#[case::ratio_legacy(vec![Ratio(1, 2)], vec![0..100], Flex::Legacy)]
#[case::ratio_start(vec![Ratio(1, 2)], vec![0..50], Flex::Start)]
#[case::ratio_end(vec![Ratio(1, 2)], vec![50..100], Flex::End)]
#[case::ratio_center(vec![Ratio(1, 2)], vec![25..75], Flex::Center)]
#[case::percent_legacy(vec![Percentage(50)], vec![0..100], Flex::Legacy)]
#[case::percent_start(vec![Percentage(50)], vec![0..50], Flex::Start)]
#[case::percent_end(vec![Percentage(50)], vec![50..100], Flex::End)]
#[case::percent_center(vec![Percentage(50)], vec![25..75], Flex::Center)]
#[case::min_legacy(vec![Min(50)], vec![0..100], Flex::Legacy)]
#[case::min_start(vec![Min(50)], vec![0..100], Flex::Start)]
#[case::min_end(vec![Min(50)], vec![0..100], Flex::End)]
#[case::min_center(vec![Min(50)], vec![0..100], Flex::Center)]
#[case::max_legacy(vec![Max(50)], vec![0..100], Flex::Legacy)]
#[case::max_start(vec![Max(50)], vec![0..50], Flex::Start)]
#[case::max_end(vec![Max(50)], vec![50..100], Flex::End)]
#[case::max_center(vec![Max(50)], vec![25..75], Flex::Center)]
#[case::spacebetween_becomes_stretch1(vec![Min(1)], vec![0..100], Flex::SpaceBetween)]
#[case::spacebetween_becomes_stretch2(vec![Max(20)], vec![0..100], Flex::SpaceBetween)]
#[case::spacebetween_becomes_stretch3(vec![Length(20)], vec![0..100], Flex::SpaceBetween)]
#[case::length_legacy2(vec![Length(25), Length(25)], vec![0..25, 25..100], Flex::Legacy)]
#[case::length_start2(vec![Length(25), Length(25)], vec![0..25, 25..50], Flex::Start)]
#[case::length_center2(vec![Length(25), Length(25)], vec![25..50, 50..75], Flex::Center)]
#[case::length_end2(vec![Length(25), Length(25)], vec![50..75, 75..100], Flex::End)]
#[case::length_spacebetween(vec![Length(25), Length(25)], vec![0..25, 75..100], Flex::SpaceBetween)]
#[case::length_spacearound(vec![Length(25), Length(25)], vec![17..42, 58..83], Flex::SpaceAround)]
#[case::percentage_legacy(vec![Percentage(25), Percentage(25)], vec![0..25, 25..100], Flex::Legacy)]
#[case::percentage_start(vec![Percentage(25), Percentage(25)], vec![0..25, 25..50], Flex::Start)]
#[case::percentage_center(vec![Percentage(25), Percentage(25)], vec![25..50, 50..75], Flex::Center)]
#[case::percentage_end(vec![Percentage(25), Percentage(25)], vec![50..75, 75..100], Flex::End)]
#[case::percentage_spacebetween(vec![Percentage(25), Percentage(25)], vec![0..25, 75..100], Flex::SpaceBetween)]
#[case::percentage_spacearound(vec![Percentage(25), Percentage(25)], vec![17..42, 58..83], Flex::SpaceAround)]
#[case::min_legacy2(vec![Min(25), Min(25)], vec![0..25, 25..100], Flex::Legacy)]
#[case::min_start2(vec![Min(25), Min(25)], vec![0..50, 50..100], Flex::Start)]
#[case::min_center2(vec![Min(25), Min(25)], vec![0..50, 50..100], Flex::Center)]
#[case::min_end2(vec![Min(25), Min(25)], vec![0..50, 50..100], Flex::End)]
#[case::min_spacebetween(vec![Min(25), Min(25)], vec![0..50, 50..100], Flex::SpaceBetween)]
#[case::min_spacearound(vec![Min(25), Min(25)], vec![0..50, 50..100], Flex::SpaceAround)]
#[case::max_legacy2(vec![Max(25), Max(25)], vec![0..25, 25..100], Flex::Legacy)]
#[case::max_start2(vec![Max(25), Max(25)], vec![0..25, 25..50], Flex::Start)]
#[case::max_center2(vec![Max(25), Max(25)], vec![25..50, 50..75], Flex::Center)]
#[case::max_end2(vec![Max(25), Max(25)], vec![50..75, 75..100], Flex::End)]
#[case::max_spacebetween(vec![Max(25), Max(25)], vec![0..25, 75..100], Flex::SpaceBetween)]
#[case::max_spacearound(vec![Max(25), Max(25)], vec![17..42, 58..83], Flex::SpaceAround)]
#[case::length_spaced_around(vec![Length(25), Length(25), Length(25)], vec![0..25, 38..63, 75..100], Flex::SpaceBetween)]
#[case::length(vec![(0, 100)], vec![Length(50)], Flex::Legacy)]
#[case::length(vec![(0, 50)], vec![Length(50)], Flex::Start)]
#[case::length(vec![(50, 50)], vec![Length(50)], Flex::End)]
#[case::length(vec![(25, 50)], vec![Length(50)], Flex::Center)]
#[case::ratio(vec![(0, 100)], vec![Ratio(1, 2)], Flex::Legacy)]
#[case::ratio(vec![(0, 50)], vec![Ratio(1, 2)], Flex::Start)]
#[case::ratio(vec![(50, 50)], vec![Ratio(1, 2)], Flex::End)]
#[case::ratio(vec![(25, 50)], vec![Ratio(1, 2)], Flex::Center)]
#[case::percent(vec![(0, 100)], vec![Percentage(50)], Flex::Legacy)]
#[case::percent(vec![(0, 50)], vec![Percentage(50)], Flex::Start)]
#[case::percent(vec![(50, 50)], vec![Percentage(50)], Flex::End)]
#[case::percent(vec![(25, 50)], vec![Percentage(50)], Flex::Center)]
#[case::min(vec![(0, 100)], vec![Min(50)], Flex::Legacy)]
#[case::min(vec![(0, 100)], vec![Min(50)], Flex::Start)]
#[case::min(vec![(0, 100)], vec![Min(50)], Flex::End)]
#[case::min(vec![(0, 100)], vec![Min(50)], Flex::Center)]
#[case::max(vec![(0, 100)], vec![Max(50)], Flex::Legacy)]
#[case::max(vec![(0, 50)], vec![Max(50)], Flex::Start)]
#[case::max(vec![(50, 50)], vec![Max(50)], Flex::End)]
#[case::max(vec![(25, 50)], vec![Max(50)], Flex::Center)]
#[case::spacebetween_becomes_stretch(vec![(0, 100)], vec![Min(1)], Flex::SpaceBetween)]
#[case::spacebetween_becomes_stretch(vec![(0, 100)], vec![Max(20)], Flex::SpaceBetween)]
#[case::spacebetween_becomes_stretch(vec![(0, 100)], vec![Length(20)], Flex::SpaceBetween)]
#[case::length(vec![(0, 25), (25, 75)], vec![Length(25), Length(25)], Flex::Legacy)]
#[case::length(vec![(0, 25), (25, 25)], vec![Length(25), Length(25)], Flex::Start)]
#[case::length(vec![(25, 25), (50, 25)], vec![Length(25), Length(25)], Flex::Center)]
#[case::length(vec![(50, 25), (75, 25)], vec![Length(25), Length(25)], Flex::End)]
#[case::length(vec![(0, 25), (75, 25)], vec![Length(25), Length(25)], Flex::SpaceBetween)]
#[case::length(vec![(17, 25), (58, 25)], vec![Length(25), Length(25)], Flex::SpaceAround)]
#[case::percentage(vec![(0, 25), (25, 75)], vec![Percentage(25), Percentage(25)], Flex::Legacy)]
#[case::percentage(vec![(0, 25), (25, 25)], vec![Percentage(25), Percentage(25)], Flex::Start)]
#[case::percentage(vec![(25, 25), (50, 25)], vec![Percentage(25), Percentage(25)], Flex::Center)]
#[case::percentage(vec![(50, 25), (75, 25)], vec![Percentage(25), Percentage(25)], Flex::End)]
#[case::percentage(vec![(0, 25), (75, 25)], vec![Percentage(25), Percentage(25)], Flex::SpaceBetween)]
#[case::percentage(vec![(17, 25), (58, 25)], vec![Percentage(25), Percentage(25)], Flex::SpaceAround)]
#[case::min(vec![(0, 25), (25, 75)], vec![Min(25), Min(25)], Flex::Legacy)]
#[case::min(vec![(0, 50), (50, 50)], vec![Min(25), Min(25)], Flex::Start)]
#[case::min(vec![(0, 50), (50, 50)], vec![Min(25), Min(25)], Flex::Center)]
#[case::min(vec![(0, 50), (50, 50)], vec![Min(25), Min(25)], Flex::End)]
#[case::min(vec![(0, 50), (50, 50)], vec![Min(25), Min(25)], Flex::SpaceBetween)]
#[case::min(vec![(0, 50), (50, 50)], vec![Min(25), Min(25)], Flex::SpaceAround)]
#[case::max(vec![(0, 25), (25, 75)], vec![Max(25), Max(25)], Flex::Legacy)]
#[case::max(vec![(0, 25), (25, 25)], vec![Max(25), Max(25)], Flex::Start)]
#[case::max(vec![(25, 25), (50, 25)], vec![Max(25), Max(25)], Flex::Center)]
#[case::max(vec![(50, 25), (75, 25)], vec![Max(25), Max(25)], Flex::End)]
#[case::max(vec![(0, 25), (75, 25)], vec![Max(25), Max(25)], Flex::SpaceBetween)]
#[case::max(vec![(17, 25), (58, 25)], vec![Max(25), Max(25)], Flex::SpaceAround)]
#[case::length_spaced_around(vec![(0, 25), (38, 25), (75, 25)], vec![Length(25), Length(25), Length(25)], Flex::SpaceBetween)]
fn flex_constraint(
#[case] constraints: Vec<Constraint>,
#[case] expected: Vec<Range<u16>>,
#[case] flex: Flex,
) {
let rect = Rect::new(0, 0, 100, 1);
let ranges = Layout::horizontal(constraints)
.flex(flex)
.split(rect)
.iter()
.map(|r| r.left()..r.right())
.collect_vec();
assert_eq!(ranges, expected);
}
#[rstest]
#[case::length_overlap1(vec![(0 , 20) , (20 , 20) , (40 , 20)] , vec![Length(20) , Length(20) , Length(20)] , Flex::Start , 0)]
#[case::length_overlap2(vec![(0 , 20) , (19 , 20) , (38 , 20)] , vec![Length(20) , Length(20) , Length(20)] , Flex::Start , -1)]
#[case::length_overlap3(vec![(21 , 20) , (40 , 20) , (59 , 20)] , vec![Length(20) , Length(20) , Length(20)] , Flex::Center , -1)]
#[case::length_overlap4(vec![(42 , 20) , (61 , 20) , (80 , 20)] , vec![Length(20) , Length(20) , Length(20)] , Flex::End , -1)]
#[case::length_overlap5(vec![(0 , 20) , (19 , 20) , (38 , 62)] , vec![Length(20) , Length(20) , Length(20)] , Flex::Legacy , -1)]
#[case::length_overlap6(vec![(0 , 20) , (40 , 20) , (80 , 20)] , vec![Length(20) , Length(20) , Length(20)] , Flex::SpaceBetween , -1)]
#[case::length_overlap7(vec![(10 , 20) , (40 , 20) , (70 , 20)] , vec![Length(20) , Length(20) , Length(20)] , Flex::SpaceAround , -1)]
fn flex_overlap(
#[case] expected: Vec<(u16, u16)>,
#[case] constraints: Vec<Constraint>,
#[case] flex: Flex,
#[case] spacing: i16,
) {
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(constraints)
.flex(flex)
.spacing(spacing)
.split(rect);
let result = r
.split(rect)
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(result, expected);
assert_eq!(r, expected);
}
#[rstest]
@@ -2352,7 +2248,7 @@ mod tests {
#[case] expected: Vec<(u16, u16)>,
#[case] constraints: Vec<Constraint>,
#[case] flex: Flex,
#[case] spacing: i16,
#[case] spacing: u16,
) {
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(constraints)
@@ -2409,7 +2305,7 @@ mod tests {
#[case] expected: Vec<(u16, u16)>,
#[case] constraints: Vec<Constraint>,
#[case] flex: Flex,
#[case] spacing: i16,
#[case] spacing: u16,
) {
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(constraints)
@@ -2476,50 +2372,7 @@ mod tests {
#[case] expected: Vec<(u16, u16)>,
#[case] constraints: Vec<Constraint>,
#[case] flex: Flex,
#[case] spacing: i16,
) {
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(constraints)
.flex(flex)
.spacing(spacing)
.split(rect);
let result = r
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, result);
}
#[rstest]
#[case::flex0_1(vec![(0 , 55), (45 , 55)] , vec![Fill(1), Fill(1)], Flex::Legacy , -10)]
#[case::flex0_2(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::SpaceAround , -10)]
#[case::flex0_3(vec![(0 , 55), (45 , 55)] , vec![Fill(1), Fill(1)], Flex::SpaceBetween , -10)]
#[case::flex0_4(vec![(0 , 55), (45 , 55)] , vec![Fill(1), Fill(1)], Flex::Start , -10)]
#[case::flex0_5(vec![(0 , 55), (45 , 55)] , vec![Fill(1), Fill(1)], Flex::Center , -10)]
#[case::flex0_6(vec![(0 , 55), (45 , 55)] , vec![Fill(1), Fill(1)], Flex::End , -10)]
#[case::flex10_1(vec![(0 , 51), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::Legacy , -1)]
#[case::flex10_2(vec![(0 , 51), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::Start , -1)]
#[case::flex10_3(vec![(0 , 51), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::Center , -1)]
#[case::flex10_4(vec![(0 , 51), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::End , -1)]
#[case::flex10_5(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::SpaceAround , -1)]
#[case::flex10_6(vec![(0 , 51), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::SpaceBetween , -1)]
#[case::flex_length0_1(vec![(0 , 55), (45, 10), (45 , 55)] , vec![Fill(1), Length(10), Fill(1)], Flex::Legacy , -10)]
#[case::flex_length0_2(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceAround , -10)]
#[case::flex_length0_3(vec![(0 , 55), (45, 10), (45 , 55)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceBetween , -10)]
#[case::flex_length0_4(vec![(0 , 55), (45, 10), (45 , 55)] , vec![Fill(1), Length(10), Fill(1)], Flex::Start , -10)]
#[case::flex_length0_5(vec![(0 , 55), (45, 10), (45 , 55)] , vec![Fill(1), Length(10), Fill(1)], Flex::Center , -10)]
#[case::flex_length0_6(vec![(0 , 55), (45, 10), (45 , 55)] , vec![Fill(1), Length(10), Fill(1)], Flex::End , -10)]
#[case::flex_length10_1(vec![(0 , 46), (45, 10), (54 , 46)] , vec![Fill(1), Length(10), Fill(1)], Flex::Legacy , -1)]
#[case::flex_length10_2(vec![(0 , 46), (45, 10), (54 , 46)] , vec![Fill(1), Length(10), Fill(1)], Flex::Start , -1)]
#[case::flex_length10_3(vec![(0 , 46), (45, 10), (54 , 46)] , vec![Fill(1), Length(10), Fill(1)], Flex::Center , -1)]
#[case::flex_length10_4(vec![(0 , 46), (45, 10), (54 , 46)] , vec![Fill(1), Length(10), Fill(1)], Flex::End , -1)]
#[case::flex_length10_5(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceAround , -1)]
#[case::flex_length10_6(vec![(0 , 46), (45, 10), (54 , 46)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceBetween , -1)]
fn fill_overlap(
#[case] expected: Vec<(u16, u16)>,
#[case] constraints: Vec<Constraint>,
#[case] flex: Flex,
#[case] spacing: i16,
#[case] spacing: u16,
) {
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(constraints)
@@ -2539,7 +2392,7 @@ mod tests {
#[case] expected: Vec<(u16, u16)>,
#[case] constraints: Vec<Constraint>,
#[case] flex: Flex,
#[case] spacing: i16,
#[case] spacing: u16,
) {
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(constraints)
@@ -2588,33 +2441,7 @@ mod tests {
#[case] expected: Vec<(u16, u16)>,
#[case] constraints: Vec<Constraint>,
#[case] flex: Flex,
#[case] spacing: i16,
) {
let rect = Rect::new(0, 0, 100, 1);
let (_, s) = Layout::horizontal(&constraints)
.flex(flex)
.spacing(spacing)
.split_with_spacers(rect);
assert_eq!(s.len(), constraints.len() + 1);
let result = s
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, result);
}
#[rstest]
#[case::spacers_1(vec![(0, 0), (10, 0), (100, 0)], vec![Length(10), Length(10)], Flex::Legacy, -1)]
#[case::spacers_2(vec![(0, 0), (10, 80), (100, 0)], vec![Length(10), Length(10)], Flex::SpaceBetween, -1)]
#[case::spacers_3(vec![(0, 27), (37, 26), (73, 27)], vec![Length(10), Length(10)], Flex::SpaceAround, -1)]
#[case::spacers_4(vec![(0, 0), (10, 0), (19, 81)], vec![Length(10), Length(10)], Flex::Start, -1)]
#[case::spacers_5(vec![(0, 41), (51, 0), (60, 40)], vec![Length(10), Length(10)], Flex::Center, -1)]
#[case::spacers_6(vec![(0, 81), (91, 0), (100, 0)], vec![Length(10), Length(10)], Flex::End, -1)]
fn split_with_spacers_and_overlap(
#[case] expected: Vec<(u16, u16)>,
#[case] constraints: Vec<Constraint>,
#[case] flex: Flex,
#[case] spacing: i16,
#[case] spacing: u16,
) {
let rect = Rect::new(0, 0, 100, 1);
let (_, s) = Layout::horizontal(&constraints)
@@ -2640,7 +2467,7 @@ mod tests {
#[case] expected: Vec<(u16, u16)>,
#[case] constraints: Vec<Constraint>,
#[case] flex: Flex,
#[case] spacing: i16,
#[case] spacing: u16,
) {
let rect = Rect::new(0, 0, 100, 1);
let (_, s) = Layout::horizontal(&constraints)

View File

@@ -35,17 +35,6 @@ impl Iterator for Rows {
self.current_row_fwd += 1;
Some(row)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let start_count = self.current_row_fwd.saturating_sub(self.rect.top());
let end_count = self.rect.bottom().saturating_sub(self.current_row_back);
let count = self
.rect
.height
.saturating_sub(start_count)
.saturating_sub(end_count) as usize;
(count, Some(count))
}
}
impl DoubleEndedIterator for Rows {
@@ -97,17 +86,6 @@ impl Iterator for Columns {
self.current_column_fwd += 1;
Some(column)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let start_count = self.current_column_fwd.saturating_sub(self.rect.left());
let end_count = self.rect.right().saturating_sub(self.current_column_back);
let count = self
.rect
.width
.saturating_sub(start_count)
.saturating_sub(end_count) as usize;
(count, Some(count))
}
}
impl DoubleEndedIterator for Columns {
@@ -129,9 +107,9 @@ impl DoubleEndedIterator for Columns {
/// The iterator will yield all positions within the `Rect` in a row-major order.
pub struct Positions {
/// The `Rect` associated with the positions.
rect: Rect,
pub rect: Rect,
/// The current position within the `Rect`.
current_position: Position,
pub current_position: Position,
}
impl Positions {
@@ -162,19 +140,6 @@ impl Iterator for Positions {
}
Some(position)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let row_count = self.rect.bottom().saturating_sub(self.current_position.y);
if row_count == 0 {
return (0, Some(0));
}
let column_count = self.rect.right().saturating_sub(self.current_position.x);
// subtract 1 from the row count to account for the current row
let count = (row_count - 1)
.saturating_mul(self.rect.width)
.saturating_add(column_count) as usize;
(count, Some(count))
}
}
#[cfg(test)]
@@ -185,106 +150,68 @@ mod tests {
fn rows() {
let rect = Rect::new(0, 0, 2, 3);
let mut rows = Rows::new(rect);
assert_eq!(rows.size_hint(), (3, Some(3)));
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.size_hint(), (2, Some(2)));
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.size_hint(), (1, Some(1)));
assert_eq!(rows.next(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next_back(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
}
#[test]
fn rows_back() {
let rect = Rect::new(0, 0, 2, 3);
let mut rows = Rows::new(rect);
assert_eq!(rows.size_hint(), (3, Some(3)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.size_hint(), (2, Some(2)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.size_hint(), (1, Some(1)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next_back(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
}
#[test]
fn rows_meet_in_the_middle() {
let rect = Rect::new(0, 0, 2, 4);
let mut rows = Rows::new(rect);
assert_eq!(rows.size_hint(), (4, Some(4)));
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.size_hint(), (3, Some(3)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 3, 2, 1)));
assert_eq!(rows.size_hint(), (2, Some(2)));
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.size_hint(), (1, Some(1)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next_back(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
}
#[test]
fn columns() {
let rect = Rect::new(0, 0, 3, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.size_hint(), (3, Some(3)));
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.size_hint(), (2, Some(2)));
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.size_hint(), (1, Some(1)));
assert_eq!(columns.next(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next_back(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
}
#[test]
fn columns_back() {
let rect = Rect::new(0, 0, 3, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.size_hint(), (3, Some(3)));
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.size_hint(), (2, Some(2)));
assert_eq!(columns.next_back(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.size_hint(), (1, Some(1)));
assert_eq!(columns.next_back(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next_back(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
}
#[test]
fn columns_meet_in_the_middle() {
let rect = Rect::new(0, 0, 4, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.size_hint(), (4, Some(4)));
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.size_hint(), (3, Some(3)));
assert_eq!(columns.next_back(), Some(Rect::new(3, 0, 1, 2)));
assert_eq!(columns.size_hint(), (2, Some(2)));
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.size_hint(), (1, Some(1)));
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next_back(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
}
/// We allow a total of `65536` columns in the range `(0..=65535)`. In this test we iterate
@@ -314,16 +241,10 @@ mod tests {
fn positions() {
let rect = Rect::new(0, 0, 2, 2);
let mut positions = Positions::new(rect);
assert_eq!(positions.size_hint(), (4, Some(4)));
assert_eq!(positions.next(), Some(Position::new(0, 0)));
assert_eq!(positions.size_hint(), (3, Some(3)));
assert_eq!(positions.next(), Some(Position::new(1, 0)));
assert_eq!(positions.size_hint(), (2, Some(2)));
assert_eq!(positions.next(), Some(Position::new(0, 1)));
assert_eq!(positions.size_hint(), (1, Some(1)));
assert_eq!(positions.next(), Some(Position::new(1, 1)));
assert_eq!(positions.size_hint(), (0, Some(0)));
assert_eq!(positions.next(), None);
assert_eq!(positions.size_hint(), (0, Some(0)));
}
}

View File

@@ -18,42 +18,28 @@
//! lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
//! Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
//!
//! ## Quickstart
//! ## Installation
//!
//! Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
//! Add `ratatui` as a dependency to your cargo.toml:
//!
//! ```shell
//! cargo add ratatui crossterm
//! cargo add ratatui
//! ```
//!
//! Then you can create a simple "Hello World" application:
//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
//! section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
//! [Termwiz]).
//!
//! ```rust,no_run
//! use crossterm::event::{self, Event};
//! use ratatui::{text::Text, Frame};
//! ## Introduction
//!
//! fn main() {
//! let mut terminal = ratatui::init();
//! loop {
//! terminal.draw(draw).expect("failed to draw frame");
//! if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
//! break;
//! }
//! }
//! ratatui::restore();
//! }
//! Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
//! that for each frame, your app must render all widgets that are supposed to be part of the UI.
//! This is in contrast to the retained mode style of rendering where widgets are updated and then
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
//! for more info.
//!
//! fn draw(frame: &mut Frame) {
//! let text = Text::raw("Hello World!");
//! frame.render_widget(text, frame.area());
//! }
//! ```
//!
//! The full code for this example which contains a little more detail is in the [Examples]
//! directory. For more guidance on different ways to structure your application see the
//! [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
//! various [Examples]. There are also several starter templates available in the [templates]
//! repository.
//! 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
//!
@@ -65,84 +51,46 @@
//! - [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
//! - [Breaking Changes] - a list of breaking changes in the library.
//!
//! 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.
//! ## Quickstart
//!
//! ## Introduction
//!
//! Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
//! that for each frame, your app must render all widgets that are supposed to be part of the UI.
//! This is in contrast to the retained mode style of rendering where widgets are updated and then
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
//! for more info.
//!
//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
//! section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
//! [Termwiz]).
//! The following example demonstrates the minimal amount of code necessary to setup a terminal and
//! render "Hello World!". The full code for this example which contains a little more detail is in
//! the [Examples] directory. For more guidance on different ways to structure your application see
//! the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
//! various [Examples]. There are also several starter templates available in the [templates]
//! repository.
//!
//! Every application built with `ratatui` needs to implement the following steps:
//!
//! - Initialize the terminal
//! - A main loop that:
//! - Draws the UI
//! - Handles input events
//! - A main loop to:
//! - Handle input events
//! - Draw the UI
//! - Restore the terminal state
//!
//! The library contains a [`prelude`] module that re-exports the most commonly used traits and
//! types for convenience. Most examples in the documentation will use this instead of showing the
//! full path of each type.
//!
//! ### Initialize and restore the terminal
//!
//! The [`Terminal`] type is the main entry point for any Ratatui application. It is generic over a
//! a choice of [`Backend`] implementations that each provide functionality to draw frames, clear
//! the screen, hide the cursor, etc. There are backend implementations for [Crossterm], [Termion]
//! and [Termwiz].
//! The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
//! abstraction over a choice of [`Backend`] implementations that provides functionality to draw
//! each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
//! implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
//! [Termwiz].
//!
//! The simplest way to initialize the terminal is to use the [`init`] function which returns a
//! [`DefaultTerminal`] instance with the default options, enters the Alternate Screen and Raw mode
//! and sets up a panic hook that restores the terminal in case of panic. This instance can then be
//! used to draw frames and interact with the terminal state. (The [`DefaultTerminal`] instance is a
//! type alias for a terminal with the [`crossterm`] backend.) The [`restore`] function restores the
//! terminal to its original state.
//!
//! ```rust,no_run
//! fn main() -> std::io::Result<()> {
//! let mut terminal = ratatui::init();
//! let result = run(&mut terminal);
//! ratatui::restore();
//! result
//! }
//! # fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> { Ok(()) }
//! ```
//!
//! See the [`backend` module] and the [Backends] section of the [Ratatui Website] for more info on
//! the alternate screen and raw mode.
//! Most applications should enter the Alternate Screen when starting and leave it when exiting and
//! also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
//! module] and the [Backends] section of the [Ratatui Website] for more info.
//!
//! ### Drawing the UI
//!
//! Drawing the UI is done by calling the [`Terminal::draw`] method on the terminal instance. This
//! method takes a closure that is called with a [`Frame`] instance. The [`Frame`] provides the size
//! of the area to draw to and allows the app to render any [`Widget`] using the provided
//! [`render_widget`] method. After this closure returns, a diff is performed and only the changes
//! are drawn to the terminal. See the [Widgets] section of the [Ratatui Website] for more info.
//!
//! The closure passed to the [`Terminal::draw`] method should handle the rendering of a full frame.
//!
//! ```rust,no_run
//! use ratatui::{widgets::Paragraph, Frame};
//!
//! fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
//! loop {
//! terminal.draw(|frame| draw(frame))?;
//! if handle_events()? {
//! break Ok(());
//! }
//! }
//! }
//!
//! fn draw(frame: &mut Frame) {
//! let text = Paragraph::new("Hello World!");
//! frame.render_widget(text, frame.area());
//! }
//! # fn handle_events() -> std::io::Result<bool> { Ok(false) }
//! ```
//! The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
//! [`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
//! using the provided [`render_widget`] method. After this closure returns, a diff is performed and
//! only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
//! for more info.
//!
//! ### Handling events
//!
@@ -151,23 +99,38 @@
//! Website] for more info. For example, if you are using [Crossterm], you can use the
//! [`crossterm::event`] module to handle events.
//!
//! ```rust,no_run
//! use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
//! ### Example
//!
//! fn handle_events() -> std::io::Result<bool> {
//! match event::read()? {
//! Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
//! KeyCode::Char('q') => return Ok(true),
//! // handle other key events
//! _ => {}
//! },
//! // handle other events
//! _ => {}
//! ```rust,no_run
//! use ratatui::{
//! crossterm::event::{self, Event, KeyCode, KeyEventKind},
//! widgets::{Block, Paragraph},
//! };
//!
//! fn main() -> std::io::Result<()> {
//! let mut terminal = ratatui::init();
//! loop {
//! terminal.draw(|frame| {
//! frame.render_widget(
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
//! frame.area(),
//! );
//! })?;
//! if let Event::Key(key) = event::read()? {
//! if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
//! break;
//! }
//! }
//! }
//! Ok(false)
//! ratatui::restore();
//! Ok(())
//! }
//! ```
//!
//! Running this example produces the following output:
//!
//! ![docsrs-hello]
//!
//! ## Layout
//!
//! The library comes with a basic yet useful layout management object called [`Layout`] which
@@ -183,12 +146,15 @@
//! };
//!
//! fn draw(frame: &mut Frame) {
//! use Constraint::{Fill, Length, Min};
//!
//! let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
//! let [title_area, main_area, status_area] = vertical.areas(frame.area());
//! let horizontal = Layout::horizontal([Fill(1); 2]);
//! let [left_area, right_area] = horizontal.areas(main_area);
//! let [title_area, main_area, status_area] = Layout::vertical([
//! Constraint::Length(1),
//! Constraint::Min(0),
//! Constraint::Length(1),
//! ])
//! .areas(frame.area());
//! let [left_area, right_area] =
//! Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
//! .areas(main_area);
//!
//! frame.render_widget(Block::bordered().title("Title Bar"), title_area);
//! frame.render_widget(Block::bordered().title("Status Bar"), status_area);
@@ -199,13 +165,7 @@
//!
//! Running this example produces the following output:
//!
//! ```text
//! Title Bar───────────────────────────────────
//! ┌Left────────────────┐┌Right───────────────┐
//! │ ││ │
//! └────────────────────┘└────────────────────┘
//! Status Bar──────────────────────────────────
//! ```
//! ![docsrs-layout]
//!
//! ## Text and styling
//!
@@ -257,6 +217,10 @@
//! frame.render_widget(paragraph, areas[3]);
//! }
//! ```
//!
//! Running this example produces the following output:
//!
//! ![docsrs-styling]
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
@@ -282,6 +246,9 @@
//! [Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
//! [Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
//! [FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
//! [docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
//! [docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
//! [docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
//! [`Frame`]: terminal::Frame
//! [`render_widget`]: terminal::Frame::render_widget
//! [`Widget`]: widgets::Widget
@@ -328,9 +295,6 @@
/// re-export the `crossterm` crate so that users don't have to add it as a dependency
#[cfg(feature = "crossterm")]
pub use crossterm;
/// re-export the `palette` crate so that users don't have to add it as a dependency
#[cfg(feature = "palette")]
pub use palette;
#[cfg(feature = "crossterm")]
pub use terminal::{
init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal,

View File

@@ -1,15 +1,5 @@
//! A prelude for conveniently writing applications using this library.
//!
//! The prelude module is no longer used universally in Ratatui, as it can make it harder to
//! distinguish between library and non-library types, especially when viewing source code
//! outside of an IDE (such as on GitHub or in a git diff). For more details and user feedback,
//! see [Issue #1150]. However, the prelude is still available for backward compatibility and for
//! those who prefer to use it.
//!
//! [Issue #1150]: https://github.com/ratatui/ratatui/issues/1150
//!
//! # Examples
//!
//! ```rust,no_run
//! use ratatui::prelude::*;
//! ```

View File

@@ -38,7 +38,8 @@
//! - [`Span`]s can be styled again, which will merge the styles.
//! - Many widget types can be styled directly rather than calling their `style()` method.
//!
//! See the [`Stylize`] and [`Styled`] traits for more information.
//! See the [`Stylize`] and [`Styled`] traits for more information. These traits are re-exported in
//! the [`prelude`] module for convenience.
//!
//! ## Example
//!
@@ -71,6 +72,7 @@
//! );
//! ```
//!
//! [`prelude`]: crate::prelude
//! [`Span`]: crate::text::Span
use std::fmt;

View File

@@ -371,54 +371,32 @@ impl Color {
/// Converts a HSL representation to a `Color::Rgb` instance.
///
/// The `from_hsl` function converts the Hue, Saturation and Lightness values to a corresponding
/// `Color` RGB equivalent.
/// The `from_hsl` function converts the Hue, Saturation and Lightness values to a
/// corresponding `Color` RGB equivalent.
///
/// Hue values should be in the range [-180..180]. Values outside this range are normalized by
/// wrapping.
///
/// Saturation and L values should be in the range [0.0..1.0]. Values outside this range are
/// clamped.
///
/// Clamping to valid ranges happens before conversion to RGB.
/// Hue values should be in the range [0, 360].
/// Saturation and L values should be in the range [0, 100].
/// Values that are not in the range are clamped to be within the range.
///
/// # Examples
///
/// ```
/// use ratatui::{palette::Hsl, style::Color};
/// use ratatui::style::Color;
///
/// // Minimum Lightness is black
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(0, 0, 0));
///
/// // Maximum Lightness is white
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 1.0));
/// let color: Color = Color::from_hsl(360.0, 100.0, 100.0);
/// assert_eq!(color, Color::Rgb(255, 255, 255));
///
/// // Minimum Saturation is fully desaturated red = gray
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.5));
/// assert_eq!(color, Color::Rgb(128, 128, 128));
///
/// // Bright red
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 1.0, 0.5));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
///
/// // Bright blue
/// let color: Color = Color::from_hsl(Hsl::new(-120.0, 1.0, 0.5));
/// assert_eq!(color, Color::Rgb(0, 0, 255));
/// let color: Color = Color::from_hsl(0.0, 0.0, 0.0);
/// assert_eq!(color, Color::Rgb(0, 0, 0));
/// ```
#[cfg(feature = "palette")]
pub fn from_hsl(hsl: palette::Hsl) -> Self {
use palette::{Clamp, FromColor, Srgb};
let hsl = hsl.clamp();
let Srgb {
red,
green,
blue,
standard: _,
}: Srgb<u8> = Srgb::from_color(hsl).into();
pub fn from_hsl(h: f64, s: f64, l: f64) -> Self {
// Clamp input values to valid ranges
let h = h.clamp(0.0, 360.0);
let s = s.clamp(0.0, 100.0);
let l = l.clamp(0.0, 100.0);
Self::Rgb(red, green, blue)
// Delegate to the function for normalized HSL to RGB conversion
normalized_hsl_to_rgb(h / 360.0, s / 100.0, l / 100.0)
}
/// Converts a `HSLuv` representation to a `Color::Rgb` instance.
@@ -426,43 +404,26 @@ impl Color {
/// The `from_hsluv` function converts the Hue, Saturation and Lightness values to a
/// corresponding `Color` RGB equivalent.
///
/// Hue values should be in the range [-180.0..180.0]. Values outside this range are normalized
/// by wrapping.
///
/// Saturation and L values should be in the range [0.0..100.0]. Values outside this range are
/// clamped.
///
/// Clamping to valid ranges happens before conversion to RGB.
/// Hue values should be in the range [0, 360].
/// Saturation and L values should be in the range [0, 100].
/// Values that are not in the range are clamped to be within the range.
///
/// # Examples
///
/// ```
/// use ratatui::{palette::Hsluv, style::Color};
/// use ratatui::prelude::*;
///
/// // Minimum Lightness is black
/// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 100.0, 0.0));
/// let color = Color::from_hsluv(360.0, 50.0, 75.0);
/// assert_eq!(color, Color::Rgb(223, 171, 181));
///
/// let color: Color = Color::from_hsluv(0.0, 0.0, 0.0);
/// assert_eq!(color, Color::Rgb(0, 0, 0));
///
/// // Maximum Lightness is white
/// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 100.0));
/// assert_eq!(color, Color::Rgb(255, 255, 255));
///
/// // Minimum Saturation is fully desaturated red = gray
/// let color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 50.0));
/// assert_eq!(color, Color::Rgb(119, 119, 119));
///
/// // Bright Red
/// let color = Color::from_hsluv(Hsluv::new(12.18, 100.0, 53.2));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
///
/// // Bright Blue
/// let color = Color::from_hsluv(Hsluv::new(-94.13, 100.0, 32.3));
/// assert_eq!(color, Color::Rgb(0, 0, 255));
/// ```
#[cfg(feature = "palette")]
pub fn from_hsluv(hsluv: palette::Hsluv) -> Self {
use palette::{Clamp, FromColor, Srgb};
let hsluv = hsluv.clamp();
pub fn from_hsluv(h: f64, s: f64, l: f64) -> Self {
use palette::{Clamp, FromColor, Hsluv, Srgb};
let hsluv = Hsluv::new(h, s, l).clamp();
let Srgb {
red,
green,
@@ -474,66 +435,145 @@ 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(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`.
//
// If it is `const` and made public, users can write the following:
//
// ```rust
// const SLATE_50: Color = normalized_hsl_to_rgb(0.210, 0.40, 0.98);
// ```
//
// For it to be const now, we need `#![feature(const_fn_floating_point_arithmetic)]`
// Tracking issue: https://github.com/rust-lang/rust/issues/57241
//
// We would also need to remove the use of `.round()` in this function, i.e.:
//
// ```rust
// Color::Rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
// ```
// Initialize RGB components
let red: f64;
let green: f64;
let blue: f64;
// Check if the color is achromatic (grayscale)
if saturation == 0.0 {
red = lightness;
green = lightness;
blue = lightness;
} else {
// Calculate RGB components for colored cases
let q = if lightness < 0.5 {
lightness * (1.0 + saturation)
} else {
lightness + saturation - lightness * saturation
};
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(
(red * 255.0).round() as u8,
(green * 255.0).round() as u8,
(blue * 255.0).round() as u8,
)
}
/// Helper function to calculate RGB component for a specific hue value.
fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
// Adjust the hue value to be within the valid range [0, 1]
let mut t = t;
if t < 0.0 {
t += 1.0;
}
if t > 1.0 {
t -= 1.0;
}
// Calculate the RGB component based on the hue value
if t < 1.0 / 6.0 {
p + (q - p) * 6.0 * t
} else if t < 1.0 / 2.0 {
q
} else if t < 2.0 / 3.0 {
p + (q - p) * (2.0 / 3.0 - t) * 6.0
} else {
p
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
#[cfg(feature = "palette")]
use palette::{Hsl, Hsluv};
#[cfg(feature = "palette")]
use rstest::rstest;
#[cfg(feature = "serde")]
use serde::de::{Deserialize, IntoDeserializer};
use super::*;
#[cfg(feature = "palette")]
#[rstest]
#[case::black(Hsl::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
#[case::white(Hsl::new(0.0, 0.0, 1.0), Color::Rgb(255, 255, 255))]
#[case::valid(Hsl::new(120.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
#[case::min_hue(Hsl::new(-180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
#[case::max_hue(Hsl::new(180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
#[case::min_saturation(Hsl::new(0.0, 0.0, 0.5), Color::Rgb(128, 128, 128))]
#[case::max_saturation(Hsl::new(0.0, 1.0, 0.5), Color::Rgb(255, 0, 0))]
#[case::min_lightness(Hsl::new(0.0, 0.5, 0.0), Color::Rgb(0, 0, 0))]
#[case::max_lightness(Hsl::new(0.0, 0.5, 1.0), Color::Rgb(255, 255, 255))]
#[case::under_hue_wraps(Hsl::new(-240.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
#[case::over_hue_wraps(Hsl::new(480.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
#[case::under_saturation_clamps(Hsl::new(0.0, -0.5, 0.75), Color::Rgb(191, 191, 191))]
#[case::over_saturation_clamps(Hsl::new(0.0, 1.2, 0.75), Color::Rgb(255, 128, 128))]
#[case::under_lightness_clamps(Hsl::new(0.0, 0.5, -0.20), Color::Rgb(0, 0, 0))]
#[case::over_lightness_clamps(Hsl::new(0.0, 0.5, 1.5), Color::Rgb(255, 255, 255))]
#[case::under_saturation_lightness_clamps(Hsl::new(0.0, -0.5, -0.20), Color::Rgb(0, 0, 0))]
#[case::over_saturation_lightness_clamps(Hsl::new(0.0, 1.2, 1.5), Color::Rgb(255, 255, 255))]
fn test_hsl_to_rgb(#[case] hsl: palette::Hsl, #[case] expected: Color) {
assert_eq!(Color::from_hsl(hsl), expected);
#[test]
fn test_hsl_to_rgb() {
// Test with valid HSL values
let color = Color::from_hsl(120.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(159, 223, 159));
// Test with H value at upper bound
let color = Color::from_hsl(360.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(223, 159, 159));
// Test with H value exceeding the upper bound
let color = Color::from_hsl(400.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(223, 159, 159));
// Test with S and L values exceeding the upper bound
let color = Color::from_hsl(240.0, 120.0, 150.0);
assert_eq!(color, Color::Rgb(255, 255, 255));
// Test with H, S, and L values below the lower bound
let color = Color::from_hsl(-20.0, -50.0, -20.0);
assert_eq!(color, Color::Rgb(0, 0, 0));
// Test with S and L values below the lower bound
let color = Color::from_hsl(60.0, -20.0, -10.0);
assert_eq!(color, Color::Rgb(0, 0, 0));
}
#[cfg(feature = "palette")]
#[rstest]
#[case::black(Hsluv::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
#[case::white(Hsluv::new(0.0, 0.0, 100.0), Color::Rgb(255, 255, 255))]
#[case::valid(Hsluv::new(120.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
#[case::min_hue(Hsluv::new(-180.0, 50.0, 75.0), Color::Rgb(135,196, 188))]
#[case::max_hue(Hsluv::new(180.0, 50.0, 75.0), Color::Rgb(135, 196, 188))]
#[case::min_saturation(Hsluv::new(0.0, 0.0, 75.0), Color::Rgb(185, 185, 185))]
#[case::max_saturation(Hsluv::new(0.0, 100.0, 75.0), Color::Rgb(255, 156, 177))]
#[case::min_lightness(Hsluv::new(0.0, 50.0, 0.0), Color::Rgb(0, 0, 0))]
#[case::max_lightness(Hsluv::new(0.0, 50.0, 100.0), Color::Rgb(255, 255, 255))]
#[case::under_hue_wraps(Hsluv::new(-240.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
#[case::over_hue_wraps(Hsluv::new(480.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
#[case::under_saturation_clamps(Hsluv::new(0.0, -50.0, 75.0), Color::Rgb(185, 185, 185))]
#[case::over_saturation_clamps(Hsluv::new(0.0, 150.0, 75.0), Color::Rgb(255, 156, 177))]
#[case::under_lightness_clamps(Hsluv::new(0.0, 50.0, -20.0), Color::Rgb(0, 0, 0))]
#[case::over_lightness_clamps(Hsluv::new(0.0, 50.0, 150.0), Color::Rgb(255, 255, 255))]
#[case::under_saturation_lightness_clamps(Hsluv::new(0.0, -50.0, -20.0), Color::Rgb(0, 0, 0))]
#[case::over_saturation_lightness_clamps(
Hsluv::new(0.0, 150.0, 150.0),
Color::Rgb(255, 255, 255)
)]
fn test_hsluv_to_rgb(#[case] hsluv: palette::Hsluv, #[case] expected: Color) {
assert_eq!(Color::from_hsluv(hsluv), expected);
#[test]
fn test_hsluv_to_rgb() {
// Test with valid HSLuv values
let color = Color::from_hsluv(120.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(147, 198, 129));
// Test with H value at upper bound
let color = Color::from_hsluv(360.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(223, 171, 181));
// Test with H value exceeding the upper bound
let color = Color::from_hsluv(400.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(226, 174, 140));
// Test with S and L values exceeding the upper bound
let color = Color::from_hsluv(240.0, 120.0, 150.0);
assert_eq!(color, Color::Rgb(255, 255, 255));
// Test with H, S, and L values below the lower bound
let color = Color::from_hsluv(0.0, 0.0, 0.0);
assert_eq!(color, Color::Rgb(0, 0, 0));
// Test with S and L values below the lower bound
let color = Color::from_hsluv(60.0, 0.0, 0.0);
assert_eq!(color, Color::Rgb(0, 0, 0));
}
#[test]

View File

@@ -203,14 +203,6 @@ pub mod scrollbar {
};
}
pub mod shade {
pub const EMPTY: &str = " ";
pub const LIGHT: &str = "";
pub const MEDIUM: &str = "";
pub const DARK: &str = "";
pub const FULL: &str = "";
}
#[cfg(test)]
mod tests {
use strum::ParseError;

View File

@@ -476,8 +476,7 @@ where
.set_cursor_position(self.viewport_area.as_position())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(_) => {
let area = self.viewport_area;
Viewport::Fixed(area) => {
for y in area.top()..area.bottom() {
self.backend.set_cursor_position(Position { x: 0, y })?;
self.backend.clear_region(ClearType::AfterCursor)?;

21
src/text/line.rs Executable file → Normal file
View File

@@ -687,21 +687,6 @@ impl Widget for Line<'_> {
impl WidgetRef for Line<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
self.render_with_alignment(area, buf, None);
}
}
impl Line<'_> {
/// An internal implementation method for `WidgetRef::render_ref`
///
/// Allows the parent widget to define a default alignment, to be
/// used if `Line::alignment` is `None`.
pub(crate) fn render_with_alignment(
&self,
area: Rect,
buf: &mut Buffer,
parent_alignment: Option<Alignment>,
) {
let area = area.intersection(buf.area);
if area.is_empty() {
return;
@@ -714,12 +699,10 @@ impl Line<'_> {
buf.set_style(area, self.style);
let alignment = self.alignment.or(parent_alignment);
let area_width = usize::from(area.width);
let can_render_complete_line = line_width <= area_width;
if can_render_complete_line {
let indent_width = match alignment {
let indent_width = match self.alignment {
Some(Alignment::Center) => (area_width.saturating_sub(line_width)) / 2,
Some(Alignment::Right) => area_width.saturating_sub(line_width),
Some(Alignment::Left) | None => 0,
@@ -730,7 +713,7 @@ impl Line<'_> {
} else {
// There is not enough space to render the whole line. As the right side is truncated by
// the area width, only truncate the left.
let skip_width = match alignment {
let skip_width = match self.alignment {
Some(Alignment::Center) => (line_width.saturating_sub(area_width)) / 2,
Some(Alignment::Right) => line_width.saturating_sub(area_width),
Some(Alignment::Left) | None => 0,

46
src/text/text.rs Executable file → Normal file
View File

@@ -735,8 +735,23 @@ 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, line_area) in self.iter().zip(area.rows()) {
line.render_with_alignment(line_area, buf, self.alignment);
for (line, row) in self.iter().zip(area.rows()) {
let line_width = line.width() as u16;
let x_offset = match (self.alignment, line.alignment) {
(Some(Alignment::Center), None) => area.width.saturating_sub(line_width) / 2,
(Some(Alignment::Right), None) => area.width.saturating_sub(line_width),
_ => 0,
};
let line_area = Rect {
x: area.x + x_offset,
y: row.y,
width: area.width - x_offset,
height: 1,
};
line.render(line_area, buf);
}
}
}
@@ -1181,33 +1196,6 @@ mod tests {
assert_eq!(buf, Buffer::with_lines([" foo "]));
}
#[test]
fn render_right_aligned_with_truncation() {
let text = Text::from("123456789").alignment(Alignment::Right);
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["56789"]));
}
#[test]
fn render_centered_odd_with_truncation() {
let text = Text::from("123456789").alignment(Alignment::Center);
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["34567"]));
}
#[test]
fn render_centered_even_with_truncation() {
let text = Text::from("123456789").alignment(Alignment::Center);
let area = Rect::new(0, 0, 6, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["234567"]));
}
#[test]
fn render_one_line_right() {
let text = Text::from(vec![

View File

@@ -50,7 +50,7 @@ pub use self::{
logo::{RatatuiLogo, Size as RatatuiLogoSize},
paragraph::{Paragraph, Wrap},
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
sparkline::{RenderDirection, Sparkline, SparklineBar},
sparkline::{RenderDirection, Sparkline},
table::{Cell, HighlightSpacing, Row, Table, TableState},
tabs::Tabs,
};

View File

@@ -12,29 +12,10 @@ use crate::{
/// Widget to render a sparkline over one or more lines.
///
/// Each bar in a `Sparkline` represents a value from the provided dataset. The height of the bar
/// is determined by the value in the dataset.
///
/// You can create a `Sparkline` using [`Sparkline::default`].
///
/// The data is set using [`Sparkline::data`]. The data can be a slice of `u64`, `Option<u64>`, or a
/// [`SparklineBar`]. For the `Option<u64>` and [`SparklineBar`] cases, a data point with a value
/// of `None` is interpreted an as the _absence_ of a value.
///
/// `Sparkline` can be styled either using [`Sparkline::style`] or preferably using the methods
/// provided by the [`Stylize`](crate::style::Stylize) trait. The style may be set for the entire
/// widget or for individual bars by setting individual [`SparklineBar::style`].
///
/// The bars are rendered using a set of symbols. The default set is [`symbols::bar::NINE_LEVELS`].
/// You can change the set using [`Sparkline::bar_set`].
///
/// If the data provided is a slice of `u64` or `Option<u64>`, the bars will be styled with the
/// style of the sparkline. If the data is a slice of [`SparklineBar`], the bars will be
/// styled with the style of the sparkline combined with the style provided in the [`SparklineBar`]
/// if it is set, otherwise the sparkline style will be used.
///
/// Absent values and will be rendered with the style set by [`Sparkline::absent_value_style`] and
/// the symbol set by [`Sparkline::absent_value_symbol`].
/// provided by the [`Stylize`](crate::style::Stylize) trait.
///
/// # Setter methods
///
@@ -47,8 +28,7 @@ use crate::{
///
/// ```
/// use ratatui::{
/// style::{Color, Style, Stylize},
/// symbols,
/// style::{Style, Stylize},
/// widgets::{Block, RenderDirection, Sparkline},
/// };
///
@@ -57,9 +37,7 @@ use crate::{
/// .data(&[0, 2, 3, 4, 1, 4, 10])
/// .max(5)
/// .direction(RenderDirection::RightToLeft)
/// .style(Style::default().red().on_white())
/// .absent_value_style(Style::default().fg(Color::Red))
/// .absent_value_symbol(symbols::shade::FULL);
/// .style(Style::default().red().on_white());
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Sparkline<'a> {
@@ -67,18 +45,14 @@ pub struct Sparkline<'a> {
block: Option<Block<'a>>,
/// Widget style
style: Style,
/// Style of absent values
absent_value_style: Style,
/// The symbol to use for absent values
absent_value_symbol: AbsentValueSymbol,
/// A slice of the data to display
data: Vec<SparklineBar>,
data: &'a [u64],
/// The maximum value to take to compute the maximum bar height (if nothing is specified, the
/// widget uses the max of the dataset)
max: Option<u64>,
/// A set of bar symbols used to represent the give data
bar_set: symbols::bar::Set,
/// The direction to render the sparkline, either from left to right, or from right to left
// The direction to render the sparkine, either from left to right, or from right to left
direction: RenderDirection,
}
@@ -116,53 +90,9 @@ impl<'a> Sparkline<'a> {
self
}
/// Sets the style to use for absent values.
///
/// Absent values are values in the dataset that are `None`.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// The foreground corresponds to the bars while the background is everything else.
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn absent_value_style<S: Into<Style>>(mut self, style: S) -> Self {
self.absent_value_style = style.into();
self
}
/// Sets the symbol to use for absent values.
///
/// Absent values are values in the dataset that are `None`.
///
/// The default is [`symbols::shade::EMPTY`].
#[must_use = "method moves the value of self and returns the modified value"]
pub fn absent_value_symbol(mut self, symbol: impl Into<String>) -> Self {
self.absent_value_symbol = AbsentValueSymbol(symbol.into());
self
}
/// Sets the dataset for the sparkline.
///
/// Each item in the dataset is a bar in the sparkline. The height of the bar is determined by
/// the value in the dataset.
///
/// The data can be a slice of `u64`, `Option<u64>`, or a [`SparklineBar`]. For the
/// `Option<u64>` and [`SparklineBar`] cases, a data point with a value of `None` is
/// interpreted an as the _absence_ of a value.
///
/// If the data provided is a slice of `u64` or `Option<u64>`, the bars will be styled with the
/// style of the sparkline. If the data is a slice of [`SparklineBar`], the bars will be
/// styled with the style of the sparkline combined with the style provided in the
/// [`SparklineBar`] if it is set, otherwise the sparkline style will be used.
///
/// Absent values and will be rendered with the style set by [`Sparkline::absent_value_style`]
/// and the symbol set by [`Sparkline::absent_value_symbol`].
///
/// # Examples
///
/// Create a `Sparkline` from a slice of `u64`:
/// # Example
///
/// ```
/// use ratatui::{layout::Rect, widgets::Sparkline, Frame};
@@ -173,41 +103,9 @@ impl<'a> Sparkline<'a> {
/// frame.render_widget(sparkline, area);
/// # }
/// ```
///
/// Create a `Sparkline` from a slice of `Option<u64>` such that some bars are absent:
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// let data = vec![Some(1), None, Some(3)];
/// let sparkline = Sparkline::default().data(data);
/// frame.render_widget(sparkline, area);
/// # }
/// ```
///
/// Create a [`Sparkline`] from a a Vec of [`SparklineBar`] such that some bars are styled:
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// let data = vec![
/// SparklineBar::from(1).style(Some(Style::default().fg(Color::Red))),
/// SparklineBar::from(2),
/// SparklineBar::from(3).style(Some(Style::default().fg(Color::Blue))),
/// ];
/// let sparkline = Sparkline::default().data(data);
/// frame.render_widget(sparkline, area);
/// # }
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn data<T>(mut self, data: T) -> Self
where
T: IntoIterator,
T::Item: Into<SparklineBar>,
{
self.data = data.into_iter().map(Into::into).collect();
pub const fn data(mut self, data: &'a [u64]) -> Self {
self.data = data;
self
}
@@ -241,75 +139,6 @@ impl<'a> Sparkline<'a> {
}
}
/// An bar in a `Sparkline`.
///
/// The height of the bar is determined by the value and a value of `None` is interpreted as the
/// _absence_ of a value, as distinct from a value of `Some(0)`.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
pub struct SparklineBar {
/// The value of the bar.
///
/// If `None`, the bar is absent.
value: Option<u64>,
/// The style of the bar.
///
/// If `None`, the bar will use the style of the sparkline.
style: Option<Style>,
}
impl SparklineBar {
/// Sets the style of the bar.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// If not set, the default style of the sparkline will be used.
///
/// As well as the style of the sparkline, each [`SparklineBar`] may optionally set its own
/// style. If set, the style of the bar will be the style of the sparkline combined with
/// the style of the bar.
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Option<Style>>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
}
impl From<Option<u64>> for SparklineBar {
fn from(value: Option<u64>) -> Self {
Self { value, style: None }
}
}
impl From<u64> for SparklineBar {
fn from(value: u64) -> Self {
Self {
value: Some(value),
style: None,
}
}
}
impl From<&u64> for SparklineBar {
fn from(value: &u64) -> Self {
Self {
value: Some(*value),
style: None,
}
}
}
impl From<&Option<u64>> for SparklineBar {
fn from(value: &Option<u64>) -> Self {
Self {
value: *value,
style: None,
}
}
}
impl<'a> Styled for Sparkline<'a> {
type Item = Self;
@@ -336,98 +165,55 @@ impl WidgetRef for Sparkline<'_> {
}
}
/// A newtype wrapper for the symbol to use for absent values.
#[derive(Debug, Clone, Eq, PartialEq)]
struct AbsentValueSymbol(String);
impl Default for AbsentValueSymbol {
fn default() -> Self {
Self(symbols::shade::EMPTY.to_string())
}
}
impl Sparkline<'_> {
fn render_sparkline(&self, spark_area: Rect, buf: &mut Buffer) {
if spark_area.is_empty() {
return;
}
// determine the maximum height across all bars
let max_height = self
let max = self
.max
.unwrap_or_else(|| self.data.iter().filter_map(|s| s.value).max().unwrap_or(1));
// determine the maximum index to render
.unwrap_or_else(|| *self.data.iter().max().unwrap_or(&1));
let max_index = min(spark_area.width as usize, self.data.len());
// render each item in the data
for (i, item) in self.data.iter().take(max_index).enumerate() {
let x = match self.direction {
RenderDirection::LeftToRight => spark_area.left() + i as u16,
RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
};
// determine the height, symbol and style to use for the item
//
// if the item is not absent:
// - the height is the value of the item scaled to the height of the spark area
// - the symbol is determined by the scaled height
// - the style is the style of the item, if one is set
//
// otherwise:
// - the height is the total height of the spark area
// - the symbol is the absent value symbol
// - the style is the absent value style
let (mut height, symbol, style) = match item {
SparklineBar {
value: Some(value),
style,
} => {
let height = if max_height == 0 {
0
} else {
*value * u64::from(spark_area.height) * 8 / max_height
};
(height, None, *style)
}
_ => (
u64::from(spark_area.height) * 8,
Some(self.absent_value_symbol.0.as_str()),
Some(self.absent_value_style),
),
};
// render the item from top to bottom
//
// if the symbol is set it will be used for the entire height of the bar, otherwise the
// symbol will be determined by the _remaining_ height.
//
// if the style is set it will be used for the entire height of the bar, otherwise the
// sparkline style will be used.
for j in (0..spark_area.height).rev() {
let symbol = symbol.unwrap_or_else(|| self.symbol_for_height(height));
if height > 8 {
height -= 8;
let mut data = self
.data
.iter()
.take(max_index)
.map(|e| {
if max == 0 {
0
} else {
height = 0;
e * u64::from(spark_area.height) * 8 / max
}
})
.collect::<Vec<u64>>();
for j in (0..spark_area.height).rev() {
for (i, d) in data.iter_mut().enumerate() {
let symbol = match *d {
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
3 => self.bar_set.three_eighths,
4 => self.bar_set.half,
5 => self.bar_set.five_eighths,
6 => self.bar_set.three_quarters,
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
};
let x = match self.direction {
RenderDirection::LeftToRight => spark_area.left() + i as u16,
RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
};
buf[(x, spark_area.top() + j)]
.set_symbol(symbol)
.set_style(self.style.patch(style.unwrap_or_default()));
}
}
}
.set_style(self.style);
const fn symbol_for_height(&self, height: u64) -> &str {
match height {
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
3 => self.bar_set.three_eighths,
4 => self.bar_set.half,
5 => self.bar_set.five_eighths,
6 => self.bar_set.three_quarters,
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
if *d > 8 {
*d -= 8;
} else {
*d = 0;
}
}
}
}
}
@@ -464,78 +250,6 @@ mod tests {
);
}
#[test]
fn it_can_be_created_from_vec_of_u64() {
let data = vec![1_u64, 2, 3];
let spark_data = Sparkline::default().data(data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(2),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
#[test]
fn it_can_be_created_from_vec_of_option_u64() {
let data = vec![Some(1_u64), None, Some(3)];
let spark_data = Sparkline::default().data(data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(None),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
#[test]
fn it_can_be_created_from_array_of_u64() {
let data = [1_u64, 2, 3];
let spark_data = Sparkline::default().data(data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(2),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
#[test]
fn it_can_be_created_from_array_of_option_u64() {
let data = [Some(1_u64), None, Some(3)];
let spark_data = Sparkline::default().data(data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(None),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
#[test]
fn it_can_be_created_from_slice_of_u64() {
let data = vec![1_u64, 2, 3];
let spark_data = Sparkline::default().data(&data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(2),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
#[test]
fn it_can_be_created_from_slice_of_option_u64() {
let data = vec![Some(1_u64), None, Some(3)];
let spark_data = Sparkline::default().data(&data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(None),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
// Helper function to render a sparkline to a buffer with a given width
// filled with x symbols to make it easier to assert on the result
fn render(widget: Sparkline<'_>, width: u16) -> Buffer {
@@ -547,7 +261,7 @@ mod tests {
#[test]
fn it_does_not_panic_if_max_is_zero() {
let widget = Sparkline::default().data([0, 0, 0]);
let widget = Sparkline::default().data(&[0, 0, 0]);
let buffer = render(widget, 6);
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
}
@@ -556,31 +270,22 @@ mod tests {
fn it_does_not_panic_if_max_is_set_to_zero() {
// see https://github.com/rust-lang/rust-clippy/issues/13191
#[allow(clippy::unnecessary_min_or_max)]
let widget = Sparkline::default().data([0, 1, 2]).max(0);
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
let buffer = render(widget, 6);
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
}
#[test]
fn it_draws() {
let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
let widget = Sparkline::default().data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]);
let buffer = render(widget, 12);
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
}
#[test]
fn it_draws_double_height() {
let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
let area = Rect::new(0, 0, 12, 2);
let mut buffer = Buffer::filled(area, Cell::new("x"));
widget.render(area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines([" ▂▄▆█xxx", " ▂▄▆█████xxx"]));
}
#[test]
fn it_renders_left_to_right() {
let widget = Sparkline::default()
.data([0, 1, 2, 3, 4, 5, 6, 7, 8])
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.direction(RenderDirection::LeftToRight);
let buffer = render(widget, 12);
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
@@ -589,97 +294,12 @@ mod tests {
#[test]
fn it_renders_right_to_left() {
let widget = Sparkline::default()
.data([0, 1, 2, 3, 4, 5, 6, 7, 8])
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.direction(RenderDirection::RightToLeft);
let buffer = render(widget, 12);
assert_eq!(buffer, Buffer::with_lines(["xxx█▇▆▅▄▃▂▁ "]));
}
#[test]
fn it_renders_with_absent_value_style() {
let widget = Sparkline::default()
.absent_value_style(Style::default().fg(Color::Red))
.absent_value_symbol(symbols::shade::FULL)
.data([
None,
Some(1),
Some(2),
Some(3),
Some(4),
Some(5),
Some(6),
Some(7),
Some(8),
]);
let buffer = render(widget, 12);
let mut expected = Buffer::with_lines(["█▁▂▃▄▅▆▇█xxx"]);
expected.set_style(Rect::new(0, 0, 1, 1), Style::default().fg(Color::Red));
assert_eq!(buffer, expected);
}
#[test]
fn it_renders_with_absent_value_style_double_height() {
let widget = Sparkline::default()
.absent_value_style(Style::default().fg(Color::Red))
.absent_value_symbol(symbols::shade::FULL)
.data([
None,
Some(1),
Some(2),
Some(3),
Some(4),
Some(5),
Some(6),
Some(7),
Some(8),
]);
let area = Rect::new(0, 0, 12, 2);
let mut buffer = Buffer::filled(area, Cell::new("x"));
widget.render(area, &mut buffer);
let mut expected = Buffer::with_lines(["█ ▂▄▆█xxx", "█▂▄▆█████xxx"]);
expected.set_style(Rect::new(0, 0, 1, 2), Style::default().fg(Color::Red));
assert_eq!(buffer, expected);
}
#[test]
fn it_renders_with_custom_absent_value_style() {
let widget = Sparkline::default().absent_value_symbol('*').data([
None,
Some(1),
Some(2),
Some(3),
Some(4),
Some(5),
Some(6),
Some(7),
Some(8),
]);
let buffer = render(widget, 12);
let expected = Buffer::with_lines(["*▁▂▃▄▅▆▇█xxx"]);
assert_eq!(buffer, expected);
}
#[test]
fn it_renders_with_custom_bar_styles() {
let widget = Sparkline::default().data(vec![
SparklineBar::from(Some(0)).style(Some(Style::default().fg(Color::Red))),
SparklineBar::from(Some(1)).style(Some(Style::default().fg(Color::Red))),
SparklineBar::from(Some(2)).style(Some(Style::default().fg(Color::Red))),
SparklineBar::from(Some(3)).style(Some(Style::default().fg(Color::Green))),
SparklineBar::from(Some(4)).style(Some(Style::default().fg(Color::Green))),
SparklineBar::from(Some(5)).style(Some(Style::default().fg(Color::Green))),
SparklineBar::from(Some(6)).style(Some(Style::default().fg(Color::Blue))),
SparklineBar::from(Some(7)).style(Some(Style::default().fg(Color::Blue))),
SparklineBar::from(Some(8)).style(Some(Style::default().fg(Color::Blue))),
]);
let buffer = render(widget, 12);
let mut expected = Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]);
expected.set_style(Rect::new(0, 0, 3, 1), Style::default().fg(Color::Red));
expected.set_style(Rect::new(3, 0, 3, 1), Style::default().fg(Color::Green));
expected.set_style(Rect::new(6, 0, 3, 1), Style::default().fg(Color::Blue));
assert_eq!(buffer, expected);
}
#[test]
fn can_be_stylized() {
assert_eq!(

View File

@@ -595,7 +595,7 @@ impl<'a> Table<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{layout::Constraint, style::{Style, Stylize}, widgets::{Row, Table}};
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).row_highlight_style(Style::new().red().italic());
@@ -620,7 +620,7 @@ impl<'a> Table<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{layout::Constraint, style::{Style, Stylize}, widgets::{Row, Table}};
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).column_highlight_style(Style::new().red().italic());
@@ -645,7 +645,7 @@ impl<'a> Table<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{layout::Constraint, style::{Style, Stylize}, widgets::{Row, Table}};
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).cell_highlight_style(Style::new().red().italic());

View File

@@ -123,7 +123,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new().with_selected_column(Some(1));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -142,7 +142,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new().with_selected_cell(Some((1, 5)));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -212,7 +212,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new();
/// assert_eq!(state.selected_column(), None);
/// ```
@@ -227,7 +227,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new();
/// assert_eq!(state.selected_cell(), None);
/// ```
@@ -261,7 +261,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// *state.selected_column_mut() = Some(1);
/// ```
@@ -293,7 +293,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_column(Some(1));
/// ```
@@ -308,7 +308,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_cell(Some((1, 5)));
/// ```
@@ -349,7 +349,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_next_column();
/// ```
@@ -384,7 +384,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_previous_column();
/// ```
@@ -420,7 +420,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_first_column();
/// ```
@@ -453,7 +453,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_last();
/// ```
@@ -508,7 +508,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.scroll_right_by(4);
/// ```
@@ -526,7 +526,7 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::widgets::{TableState};
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.scroll_left_by(4);
/// ```