Compare commits
19 Commits
v0.26.1-al
...
v0.26.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efd1e47642 | ||
|
|
410d08b2b5 | ||
|
|
a4892ad444 | ||
|
|
18870ce990 | ||
|
|
1f208ffd03 | ||
|
|
e51ca6e0d2 | ||
|
|
9182f47026 | ||
|
|
91040c0865 | ||
|
|
2202059259 | ||
|
|
8fb46301a0 | ||
|
|
0dcdbea083 | ||
|
|
74a051147a | ||
|
|
c3fb25898f | ||
|
|
fae5862c6e | ||
|
|
788e6d9fb8 | ||
|
|
14c67fbb52 | ||
|
|
096346350e | ||
|
|
61a827821d | ||
|
|
fbb5dfaaa9 |
18
.github/workflows/check-pr.yml
vendored
18
.github/workflows/check-pr.yml
vendored
@@ -44,24 +44,6 @@ jobs:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
|
||||
check-signed:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
# Check commit signature and add comment if needed
|
||||
- name: Check signed commits in PR
|
||||
uses: 1Password/check-signed-commits-action@v1
|
||||
with:
|
||||
comment: |
|
||||
Thank you for opening this pull request!
|
||||
|
||||
We require commits to be signed and it looks like this PR contains unsigned commits.
|
||||
|
||||
Get help in the [CONTRIBUTING.md](https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md#sign-your-commits)
|
||||
or on [Github doc](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
|
||||
|
||||
check-breaking-change-label:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -92,7 +92,7 @@ jobs:
|
||||
- name: Generate coverage
|
||||
run: cargo make coverage
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -154,7 +154,7 @@ longer can be called from a constant context.
|
||||
[#708]: https://github.com/ratatui-org/ratatui/pull/708
|
||||
|
||||
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
|
||||
itself has a `style` field, which can be set with the `Line::style` method. Any code that creates
|
||||
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
|
||||
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
|
||||
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
|
||||
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).
|
||||
|
||||
159
CHANGELOG.md
159
CHANGELOG.md
@@ -2,6 +2,165 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.26.1](https://github.com/ratatui-org/ratatui/releases/tag/0.26.1) - 2024-02-12
|
||||
|
||||
This is a patch release that fixes bugs and adds enhancements, including new iterators, title options for blocks, and various rendering improvements. ✨
|
||||
|
||||
### Features
|
||||
|
||||
- [74a0511](https://github.com/ratatui-org/ratatui/commit/74a051147a4059990c31e08d96a8469d8220537b)
|
||||
*(rect)* Add Rect::positions iterator ([#928](https://github.com/ratatui-org/ratatui/issues/928))
|
||||
|
||||
````text
|
||||
Useful for performing some action on all the cells in a particular area.
|
||||
E.g.,
|
||||
|
||||
```rust
|
||||
fn render(area: Rect, buf: &mut Buffer) {
|
||||
for position in area.positions() {
|
||||
buf.get_mut(position.x, position.y).set_symbol("x");
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
- [9182f47](https://github.com/ratatui-org/ratatui/commit/9182f47026d1630cb749163b6f8b8987474312ae)
|
||||
*(uncategorized)* Add Block::title_top and Block::title_top_bottom ([#940](https://github.com/ratatui-org/ratatui/issues/940))
|
||||
|
||||
````text
|
||||
This adds the ability to add titles to the top and bottom of a block
|
||||
without having to use the `Title` struct (which will be removed in a
|
||||
future release - likely v0.28.0).
|
||||
|
||||
Fixes a subtle bug if the title was created from a right aligned Line
|
||||
and was also right aligned. The title would be rendered one cell too far
|
||||
to the right.
|
||||
|
||||
```rust
|
||||
Block::bordered()
|
||||
.title_top(Line::raw("A").left_aligned())
|
||||
.title_top(Line::raw("B").centered())
|
||||
.title_top(Line::raw("C").right_aligned())
|
||||
.title_bottom(Line::raw("D").left_aligned())
|
||||
.title_bottom(Line::raw("E").centered())
|
||||
.title_bottom(Line::raw("F").right_aligned())
|
||||
.render(buffer.area, &mut buffer);
|
||||
// renders
|
||||
"┌A─────B─────C┐",
|
||||
"│ │",
|
||||
"└D─────E─────F┘",
|
||||
```
|
||||
|
||||
Addresses part of https://github.com/ratatui-org/ratatui/issues/738
|
||||
````
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [2202059](https://github.com/ratatui-org/ratatui/commit/220205925911ed4377358d2a28ffca9373f11bda)
|
||||
*(block)* Fix crash on empty right aligned title ([#933](https://github.com/ratatui-org/ratatui/issues/933))
|
||||
|
||||
````text
|
||||
- Simplified implementation of the rendering for block.
|
||||
- Introduces a subtle rendering change where centered titles that are
|
||||
odd in length will now be rendered one character to the left compared
|
||||
to before. This aligns with other places that we render centered text
|
||||
and is a more consistent behavior. See
|
||||
https://github.com/ratatui-org/ratatui/pull/807#discussion_r1455645954
|
||||
for another example of this.
|
||||
````
|
||||
|
||||
Fixes: https://github.com/ratatui-org/ratatui/pull/929
|
||||
|
||||
- [14c67fb](https://github.com/ratatui-org/ratatui/commit/14c67fbb52101d10b2d2e26898c408ab8dd3ec2d)
|
||||
*(list)* Highlight symbol when using a multi-bytes char ([#924](https://github.com/ratatui-org/ratatui/issues/924))
|
||||
|
||||
````text
|
||||
ratatui v0.26.0 brought a regression in the List widget, in which the
|
||||
highlight symbol width was incorrectly calculated - specifically when
|
||||
the highlight symbol was a multi-char character, e.g. `▶`.
|
||||
````
|
||||
|
||||
- [0dcdbea](https://github.com/ratatui-org/ratatui/commit/0dcdbea083aace6d531c0d505837e0911f400675)
|
||||
*(paragraph)* Render Line::styled correctly inside a paragraph ([#930](https://github.com/ratatui-org/ratatui/issues/930))
|
||||
|
||||
````text
|
||||
Renders the styled graphemes of the line instead of the contained spans.
|
||||
````
|
||||
|
||||
- [fae5862](https://github.com/ratatui-org/ratatui/commit/fae5862c6e0947ee1488a7e4775413dbead67c8b)
|
||||
*(uncategorized)* Ensure that buffer::set_line sets the line style ([#926](https://github.com/ratatui-org/ratatui/issues/926))
|
||||
|
||||
````text
|
||||
Fixes a regression in 0.26 where buffer::set_line was no longer setting
|
||||
the style. This was due to the new style field on Line instead of being
|
||||
stored only in the spans.
|
||||
|
||||
Also adds a configuration for just running unit tests to bacon.toml.
|
||||
````
|
||||
|
||||
- [fbb5dfa](https://github.com/ratatui-org/ratatui/commit/fbb5dfaaa903efde0e63114c393dc3063d5f56fd)
|
||||
*(uncategorized)* Scrollbar rendering when no track symbols are provided ([#911](https://github.com/ratatui-org/ratatui/issues/911))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [c3fb258](https://github.com/ratatui-org/ratatui/commit/c3fb25898f3e3ffe485ee69631b680679874d2cb)
|
||||
*(rect)* Move iters to module and add docs ([#927](https://github.com/ratatui-org/ratatui/issues/927))
|
||||
|
||||
- [e51ca6e](https://github.com/ratatui-org/ratatui/commit/e51ca6e0d2705e6e0a96aeee78f1e80fcaaf34fc)
|
||||
*(uncategorized)* Finish tidying up table ([#942](https://github.com/ratatui-org/ratatui/issues/942))
|
||||
|
||||
- [91040c0](https://github.com/ratatui-org/ratatui/commit/91040c0865043b8d5e7387509523a41345ed5af3)
|
||||
*(uncategorized)* Rearrange block structure ([#939](https://github.com/ratatui-org/ratatui/issues/939))
|
||||
|
||||
### Documentation
|
||||
|
||||
- [61a8278](https://github.com/ratatui-org/ratatui/commit/61a827821dff2bd733377cfc143266edce1dbeec)
|
||||
*(canvas)* Add documentation to canvas module ([#913](https://github.com/ratatui-org/ratatui/issues/913))
|
||||
|
||||
````text
|
||||
Document the whole `canvas` module. With this, the whole `widgets`
|
||||
module is documented.
|
||||
````
|
||||
|
||||
- [d2d91f7](https://github.com/ratatui-org/ratatui/commit/d2d91f754c87458c6d07863eca20f3ea8ae319ce)
|
||||
*(changelog)* Add sponsors section ([#908](https://github.com/ratatui-org/ratatui/issues/908))
|
||||
|
||||
- [410d08b](https://github.com/ratatui-org/ratatui/commit/410d08b2b5812d7e29302adc0e8ddf18eb7d1d26)
|
||||
*(uncategorized)* Add link to FOSDEM 2024 talk ([#944](https://github.com/ratatui-org/ratatui/issues/944))
|
||||
|
||||
- [1f208ff](https://github.com/ratatui-org/ratatui/commit/1f208ffd0368b4d269854dc0c550686dcd2d1de0)
|
||||
*(uncategorized)* Add GitHub Sponsors badge ([#943](https://github.com/ratatui-org/ratatui/issues/943))
|
||||
|
||||
### Performance
|
||||
|
||||
- [0963463](https://github.com/ratatui-org/ratatui/commit/096346350e19c5de9a4d74bba64796997e9f40da)
|
||||
*(uncategorized)* Use drain instead of remove in chart examples ([#922](https://github.com/ratatui-org/ratatui/issues/922))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [a4892ad](https://github.com/ratatui-org/ratatui/commit/a4892ad444739d7a760bc45bbd954e728c66b2d2)
|
||||
*(uncategorized)* Fix typo in docsrs example ([#946](https://github.com/ratatui-org/ratatui/issues/946))
|
||||
|
||||
- [18870ce](https://github.com/ratatui-org/ratatui/commit/18870ce99063a492674de061441b2cce5dc54c60)
|
||||
*(uncategorized)* Fix the method name for setting the Line style ([#947](https://github.com/ratatui-org/ratatui/issues/947))
|
||||
|
||||
- [8fb4630](https://github.com/ratatui-org/ratatui/commit/8fb46301a00b5d065f9b890496f914d3fdc17495)
|
||||
*(uncategorized)* Remove github action bot that makes comments nudging commit signing ([#937](https://github.com/ratatui-org/ratatui/issues/937))
|
||||
|
||||
````text
|
||||
We can consider reverting this commit once this PR is merged:
|
||||
https://github.com/1Password/check-signed-commits-action/pull/9
|
||||
````
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you so much to everyone that contributed to this release!
|
||||
|
||||
Here is the list of contributors who have contributed to `ratatui` for the first time!
|
||||
|
||||
* @mo8it
|
||||
* @m4rch3n1ng
|
||||
|
||||
## [0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/0.26.0) - 2024-02-02
|
||||
|
||||
We are excited to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.26.0" # crate version
|
||||
version = "0.26.1" # crate version
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
description = "A library that's all about cooking up terminal user interfaces"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<div align="center">
|
||||
|
||||
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
|
||||
Badge]](./LICENSE)<br>
|
||||
Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
|
||||
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
|
||||
[![Matrix Badge]][Matrix]<br>
|
||||
|
||||
@@ -61,6 +61,9 @@ This is in contrast to the retained mode style of rendering where widgets are up
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
|
||||
## Other documentation
|
||||
|
||||
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
@@ -301,6 +304,7 @@ Running this example produces the following output:
|
||||
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
|
||||
[Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
|
||||
[Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
|
||||
[docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
[docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
[docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
@@ -322,6 +326,7 @@ Running this example produces the following output:
|
||||
[Termion]: https://crates.io/crates/termion
|
||||
[Termwiz]: https://crates.io/crates/termwiz
|
||||
[tui-rs]: https://crates.io/crates/tui
|
||||
[GitHub Sponsors]: https://github.com/sponsors/ratatui-org
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
|
||||
[CI Badge]:
|
||||
@@ -339,6 +344,7 @@ Running this example produces the following output:
|
||||
[Matrix Badge]:
|
||||
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
|
||||
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
|
||||
14
bacon.toml
14
bacon.toml
@@ -44,6 +44,16 @@ command = [
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.test-unit]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
@@ -97,4 +107,6 @@ ctrl-c = "job:check-crossterm"
|
||||
ctrl-t = "job:check-termion"
|
||||
ctrl-w = "job:check-termwiz"
|
||||
v = "job:coverage"
|
||||
u = "job:coverage-unit-tests-only"
|
||||
ctrl-v = "job:coverage-unit-tests-only"
|
||||
u = "job:test-unit"
|
||||
n = "job:nextest"
|
||||
|
||||
@@ -81,14 +81,12 @@ impl App {
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..5 {
|
||||
self.data1.remove(0);
|
||||
}
|
||||
self.data1.drain(0..5);
|
||||
self.data1.extend(self.signal1.by_ref().take(5));
|
||||
for _ in 0..10 {
|
||||
self.data2.remove(0);
|
||||
}
|
||||
|
||||
self.data2.drain(0..10);
|
||||
self.data2.extend(self.signal2.by_ref().take(10));
|
||||
|
||||
self.window[0] += 1.0;
|
||||
self.window[1] += 1.0;
|
||||
}
|
||||
|
||||
@@ -191,9 +191,7 @@ where
|
||||
S: Iterator,
|
||||
{
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..self.tick_rate {
|
||||
self.points.remove(0);
|
||||
}
|
||||
self.points.drain(0..self.tick_rate);
|
||||
self.points
|
||||
.extend(self.source.by_ref().take(self.tick_rate));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ use crossterm::{
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
/// Example code for libr.rs
|
||||
/// Example code for lib.rs
|
||||
///
|
||||
/// When cargo-rdme supports doc comments that import from code, this will be imported
|
||||
/// rather than copied to the lib.rs file.
|
||||
|
||||
@@ -253,7 +253,7 @@ impl Buffer {
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
line.style.patch(span.style),
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
@@ -453,6 +453,11 @@ impl Debug for Buffer {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::iter;
|
||||
|
||||
use itertools::Itertools;
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
|
||||
@@ -616,6 +621,67 @@ mod tests {
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn small_one_line_buffer() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 5, 1))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::empty("", " ")]
|
||||
#[case::one("1", "1 ")]
|
||||
#[case::full("12345", "12345")]
|
||||
#[case::overflow("123456", "12345")]
|
||||
fn set_line_raw(
|
||||
mut small_one_line_buffer: Buffer,
|
||||
#[case] content: &str,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let line = Line::raw(content);
|
||||
small_one_line_buffer.set_line(0, 0, &line, 5);
|
||||
|
||||
// note: testing with empty / set_string here instead of with_lines because with_lines calls
|
||||
// set_line
|
||||
let mut expected_buffer = Buffer::empty(small_one_line_buffer.area);
|
||||
expected_buffer.set_string(0, 0, expected, Style::default());
|
||||
assert_buffer_eq!(small_one_line_buffer, expected_buffer);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::empty("", " ")]
|
||||
#[case::one("1", "1 ")]
|
||||
#[case::full("12345", "12345")]
|
||||
#[case::overflow("123456", "12345")]
|
||||
fn set_line_styled(
|
||||
mut small_one_line_buffer: Buffer,
|
||||
#[case] content: &str,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let color = Color::Blue;
|
||||
let line = Line::styled(content, color);
|
||||
small_one_line_buffer.set_line(0, 0, &line, 5);
|
||||
|
||||
// note: manually testing the contents here as the Buffer::with_lines calls set_line
|
||||
let actual_contents = small_one_line_buffer
|
||||
.content
|
||||
.iter()
|
||||
.map(|c| c.symbol())
|
||||
.join("");
|
||||
let actual_styles = small_one_line_buffer
|
||||
.content
|
||||
.iter()
|
||||
.map(|c| c.fg)
|
||||
.collect_vec();
|
||||
|
||||
// set_line only sets the style for non-empty cells (unlike Line::render which sets the
|
||||
// style for all cells)
|
||||
let expected_styles = iter::repeat(color)
|
||||
.take(content.len().min(5))
|
||||
.chain(iter::repeat(Color::default()).take(5_usize.saturating_sub(content.len())))
|
||||
.collect_vec();
|
||||
assert_eq!(actual_contents, expected);
|
||||
assert_eq!(actual_styles, expected_styles);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
|
||||
|
||||
@@ -4,13 +4,14 @@ use std::{
|
||||
fmt,
|
||||
};
|
||||
|
||||
use layout::{Position, Size};
|
||||
|
||||
use super::{Position, Size};
|
||||
use crate::prelude::*;
|
||||
|
||||
mod offset;
|
||||
pub use offset::*;
|
||||
mod iter;
|
||||
pub use iter::*;
|
||||
|
||||
/// A Rectangular area.
|
||||
///
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
@@ -26,56 +27,17 @@ pub struct Rect {
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
/// Manages row divisions within a `Rect`.
|
||||
/// Amounts by which to move a [`Rect`](super::Rect).
|
||||
///
|
||||
/// The `Rows` struct is an iterator that allows iterating through rows of a given `Rect`.
|
||||
pub struct Rows {
|
||||
/// The `Rect` associated with the rows.
|
||||
pub rect: Rect,
|
||||
/// The y coordinate of the row within the `Rect`.
|
||||
pub current_row: u16,
|
||||
}
|
||||
|
||||
impl Iterator for Rows {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next row within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more rows to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_row >= self.rect.bottom() {
|
||||
return None;
|
||||
}
|
||||
let row = Rect::new(self.rect.x, self.current_row, self.rect.width, 1);
|
||||
self.current_row += 1;
|
||||
Some(row)
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages column divisions within a `Rect`.
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
/// The `Columns` struct is an iterator that allows iterating through columns of a given `Rect`.
|
||||
pub struct Columns {
|
||||
/// The `Rect` associated with the columns.
|
||||
pub rect: Rect,
|
||||
/// The x coordinate of the column within the `Rect`.
|
||||
pub current_column: u16,
|
||||
}
|
||||
|
||||
impl Iterator for Columns {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next column within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more columns to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_column >= self.rect.right() {
|
||||
return None;
|
||||
}
|
||||
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
|
||||
self.current_column += 1;
|
||||
Some(column)
|
||||
}
|
||||
/// See [`Rect::offset`]
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct Offset {
|
||||
/// How much to move on the X axis
|
||||
pub x: i32,
|
||||
/// How much to move on the Y axis
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl fmt::Display for Rect {
|
||||
@@ -271,47 +233,54 @@ impl Rect {
|
||||
Rect::new(x, y, width, height)
|
||||
}
|
||||
|
||||
/// Creates an iterator over rows within the `Rect`.
|
||||
/// An iterator over rows within the `Rect`.
|
||||
///
|
||||
/// This method returns a `Rows` iterator that allows iterating through rows of the `Rect`.
|
||||
///
|
||||
/// # Examples
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
/// let area = Rect::new(0, 0, 10, 5);
|
||||
/// for row in area.rows() {
|
||||
/// // Perform operations on each row of the area
|
||||
/// println!("Row: {:?}", row);
|
||||
/// # use ratatui::prelude::*;
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for row in area.rows() {
|
||||
/// Line::raw("Hello, world!").render(row, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn rows(&self) -> Rows {
|
||||
Rows {
|
||||
rect: *self,
|
||||
current_row: self.y,
|
||||
}
|
||||
pub fn rows(self) -> Rows {
|
||||
Rows::new(self)
|
||||
}
|
||||
|
||||
/// Creates an iterator over columns within the `Rect`.
|
||||
/// An iterator over columns within the `Rect`.
|
||||
///
|
||||
/// This method returns a `Columns` iterator that allows iterating through columns of the
|
||||
/// `Rect`.
|
||||
///
|
||||
/// # Examples
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
/// let area = Rect::new(0, 0, 10, 5);
|
||||
/// for column in area.columns() {
|
||||
/// // Perform operations on each column of the area
|
||||
/// println!("Column: {:?}", column);
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// if let Some(left) = area.columns().next() {
|
||||
/// Block::new().borders(Borders::LEFT).render(left, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn columns(&self) -> Columns {
|
||||
Columns {
|
||||
rect: *self,
|
||||
current_column: self.x,
|
||||
}
|
||||
pub fn columns(self) -> Columns {
|
||||
Columns::new(self)
|
||||
}
|
||||
|
||||
/// An iterator over the positions within the `Rect`.
|
||||
///
|
||||
/// The positions are returned in a row-major order (left-to-right, top-to-bottom).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for position in area.positions() {
|
||||
/// buf.get_mut(position.x, position.y).set_symbol("x");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn positions(self) -> Positions {
|
||||
Positions::new(self)
|
||||
}
|
||||
|
||||
/// Returns a [`Position`] with the same coordinates as this rect.
|
||||
|
||||
145
src/layout/rect/iter.rs
Normal file
145
src/layout/rect/iter.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use self::layout::Position;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// An iterator over rows within a `Rect`.
|
||||
pub struct Rows {
|
||||
/// The `Rect` associated with the rows.
|
||||
pub rect: Rect,
|
||||
/// The y coordinate of the row within the `Rect`.
|
||||
pub current_row: u16,
|
||||
}
|
||||
|
||||
impl Rows {
|
||||
/// Creates a new `Rows` iterator.
|
||||
pub fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_row: rect.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Rows {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next row within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more rows to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_row >= self.rect.bottom() {
|
||||
return None;
|
||||
}
|
||||
let row = Rect::new(self.rect.x, self.current_row, self.rect.width, 1);
|
||||
self.current_row += 1;
|
||||
Some(row)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over columns within a `Rect`.
|
||||
pub struct Columns {
|
||||
/// The `Rect` associated with the columns.
|
||||
pub rect: Rect,
|
||||
/// The x coordinate of the column within the `Rect`.
|
||||
pub current_column: u16,
|
||||
}
|
||||
|
||||
impl Columns {
|
||||
/// Creates a new `Columns` iterator.
|
||||
pub fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_column: rect.x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Columns {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next column within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more columns to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_column >= self.rect.right() {
|
||||
return None;
|
||||
}
|
||||
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
|
||||
self.current_column += 1;
|
||||
Some(column)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over positions within a `Rect`.
|
||||
///
|
||||
/// The iterator will yield all positions within the `Rect` in a row-major order.
|
||||
pub struct Positions {
|
||||
/// The `Rect` associated with the positions.
|
||||
pub rect: Rect,
|
||||
/// The current position within the `Rect`.
|
||||
pub current_position: Position,
|
||||
}
|
||||
|
||||
impl Positions {
|
||||
/// Creates a new `Positions` iterator.
|
||||
pub fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_position: Position::new(rect.x, rect.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Positions {
|
||||
type Item = Position;
|
||||
|
||||
/// Retrieves the next position within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more positions to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_position.y >= self.rect.bottom() {
|
||||
return None;
|
||||
}
|
||||
let position = self.current_position;
|
||||
self.current_position.x += 1;
|
||||
if self.current_position.x >= self.rect.right() {
|
||||
self.current_position.x = self.rect.x;
|
||||
self.current_position.y += 1;
|
||||
}
|
||||
Some(position)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::layout::Position;
|
||||
|
||||
#[test]
|
||||
fn rows() {
|
||||
let rect = Rect::new(0, 0, 2, 2);
|
||||
let mut rows = Rows::new(rect);
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
|
||||
assert_eq!(rows.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns() {
|
||||
let rect = Rect::new(0, 0, 2, 2);
|
||||
let mut columns = Columns::new(rect);
|
||||
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
|
||||
assert_eq!(columns.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positions() {
|
||||
let rect = Rect::new(0, 0, 2, 2);
|
||||
let mut positions = Positions::new(rect);
|
||||
assert_eq!(positions.next(), Some(Position::new(0, 0)));
|
||||
assert_eq!(positions.next(), Some(Position::new(1, 0)));
|
||||
assert_eq!(positions.next(), Some(Position::new(0, 1)));
|
||||
assert_eq!(positions.next(), Some(Position::new(1, 1)));
|
||||
assert_eq!(positions.next(), None);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/// Amounts by which to move a [`Rect`](super::Rect).
|
||||
///
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
/// See [`Rect::offset`](super::Rect::offset)
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct Offset {
|
||||
/// How much to move on the X axis
|
||||
pub x: i32,
|
||||
/// How much to move on the Y axis
|
||||
pub y: i32,
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
//! <div align="center">
|
||||
//!
|
||||
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
|
||||
//! Badge]](./LICENSE)<br>
|
||||
//! Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
|
||||
//! [![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
|
||||
//! [![Matrix Badge]][Matrix]<br>
|
||||
//!
|
||||
@@ -40,6 +40,9 @@
|
||||
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
//! for more info.
|
||||
//!
|
||||
//! You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
//! terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
//!
|
||||
//! ## Other documentation
|
||||
//!
|
||||
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
@@ -299,6 +302,7 @@
|
||||
//! [Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
|
||||
//! [Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
|
||||
//! [Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
//! [FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
|
||||
//! [docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
//! [docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
//! [docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
@@ -320,6 +324,7 @@
|
||||
//! [Termion]: https://crates.io/crates/termion
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [tui-rs]: https://crates.io/crates/tui
|
||||
//! [GitHub Sponsors]: https://github.com/sponsors/ratatui-org
|
||||
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
|
||||
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
|
||||
//! [CI Badge]:
|
||||
@@ -337,6 +342,7 @@
|
||||
//! [Matrix Badge]:
|
||||
//! https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
|
||||
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
|
||||
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![warn(missing_docs)]
|
||||
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
|
||||
//!
|
||||
//! Widgets are created for each frame as they are consumed after rendered.
|
||||
@@ -215,7 +216,12 @@ pub trait Widget {
|
||||
/// }
|
||||
/// ```
|
||||
pub trait StatefulWidget {
|
||||
/// State associated with the stateful widget.
|
||||
///
|
||||
/// If you don't need this then you probably want to implement [`Widget`] instead.
|
||||
type State;
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom stateful widget.
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
||||
|
||||
@@ -290,6 +296,8 @@ pub trait StatefulWidget {
|
||||
/// ```
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
pub trait WidgetRef {
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom widget.
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer);
|
||||
}
|
||||
|
||||
@@ -385,7 +393,12 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
|
||||
/// ```
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
pub trait StatefulWidgetRef {
|
||||
/// State associated with the stateful widget.
|
||||
///
|
||||
/// If you don't need this then you probably want to implement [`WidgetRef`] instead.
|
||||
type State;
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom stateful widget.
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![warn(missing_docs)]
|
||||
use crate::{prelude::*, widgets::Block};
|
||||
|
||||
mod bar;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![warn(missing_docs)]
|
||||
//! Elements related to the `Block` base widget.
|
||||
//!
|
||||
//! This holds everything needed to display and configure a [`Block`].
|
||||
@@ -6,6 +5,7 @@
|
||||
//! In its simplest form, a `Block` is a [border](Borders) around another widget. It can have a
|
||||
//! [title](Block::title) and [padding](Block::padding).
|
||||
|
||||
use itertools::Itertools;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{prelude::*, symbols::border, widgets::Borders};
|
||||
@@ -16,6 +16,73 @@ pub mod title;
|
||||
pub use padding::Padding;
|
||||
pub use title::{Position, Title};
|
||||
|
||||
/// Base widget to be used to display a box border around all [upper level ones](crate::widgets).
|
||||
///
|
||||
/// The borders can be configured with [`Block::borders`] and others. A block can have multiple
|
||||
/// [`Title`] using [`Block::title`]. It can also be [styled](Block::style) and
|
||||
/// [padded](Block::padding).
|
||||
///
|
||||
/// You can call the title methods multiple times to add multiple titles. Each title will be
|
||||
/// rendered with a single space separating titles that are in the same position or alignment. When
|
||||
/// both centered and non-centered titles are rendered, the centered space is calculated based on
|
||||
/// the full width of the block, rather than the leftover width.
|
||||
///
|
||||
/// Titles are not rendered in the corners of the block unless there is no border on that edge.
|
||||
/// If the block is too small and multiple titles overlap, the border may get cut off at a corner.
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌With at least a left border───
|
||||
///
|
||||
/// Without left border───
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Block::default()
|
||||
/// .title("Block")
|
||||
/// .borders(Borders::LEFT | Borders::RIGHT)
|
||||
/// .border_style(Style::default().fg(Color::White))
|
||||
/// .border_type(BorderType::Rounded)
|
||||
/// .style(Style::default().bg(Color::Black));
|
||||
/// ```
|
||||
///
|
||||
/// You may also use multiple titles like in the following:
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// };
|
||||
///
|
||||
/// Block::default()
|
||||
/// .title("Title 1")
|
||||
/// .title(Title::from("Title 2").position(Position::Bottom));
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Block<'a> {
|
||||
/// List of titles
|
||||
titles: Vec<Title<'a>>,
|
||||
/// The style to be patched to all titles of the block
|
||||
titles_style: Style,
|
||||
/// The default alignment of the titles that don't have one
|
||||
titles_alignment: Alignment,
|
||||
/// The default position of the titles that don't have one
|
||||
titles_position: Position,
|
||||
/// Visible borders
|
||||
borders: Borders,
|
||||
/// Border style
|
||||
border_style: Style,
|
||||
/// The symbols used to render the border. The default is plain lines but one can choose to
|
||||
/// have rounded or doubled lines instead or a custom set of symbols
|
||||
border_set: border::Set,
|
||||
/// Widget style
|
||||
style: Style,
|
||||
/// Block padding
|
||||
padding: Padding,
|
||||
}
|
||||
|
||||
/// The type of border of a [`Block`].
|
||||
///
|
||||
/// See the [`borders`](Block::borders) method of `Block` to configure its borders.
|
||||
@@ -89,79 +156,6 @@ pub enum BorderType {
|
||||
QuadrantOutside,
|
||||
}
|
||||
|
||||
impl BorderType {
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn border_symbols(border_type: BorderType) -> border::Set {
|
||||
match border_type {
|
||||
BorderType::Plain => border::PLAIN,
|
||||
BorderType::Rounded => border::ROUNDED,
|
||||
BorderType::Double => border::DOUBLE,
|
||||
BorderType::Thick => border::THICK,
|
||||
BorderType::QuadrantInside => border::QUADRANT_INSIDE,
|
||||
BorderType::QuadrantOutside => border::QUADRANT_OUTSIDE,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn to_border_set(self) -> border::Set {
|
||||
Self::border_symbols(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Base widget to be used to display a box border around all [upper level ones](crate::widgets).
|
||||
///
|
||||
/// The borders can be configured with [`Block::borders`] and others. A block can have multiple
|
||||
/// [`Title`] using [`Block::title`]. It can also be [styled](Block::style) and
|
||||
/// [padded](Block::padding).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Block::default()
|
||||
/// .title("Block")
|
||||
/// .borders(Borders::LEFT | Borders::RIGHT)
|
||||
/// .border_style(Style::default().fg(Color::White))
|
||||
/// .border_type(BorderType::Rounded)
|
||||
/// .style(Style::default().bg(Color::Black));
|
||||
/// ```
|
||||
///
|
||||
/// You may also use multiple titles like in the following:
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// };
|
||||
///
|
||||
/// Block::default()
|
||||
/// .title("Title 1")
|
||||
/// .title(Title::from("Title 2").position(Position::Bottom));
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Block<'a> {
|
||||
/// List of titles
|
||||
titles: Vec<Title<'a>>,
|
||||
/// The style to be patched to all titles of the block
|
||||
titles_style: Style,
|
||||
/// The default alignment of the titles that don't have one
|
||||
titles_alignment: Alignment,
|
||||
/// The default position of the titles that don't have one
|
||||
titles_position: Position,
|
||||
|
||||
/// Visible borders
|
||||
borders: Borders,
|
||||
/// Border style
|
||||
border_style: Style,
|
||||
/// The symbols used to render the border. The default is plain lines but one can choose to
|
||||
/// have rounded or doubled lines instead or a custom set of symbols
|
||||
border_set: border::Set,
|
||||
/// Widget style
|
||||
style: Style,
|
||||
/// Block padding
|
||||
padding: Padding,
|
||||
}
|
||||
|
||||
impl<'a> Block<'a> {
|
||||
/// Creates a new block with no [`Borders`] or [`Padding`].
|
||||
pub const fn new() -> Self {
|
||||
@@ -248,6 +242,62 @@ impl<'a> Block<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a title to the top of the block.
|
||||
///
|
||||
/// You can provide any type that can be converted into [`Line`] including: strings, string
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
|
||||
/// [spans](crate::text::Span) (`Vec<Span>`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{ prelude::*, widgets::* };
|
||||
/// Block::bordered()
|
||||
/// .title_top("Left1") // By default in the top left corner
|
||||
/// .title_top(Line::from("Left2").left_aligned())
|
||||
/// .title_top(Line::from("Right").right_aligned())
|
||||
/// .title_top(Line::from("Center").centered());
|
||||
///
|
||||
/// // Renders
|
||||
/// // ┌Left1─Left2───Center─────────Right┐
|
||||
/// // │ │
|
||||
/// // └──────────────────────────────────┘
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title_top<T: Into<Line<'a>>>(mut self, title: T) -> Self {
|
||||
let title = Title::from(title).position(Position::Top);
|
||||
self.titles.push(title);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a title to the bottom of the block.
|
||||
///
|
||||
/// You can provide any type that can be converted into [`Line`] including: strings, string
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
|
||||
/// [spans](crate::text::Span) (`Vec<Span>`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{ prelude::*, widgets::* };
|
||||
/// Block::bordered()
|
||||
/// .title_bottom("Left1") // By default in the top left corner
|
||||
/// .title_bottom(Line::from("Left2").left_aligned())
|
||||
/// .title_bottom(Line::from("Right").right_aligned())
|
||||
/// .title_bottom(Line::from("Center").centered());
|
||||
///
|
||||
/// // Renders
|
||||
/// // ┌──────────────────────────────────┐
|
||||
/// // │ │
|
||||
/// // └Left1─Left2───Center─────────Right┘
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title_bottom<T: Into<Line<'a>>>(mut self, title: T) -> Self {
|
||||
let title = Title::from(title).position(Position::Bottom);
|
||||
self.titles.push(title);
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies the style to all titles.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
@@ -518,6 +568,25 @@ impl<'a> Block<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl BorderType {
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn border_symbols(border_type: BorderType) -> border::Set {
|
||||
match border_type {
|
||||
BorderType::Plain => border::PLAIN,
|
||||
BorderType::Rounded => border::ROUNDED,
|
||||
BorderType::Double => border::DOUBLE,
|
||||
BorderType::Thick => border::THICK,
|
||||
BorderType::QuadrantInside => border::QUADRANT_INSIDE,
|
||||
BorderType::QuadrantOutside => border::QUADRANT_OUTSIDE,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn to_border_set(self) -> border::Set {
|
||||
Self::border_symbols(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Block<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
@@ -526,9 +595,11 @@ impl Widget for Block<'_> {
|
||||
|
||||
impl WidgetRef for Block<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
if area.is_empty() {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
self.render_borders(area, buf);
|
||||
self.render_titles(area, buf);
|
||||
}
|
||||
@@ -536,185 +607,228 @@ impl WidgetRef for Block<'_> {
|
||||
|
||||
impl Block<'_> {
|
||||
fn render_borders(&self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
let symbols = self.border_set;
|
||||
self.render_left_side(area, buf);
|
||||
self.render_top_side(area, buf);
|
||||
self.render_right_side(area, buf);
|
||||
self.render_bottom_side(area, buf);
|
||||
|
||||
// Sides
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(area.left(), y)
|
||||
.set_symbol(symbols.vertical_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
if self.borders.intersects(Borders::TOP) {
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, area.top())
|
||||
.set_symbol(symbols.horizontal_top)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
if self.borders.intersects(Borders::RIGHT) {
|
||||
let x = area.right() - 1;
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(symbols.vertical_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
if self.borders.intersects(Borders::BOTTOM) {
|
||||
let y = area.bottom() - 1;
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(symbols.horizontal_bottom)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
// Corners
|
||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
.set_symbol(symbols.bottom_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
||||
buf.get_mut(area.right() - 1, area.top())
|
||||
.set_symbol(symbols.top_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.left(), area.bottom() - 1)
|
||||
.set_symbol(symbols.bottom_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
buf.get_mut(area.left(), area.top())
|
||||
.set_symbol(symbols.top_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
/* Titles Rendering */
|
||||
fn get_title_y(&self, position: Position, area: Rect) -> u16 {
|
||||
match position {
|
||||
Position::Bottom => area.bottom() - 1,
|
||||
Position::Top => area.top(),
|
||||
}
|
||||
}
|
||||
|
||||
fn title_filter(&self, title: &Title, alignment: Alignment, position: Position) -> bool {
|
||||
title.alignment.unwrap_or(self.titles_alignment) == alignment
|
||||
&& title.position.unwrap_or(self.titles_position) == position
|
||||
}
|
||||
|
||||
fn calculate_title_area_offsets(&self, area: Rect) -> (u16, u16, u16) {
|
||||
let left_border_dx = u16::from(self.borders.intersects(Borders::LEFT));
|
||||
let right_border_dx = u16::from(self.borders.intersects(Borders::RIGHT));
|
||||
|
||||
let title_area_width = area
|
||||
.width
|
||||
.saturating_sub(left_border_dx)
|
||||
.saturating_sub(right_border_dx);
|
||||
|
||||
(left_border_dx, right_border_dx, title_area_width)
|
||||
}
|
||||
|
||||
fn render_left_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
let (left_border_dx, _, title_area_width) = self.calculate_title_area_offsets(area);
|
||||
|
||||
let mut current_offset = left_border_dx;
|
||||
self.titles
|
||||
.iter()
|
||||
.filter(|title| self.title_filter(title, Alignment::Left, position))
|
||||
.for_each(|title| {
|
||||
let title_x = current_offset;
|
||||
current_offset += title.content.width() as u16 + 1;
|
||||
|
||||
// Clone the title's content, applying block title style then the title style
|
||||
let mut content = title.content.clone();
|
||||
for span in content.spans.iter_mut() {
|
||||
span.style = self.titles_style.patch(span.style);
|
||||
}
|
||||
|
||||
buf.set_line(
|
||||
title_x + area.left(),
|
||||
self.get_title_y(position, area),
|
||||
&content,
|
||||
title_area_width,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_center_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
let (_, _, title_area_width) = self.calculate_title_area_offsets(area);
|
||||
|
||||
let titles = self
|
||||
.titles
|
||||
.iter()
|
||||
.filter(|title| self.title_filter(title, Alignment::Center, position));
|
||||
|
||||
let titles_sum = titles
|
||||
.clone()
|
||||
.fold(-1, |acc, f| acc + f.content.width() as i16 + 1); // First element isn't spaced
|
||||
|
||||
let mut current_offset = area.width.saturating_sub(titles_sum as u16) / 2;
|
||||
titles.for_each(|title| {
|
||||
let title_x = current_offset;
|
||||
current_offset += title.content.width() as u16 + 1;
|
||||
|
||||
// Clone the title's content, applying block title style then the title style
|
||||
let mut content = title.content.clone();
|
||||
for span in content.spans.iter_mut() {
|
||||
span.style = self.titles_style.patch(span.style);
|
||||
}
|
||||
|
||||
buf.set_line(
|
||||
title_x + area.left(),
|
||||
self.get_title_y(position, area),
|
||||
&content,
|
||||
title_area_width,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
let (_, right_border_dx, title_area_width) = self.calculate_title_area_offsets(area);
|
||||
|
||||
let mut current_offset = right_border_dx;
|
||||
self.titles
|
||||
.iter()
|
||||
.filter(|title| self.title_filter(title, Alignment::Right, position))
|
||||
.rev() // so that the titles appear in the order they have been set
|
||||
.for_each(|title| {
|
||||
current_offset += title.content.width() as u16 + 1;
|
||||
let title_x = current_offset - 1; // First element isn't spaced
|
||||
|
||||
// Clone the title's content, applying block title style then the title style
|
||||
let mut content = title.content.clone();
|
||||
for span in content.spans.iter_mut() {
|
||||
span.style = self.titles_style.patch(span.style);
|
||||
}
|
||||
|
||||
buf.set_line(
|
||||
area.width.saturating_sub(title_x) + area.left(),
|
||||
self.get_title_y(position, area),
|
||||
&content,
|
||||
title_area_width,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_title_position(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
// Note: the order in which these functions are called define the overlapping behavior
|
||||
self.render_right_titles(position, area, buf);
|
||||
self.render_center_titles(position, area, buf);
|
||||
self.render_left_titles(position, area, buf);
|
||||
self.render_bottom_right_corner(buf, area);
|
||||
self.render_top_right_corner(buf, area);
|
||||
self.render_bottom_left_corner(buf, area);
|
||||
self.render_top_left_corner(buf, area);
|
||||
}
|
||||
|
||||
fn render_titles(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_title_position(Position::Top, area, buf);
|
||||
self.render_title_position(Position::Bottom, area, buf);
|
||||
}
|
||||
|
||||
fn render_title_position(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
// NOTE: the order in which these functions are called defines the overlapping behavior
|
||||
self.render_right_titles(position, area, buf);
|
||||
self.render_center_titles(position, area, buf);
|
||||
self.render_left_titles(position, area, buf);
|
||||
}
|
||||
|
||||
fn render_left_side(&self, area: Rect, buf: &mut Buffer) {
|
||||
if self.borders.contains(Borders::LEFT) {
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(area.left(), y)
|
||||
.set_symbol(self.border_set.vertical_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_top_side(&self, area: Rect, buf: &mut Buffer) {
|
||||
if self.borders.contains(Borders::TOP) {
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, area.top())
|
||||
.set_symbol(self.border_set.horizontal_top)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_right_side(&self, area: Rect, buf: &mut Buffer) {
|
||||
if self.borders.contains(Borders::RIGHT) {
|
||||
let x = area.right() - 1;
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(self.border_set.vertical_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_bottom_side(&self, area: Rect, buf: &mut Buffer) {
|
||||
if self.borders.contains(Borders::BOTTOM) {
|
||||
let y = area.bottom() - 1;
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(self.border_set.horizontal_bottom)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_bottom_right_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
.set_symbol(self.border_set.bottom_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_top_right_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
||||
buf.get_mut(area.right() - 1, area.top())
|
||||
.set_symbol(self.border_set.top_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_bottom_left_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.left(), area.bottom() - 1)
|
||||
.set_symbol(self.border_set.bottom_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_top_left_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
buf.get_mut(area.left(), area.top())
|
||||
.set_symbol(self.border_set.top_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Render titles aligned to the right of the block
|
||||
///
|
||||
/// Currently (due to the way lines are truncated), the right side of the leftmost title will
|
||||
/// be cut off if the block is too small to fit all titles. This is not ideal and should be
|
||||
/// the left side of that leftmost that is cut off. This is due to the line being truncated
|
||||
/// incorrectly. See https://github.com/ratatui-org/ratatui/issues/932
|
||||
fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
let titles = self.filtered_titles(position, Alignment::Right);
|
||||
let mut titles_area = self.titles_area(area, position);
|
||||
|
||||
// render titles in reverse order to align them to the right
|
||||
for title in titles.rev() {
|
||||
if titles_area.is_empty() {
|
||||
break;
|
||||
}
|
||||
let title_width = title.content.width() as u16;
|
||||
let title_area = Rect {
|
||||
x: titles_area
|
||||
.right()
|
||||
.saturating_sub(title_width)
|
||||
.max(titles_area.left()),
|
||||
width: title_width.min(titles_area.width),
|
||||
..titles_area
|
||||
};
|
||||
buf.set_style(title_area, self.titles_style);
|
||||
title.content.render_ref(title_area, buf);
|
||||
|
||||
// bump the width of the titles area to the left
|
||||
titles_area.width = titles_area
|
||||
.width
|
||||
.saturating_sub(title_width)
|
||||
.saturating_sub(1); // space between titles
|
||||
}
|
||||
}
|
||||
|
||||
/// Render titles in the center of the block
|
||||
///
|
||||
/// Currently this method aligns the titles to the left inside a centered area. This is not
|
||||
/// ideal and should be fixed in the future to align the titles to the center of the block and
|
||||
/// truncate both sides of the titles if the block is too small to fit all titles.
|
||||
fn render_center_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
let titles = self
|
||||
.filtered_titles(position, Alignment::Center)
|
||||
.collect_vec();
|
||||
let total_width = titles
|
||||
.iter()
|
||||
.map(|title| title.content.width() as u16 + 1) // space between titles
|
||||
.sum::<u16>()
|
||||
.saturating_sub(1); // no space for the last title
|
||||
|
||||
let titles_area = self.titles_area(area, position);
|
||||
let mut titles_area = Rect {
|
||||
x: titles_area.left() + (titles_area.width.saturating_sub(total_width) / 2),
|
||||
..titles_area
|
||||
};
|
||||
for title in titles {
|
||||
if titles_area.is_empty() {
|
||||
break;
|
||||
}
|
||||
let title_width = title.content.width() as u16;
|
||||
let title_area = Rect {
|
||||
width: title_width.min(titles_area.width),
|
||||
..titles_area
|
||||
};
|
||||
buf.set_style(title_area, self.titles_style);
|
||||
title.content.render_ref(title_area, buf);
|
||||
|
||||
// bump the titles area to the right and reduce its width
|
||||
titles_area.x = titles_area.x.saturating_add(title_width + 1);
|
||||
titles_area.width = titles_area.width.saturating_sub(title_width + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Render titles aligned to the left of the block
|
||||
fn render_left_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
let titles = self.filtered_titles(position, Alignment::Left);
|
||||
let mut titles_area = self.titles_area(area, position);
|
||||
for title in titles {
|
||||
if titles_area.is_empty() {
|
||||
break;
|
||||
}
|
||||
let title_width = title.content.width() as u16;
|
||||
let title_area = Rect {
|
||||
width: title_width.min(titles_area.width),
|
||||
..titles_area
|
||||
};
|
||||
buf.set_style(title_area, self.titles_style);
|
||||
title.content.render_ref(title_area, buf);
|
||||
|
||||
// bump the titles area to the right and reduce its width
|
||||
titles_area.x = titles_area.x.saturating_add(title_width + 1);
|
||||
titles_area.width = titles_area.width.saturating_sub(title_width + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the titles that match the position and alignment
|
||||
fn filtered_titles(
|
||||
&self,
|
||||
position: Position,
|
||||
alignment: Alignment,
|
||||
) -> impl DoubleEndedIterator<Item = &Title> {
|
||||
self.titles.iter().filter(move |title| {
|
||||
title.position.unwrap_or(self.titles_position) == position
|
||||
&& title.alignment.unwrap_or(self.titles_alignment) == alignment
|
||||
})
|
||||
}
|
||||
|
||||
/// An area that is one line tall and spans the width of the block excluding the borders and
|
||||
/// is positioned at the top or bottom of the block.
|
||||
fn titles_area(&self, area: Rect, position: Position) -> Rect {
|
||||
let left_border = u16::from(self.borders.contains(Borders::LEFT));
|
||||
let right_border = u16::from(self.borders.contains(Borders::RIGHT));
|
||||
Rect {
|
||||
x: area.left() + left_border,
|
||||
y: match position {
|
||||
Position::Top => area.top(),
|
||||
Position::Bottom => area.bottom() - 1,
|
||||
},
|
||||
width: area
|
||||
.width
|
||||
.saturating_sub(left_border)
|
||||
.saturating_sub(right_border),
|
||||
height: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An extension trait for [`Block`] that provides some convenience methods.
|
||||
@@ -753,7 +867,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
assert_buffer_eq,
|
||||
layout::Rect,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Modifier, Stylize},
|
||||
};
|
||||
|
||||
@@ -1087,6 +1201,50 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
use Alignment::*;
|
||||
use Position::*;
|
||||
Block::bordered()
|
||||
.title(Title::from("A").position(Top).alignment(Left))
|
||||
.title(Title::from("B").position(Top).alignment(Center))
|
||||
.title(Title::from("C").position(Top).alignment(Right))
|
||||
.title(Title::from("D").position(Bottom).alignment(Left))
|
||||
.title(Title::from("E").position(Bottom).alignment(Center))
|
||||
.title(Title::from("F").position(Bottom).alignment(Right))
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
"┌A─────B─────C┐",
|
||||
"│ │",
|
||||
"└D─────E─────F┘",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_top_bottom() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
Block::bordered()
|
||||
.title_top(Line::raw("A").left_aligned())
|
||||
.title_top(Line::raw("B").centered())
|
||||
.title_top(Line::raw("C").right_aligned())
|
||||
.title_bottom(Line::raw("D").left_aligned())
|
||||
.title_bottom(Line::raw("E").centered())
|
||||
.title_bottom(Line::raw("F").right_aligned())
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
"┌A─────B─────C┐",
|
||||
"│ │",
|
||||
"└D─────E─────F┘",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_alignment() {
|
||||
let tests = vec![
|
||||
@@ -1121,6 +1279,24 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a regression test for bug https://github.com/ratatui-org/ratatui/issues/929
|
||||
#[test]
|
||||
fn render_right_aligned_empty_title() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
Block::default()
|
||||
.title("")
|
||||
.title_alignment(Alignment::Right)
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_position() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
|
||||
|
||||
@@ -115,18 +115,25 @@ where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self::default().content(value.into())
|
||||
let content = value.into();
|
||||
let alignment = content.alignment;
|
||||
Self {
|
||||
content,
|
||||
alignment,
|
||||
position: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn position_tostring() {
|
||||
fn position_to_string() {
|
||||
assert_eq!(Position::Top.to_string(), "Top");
|
||||
assert_eq!(Position::Bottom.to_string(), "Bottom");
|
||||
}
|
||||
@@ -137,4 +144,24 @@ mod tests {
|
||||
assert_eq!("Bottom".parse::<Position>(), Ok(Position::Bottom));
|
||||
assert_eq!("".parse::<Position>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_from_line() {
|
||||
let title = Title::from(Line::raw("Title"));
|
||||
assert_eq!(title.content, Line::from("Title"));
|
||||
assert_eq!(title.alignment, None);
|
||||
assert_eq!(title.position, None);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::left(Alignment::Left)]
|
||||
#[case::center(Alignment::Center)]
|
||||
#[case::right(Alignment::Right)]
|
||||
fn title_from_line_with_alignment(#[case] alignment: Alignment) {
|
||||
let line = Line::raw("Title").alignment(alignment);
|
||||
let title = Title::from(line.clone());
|
||||
assert_eq!(title.content, line);
|
||||
assert_eq!(title.alignment, Some(alignment));
|
||||
assert_eq!(title.position, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
//! A [`Canvas`] and a collection of [`Shape`]s.
|
||||
//!
|
||||
//! The [`Canvas`] is a blank space on which you can draw anything manually or use one of the
|
||||
//! predefined [`Shape`]s.
|
||||
//!
|
||||
//! The available shapes are:
|
||||
//!
|
||||
//! - [`Circle`]: A basic circle
|
||||
//! - [`Line`]: A line between two points
|
||||
//! - [`Map`]: A world map
|
||||
//! - [`Points`]: A scatter of points
|
||||
//! - [`Rectangle`]: A basic rectangle
|
||||
//!
|
||||
//! You can also implement your own custom [`Shape`]s.
|
||||
mod circle;
|
||||
mod line;
|
||||
mod map;
|
||||
@@ -18,8 +32,14 @@ pub use self::{
|
||||
};
|
||||
use crate::{prelude::*, symbols, text::Line as TextLine, widgets::Block};
|
||||
|
||||
/// Interface for all shapes that may be drawn on a Canvas widget.
|
||||
/// Something that can be drawn on a [`Canvas`].
|
||||
///
|
||||
/// You may implement your own canvas custom widgets by implementing this trait.
|
||||
pub trait Shape {
|
||||
/// Draws this [`Shape`] using the given [`Painter`].
|
||||
///
|
||||
/// This is the only method required to implement a custom widget that can be drawn on a
|
||||
/// [`Canvas`].
|
||||
fn draw(&self, painter: &mut Painter);
|
||||
}
|
||||
|
||||
@@ -37,10 +57,10 @@ pub struct Label<'a> {
|
||||
/// multiple shapes on the canvas in specific order.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
struct Layer {
|
||||
// a string of characters representing the grid. This will be wrapped to the width of the grid
|
||||
// A string of characters representing the grid. This will be wrapped to the width of the grid
|
||||
// when rendering
|
||||
string: String,
|
||||
// colors for foreground and background
|
||||
// Colors for foreground and background of each cell
|
||||
colors: Vec<(Color, Color)>,
|
||||
}
|
||||
|
||||
@@ -55,16 +75,18 @@ trait Grid: Debug {
|
||||
fn width(&self) -> u16;
|
||||
/// Get the height of the grid in number of terminal rows
|
||||
fn height(&self) -> u16;
|
||||
/// Get the resolution of the grid in number of dots. This doesn't have to be the same as the
|
||||
/// number of rows and columns of the grid. For example, a grid of Braille patterns will have a
|
||||
/// resolution of 2x4 dots per cell. This means that a grid of 10x10 cells will have a
|
||||
/// resolution of 20x40 dots.
|
||||
/// Get the resolution of the grid in number of dots.
|
||||
///
|
||||
/// This doesn't have to be the same as the number of rows and columns of the grid. For example,
|
||||
/// a grid of Braille patterns will have a resolution of 2x4 dots per cell. This means that a
|
||||
/// grid of 10x10 cells will have a resolution of 20x40 dots.
|
||||
fn resolution(&self) -> (f64, f64);
|
||||
/// Paint a point of the grid. The point is expressed in number of dots starting at the origin
|
||||
/// of the grid in the top left corner. Note that this is not the same as the (x, y) coordinates
|
||||
/// of the canvas.
|
||||
/// Paint a point of the grid.
|
||||
///
|
||||
/// The point is expressed in number of dots starting at the origin of the grid in the top left
|
||||
/// corner. Note that this is not the same as the `(x, y)` coordinates of the canvas.
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color);
|
||||
/// Save the current state of the grid as a layer to be rendered
|
||||
/// Save the current state of the [`Grid`] as a layer to be rendered
|
||||
fn save(&self) -> Layer;
|
||||
/// Reset the grid to its initial state
|
||||
fn reset(&mut self);
|
||||
@@ -81,12 +103,12 @@ trait Grid: Debug {
|
||||
/// to set the individual color of each dot in the braille pattern.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
struct BrailleGrid {
|
||||
/// width of the grid in number of terminal columns
|
||||
/// Width of the grid in number of terminal columns
|
||||
width: u16,
|
||||
/// height of the grid in number of terminal rows
|
||||
/// Height of the grid in number of terminal rows
|
||||
height: u16,
|
||||
/// represents the unicode braille patterns. Will take a value between 0x2800 and 0x28FF
|
||||
/// this is converted to a utf16 string when converting to a layer. See
|
||||
/// Represents the unicode braille patterns. Will take a value between `0x2800` and `0x28FF`
|
||||
/// this is converted to an utf16 string when converting to a layer. See
|
||||
/// <https://en.wikipedia.org/wiki/Braille_Patterns> for more info.
|
||||
utf16_code_points: Vec<u16>,
|
||||
/// The color of each cell only supports foreground colors for now as there's no way to
|
||||
@@ -152,11 +174,11 @@ impl Grid for BrailleGrid {
|
||||
/// when you want to draw shapes with a low resolution.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
struct CharGrid {
|
||||
/// width of the grid in number of terminal columns
|
||||
/// Width of the grid in number of terminal columns
|
||||
width: u16,
|
||||
/// height of the grid in number of terminal rows
|
||||
/// Height of the grid in number of terminal rows
|
||||
height: u16,
|
||||
/// represents a single character for each cell
|
||||
/// Represents a single character for each cell
|
||||
cells: Vec<char>,
|
||||
/// The color of each cell
|
||||
colors: Vec<Color>,
|
||||
@@ -232,17 +254,17 @@ impl Grid for CharGrid {
|
||||
/// character for each cell.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
struct HalfBlockGrid {
|
||||
/// width of the grid in number of terminal columns
|
||||
/// Width of the grid in number of terminal columns
|
||||
width: u16,
|
||||
/// height of the grid in number of terminal rows
|
||||
/// Height of the grid in number of terminal rows
|
||||
height: u16,
|
||||
/// represents a single color for each "pixel" arranged in column, row order
|
||||
/// Represents a single color for each "pixel" arranged in column, row order
|
||||
pixels: Vec<Vec<Color>>,
|
||||
}
|
||||
|
||||
impl HalfBlockGrid {
|
||||
/// Create a new HalfBlockGrid with the given width and height measured in terminal columns and
|
||||
/// rows respectively.
|
||||
/// Create a new `HalfBlockGrid` with the given width and height measured in terminal columns
|
||||
/// and rows respectively.
|
||||
fn new(width: u16, height: u16) -> HalfBlockGrid {
|
||||
HalfBlockGrid {
|
||||
width,
|
||||
@@ -346,33 +368,39 @@ pub struct Painter<'a, 'b> {
|
||||
}
|
||||
|
||||
impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// Convert the (x, y) coordinates to location of a point on the grid
|
||||
/// Convert the `(x, y)` coordinates to location of a point on the grid
|
||||
///
|
||||
/// (x, y) coordinates are expressed in the coordinate system of the canvas. The origin is in
|
||||
/// the lower left corner of the canvas (unlike most other coordinates in Ratatui where the
|
||||
/// origin is the upper left corner). The x and y bounds of the canvas define the specific area
|
||||
/// of some coordinate system that will be drawn on the canvas. The resolution of the grid is
|
||||
/// used to convert the (x, y) coordinates to the location of a point on the grid.
|
||||
/// `(x, y)` coordinates are expressed in the coordinate system of the canvas. The origin is in
|
||||
/// the lower left corner of the canvas (unlike most other coordinates in `Ratatui` where the
|
||||
/// origin is the upper left corner). The `x` and `y` bounds of the canvas define the specific
|
||||
/// area of some coordinate system that will be drawn on the canvas. The resolution of the grid
|
||||
/// is used to convert the `(x, y)` coordinates to the location of a point on the grid.
|
||||
///
|
||||
/// The grid coordinates are expressed in the coordinate system of the grid. The origin is in
|
||||
/// the top left corner of the grid. The x and y bounds of the grid are always [0, width - 1]
|
||||
/// and [0, height - 1] respectively. The resolution of the grid is used to convert the (x, y)
|
||||
/// coordinates to the location of a point on the grid.
|
||||
/// the top left corner of the grid. The x and y bounds of the grid are always `[0, width - 1]`
|
||||
/// and `[0, height - 1]` respectively. The resolution of the grid is used to convert the
|
||||
/// `(x, y)` coordinates to the location of a point on the grid.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
///
|
||||
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
///
|
||||
/// let point = painter.get_point(1.0, 0.0);
|
||||
/// assert_eq!(point, Some((0, 7)));
|
||||
///
|
||||
/// let point = painter.get_point(1.5, 1.0);
|
||||
/// assert_eq!(point, Some((1, 3)));
|
||||
///
|
||||
/// let point = painter.get_point(0.0, 0.0);
|
||||
/// assert_eq!(point, None);
|
||||
///
|
||||
/// let point = painter.get_point(2.0, 2.0);
|
||||
/// assert_eq!(point, Some((3, 0)));
|
||||
///
|
||||
/// let point = painter.get_point(1.0, 2.0);
|
||||
/// assert_eq!(point, Some((0, 0)));
|
||||
/// ```
|
||||
@@ -396,13 +424,14 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
|
||||
/// Paint a point of the grid
|
||||
///
|
||||
/// # Examples:
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
///
|
||||
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
/// let cell = painter.paint(1, 3, Color::Red);
|
||||
/// painter.paint(1, 3, Color::Red);
|
||||
/// ```
|
||||
pub fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
self.context.grid.paint(x, y, color);
|
||||
@@ -419,7 +448,7 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the state of the Canvas when painting to it.
|
||||
/// Holds the state of the [`Canvas`] when painting to it.
|
||||
///
|
||||
/// This is used by the [`Canvas`] widget to draw shapes on the grid. It can be useful to think of
|
||||
/// this as similar to the [`Frame`] struct that is used to draw widgets on the terminal.
|
||||
@@ -437,14 +466,14 @@ pub struct Context<'a> {
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
/// Create a new Context with the given width and height measured in terminal columns and rows
|
||||
/// respectively. The x and y bounds define the specific area of some coordinate system that
|
||||
/// respectively. The `x` and `y` bounds define the specific area of some coordinate system that
|
||||
/// will be drawn on the canvas. The marker defines the type of points used to draw the shapes.
|
||||
///
|
||||
/// Applications should not use this directly but rather use the [`Canvas`] widget. This will be
|
||||
/// created by the [`Canvas::paint`] moethod and passed to the closure that is used to draw on
|
||||
/// created by the [`Canvas::paint`] method and passed to the closure that is used to draw on
|
||||
/// the canvas.
|
||||
///
|
||||
/// The x and y bounds should be specified as left/right and bottom/top respectively. For
|
||||
/// The `x` and `y` bounds should be specified as left/right and bottom/top respectively. For
|
||||
/// example, if you want to draw a map of the world, you might want to use the following bounds:
|
||||
///
|
||||
/// ```
|
||||
@@ -485,7 +514,7 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw any object that may implement the Shape trait
|
||||
/// Draw the given [`Shape`] in this context
|
||||
pub fn draw<S>(&mut self, shape: &S)
|
||||
where
|
||||
S: Shape,
|
||||
@@ -495,16 +524,23 @@ impl<'a> Context<'a> {
|
||||
shape.draw(&mut painter);
|
||||
}
|
||||
|
||||
/// Save the existing state of the grid as a layer to be rendered and reset the grid to its
|
||||
/// initial state for the next layer.
|
||||
/// Save the existing state of the grid as a layer.
|
||||
///
|
||||
/// Save the existing state as a layer to be rendered and reset the grid to its initial
|
||||
/// state for the next layer.
|
||||
///
|
||||
/// This allows the canvas to be drawn in multiple layers. This is useful if you want to
|
||||
/// draw multiple shapes on the [`Canvas`] in specific order.
|
||||
pub fn layer(&mut self) {
|
||||
self.layers.push(self.grid.save());
|
||||
self.grid.reset();
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
/// Print a string on the canvas at the given position. Note that the text is always printed
|
||||
/// on top of the canvas and is not affected by the layers.
|
||||
/// Print a [`Text`] on the [`Canvas`] at the given position.
|
||||
///
|
||||
/// Note that the text is always printed on top of the canvas and is **not** affected by the
|
||||
/// layers.
|
||||
pub fn print<T>(&mut self, x: f64, y: f64, line: T)
|
||||
where
|
||||
T: Into<TextLine<'a>>,
|
||||
@@ -516,7 +552,7 @@ impl<'a> Context<'a> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Push the last layer if necessary
|
||||
/// Save the last layer if necessary
|
||||
fn finish(&mut self) {
|
||||
if self.dirty {
|
||||
self.layer();
|
||||
@@ -619,15 +655,22 @@ impl<'a, F> Canvas<'a, F>
|
||||
where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
/// Set the block that will be rendered around the canvas
|
||||
/// Wraps the canvas with a custom [`Block`] widget.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn block(mut self, block: Block<'a>) -> Canvas<'a, F> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
/// Define the viewport of the canvas.
|
||||
///
|
||||
/// If you were to "zoom" to a certain part of the world you may want to choose different
|
||||
/// bounds.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
|
||||
self.x_bounds = bounds;
|
||||
self
|
||||
@@ -637,31 +680,48 @@ where
|
||||
///
|
||||
/// If you were to "zoom" to a certain part of the world you may want to choose different
|
||||
/// bounds.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
|
||||
self.y_bounds = bounds;
|
||||
self
|
||||
}
|
||||
|
||||
/// Store the closure that will be used to draw to the Canvas
|
||||
/// Store the closure that will be used to draw to the [`Canvas`]
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn paint(mut self, f: F) -> Canvas<'a, F> {
|
||||
self.paint_func = Some(f);
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the background color of the canvas
|
||||
/// Change the background [`Color`] of the entire canvas
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn background_color(mut self, color: Color) -> Canvas<'a, F> {
|
||||
self.background_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the type of points used to draw the shapes. By default the braille patterns are used
|
||||
/// as they provide a more fine grained result but you might want to use the simple dot or
|
||||
/// block instead if the targeted terminal does not support those symbols.
|
||||
/// Change the type of points used to draw the shapes.
|
||||
///
|
||||
/// The HalfBlock marker is useful when you want to draw shapes with a higher resolution than a
|
||||
/// CharGrid but lower than a BrailleGrid. This grid type supports a foreground and background
|
||||
/// color for each terminal cell. This allows for more flexibility than the BrailleGrid which
|
||||
/// only supports a single foreground color for each 2x4 dots cell.
|
||||
/// By default the [`Braille`] patterns are used as they provide a more fine grained result,
|
||||
/// but you might want to use the simple [`Dot`] or [`Block`] instead if the targeted terminal
|
||||
/// does not support those symbols.
|
||||
///
|
||||
/// The [`HalfBlock`] marker is useful when you want to draw shapes with a higher resolution
|
||||
/// than with a grid of characters (e.g. with [`Block`] or [`Dot`]) but lower than with
|
||||
/// [`Braille`]. This grid type supports a foreground and background color for each terminal
|
||||
/// cell. This allows for more flexibility than the BrailleGrid which only supports a single
|
||||
/// foreground color for each 2x4 dots cell.
|
||||
///
|
||||
/// [`Braille`]: crate::symbols::Marker::Braille
|
||||
/// [`HalfBlock`]: crate::symbols::Marker::HalfBlock
|
||||
/// [`Dot`]: crate::symbols::Marker::Dot
|
||||
/// [`Block`]: crate::symbols::Marker::Block
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -671,12 +731,15 @@ where
|
||||
/// Canvas::default()
|
||||
/// .marker(symbols::Marker::Braille)
|
||||
/// .paint(|ctx| {});
|
||||
///
|
||||
/// Canvas::default()
|
||||
/// .marker(symbols::Marker::HalfBlock)
|
||||
/// .paint(|ctx| {});
|
||||
///
|
||||
/// Canvas::default()
|
||||
/// .marker(symbols::Marker::Dot)
|
||||
/// .paint(|ctx| {});
|
||||
///
|
||||
/// Canvas::default()
|
||||
/// .marker(symbols::Marker::Block)
|
||||
/// .paint(|ctx| {});
|
||||
|
||||
@@ -3,12 +3,16 @@ use crate::{
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
|
||||
/// Shape to draw a circle with a given center and radius and with the given color
|
||||
/// A circle with a given center and radius and with a given color
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Circle {
|
||||
/// `x` coordinate of the circle's center
|
||||
pub x: f64,
|
||||
/// `y` coordinate of the circle's center
|
||||
pub y: f64,
|
||||
/// Radius of the circle
|
||||
pub radius: f64,
|
||||
/// Color of the circle
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,23 @@ use crate::{
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
|
||||
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
|
||||
/// A line from `(x1, y1)` to `(x2, y2)` with the given color
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Line {
|
||||
/// `x` of the starting point
|
||||
pub x1: f64,
|
||||
/// `y` of the starting point
|
||||
pub y1: f64,
|
||||
/// `x` of the ending point
|
||||
pub x2: f64,
|
||||
/// `y` of the ending point
|
||||
pub y2: f64,
|
||||
/// Color of the line
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Line {
|
||||
/// Create a new line from (x1, y1) to (x2, y2) with the given color
|
||||
/// Create a new line from `(x1, y1)` to `(x2, y2)` with the given color
|
||||
pub fn new(x1: f64, y1: f64, x2: f64, y2: f64, color: Color) -> Self {
|
||||
Self {
|
||||
x1,
|
||||
|
||||
@@ -8,10 +8,21 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
/// Defines how many points are going to be used to draw a [`Map`].
|
||||
///
|
||||
/// You generally want a [high](MapResolution::High) resolution map.
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum MapResolution {
|
||||
/// A lesser resolution for the [`Map`] [`Shape`].
|
||||
///
|
||||
/// Contains about 1000 points.
|
||||
#[default]
|
||||
Low,
|
||||
/// A higher resolution for the [`Map`] [`Shape`].
|
||||
///
|
||||
/// Contains about 5000 points, you likely want to use [`Marker::Braille`] with this.
|
||||
///
|
||||
/// [`Marker::Braille`]: (crate::symbols::Marker::Braille)
|
||||
High,
|
||||
}
|
||||
|
||||
@@ -24,10 +35,18 @@ impl MapResolution {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shape to draw a world map with the given resolution and color
|
||||
/// A world map
|
||||
///
|
||||
/// A world map can be rendered with different [resolutions](MapResolution) and [colors](Color).
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Map {
|
||||
/// The resolution of the map.
|
||||
///
|
||||
/// This is the number of points used to draw the map.
|
||||
pub resolution: MapResolution,
|
||||
/// Map color
|
||||
///
|
||||
/// This is the color of the points of the map.
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ use crate::{
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
|
||||
/// A shape to draw a group of points with the given color
|
||||
/// A group of points with a given color
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Points<'a> {
|
||||
/// List of points to draw
|
||||
pub coords: &'a [(f64, f64)],
|
||||
/// Color of the points
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,25 @@ use crate::{
|
||||
widgets::canvas::{Line, Painter, Shape},
|
||||
};
|
||||
|
||||
/// Shape to draw a rectangle from a `Rect` with the given color
|
||||
/// A rectangle to draw on a [`Canvas`](super::Canvas)
|
||||
///
|
||||
/// Sizes used here are **not** in terminal cell. This is much more similar to the
|
||||
/// mathematic coordinate system.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Rectangle {
|
||||
/// The `x` position of the rectangle.
|
||||
///
|
||||
/// The rectangle is positioned from its bottom left corner.
|
||||
pub x: f64,
|
||||
/// The `y` position of the rectangle.
|
||||
///
|
||||
/// The rectangle is positioned from its bottom left corner.
|
||||
pub y: f64,
|
||||
/// The width of the rectangle.
|
||||
pub width: f64,
|
||||
/// The height of the rectangle.
|
||||
pub height: f64,
|
||||
/// The color of the rectangle.
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::cmp::max;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use crate::{prelude::*, widgets::Block};
|
||||
|
||||
/// A widget to display a progress bar.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![warn(missing_docs)]
|
||||
use strum::{Display, EnumString};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -896,7 +895,7 @@ impl StatefulWidgetRef for List<'_> {
|
||||
let is_selected = state.selected.map_or(false, |s| s == i);
|
||||
|
||||
let item_area = if selection_spacing {
|
||||
let highlight_symbol_width = self.highlight_symbol.unwrap_or("").len() as u16;
|
||||
let highlight_symbol_width = self.highlight_symbol.unwrap_or("").width() as u16;
|
||||
Rect {
|
||||
x: row_area.x + highlight_symbol_width,
|
||||
width: row_area.width - highlight_symbol_width,
|
||||
|
||||
@@ -340,9 +340,7 @@ impl Paragraph<'_> {
|
||||
}
|
||||
|
||||
let styled = self.text.iter().map(|line| {
|
||||
let graphemes = line
|
||||
.iter()
|
||||
.flat_map(|span| span.styled_graphemes(self.style));
|
||||
let graphemes = line.styled_graphemes(self.style);
|
||||
let alignment = line.alignment.unwrap_or(self.alignment);
|
||||
(graphemes, alignment)
|
||||
});
|
||||
@@ -593,6 +591,40 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_line_styled() {
|
||||
let l0 = Line::raw("unformatted");
|
||||
let l1 = Line::styled("bold text", Style::new().bold());
|
||||
let l2 = Line::styled("cyan text", Style::new().cyan());
|
||||
let l3 = Line::styled("dim text", Style::new().dim());
|
||||
let paragraph = Paragraph::new(vec![l0, l1, l2, l3]);
|
||||
|
||||
let mut expected =
|
||||
Buffer::with_lines(vec!["unformatted", "bold text", "cyan text", "dim text"]);
|
||||
expected.set_style(Rect::new(0, 1, 9, 1), Style::new().bold());
|
||||
expected.set_style(Rect::new(0, 2, 9, 1), Style::new().cyan());
|
||||
expected.set_style(Rect::new(0, 3, 8, 1), Style::new().dim());
|
||||
|
||||
test_case(¶graph, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_line_spans_styled() {
|
||||
let l0 = Line::default().spans(vec![
|
||||
Span::styled("bold", Style::new().bold()),
|
||||
Span::raw(" and "),
|
||||
Span::styled("cyan", Style::new().cyan()),
|
||||
]);
|
||||
let l1 = Line::default().spans(vec![Span::raw("unformatted")]);
|
||||
let paragraph = Paragraph::new(vec![l0, l1]);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec!["bold and cyan", "unformatted"]);
|
||||
expected.set_style(Rect::new(0, 0, 4, 1), Style::new().bold());
|
||||
expected.set_style(Rect::new(9, 0, 4, 1), Style::new().cyan());
|
||||
|
||||
test_case(¶graph, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_block_with_bottom_title_and_border() {
|
||||
let block = Block::default()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::iter;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
@@ -454,7 +453,7 @@ impl<'a> StatefulWidget for Scrollbar<'a> {
|
||||
let area = self.scollbar_area(area);
|
||||
for x in area.left()..area.right() {
|
||||
for y in area.top()..area.bottom() {
|
||||
if let Some((symbol, style)) = bar.next() {
|
||||
if let Some(Some((symbol, style))) = bar.next() {
|
||||
buf.set_string(x, y, symbol, style);
|
||||
}
|
||||
}
|
||||
@@ -468,23 +467,23 @@ impl Scrollbar<'_> {
|
||||
&self,
|
||||
area: Rect,
|
||||
state: &mut ScrollbarState,
|
||||
) -> impl Iterator<Item = (&str, Style)> {
|
||||
) -> impl Iterator<Item = Option<(&str, Style)>> {
|
||||
let (track_start_len, thumb_len, track_end_len) = self.part_lengths(area, state);
|
||||
|
||||
let begin = self.begin_symbol.map(|s| (s, self.begin_style));
|
||||
let track = self.track_symbol.map(|s| (s, self.track_style));
|
||||
let thumb = Some((self.thumb_symbol, self.thumb_style));
|
||||
let end = self.end_symbol.map(|s| (s, self.end_style));
|
||||
let begin = self.begin_symbol.map(|s| Some((s, self.begin_style)));
|
||||
let track = Some(self.track_symbol.map(|s| (s, self.track_style)));
|
||||
let thumb = Some(Some((self.thumb_symbol, self.thumb_style)));
|
||||
let end = self.end_symbol.map(|s| Some((s, self.end_style)));
|
||||
|
||||
// `<``
|
||||
// `<`
|
||||
iter::once(begin)
|
||||
// `<═══`
|
||||
.chain(iter::repeat(track).take(track_start_len))
|
||||
// `<═══█████`
|
||||
.chain(iter::repeat(thumb).take(thumb_len))
|
||||
// `<═══█████═══════``
|
||||
// `<═══█████═══════`
|
||||
.chain(iter::repeat(track).take(track_end_len))
|
||||
// `<═══█████═══════>``
|
||||
// `<═══█████═══════>`
|
||||
.chain(iter::once(end))
|
||||
.flatten()
|
||||
}
|
||||
@@ -769,6 +768,83 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("█████ ", 0, 10, "position_0")]
|
||||
#[case(" █████ ", 1, 10, "position_1")]
|
||||
#[case(" █████ ", 2, 10, "position_2")]
|
||||
#[case(" █████ ", 3, 10, "position_3")]
|
||||
#[case(" █████ ", 4, 10, "position_4")]
|
||||
#[case(" █████ ", 5, 10, "position_5")]
|
||||
#[case(" █████ ", 6, 10, "position_6")]
|
||||
#[case(" █████ ", 7, 10, "position_7")]
|
||||
#[case(" █████ ", 8, 10, "position_8")]
|
||||
#[case(" █████", 9, 10, "position_9")]
|
||||
#[case(" █████", 100, 10, "position_out_of_bounds")]
|
||||
fn render_scrollbar_without_track_symbols(
|
||||
#[case] expected: &str,
|
||||
#[case] position: usize,
|
||||
#[case] content_length: usize,
|
||||
#[case] assertion_message: &str,
|
||||
) {
|
||||
let size = expected.width() as u16;
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
|
||||
let mut state = ScrollbarState::default()
|
||||
.position(position)
|
||||
.content_length(content_length);
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.track_symbol(None)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![expected]),
|
||||
"{}",
|
||||
assertion_message
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("█████-----", 0, 10, "position_0")]
|
||||
#[case("-█████----", 1, 10, "position_1")]
|
||||
#[case("-█████----", 2, 10, "position_2")]
|
||||
#[case("--█████---", 3, 10, "position_3")]
|
||||
#[case("--█████---", 4, 10, "position_4")]
|
||||
#[case("---█████--", 5, 10, "position_5")]
|
||||
#[case("---█████--", 6, 10, "position_6")]
|
||||
#[case("----█████-", 7, 10, "position_7")]
|
||||
#[case("----█████-", 8, 10, "position_8")]
|
||||
#[case("-----█████", 9, 10, "position_9")]
|
||||
#[case("-----█████", 100, 10, "position_out_of_bounds")]
|
||||
fn render_scrollbar_without_track_symbols_over_content(
|
||||
#[case] expected: &str,
|
||||
#[case] position: usize,
|
||||
#[case] content_length: usize,
|
||||
#[case] assertion_message: &str,
|
||||
) {
|
||||
let size = expected.width() as u16;
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
|
||||
let width = buffer.area.width as usize;
|
||||
let s = "";
|
||||
Text::from(format!("{s:-^width$}")).render(buffer.area, &mut buffer);
|
||||
let mut state = ScrollbarState::default()
|
||||
.position(position)
|
||||
.content_length(content_length);
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.track_symbol(None)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.render(buffer.area, &mut buffer, &mut state);
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![expected]),
|
||||
"{}",
|
||||
assertion_message
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("<####---->", 0, 10, "position_0")]
|
||||
#[case("<#####--->", 1, 10, "position_1")]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::cmp::min;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
@@ -1,87 +1,12 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
mod cell;
|
||||
mod highlight_spacing;
|
||||
mod row;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod table;
|
||||
mod table_state;
|
||||
|
||||
pub use cell::Cell;
|
||||
pub use row::Row;
|
||||
pub use table::Table;
|
||||
pub use table_state::TableState;
|
||||
|
||||
/// This option allows the user to configure the "highlight symbol" column width spacing
|
||||
#[derive(Debug, Display, EnumString, PartialEq, Eq, Clone, Default, Hash)]
|
||||
pub enum HighlightSpacing {
|
||||
/// Always add spacing for the selection symbol column
|
||||
///
|
||||
/// With this variant, the column for the selection symbol will always be allocated, and so the
|
||||
/// table will never change size, regardless of if a row is selected or not
|
||||
Always,
|
||||
|
||||
/// Only add spacing for the selection symbol column if a row is selected
|
||||
///
|
||||
/// With this variant, the column for the selection symbol will only be allocated if there is a
|
||||
/// selection, causing the table to shift if selected / unselected
|
||||
#[default]
|
||||
WhenSelected,
|
||||
|
||||
/// Never add spacing to the selection symbol column, regardless of whether something is
|
||||
/// selected or not
|
||||
///
|
||||
/// This means that the highlight symbol will never be drawn
|
||||
Never,
|
||||
}
|
||||
|
||||
impl HighlightSpacing {
|
||||
/// Determine if a selection column should be displayed
|
||||
///
|
||||
/// has_selection: true if a row is selected in the table
|
||||
///
|
||||
/// Returns true if a selection column should be displayed
|
||||
pub(crate) fn should_add(&self, has_selection: bool) -> bool {
|
||||
match self {
|
||||
HighlightSpacing::Always => true,
|
||||
HighlightSpacing::WhenSelected => has_selection,
|
||||
HighlightSpacing::Never => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn highlight_spacing_to_string() {
|
||||
assert_eq!(HighlightSpacing::Always.to_string(), "Always".to_string());
|
||||
assert_eq!(
|
||||
HighlightSpacing::WhenSelected.to_string(),
|
||||
"WhenSelected".to_string()
|
||||
);
|
||||
assert_eq!(HighlightSpacing::Never.to_string(), "Never".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlight_spacing_from_str() {
|
||||
assert_eq!(
|
||||
"Always".parse::<HighlightSpacing>(),
|
||||
Ok(HighlightSpacing::Always)
|
||||
);
|
||||
assert_eq!(
|
||||
"WhenSelected".parse::<HighlightSpacing>(),
|
||||
Ok(HighlightSpacing::WhenSelected)
|
||||
);
|
||||
assert_eq!(
|
||||
"Never".parse::<HighlightSpacing>(),
|
||||
Ok(HighlightSpacing::Never)
|
||||
);
|
||||
assert_eq!(
|
||||
"".parse::<HighlightSpacing>(),
|
||||
Err(strum::ParseError::VariantNotFound)
|
||||
);
|
||||
}
|
||||
}
|
||||
pub use cell::*;
|
||||
pub use highlight_spacing::*;
|
||||
pub use row::*;
|
||||
pub use table::*;
|
||||
pub use table_state::*;
|
||||
|
||||
74
src/widgets/table/highlight_spacing.rs
Normal file
74
src/widgets/table/highlight_spacing.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
/// This option allows the user to configure the "highlight symbol" column width spacing
|
||||
#[derive(Debug, Display, EnumString, PartialEq, Eq, Clone, Default, Hash)]
|
||||
pub enum HighlightSpacing {
|
||||
/// Always add spacing for the selection symbol column
|
||||
///
|
||||
/// With this variant, the column for the selection symbol will always be allocated, and so the
|
||||
/// table will never change size, regardless of if a row is selected or not
|
||||
Always,
|
||||
|
||||
/// Only add spacing for the selection symbol column if a row is selected
|
||||
///
|
||||
/// With this variant, the column for the selection symbol will only be allocated if there is a
|
||||
/// selection, causing the table to shift if selected / unselected
|
||||
#[default]
|
||||
WhenSelected,
|
||||
|
||||
/// Never add spacing to the selection symbol column, regardless of whether something is
|
||||
/// selected or not
|
||||
///
|
||||
/// This means that the highlight symbol will never be drawn
|
||||
Never,
|
||||
}
|
||||
|
||||
impl HighlightSpacing {
|
||||
/// Determine if a selection column should be displayed
|
||||
///
|
||||
/// has_selection: true if a row is selected in the table
|
||||
///
|
||||
/// Returns true if a selection column should be displayed
|
||||
pub(crate) fn should_add(&self, has_selection: bool) -> bool {
|
||||
match self {
|
||||
HighlightSpacing::Always => true,
|
||||
HighlightSpacing::WhenSelected => has_selection,
|
||||
HighlightSpacing::Never => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
assert_eq!(HighlightSpacing::Always.to_string(), "Always".to_string());
|
||||
assert_eq!(
|
||||
HighlightSpacing::WhenSelected.to_string(),
|
||||
"WhenSelected".to_string()
|
||||
);
|
||||
assert_eq!(HighlightSpacing::Never.to_string(), "Never".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(
|
||||
"Always".parse::<HighlightSpacing>(),
|
||||
Ok(HighlightSpacing::Always)
|
||||
);
|
||||
assert_eq!(
|
||||
"WhenSelected".parse::<HighlightSpacing>(),
|
||||
Ok(HighlightSpacing::WhenSelected)
|
||||
);
|
||||
assert_eq!(
|
||||
"Never".parse::<HighlightSpacing>(),
|
||||
Ok(HighlightSpacing::Never)
|
||||
);
|
||||
assert_eq!(
|
||||
"".parse::<HighlightSpacing>(),
|
||||
Err(strum::ParseError::VariantNotFound)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
#![deny(missing_docs)]
|
||||
use crate::{prelude::*, widgets::Block};
|
||||
|
||||
const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);
|
||||
|
||||
@@ -15,21 +15,11 @@ use ratatui::{
|
||||
fn widgets_block_renders() {
|
||||
let backend = TestBackend::new(10, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let block = Block::default()
|
||||
.title(Span::styled("Title", Style::default().fg(Color::LightBlue)))
|
||||
.borders(Borders::ALL);
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let block = Block::default()
|
||||
.title(Span::styled("Title", Style::default().fg(Color::LightBlue)))
|
||||
.borders(Borders::ALL);
|
||||
f.render_widget(
|
||||
block,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 8,
|
||||
height: 8,
|
||||
},
|
||||
);
|
||||
})
|
||||
.draw(|frame| frame.render_widget(block, Rect::new(0, 0, 8, 8)))
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"┌Title─┐ ",
|
||||
@@ -51,16 +41,15 @@ fn widgets_block_renders() {
|
||||
|
||||
#[test]
|
||||
fn widgets_block_titles_overlap() {
|
||||
let test_case = |block, area: Rect, expected| {
|
||||
#[track_caller]
|
||||
fn test_case(block: Block, area: Rect, expected: Buffer) {
|
||||
let backend = TestBackend::new(area.width, area.height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
f.render_widget(block, area);
|
||||
})
|
||||
.draw(|frame| frame.render_widget(block, area))
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
}
|
||||
|
||||
// Left overrides the center
|
||||
test_case(
|
||||
@@ -68,12 +57,7 @@ fn widgets_block_titles_overlap() {
|
||||
.title(Title::from("aaaaa").alignment(Alignment::Left))
|
||||
.title(Title::from("bbb").alignment(Alignment::Center))
|
||||
.title(Title::from("ccc").alignment(Alignment::Right)),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 10, 1),
|
||||
Buffer::with_lines(vec!["aaaaab ccc"]),
|
||||
);
|
||||
|
||||
@@ -83,12 +67,7 @@ fn widgets_block_titles_overlap() {
|
||||
.title(Title::from("aaaaa").alignment(Alignment::Left))
|
||||
.title(Title::from("bbbbb").alignment(Alignment::Center))
|
||||
.title(Title::from("ccccc").alignment(Alignment::Right)),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 11,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 11, 1),
|
||||
Buffer::with_lines(vec!["aaaaabbbccc"]),
|
||||
);
|
||||
|
||||
@@ -99,12 +78,7 @@ fn widgets_block_titles_overlap() {
|
||||
.title(Title::from("aaaaa").alignment(Alignment::Left))
|
||||
.title(Title::from("bbbbb").alignment(Alignment::Center))
|
||||
.title(Title::from("ccccc").alignment(Alignment::Right)),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 11,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 11, 1),
|
||||
Buffer::with_lines(vec!["aaaaabaaaaa"]),
|
||||
);
|
||||
|
||||
@@ -113,28 +87,22 @@ fn widgets_block_titles_overlap() {
|
||||
Block::default()
|
||||
.title(Title::from("bbbbb").alignment(Alignment::Center))
|
||||
.title(Title::from("ccccccccccc").alignment(Alignment::Right)),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 11,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 11, 1),
|
||||
Buffer::with_lines(vec!["cccbbbbbccc"]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_block_renders_on_small_areas() {
|
||||
let test_case = |block, area: Rect, expected| {
|
||||
#[track_caller]
|
||||
fn test_case(block: Block, area: Rect, expected: Buffer) {
|
||||
let backend = TestBackend::new(area.width, area.height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
f.render_widget(block, area);
|
||||
})
|
||||
.draw(|frame| frame.render_widget(block, area))
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
}
|
||||
|
||||
let one_cell_test_cases = [
|
||||
(Borders::NONE, "T"),
|
||||
@@ -147,152 +115,78 @@ fn widgets_block_renders_on_small_areas() {
|
||||
for (borders, symbol) in one_cell_test_cases.iter().cloned() {
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}),
|
||||
Rect::new(0, 0, 0, 0),
|
||||
Buffer::empty(Rect::new(0, 0, 0, 0)),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0,
|
||||
},
|
||||
Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0,
|
||||
}),
|
||||
Rect::new(0, 0, 1, 0),
|
||||
Buffer::empty(Rect::new(0, 0, 1, 0)),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect::new(0, 0, 0, 1),
|
||||
Buffer::empty(Rect::new(0, 0, 0, 1)),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 1, 1),
|
||||
Buffer::with_lines(vec![symbol]),
|
||||
);
|
||||
}
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::LEFT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 4, 1),
|
||||
Buffer::with_lines(vec!["│Tes"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::RIGHT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 4, 1),
|
||||
Buffer::with_lines(vec!["Tes│"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::RIGHT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 4, 1),
|
||||
Buffer::with_lines(vec!["Tes│"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 4, 1),
|
||||
Buffer::with_lines(vec!["│Te│"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 4, 1),
|
||||
Buffer::with_lines(vec!["Test"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 5,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 5, 1),
|
||||
Buffer::with_lines(vec!["Test─"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.borders(Borders::LEFT | Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 5,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 5, 1),
|
||||
Buffer::with_lines(vec!["┌Test"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.borders(Borders::LEFT | Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 6,
|
||||
height: 1,
|
||||
},
|
||||
Rect::new(0, 0, 6, 1),
|
||||
Buffer::with_lines(vec!["┌Test─"]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_block_title_alignment() {
|
||||
let test_case = |alignment, borders, expected| {
|
||||
let backend = TestBackend::new(15, 2);
|
||||
#[track_caller]
|
||||
fn test_case(alignment: Alignment, borders: Borders, expected: Buffer) {
|
||||
let backend = TestBackend::new(15, 3);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let block1 = Block::default()
|
||||
@@ -304,270 +198,371 @@ fn widgets_block_title_alignment() {
|
||||
.title_alignment(alignment)
|
||||
.borders(borders);
|
||||
|
||||
let area = Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 13,
|
||||
height: 2,
|
||||
};
|
||||
let area = Rect::new(1, 0, 13, 3);
|
||||
|
||||
for block in [block1, block2] {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
f.render_widget(block, area);
|
||||
})
|
||||
.draw(|frame| frame.render_widget(block, area))
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// title top-left with all borders
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌Title──────┐ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌Title──────┐ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-left without top border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" │Title │ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" │Title │ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-left with no left border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" Title───────┐ ", " ────────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" Title───────┐ ",
|
||||
" │ ",
|
||||
" ────────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-left without right border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌Title─────── ", " └──────────── "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌Title─────── ",
|
||||
" │ ",
|
||||
" └──────────── ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-left without borders
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" Title ", " "]),
|
||||
Buffer::with_lines(vec![
|
||||
" Title ",
|
||||
" ",
|
||||
" ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center with all borders
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌───Title───┐ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌───Title───┐ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center without top border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" │ Title │ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" │ Title │ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center with no left border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ────Title───┐ ", " ────────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ───Title────┐ ",
|
||||
" │ ",
|
||||
" ────────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center without right border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌───Title──── ", " └──────────── "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌───Title──── ",
|
||||
" │ ",
|
||||
" └──────────── ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center without borders
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" Title ", " "]),
|
||||
Buffer::with_lines(vec![
|
||||
" Title ",
|
||||
" ",
|
||||
" ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-right with all borders
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌──────Title┐ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌──────Title┐ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-right without top border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" │ Title│ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" │ Title│ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-right with no left border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ───────Title┐ ", " ────────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ───────Title┐ ",
|
||||
" │ ",
|
||||
" ────────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-right without right border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌───────Title ", " └──────────── "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌───────Title ",
|
||||
" │ ",
|
||||
" └──────────── ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-right without borders
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" Title ", " "]),
|
||||
Buffer::with_lines(vec![
|
||||
" Title ",
|
||||
" ",
|
||||
" ",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_block_title_alignment_bottom() {
|
||||
let test_case = |alignment, borders, expected| {
|
||||
let backend = TestBackend::new(15, 2);
|
||||
#[track_caller]
|
||||
fn test_case(alignment: Alignment, borders: Borders, expected: Buffer) {
|
||||
let backend = TestBackend::new(15, 3);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let block = Block::default()
|
||||
.title(
|
||||
Title::from(Span::styled("Title", Style::default()))
|
||||
.alignment(alignment)
|
||||
.position(Position::Bottom),
|
||||
)
|
||||
.borders(borders);
|
||||
|
||||
let area = Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 13,
|
||||
height: 2,
|
||||
};
|
||||
|
||||
let title = Title::from(Span::styled("Title", Style::default()))
|
||||
.alignment(alignment)
|
||||
.position(Position::Bottom);
|
||||
let block = Block::default().title(title).borders(borders);
|
||||
let area = Rect::new(1, 0, 13, 3);
|
||||
terminal
|
||||
.draw(|f| {
|
||||
f.render_widget(block, area);
|
||||
})
|
||||
.draw(|frame| frame.render_widget(block, area))
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
}
|
||||
|
||||
// title bottom-left with all borders
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " └Title──────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌───────────┐ ",
|
||||
" │ │ ",
|
||||
" └Title──────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title bottom-left without bottom border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::LEFT | Borders::TOP | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " │Title │ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌───────────┐ ",
|
||||
" │ │ ",
|
||||
" │Title │ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title bottom-left with no left border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ────────────┐ ", " Title───────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ────────────┐ ",
|
||||
" │ ",
|
||||
" Title───────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title bottom-left without right border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌──────────── ", " └Title─────── "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌──────────── ",
|
||||
" │ ",
|
||||
" └Title─────── ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title bottom-left without borders
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" ", " Title "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" Title ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center with all borders
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " └───Title───┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌───────────┐ ",
|
||||
" │ │ ",
|
||||
" └───Title───┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center without bottom border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::LEFT | Borders::TOP | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " │ Title │ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌───────────┐ ",
|
||||
" │ │ ",
|
||||
" │ Title │ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center with no left border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ────────────┐ ", " ────Title───┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ────────────┐ ",
|
||||
" │ ",
|
||||
" ───Title────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center without right border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌──────────── ", " └───Title──── "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌──────────── ",
|
||||
" │ ",
|
||||
" └───Title──── ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center without borders
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" ", " Title "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" Title ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title bottom-right with all borders
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " └──────Title┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌───────────┐ ",
|
||||
" │ │ ",
|
||||
" └──────Title┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title bottom-right without bottom border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::LEFT | Borders::TOP | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " │ Title│ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌───────────┐ ",
|
||||
" │ │ ",
|
||||
" │ Title│ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title bottom-right with no left border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ────────────┐ ", " ───────Title┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ────────────┐ ",
|
||||
" │ ",
|
||||
" ───────Title┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title bottom-right without right border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌──────────── ", " └───────Title "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌──────────── ",
|
||||
" │ ",
|
||||
" └───────Title ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title bottom-right without borders
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" ", " Title "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" Title ",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_block_multiple_titles() {
|
||||
let test_case = |title_a, title_b, borders, expected| {
|
||||
let backend = TestBackend::new(15, 2);
|
||||
#[track_caller]
|
||||
fn test_case(title_a: Title, title_b: Title, borders: Borders, expected: Buffer) {
|
||||
let backend = TestBackend::new(15, 3);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let block = Block::default()
|
||||
@@ -575,12 +570,7 @@ fn widgets_block_multiple_titles() {
|
||||
.title(title_b)
|
||||
.borders(borders);
|
||||
|
||||
let area = Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 13,
|
||||
height: 2,
|
||||
};
|
||||
let area = Rect::new(1, 0, 13, 3);
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
@@ -589,14 +579,18 @@ fn widgets_block_multiple_titles() {
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
}
|
||||
|
||||
// title bottom-left with all borders
|
||||
test_case(
|
||||
Title::from("foo"),
|
||||
Title::from("bar"),
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌foo─bar────┐ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌foo─bar────┐ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-left without top border
|
||||
@@ -604,7 +598,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo"),
|
||||
Title::from("bar"),
|
||||
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" │foo bar │ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" │foo bar │ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-left with no left border
|
||||
@@ -612,7 +610,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo"),
|
||||
Title::from("bar"),
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" foo─bar─────┐ ", " ────────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" foo─bar─────┐ ",
|
||||
" │ ",
|
||||
" ────────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-left without right border
|
||||
@@ -620,7 +622,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo"),
|
||||
Title::from("bar"),
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌foo─bar───── ", " └──────────── "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌foo─bar───── ",
|
||||
" │ ",
|
||||
" └──────────── ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-left without borders
|
||||
@@ -628,7 +634,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo"),
|
||||
Title::from("bar"),
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" foo bar ", " "]),
|
||||
Buffer::with_lines(vec![
|
||||
" foo bar ",
|
||||
" ",
|
||||
" ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center with all borders
|
||||
@@ -636,7 +646,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo").alignment(Alignment::Center),
|
||||
Title::from("bar").alignment(Alignment::Center),
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌──foo─bar──┐ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌──foo─bar──┐ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center without top border
|
||||
@@ -644,7 +658,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo").alignment(Alignment::Center),
|
||||
Title::from("bar").alignment(Alignment::Center),
|
||||
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" │ foo bar │ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" │ foo bar │ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center with no left border
|
||||
@@ -652,7 +670,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo").alignment(Alignment::Center),
|
||||
Title::from("bar").alignment(Alignment::Center),
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ───foo─bar──┐ ", " ────────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ──foo─bar───┐ ",
|
||||
" │ ",
|
||||
" ────────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center without right border
|
||||
@@ -660,7 +682,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo").alignment(Alignment::Center),
|
||||
Title::from("bar").alignment(Alignment::Center),
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌──foo─bar─── ", " └──────────── "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌──foo─bar─── ",
|
||||
" │ ",
|
||||
" └──────────── ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title center without borders
|
||||
@@ -668,7 +694,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo").alignment(Alignment::Center),
|
||||
Title::from("bar").alignment(Alignment::Center),
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" foo bar ", " "]),
|
||||
Buffer::with_lines(vec![
|
||||
" foo bar ",
|
||||
" ",
|
||||
" ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-right with all borders
|
||||
@@ -676,7 +706,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo").alignment(Alignment::Right),
|
||||
Title::from("bar").alignment(Alignment::Right),
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌────foo─bar┐ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌────foo─bar┐ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-right without top border
|
||||
@@ -684,7 +718,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo").alignment(Alignment::Right),
|
||||
Title::from("bar").alignment(Alignment::Right),
|
||||
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" │ foo bar│ ", " └───────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" │ foo bar│ ",
|
||||
" │ │ ",
|
||||
" └───────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-right with no left border
|
||||
@@ -692,7 +730,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo").alignment(Alignment::Right),
|
||||
Title::from("bar").alignment(Alignment::Right),
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ─────foo─bar┐ ", " ────────────┘ "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ─────foo─bar┐ ",
|
||||
" │ ",
|
||||
" ────────────┘ ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-right without right border
|
||||
@@ -700,7 +742,11 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo").alignment(Alignment::Right),
|
||||
Title::from("bar").alignment(Alignment::Right),
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌─────foo─bar ", " └──────────── "]),
|
||||
Buffer::with_lines(vec![
|
||||
" ┌─────foo─bar ",
|
||||
" │ ",
|
||||
" └──────────── ",
|
||||
]),
|
||||
);
|
||||
|
||||
// title top-right without borders
|
||||
@@ -708,6 +754,10 @@ fn widgets_block_multiple_titles() {
|
||||
Title::from("foo").alignment(Alignment::Right),
|
||||
Title::from("bar").alignment(Alignment::Right),
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" foo bar ", " "]),
|
||||
Buffer::with_lines(vec![
|
||||
" foo bar ",
|
||||
" ",
|
||||
" ",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,38 @@ fn widgets_list_should_highlight_the_selected_item() {
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_list_should_highlight_the_selected_item_wide_symbol() {
|
||||
let backend = TestBackend::new(10, 3);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = ListState::default();
|
||||
|
||||
let wide_symbol = "▶ ";
|
||||
|
||||
state.select(Some(1));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_style(Style::default().bg(Color::Yellow))
|
||||
.highlight_symbol(wide_symbol);
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![" Item 1 ", "▶ Item 2 ", " Item 3 "]);
|
||||
|
||||
for x in 0..10 {
|
||||
expected.get_mut(x, 1).set_bg(Color::Yellow);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_list_should_truncate_items() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
|
||||
Reference in New Issue
Block a user