Compare commits
23 Commits
v0.25.0
...
layout-hel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84243c7ce8 | ||
|
|
6a50f2085e | ||
|
|
beaa2bf58d | ||
|
|
50374b2456 | ||
|
|
49df5d4626 | ||
|
|
7ab12ed8ce | ||
|
|
b459228e26 | ||
|
|
8f56fabcdd | ||
|
|
a62632a947 | ||
|
|
f025d2bfa2 | ||
|
|
63645333d6 | ||
|
|
5d410c6895 | ||
|
|
8d77b734bb | ||
|
|
9574198958 | ||
|
|
ee54493163 | ||
|
|
c977293f14 | ||
|
|
b0ed658970 | ||
|
|
37c183636b | ||
|
|
e67d3c64e0 | ||
|
|
4f2db82a77 | ||
|
|
d6b851301e | ||
|
|
b7a479392e | ||
|
|
e1cc849554 |
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit on error. Append "|| true" if you expect an error.
|
||||
set -o errexit
|
||||
# Exit on error inside any functions or subshells.
|
||||
set -o errtrace
|
||||
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
|
||||
set -o nounset
|
||||
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
|
||||
set -o pipefail
|
||||
# Turn on traces, useful while debugging but commented out by default
|
||||
# set -o xtrace
|
||||
|
||||
last_release="$(git tag --sort=committerdate | grep -E "v0\.\d+\.\d+$" | tail -1)"
|
||||
echo "🐭 Last release: ${last_release}"
|
||||
|
||||
# detect breaking changes
|
||||
if git log --oneline ${last_release}..HEAD | grep -q '!:' || true; then
|
||||
echo "🐭 Breaking changes detected since ${last_release}"
|
||||
git log --oneline ${last_release}..HEAD | grep '!:'
|
||||
# increment the minor version
|
||||
minor="${last_release##v0.}"
|
||||
minor="${minor%.*}"
|
||||
next_minor="$((minor + 1))"
|
||||
next_release="v0.${next_minor}.0"
|
||||
else
|
||||
# increment the patch version
|
||||
patch="${last_release##*.}"
|
||||
next_patch="$((patch + 1))"
|
||||
next_release="${last_release/%${patch}/${next_patch}}"
|
||||
fi
|
||||
echo "🐭 Next release: ${next_release}"
|
||||
|
||||
suffix="alpha"
|
||||
last_tag="$(git tag --sort=committerdate | tail -1)"
|
||||
if [[ "${last_tag}" = "${next-release}-${suffix}"* ]]; then
|
||||
echo "🐭 Last alpha release: ${last_tag}"
|
||||
# increment the alpha version
|
||||
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
|
||||
alpha="${last_tag##*-${suffix}.}"
|
||||
next_alpha="$((alpha + 1))"
|
||||
next_tag="${last_tag/%${alpha}/${next_alpha}}"
|
||||
else
|
||||
# increment the patch and start the alpha version from 0
|
||||
# e.g. v0.22.0 -> v0.22.1-alpha.0
|
||||
next_tag="${next_release}-${suffix}.0"
|
||||
fi
|
||||
# update the crate version
|
||||
msg="# crate version"
|
||||
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
|
||||
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
|
||||
echo "🐭 Next alpha release: ${next_tag}"
|
||||
22
.github/workflows/cd.yml
vendored
22
.github/workflows/cd.yml
vendored
@@ -28,27 +28,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Calculate the next release
|
||||
run: |
|
||||
suffix="alpha"
|
||||
last_tag="$(git tag --sort=committerdate | tail -1)"
|
||||
if [[ "${last_tag}" = *"-${suffix}"* ]]; then
|
||||
# increment the alpha version
|
||||
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
|
||||
alpha="${last_tag##*-${suffix}.}"
|
||||
next_alpha="$((alpha + 1))"
|
||||
next_tag="${last_tag/%${alpha}/${next_alpha}}"
|
||||
else
|
||||
# increment the patch and start the alpha version from 0
|
||||
# e.g. v0.22.0 -> v0.22.1-alpha.0
|
||||
patch="${last_tag##*.}"
|
||||
next_patch="$((patch + 1))"
|
||||
next_tag="${last_tag/%${patch}/${next_patch}}-${suffix}.0"
|
||||
fi
|
||||
# update the crate version
|
||||
msg="# crate version"
|
||||
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
|
||||
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
|
||||
echo "Next alpha release: ${next_tag} 🐭"
|
||||
run: .github/workflows/calculate-alpha-release.bash
|
||||
|
||||
- name: Publish on crates.io
|
||||
uses: actions-rs/cargo@v1
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -151,6 +151,8 @@ jobs:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: Test ${{ matrix.backend }}
|
||||
run: cargo make test-backend ${{ matrix.backend }}
|
||||
env:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Breaking Changes
|
||||
|
||||
This document contains a list of breaking changes in each version and some notes to help migrate
|
||||
between versions. It is compile manually from the commit history and changelog. We also tag PRs on
|
||||
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
|
||||
github with a [breaking change] label.
|
||||
|
||||
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
|
||||
@@ -10,6 +10,9 @@ github with a [breaking change] label.
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [v0.26.0 (unreleased)](#v0260-unreleased)
|
||||
- `Line` now has an extra `style` field which applies the style to the entire line
|
||||
- `Block` style methods cannot be created in a const context
|
||||
- [v0.25.0](#v0250)
|
||||
- Removed `Axis::title_style` and `Buffer::set_background`
|
||||
- `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>`
|
||||
@@ -39,9 +42,47 @@ This is a quick summary of the sections below:
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## v0.26.0 (unreleased)
|
||||
|
||||
### `Block` style methods cannot be used in a const context ([#720])
|
||||
|
||||
[#720]: https://github.com/ratatui-org/ratatui/pull/720
|
||||
|
||||
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
|
||||
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
|
||||
longer can be called from a constant context.
|
||||
|
||||
### `Line` now has a `style` field that applies to the entire line ([#708])
|
||||
|
||||
[#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
|
||||
`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()`).
|
||||
|
||||
Each `Span` contained within the line will no longer have the style that is applied to the line in
|
||||
the `Span::style` field.
|
||||
|
||||
```diff
|
||||
let line = Line {
|
||||
spans: vec!["".into()],
|
||||
alignment: Alignment::Left,
|
||||
+ ..Default::default()
|
||||
};
|
||||
|
||||
// or
|
||||
|
||||
let line = Line::raw(vec!["".into()])
|
||||
.alignment(Alignment::Left);
|
||||
```
|
||||
|
||||
## [v0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.25.0)
|
||||
|
||||
### Removed `Axis::title_style` and `Buffer::set_background`
|
||||
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
|
||||
|
||||
[#691]: https://github.com/ratatui-org/ratatui/pull/691
|
||||
|
||||
These items were deprecated since 0.10.
|
||||
|
||||
@@ -57,7 +98,7 @@ instead of `Axis::title_style`
|
||||
|
||||
[#672]: https://github.com/ratatui-org/ratatui/pull/672
|
||||
|
||||
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
|
||||
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
|
||||
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
|
||||
|
||||
E.g.
|
||||
@@ -70,18 +111,17 @@ E.g.
|
||||
|
||||
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
|
||||
|
||||
[#635]: https://github.com/ratatui-org/ratatui/pull/635
|
||||
|
||||
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
|
||||
widget in the default configuration would not show any indication of the selected tab.
|
||||
|
||||
[#635]: https://github.com/ratatui-org/ratatui/pull/635
|
||||
|
||||
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
|
||||
|
||||
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
|
||||
widget in the default configuration would not show any indication of the selected tab.
|
||||
|
||||
|
||||
### `Table::new()` now requires specifying the widths of the columns (#664)
|
||||
### `Table::new()` now requires specifying the widths of the columns ([#664])
|
||||
|
||||
[#664]: https://github.com/ratatui-org/ratatui/pull/664
|
||||
|
||||
|
||||
@@ -90,12 +90,21 @@ args = [
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.install-nextest]
|
||||
description = "Install cargo-nextest"
|
||||
install_crate = { crate_name = "cargo-nextest", binary = "cargo-nextest", test_arg = "--help" }
|
||||
|
||||
[tasks.test]
|
||||
description = "Run tests"
|
||||
dependencies = ["test-doc"]
|
||||
run_task = { name = ["test-lib", "test-doc"] }
|
||||
|
||||
[tasks.test-lib]
|
||||
description = "Run default tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"nextest",
|
||||
"run",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
@@ -109,9 +118,11 @@ args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
|
||||
[tasks.test-backend]
|
||||
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
|
||||
description = "Run backend-specific tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"nextest",
|
||||
"run",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
|
||||
@@ -286,7 +286,8 @@ cargo run --example=tabs --features=crossterm
|
||||
|
||||
Demonstrates one approach to accepting user input. Source [user_input.rs](./user_input.rs).
|
||||
|
||||
> [!NOTE] Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
|
||||
> [!NOTE]
|
||||
> Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
|
||||
> [`tui-input`](https://crates.io/crates/tui-input) crates for more functional text entry UIs.
|
||||
|
||||
```shell
|
||||
|
||||
@@ -175,7 +175,7 @@ fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
mod cals {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
pub(super) fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
use Month::*;
|
||||
match m {
|
||||
May => example1(m, y, es),
|
||||
@@ -188,7 +188,7 @@ mod cals {
|
||||
}
|
||||
}
|
||||
|
||||
fn default<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn default<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
@@ -198,7 +198,7 @@ mod cals {
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example1<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example1<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
@@ -209,7 +209,7 @@ mod cals {
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example2<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example2<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::DIM)
|
||||
@@ -225,7 +225,7 @@ mod cals {
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example3<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example3<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
@@ -241,7 +241,7 @@ mod cals {
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example4<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example4<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
@@ -255,7 +255,7 @@ mod cals {
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example5<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example5<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
@@ -9,18 +9,10 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
|
||||
const DATA2: [(f64, f64); 7] = [
|
||||
(0.0, 0.0),
|
||||
(10.0, 1.0),
|
||||
(20.0, 0.5),
|
||||
(30.0, 1.5),
|
||||
(40.0, 1.0),
|
||||
(50.0, 2.5),
|
||||
(60.0, 3.0),
|
||||
];
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{block::Title, *},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
@@ -142,14 +134,28 @@ fn run_app<B: Backend>(
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
])
|
||||
.split(size);
|
||||
let vertical_chunks = Layout::new(
|
||||
Direction::Vertical,
|
||||
[Constraint::Percentage(40), Constraint::Percentage(60)],
|
||||
)
|
||||
.split(size);
|
||||
|
||||
// top chart
|
||||
render_chart1(f, vertical_chunks[0], app);
|
||||
|
||||
let horizontal_chunks = Layout::new(
|
||||
Direction::Horizontal,
|
||||
[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
|
||||
)
|
||||
.split(vertical_chunks[1]);
|
||||
|
||||
// bottom left
|
||||
render_line_chart(f, horizontal_chunks[0]);
|
||||
// bottom right
|
||||
render_scatter(f, horizontal_chunks[1]);
|
||||
}
|
||||
|
||||
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}", app.window[0]),
|
||||
@@ -194,64 +200,164 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
|
||||
.bounds([-20.0, 20.0]),
|
||||
);
|
||||
f.render_widget(chart, chunks[0]);
|
||||
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
|
||||
fn render_line_chart(f: &mut Frame, area: Rect) {
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.name("Line from only 2 points")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA)];
|
||||
.data(&[(1., 1.), (4., 4.)])];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 2".cyan().bold())
|
||||
.title(
|
||||
Title::default()
|
||||
.content("Line chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
f.render_widget(chart, chunks[1]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA2)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 3".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 50.0])
|
||||
.labels(vec!["0".bold(), "25".into(), "50".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5".bold()]),
|
||||
)
|
||||
.legend_position(Some(LegendPosition::TopLeft))
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
f.render_widget(chart, chunks[2]);
|
||||
|
||||
f.render_widget(chart, area)
|
||||
}
|
||||
|
||||
fn render_scatter(f: &mut Frame, area: Rect) {
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("Heavy")
|
||||
.marker(Marker::Dot)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().yellow())
|
||||
.data(&HEAVY_PAYLOAD_DATA),
|
||||
Dataset::default()
|
||||
.name("Medium")
|
||||
.marker(Marker::Braille)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().magenta())
|
||||
.data(&MEDIUM_PAYLOAD_DATA),
|
||||
Dataset::default()
|
||||
.name("Small")
|
||||
.marker(Marker::Dot)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().cyan())
|
||||
.data(&SMALL_PAYLOAD_DATA),
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::new().borders(Borders::all()).title(
|
||||
Title::default()
|
||||
.content("Scatter chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("Year")
|
||||
.bounds([1960., 2020.])
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(vec!["1960".into(), "1990".into(), "2020".into()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Cost")
|
||||
.bounds([0., 75000.])
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(vec!["0".into(), "37 500".into(), "75 000".into()]),
|
||||
)
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
|
||||
// Data from https://ourworldindata.org/space-exploration-satellites
|
||||
const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
|
||||
(1965., 8200.),
|
||||
(1967., 5400.),
|
||||
(1981., 65400.),
|
||||
(1989., 30800.),
|
||||
(1997., 10200.),
|
||||
(2004., 11600.),
|
||||
(2014., 4500.),
|
||||
(2016., 7900.),
|
||||
(2018., 1500.),
|
||||
];
|
||||
|
||||
const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
|
||||
(1963., 29500.),
|
||||
(1964., 30600.),
|
||||
(1965., 177900.),
|
||||
(1965., 21000.),
|
||||
(1966., 17900.),
|
||||
(1966., 8400.),
|
||||
(1975., 17500.),
|
||||
(1982., 8300.),
|
||||
(1985., 5100.),
|
||||
(1988., 18300.),
|
||||
(1990., 38800.),
|
||||
(1990., 9900.),
|
||||
(1991., 18700.),
|
||||
(1992., 9100.),
|
||||
(1994., 10500.),
|
||||
(1994., 8500.),
|
||||
(1994., 8700.),
|
||||
(1997., 6200.),
|
||||
(1999., 18000.),
|
||||
(1999., 7600.),
|
||||
(1999., 8900.),
|
||||
(1999., 9600.),
|
||||
(2000., 16000.),
|
||||
(2001., 10000.),
|
||||
(2002., 10400.),
|
||||
(2002., 8100.),
|
||||
(2010., 2600.),
|
||||
(2013., 13600.),
|
||||
(2017., 8000.),
|
||||
];
|
||||
|
||||
const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
|
||||
(1961., 118500.),
|
||||
(1962., 14900.),
|
||||
(1975., 21400.),
|
||||
(1980., 32800.),
|
||||
(1988., 31100.),
|
||||
(1990., 41100.),
|
||||
(1993., 23600.),
|
||||
(1994., 20600.),
|
||||
(1994., 34600.),
|
||||
(1996., 50600.),
|
||||
(1997., 19200.),
|
||||
(1997., 45800.),
|
||||
(1998., 19100.),
|
||||
(2000., 73100.),
|
||||
(2003., 11200.),
|
||||
(2008., 12600.),
|
||||
(2010., 30500.),
|
||||
(2012., 20000.),
|
||||
(2013., 10600.),
|
||||
(2013., 34500.),
|
||||
(2015., 10600.),
|
||||
(2018., 23100.),
|
||||
(2019., 17300.),
|
||||
];
|
||||
|
||||
@@ -127,6 +127,13 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.bottom_margin(1);
|
||||
let footer_cells = ["Footer1", "Footer2", "Footer3"]
|
||||
.iter()
|
||||
.map(|f| Cell::from(*f).style(Style::default().fg(Color::Yellow)));
|
||||
let footer = Row::new(footer_cells)
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.top_margin(1);
|
||||
let rows = app.items.iter().map(|item| {
|
||||
let height = item
|
||||
.iter()
|
||||
@@ -146,6 +153,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
],
|
||||
)
|
||||
.header(header)
|
||||
.footer(footer)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#readme
|
||||
//! [Backend Comparison]:
|
||||
//! https://ratatui-org.github.io/ratatui-book/concepts/backends/comparison.html
|
||||
//! https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Ratatui Website]: https://ratatui-org.github.io/ratatui-book
|
||||
use std::io;
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ use std::{error::Error, io};
|
||||
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, SrgbaTuple},
|
||||
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
};
|
||||
@@ -20,7 +20,7 @@ use crate::{
|
||||
buffer::Cell,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
|
||||
@@ -249,12 +249,73 @@ impl Backend for TermwizBackend {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CellAttributes> for Style {
|
||||
fn from(value: CellAttributes) -> Self {
|
||||
let mut style = Style::new()
|
||||
.add_modifier(value.intensity().into())
|
||||
.add_modifier(value.underline().into())
|
||||
.add_modifier(value.blink().into());
|
||||
|
||||
if value.italic() {
|
||||
style.add_modifier |= Modifier::ITALIC;
|
||||
}
|
||||
if value.reverse() {
|
||||
style.add_modifier |= Modifier::REVERSED;
|
||||
}
|
||||
if value.strikethrough() {
|
||||
style.add_modifier |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
if value.invisible() {
|
||||
style.add_modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
|
||||
style.fg = Some(value.foreground().into());
|
||||
style.bg = Some(value.background().into());
|
||||
#[cfg(feature = "underline_color")]
|
||||
{
|
||||
style.underline_color = Some(value.underline_color().into());
|
||||
}
|
||||
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Intensity> for Modifier {
|
||||
fn from(value: Intensity) -> Self {
|
||||
match value {
|
||||
Intensity::Normal => Modifier::empty(),
|
||||
Intensity::Bold => Modifier::BOLD,
|
||||
Intensity::Half => Modifier::DIM,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Underline> for Modifier {
|
||||
fn from(value: Underline) -> Self {
|
||||
match value {
|
||||
Underline::None => Modifier::empty(),
|
||||
_ => Modifier::UNDERLINED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Blink> for Modifier {
|
||||
fn from(value: Blink) -> Self {
|
||||
match value {
|
||||
Blink::None => Modifier::empty(),
|
||||
Blink::Slow => Modifier::SLOW_BLINK,
|
||||
Blink::Rapid => Modifier::RAPID_BLINK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for ColorAttribute {
|
||||
fn from(color: Color) -> ColorAttribute {
|
||||
match color {
|
||||
Color::Reset => ColorAttribute::Default,
|
||||
Color::Black => AnsiColor::Black.into(),
|
||||
Color::Gray | Color::DarkGray => AnsiColor::Grey.into(),
|
||||
Color::DarkGray => AnsiColor::Grey.into(),
|
||||
Color::Gray => AnsiColor::Silver.into(),
|
||||
Color::Red => AnsiColor::Maroon.into(),
|
||||
Color::LightRed => AnsiColor::Red.into(),
|
||||
Color::Green => AnsiColor::Green.into(),
|
||||
@@ -276,7 +337,326 @@ impl From<Color> for ColorAttribute {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnsiColor> for Color {
|
||||
fn from(value: AnsiColor) -> Self {
|
||||
match value {
|
||||
AnsiColor::Black => Color::Black,
|
||||
AnsiColor::Grey => Color::DarkGray,
|
||||
AnsiColor::Silver => Color::Gray,
|
||||
AnsiColor::Maroon => Color::Red,
|
||||
AnsiColor::Red => Color::LightRed,
|
||||
AnsiColor::Green => Color::Green,
|
||||
AnsiColor::Lime => Color::LightGreen,
|
||||
AnsiColor::Olive => Color::Yellow,
|
||||
AnsiColor::Yellow => Color::LightYellow,
|
||||
AnsiColor::Purple => Color::Magenta,
|
||||
AnsiColor::Fuchsia => Color::LightMagenta,
|
||||
AnsiColor::Teal => Color::Cyan,
|
||||
AnsiColor::Aqua => Color::LightCyan,
|
||||
AnsiColor::White => Color::White,
|
||||
AnsiColor::Navy => Color::Blue,
|
||||
AnsiColor::Blue => Color::LightBlue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorAttribute> for Color {
|
||||
fn from(value: ColorAttribute) -> Self {
|
||||
match value {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(srgba)
|
||||
| ColorAttribute::TrueColorWithPaletteFallback(srgba, _) => srgba.into(),
|
||||
ColorAttribute::PaletteIndex(i) => Color::Indexed(i),
|
||||
ColorAttribute::Default => Color::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorSpec> for Color {
|
||||
fn from(value: ColorSpec) -> Self {
|
||||
match value {
|
||||
ColorSpec::Default => Color::Reset,
|
||||
ColorSpec::PaletteIndex(i) => Color::Indexed(i),
|
||||
ColorSpec::TrueColor(srgba) => srgba.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SrgbaTuple> for Color {
|
||||
fn from(value: SrgbaTuple) -> Self {
|
||||
let (r, g, b, _) = value.to_srgb_u8();
|
||||
Color::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RgbColor> for Color {
|
||||
fn from(value: RgbColor) -> Self {
|
||||
let (r, g, b) = value.to_tuple_rgb8();
|
||||
Color::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LinearRgba> for Color {
|
||||
fn from(value: LinearRgba) -> Self {
|
||||
value.to_srgb().into()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn u16_max(i: usize) -> u16 {
|
||||
u16::try_from(i).unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
mod into_color {
|
||||
use Color as C;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_linear_rgba() {
|
||||
// full black + opaque
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 0., 1.)), Color::Rgb(0, 0, 0));
|
||||
// full black + transparent
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 0., 0.)), Color::Rgb(0, 0, 0));
|
||||
|
||||
// full white + opaque
|
||||
assert_eq!(C::from(LinearRgba(1., 1., 1., 1.)), C::Rgb(254, 254, 254));
|
||||
// full white + transparent
|
||||
assert_eq!(C::from(LinearRgba(1., 1., 1., 0.)), C::Rgb(254, 254, 254));
|
||||
|
||||
// full red
|
||||
assert_eq!(C::from(LinearRgba(1., 0., 0., 1.)), C::Rgb(254, 0, 0));
|
||||
// full green
|
||||
assert_eq!(C::from(LinearRgba(0., 1., 0., 1.)), C::Rgb(0, 254, 0));
|
||||
// full blue
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 1., 1.)), C::Rgb(0, 0, 254));
|
||||
|
||||
// See https://stackoverflow.com/questions/12524623/what-are-the-practical-differences-when-working-with-colors-in-a-linear-vs-a-no
|
||||
// for an explanation
|
||||
|
||||
// half red
|
||||
assert_eq!(C::from(LinearRgba(0.214, 0., 0., 1.)), C::Rgb(127, 0, 0));
|
||||
// half green
|
||||
assert_eq!(C::from(LinearRgba(0., 0.214, 0., 1.)), C::Rgb(0, 127, 0));
|
||||
// half blue
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 0.214, 1.)), C::Rgb(0, 0, 127));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_srgba() {
|
||||
// full black + opaque
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 0., 1.)), Color::Rgb(0, 0, 0));
|
||||
// full black + transparent
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 0., 0.)), Color::Rgb(0, 0, 0));
|
||||
|
||||
// full white + opaque
|
||||
assert_eq!(C::from(SrgbaTuple(1., 1., 1., 1.)), C::Rgb(255, 255, 255));
|
||||
// full white + transparent
|
||||
assert_eq!(C::from(SrgbaTuple(1., 1., 1., 0.)), C::Rgb(255, 255, 255));
|
||||
|
||||
// full red
|
||||
assert_eq!(C::from(SrgbaTuple(1., 0., 0., 1.)), C::Rgb(255, 0, 0));
|
||||
// full green
|
||||
assert_eq!(C::from(SrgbaTuple(0., 1., 0., 1.)), C::Rgb(0, 255, 0));
|
||||
// full blue
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 1., 1.)), C::Rgb(0, 0, 255));
|
||||
|
||||
// half red
|
||||
assert_eq!(C::from(SrgbaTuple(0.5, 0., 0., 1.)), C::Rgb(127, 0, 0));
|
||||
// half green
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0.5, 0., 1.)), C::Rgb(0, 127, 0));
|
||||
// half blue
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 0.5, 1.)), C::Rgb(0, 0, 127));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rgbcolor() {
|
||||
// full black
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 0)), Color::Rgb(0, 0, 0));
|
||||
// full white
|
||||
assert_eq!(
|
||||
C::from(RgbColor::new_8bpc(255, 255, 255)),
|
||||
C::Rgb(255, 255, 255)
|
||||
);
|
||||
|
||||
// full red
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(255, 0, 0)), C::Rgb(255, 0, 0));
|
||||
// full green
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 255, 0)), C::Rgb(0, 255, 0));
|
||||
// full blue
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 255)), C::Rgb(0, 0, 255));
|
||||
|
||||
// half red
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(127, 0, 0)), C::Rgb(127, 0, 0));
|
||||
// half green
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 127, 0)), C::Rgb(0, 127, 0));
|
||||
// half blue
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 127)), C::Rgb(0, 0, 127));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_colorspec() {
|
||||
assert_eq!(C::from(ColorSpec::Default), C::Reset);
|
||||
assert_eq!(C::from(ColorSpec::PaletteIndex(33)), C::Indexed(33));
|
||||
assert_eq!(
|
||||
C::from(ColorSpec::TrueColor(SrgbaTuple(0., 0., 0., 1.))),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_colorattribute() {
|
||||
assert_eq!(C::from(ColorAttribute::Default), C::Reset);
|
||||
assert_eq!(C::from(ColorAttribute::PaletteIndex(32)), C::Indexed(32));
|
||||
assert_eq!(
|
||||
C::from(ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple(
|
||||
0., 0., 0., 1.
|
||||
))),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
C::from(ColorAttribute::TrueColorWithPaletteFallback(
|
||||
SrgbaTuple(0., 0., 0., 1.),
|
||||
31
|
||||
)),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ansicolor() {
|
||||
assert_eq!(C::from(AnsiColor::Black), Color::Black);
|
||||
assert_eq!(C::from(AnsiColor::Grey), Color::DarkGray);
|
||||
assert_eq!(C::from(AnsiColor::Silver), Color::Gray);
|
||||
assert_eq!(C::from(AnsiColor::Maroon), Color::Red);
|
||||
assert_eq!(C::from(AnsiColor::Red), Color::LightRed);
|
||||
assert_eq!(C::from(AnsiColor::Green), Color::Green);
|
||||
assert_eq!(C::from(AnsiColor::Lime), Color::LightGreen);
|
||||
assert_eq!(C::from(AnsiColor::Olive), Color::Yellow);
|
||||
assert_eq!(C::from(AnsiColor::Yellow), Color::LightYellow);
|
||||
assert_eq!(C::from(AnsiColor::Purple), Color::Magenta);
|
||||
assert_eq!(C::from(AnsiColor::Fuchsia), Color::LightMagenta);
|
||||
assert_eq!(C::from(AnsiColor::Teal), Color::Cyan);
|
||||
assert_eq!(C::from(AnsiColor::Aqua), Color::LightCyan);
|
||||
assert_eq!(C::from(AnsiColor::White), Color::White);
|
||||
assert_eq!(C::from(AnsiColor::Navy), Color::Blue);
|
||||
assert_eq!(C::from(AnsiColor::Blue), Color::LightBlue);
|
||||
}
|
||||
}
|
||||
|
||||
mod into_modifier {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_intensity() {
|
||||
assert_eq!(Modifier::from(Intensity::Normal), Modifier::empty());
|
||||
assert_eq!(Modifier::from(Intensity::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from(Intensity::Half), Modifier::DIM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_underline() {
|
||||
assert_eq!(Modifier::from(Underline::None), Modifier::empty());
|
||||
assert_eq!(Modifier::from(Underline::Single), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Double), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Curly), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Dashed), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Dotted), Modifier::UNDERLINED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_blink() {
|
||||
assert_eq!(Modifier::from(Blink::None), Modifier::empty());
|
||||
assert_eq!(Modifier::from(Blink::Slow), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from(Blink::Rapid), Modifier::RAPID_BLINK);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_cell_attribute_for_style() {
|
||||
// default
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset)
|
||||
);
|
||||
// foreground color
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_foreground(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Indexed(31)).bg(Color::Reset)
|
||||
);
|
||||
// background color
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_background(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Indexed(31))
|
||||
);
|
||||
// underline color
|
||||
#[cfg(feature = "underline_color")]
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_underline_color(AnsiColor::Red)
|
||||
.set
|
||||
.to_owned()
|
||||
),
|
||||
Style::new()
|
||||
.fg(Color::Reset)
|
||||
.bg(Color::Reset)
|
||||
.underline_color(Color::Red)
|
||||
);
|
||||
// underlined
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_underline(Underline::Single)
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).underlined()
|
||||
);
|
||||
// blink
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).slow_blink()
|
||||
);
|
||||
// intensity
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_intensity(Intensity::Bold)
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).bold()
|
||||
);
|
||||
// italic
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_italic(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).italic()
|
||||
);
|
||||
// reversed
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_reverse(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).reversed()
|
||||
);
|
||||
// strikethrough
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_strikethrough(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).crossed_out()
|
||||
);
|
||||
// hidden
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_invisible(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).hidden()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
1069
src/buffer.rs
1069
src/buffer.rs
File diff suppressed because it is too large
Load Diff
82
src/buffer/assert.rs
Normal file
82
src/buffer/assert.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
/// Assert that two buffers are equal by comparing their areas and content.
|
||||
///
|
||||
/// On panic, displays the areas or the content and a diff of the contents.
|
||||
#[macro_export]
|
||||
macro_rules! assert_buffer_eq {
|
||||
($actual_expr:expr, $expected_expr:expr) => {
|
||||
match (&$actual_expr, &$expected_expr) {
|
||||
(actual, expected) => {
|
||||
if actual.area != expected.area {
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer areas not equal
|
||||
expected: {:?}
|
||||
actual: {:?}"
|
||||
),
|
||||
expected, actual
|
||||
);
|
||||
}
|
||||
let diff = expected.diff(&actual);
|
||||
if !diff.is_empty() {
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
indoc::formatdoc! {"
|
||||
{i}: at ({x}, {y})
|
||||
expected: {expected_cell:?}
|
||||
actual: {cell:?}
|
||||
"}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer contents not equal
|
||||
expected: {:?}
|
||||
actual: {:?}
|
||||
diff:
|
||||
{}"
|
||||
),
|
||||
expected, actual, nice_diff
|
||||
);
|
||||
}
|
||||
// shouldn't get here, but this guards against future behavior
|
||||
// that changes equality but not area or content
|
||||
assert_eq!(actual, expected, "buffers not equal");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_area() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_style() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
}
|
||||
891
src/buffer/buffer.rs
Normal file
891
src/buffer/buffer.rs
Normal file
@@ -0,0 +1,891 @@
|
||||
use std::{
|
||||
cmp::min,
|
||||
fmt::{Debug, Formatter, Result},
|
||||
};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{buffer::Cell, prelude::*};
|
||||
|
||||
/// A buffer that maps to the desired content of the terminal after the draw call
|
||||
///
|
||||
/// No widget in the library interacts directly with the terminal. Instead each of them is required
|
||||
/// to draw their state to an intermediate buffer. It is basically a grid where each cell contains
|
||||
/// a grapheme, a foreground color and a background color. This grid will then be used to output
|
||||
/// the appropriate escape sequences and characters to draw the UI as the user has defined it.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{buffer::Cell, prelude::*};
|
||||
///
|
||||
/// let mut buf = Buffer::empty(Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 10,
|
||||
/// height: 5,
|
||||
/// });
|
||||
/// buf.get_mut(0, 2).set_symbol("x");
|
||||
/// assert_eq!(buf.get(0, 2).symbol(), "x");
|
||||
///
|
||||
/// buf.set_string(
|
||||
/// 3,
|
||||
/// 0,
|
||||
/// "string",
|
||||
/// Style::default().fg(Color::Red).bg(Color::White),
|
||||
/// );
|
||||
/// let cell = buf.get_mut(5, 0);
|
||||
/// assert_eq!(cell.symbol(), "r");
|
||||
/// assert_eq!(cell.fg, Color::Red);
|
||||
/// assert_eq!(cell.bg, Color::White);
|
||||
///
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol(), "x");
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
/// The content of the buffer. The length of this Vec should always be equal to area.width *
|
||||
/// area.height
|
||||
pub content: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
let cell = Cell::default();
|
||||
Buffer::filled(area, &cell)
|
||||
}
|
||||
|
||||
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
|
||||
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
|
||||
let size = area.area() as usize;
|
||||
let mut content = Vec::with_capacity(size);
|
||||
for _ in 0..size {
|
||||
content.push(cell.clone());
|
||||
}
|
||||
Buffer { area, content }
|
||||
}
|
||||
|
||||
/// Returns a Buffer containing the given lines
|
||||
pub fn with_lines<'a, S>(lines: Vec<S>) -> Buffer
|
||||
where
|
||||
S: Into<Line<'a>>,
|
||||
{
|
||||
let lines = lines.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
let height = lines.len() as u16;
|
||||
let width = lines.iter().map(Line::width).max().unwrap_or_default() as u16;
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
|
||||
for (y, line) in lines.iter().enumerate() {
|
||||
buffer.set_line(0, y as u16, line, width);
|
||||
}
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Returns the content of the buffer as a slice
|
||||
pub fn content(&self) -> &[Cell] {
|
||||
&self.content
|
||||
}
|
||||
|
||||
/// Returns the area covered by this buffer
|
||||
pub fn area(&self) -> &Rect {
|
||||
&self.area
|
||||
}
|
||||
|
||||
/// Returns a reference to Cell at the given coordinates
|
||||
pub fn get(&self, x: u16, y: u16) -> &Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&self.content[i]
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to Cell at the given coordinates
|
||||
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&mut self.content[i]
|
||||
}
|
||||
|
||||
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Global coordinates to the top corner of this buffer's area
|
||||
/// assert_eq!(buffer.index_of(200, 100), 0);
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
||||
/// // starts at (200, 100).
|
||||
/// buffer.index_of(0, 0); // Panics
|
||||
/// ```
|
||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||
debug_assert!(
|
||||
x >= self.area.left()
|
||||
&& x < self.area.right()
|
||||
&& y >= self.area.top()
|
||||
&& y < self.area.bottom(),
|
||||
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
|
||||
self.area
|
||||
);
|
||||
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
|
||||
}
|
||||
|
||||
/// Returns the (global) coordinates of a cell given its index
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
/// assert_eq!(buffer.pos_of(14), (204, 101));
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an index that is outside the Buffer's content.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
||||
/// buffer.pos_of(100); // Panics
|
||||
/// ```
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
|
||||
self.content.len()
|
||||
);
|
||||
(
|
||||
self.area.x + (i as u16) % self.area.width,
|
||||
self.area.y + (i as u16) / self.area.width,
|
||||
)
|
||||
}
|
||||
|
||||
/// Print a string, starting at the position (x, y)
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_string<T, S>(&mut self, x: u16, y: u16, string: T, style: S)
|
||||
where
|
||||
T: AsRef<str>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
self.set_stringn(x, y, string, usize::MAX, style.into());
|
||||
}
|
||||
|
||||
/// Print at most the first n characters of a string if enough space is available
|
||||
/// until the end of the line
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_stringn<T, S>(
|
||||
&mut self,
|
||||
x: u16,
|
||||
y: u16,
|
||||
string: T,
|
||||
width: usize,
|
||||
style: S,
|
||||
) -> (u16, u16)
|
||||
where
|
||||
T: AsRef<str>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
let style = style.into();
|
||||
let mut index = self.index_of(x, y);
|
||||
let mut x_offset = x as usize;
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
||||
for s in graphemes {
|
||||
let width = s.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
||||
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
if width > max_offset.saturating_sub(x_offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.content[index].set_symbol(s);
|
||||
self.content[index].set_style(style);
|
||||
// Reset following cells if multi-width (they would be hidden by the grapheme),
|
||||
for i in index + 1..index + width {
|
||||
self.content[i].reset();
|
||||
}
|
||||
index += width;
|
||||
x_offset += width;
|
||||
}
|
||||
(x_offset as u16, y)
|
||||
}
|
||||
|
||||
/// Print a line, starting at the position (x, y)
|
||||
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &line.spans {
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = self.set_stringn(
|
||||
x,
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
remaining_width = remaining_width.saturating_sub(w);
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
|
||||
/// Print a span, starting at the position (x, y)
|
||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
|
||||
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
|
||||
}
|
||||
|
||||
/// Set the style of all cells in the given area.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_style<S: Into<Style>>(&mut self, area: Rect, style: S) {
|
||||
let style = style.into();
|
||||
let area = self.area.intersection(area);
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
self.get_mut(x, y).set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize the buffer so that the mapped area matches the given area and that the buffer
|
||||
/// length is equal to area.width * area.height
|
||||
pub fn resize(&mut self, area: Rect) {
|
||||
let length = area.area() as usize;
|
||||
if self.content.len() > length {
|
||||
self.content.truncate(length);
|
||||
} else {
|
||||
self.content.resize(length, Cell::default());
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Reset all cells in the buffer
|
||||
pub fn reset(&mut self) {
|
||||
for c in &mut self.content {
|
||||
c.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge an other buffer into this one
|
||||
pub fn merge(&mut self, other: &Buffer) {
|
||||
let area = self.area.union(other.area);
|
||||
let cell = Cell::default();
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
|
||||
// Move original content to the appropriate space
|
||||
let size = self.area.area() as usize;
|
||||
for i in (0..size).rev() {
|
||||
let (x, y) = self.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
if i != k {
|
||||
self.content[k] = self.content[i].clone();
|
||||
self.content[i] = cell.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Push content of the other buffer into this one (may erase previous
|
||||
// data)
|
||||
let size = other.area.area() as usize;
|
||||
for i in 0..size {
|
||||
let (x, y) = other.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
self.content[k] = other.content[i].clone();
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
|
||||
/// self to other.
|
||||
///
|
||||
/// We're assuming that buffers are well-formed, that is no double-width cell is followed by
|
||||
/// a non-blank cell.
|
||||
///
|
||||
/// # Multi-width characters handling:
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `コ`
|
||||
/// Next: `aa`
|
||||
/// Updates: `0: a, 1: a'
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `a `
|
||||
/// Next: `コ`
|
||||
/// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `012`
|
||||
/// Prev: `aaa`
|
||||
/// Next: `aコ`
|
||||
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
|
||||
/// ```
|
||||
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
|
||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||
// Cells invalidated by drawing/replacing preceding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceding multi-width characters taking
|
||||
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
|
||||
to_skip = current.symbol().width().saturating_sub(1);
|
||||
|
||||
let affected_width = std::cmp::max(current.symbol().width(), previous.symbol().width());
|
||||
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
|
||||
}
|
||||
updates
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Buffer {
|
||||
/// Writes a debug representation of the buffer to the given formatter.
|
||||
///
|
||||
/// The format is like a pretty printed struct, with the following fields:
|
||||
/// * `area`: displayed as `Rect { x: 1, y: 2, width: 3, height: 4 }`
|
||||
/// * `content`: displayed as a list of strings representing the content of the buffer
|
||||
/// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
|
||||
/// modifier: Modifier::BOLD }` only showing a value when there is a change in style.
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.write_fmt(format_args!(
|
||||
"Buffer {{\n area: {:?},\n content: [\n",
|
||||
&self.area
|
||||
))?;
|
||||
let mut last_style = None;
|
||||
let mut styles = vec![];
|
||||
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
f.write_str(" \"")?;
|
||||
for (x, c) in line.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
f.write_str(c.symbol())?;
|
||||
} else {
|
||||
overwritten.push((x, c.symbol()));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.underline_color, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.modifier));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !overwritten.is_empty() {
|
||||
f.write_fmt(format_args!(
|
||||
"// hidden by multi-width symbols: {overwritten:?}"
|
||||
))?;
|
||||
}
|
||||
f.write_str("\",\n")?;
|
||||
}
|
||||
f.write_str(" ],\n styles: [\n")?;
|
||||
for s in styles {
|
||||
#[cfg(feature = "underline-color")]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4, s.5
|
||||
))?;
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4
|
||||
))?;
|
||||
}
|
||||
f.write_str(" ]\n}")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
|
||||
fn cell(s: &str) -> Cell {
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol(s);
|
||||
cell
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
|
||||
buf.set_string(0, 0, "Hello World!", Style::default());
|
||||
buf.set_string(
|
||||
0,
|
||||
1,
|
||||
"G'day World!",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
#[cfg(feature = "underline-color")]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_translates_to_and_from_coordinates() {
|
||||
let rect = Rect::new(200, 100, 50, 80);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// First cell is at the upper left corner.
|
||||
assert_eq!(buf.pos_of(0), (200, 100));
|
||||
assert_eq!(buf.index_of(200, 100), 0);
|
||||
|
||||
// Last cell is in the lower right.
|
||||
assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
|
||||
assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
fn pos_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
|
||||
buf.pos_of(100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
fn index_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// width is 10; zero-indexed means that 10 would be the 11th cell.
|
||||
buf.index_of(10, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
|
||||
// Width limit:
|
||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// Width truncation:
|
||||
buffer.set_string(0, 0, "123456", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// multi-line
|
||||
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
buffer.set_string(0, 1, "67890", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string_multi_width_overwrite() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// multi-width overwrite
|
||||
buffer.set_string(0, 0, "aaaaa", Style::default());
|
||||
buffer.set_string(0, 0, "称号", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string_zero_width() {
|
||||
let area = Rect::new(0, 0, 1, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Leading grapheme with zero width
|
||||
let s = "\u{1}a";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
|
||||
// Trailing grapheme with zero with
|
||||
let s = "a\u{1}";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string_double_width() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer.set_string(0, 0, "コン", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
|
||||
buffer.set_style(Rect::new(0, 1, 5, 1), Style::new().red());
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".into(),])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style_does_not_panic_when_out_of_area() {
|
||||
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
|
||||
buffer.set_style(Rect::new(0, 1, 10, 3), Style::new().red());
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".red(),])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lines() {
|
||||
let buffer =
|
||||
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
|
||||
assert_eq!(buffer.area.x, 0);
|
||||
assert_eq!(buffer.area.y, 0);
|
||||
assert_eq!(buffer.area.width, 10);
|
||||
assert_eq!(buffer.area.height, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_empty_empty() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::empty(area);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_empty_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff.len(), 40 * 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_filled_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_single_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌TITLE─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(2, 1, &cell("I")),
|
||||
(3, 1, &cell("T")),
|
||||
(4, 1, &cell("L")),
|
||||
(5, 1, &cell("E")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn diff_multi_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
"┌Title─┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
"┌称号──┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(1, 0, &cell("称")),
|
||||
// Skipped "i"
|
||||
(3, 0, &cell("号")),
|
||||
// Skipped "l"
|
||||
(5, 0, &cell("─")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_multi_width_offset() {
|
||||
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
|
||||
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_skip() {
|
||||
let prev = Buffer::with_lines(vec!["123"]);
|
||||
let mut next = Buffer::with_lines(vec!["456"]);
|
||||
for i in 1..3 {
|
||||
next.content[i].set_skip(true);
|
||||
}
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(
|
||||
one,
|
||||
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge3() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 3,
|
||||
y: 3,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 3,
|
||||
height: 4,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
|
||||
merged.area = Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_buffer_eq!(one, merged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_skip() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2").set_skip(true),
|
||||
);
|
||||
one.merge(&two);
|
||||
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
|
||||
assert_eq!(skipped, vec![false, false, true, true, true, true]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_skip2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1").set_skip(true),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
|
||||
assert_eq!(skipped, vec![true, true, false, false, false, false]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lines_accepts_into_lines() {
|
||||
use crate::style::Stylize;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 2));
|
||||
buf.set_string(0, 0, "foo", Style::new().red());
|
||||
buf.set_string(0, 1, "bar", Style::new().blue());
|
||||
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
|
||||
}
|
||||
}
|
||||
160
src/buffer/cell.rs
Normal file
160
src/buffer/cell.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Cell {
|
||||
#[deprecated(
|
||||
since = "0.24.1",
|
||||
note = "This field will be hidden at next major version. Use `Cell::symbol` method to get \
|
||||
the value. Use `Cell::set_symbol` to update the field. Use `Cell::default` to \
|
||||
create `Cell` instance"
|
||||
)]
|
||||
/// The string to be drawn in the cell.
|
||||
///
|
||||
/// This accepts unicode grapheme clusters which might take up more than one cell.
|
||||
pub symbol: String,
|
||||
|
||||
/// The foreground color of the cell.
|
||||
pub fg: Color,
|
||||
|
||||
/// The background color of the cell.
|
||||
pub bg: Color,
|
||||
|
||||
/// The underline color of the cell.
|
||||
///
|
||||
/// This is only used when the `underline-color` feature is enabled.
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub underline_color: Color,
|
||||
|
||||
/// The modifier of the cell.
|
||||
pub modifier: Modifier,
|
||||
|
||||
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
|
||||
pub skip: bool,
|
||||
}
|
||||
|
||||
#[allow(deprecated)] // For Cell::symbol
|
||||
impl Cell {
|
||||
/// Gets the symbol of the cell.
|
||||
pub fn symbol(&self) -> &str {
|
||||
self.symbol.as_str()
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell.
|
||||
pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
|
||||
self.symbol.clear();
|
||||
self.symbol.push_str(symbol);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell to a single character.
|
||||
pub fn set_char(&mut self, ch: char) -> &mut Cell {
|
||||
self.symbol.clear();
|
||||
self.symbol.push(ch);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the foreground color of the cell.
|
||||
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
|
||||
self.fg = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the background color of the cell.
|
||||
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
|
||||
self.bg = color;
|
||||
self
|
||||
}
|
||||
/// Sets the style of the cell.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Cell {
|
||||
let style = style.into();
|
||||
if let Some(c) = style.fg {
|
||||
self.fg = c;
|
||||
}
|
||||
if let Some(c) = style.bg {
|
||||
self.bg = c;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if let Some(c) = style.underline_color {
|
||||
self.underline_color = c;
|
||||
}
|
||||
self.modifier.insert(style.add_modifier);
|
||||
self.modifier.remove(style.sub_modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the style of the cell.
|
||||
pub fn style(&self) -> Style {
|
||||
#[cfg(feature = "underline-color")]
|
||||
return Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.underline_color(self.underline_color)
|
||||
.add_modifier(self.modifier);
|
||||
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
return Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.add_modifier(self.modifier);
|
||||
}
|
||||
|
||||
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
|
||||
///
|
||||
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
|
||||
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
|
||||
pub fn set_skip(&mut self, skip: bool) -> &mut Cell {
|
||||
self.skip = skip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the cell to the default state.
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol.clear();
|
||||
self.symbol.push(' ');
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
self.underline_color = Color::Reset;
|
||||
}
|
||||
self.modifier = Modifier::empty();
|
||||
self.skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Cell {
|
||||
#[allow(deprecated)] // For Cell::symbol
|
||||
Cell {
|
||||
symbol: " ".into(),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn symbol_field() {
|
||||
let mut cell = Cell::default();
|
||||
assert_eq!(cell.symbol(), " ");
|
||||
cell.set_symbol("あ"); // Multi-byte character
|
||||
assert_eq!(cell.symbol(), "あ");
|
||||
cell.set_symbol("👨👩👧👦"); // Multiple code units combined with ZWJ
|
||||
assert_eq!(cell.symbol(), "👨👩👧👦");
|
||||
}
|
||||
}
|
||||
217
src/layout.rs
217
src/layout.rs
@@ -50,6 +50,27 @@ thread_local! {
|
||||
/// calls with the same parameters are faster. The cache is a simple HashMap, and grows
|
||||
/// indefinitely. (See <https://github.com/ratatui-org/ratatui/issues/402> for more information)
|
||||
///
|
||||
/// # Constructors
|
||||
///
|
||||
/// There are four ways to create a new layout:
|
||||
///
|
||||
/// - [`Layout::default`]: create a new layout with default values
|
||||
/// - [`Layout::new`]: create a new layout with a given direction and constraints
|
||||
/// - [`Layout::vertical`]: create a new vertical layout with the given constraints
|
||||
/// - [`Layout::horizontal`]: create a new horizontal layout with the given constraints
|
||||
///
|
||||
/// # Setters
|
||||
///
|
||||
/// There are several setters to modify the layout:
|
||||
///
|
||||
/// - [`Layout::direction`]: set the direction of the layout
|
||||
/// - [`Layout::constraints`]: set the constraints of the layout
|
||||
/// - [`Layout::margin`]: set the margin of the layout
|
||||
/// - [`Layout::horizontal_margin`]: set the horizontal margin of the layout
|
||||
/// - [`Layout::vertical_margin`]: set the vertical margin of the layout
|
||||
/// - [`Layout::segment_size`]: set the way the space is distributed when the constraints are
|
||||
/// satisfied
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
@@ -284,6 +305,47 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new vertical layout with default values.
|
||||
///
|
||||
/// The `constraints` parameter accepts any type that implements `IntoIterator<Item =
|
||||
/// AsRef<Constraint>>`. This includes arrays, slices, vectors, iterators, etc.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let layout = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
|
||||
/// ```
|
||||
pub fn vertical<I>(constraints: I) -> Layout
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Constraint>,
|
||||
{
|
||||
Layout::new(Direction::Vertical, constraints.into_iter().map(Into::into))
|
||||
}
|
||||
|
||||
/// Creates a new horizontal layout with default values.
|
||||
///
|
||||
/// The `constraints` parameter accepts any type that implements `IntoIterator<Item =
|
||||
/// AsRef<Constraint>>`. This includes arrays, slices, vectors, iterators, etc.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let layout = Layout::horizontal([Constraint::Length(5), Constraint::Min(0)]);
|
||||
/// ```
|
||||
pub fn horizontal<I>(constraints: I) -> Layout
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Constraint>,
|
||||
{
|
||||
Layout::new(
|
||||
Direction::Horizontal,
|
||||
constraints.into_iter().map(Into::into),
|
||||
)
|
||||
}
|
||||
|
||||
/// Initialize an empty cache with a custom size. The cache is keyed on the layout and area, so
|
||||
/// that subsequent calls with the same parameters are faster. The cache is a LruCache, and
|
||||
/// grows until `cache_size` is reached.
|
||||
@@ -617,6 +679,29 @@ impl Layout {
|
||||
.collect::<Rc<[Rect]>>();
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// An ergonomic wrapper around [`Layout::split`] that returns an array instead of `Rc<[Rect]>`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the number of constraints is not equal to the length of the returned array.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # fn render(frame: &mut Frame) {
|
||||
/// let area = frame.size();
|
||||
/// let [top, main] =
|
||||
/// Layout::new(Direction::Vertical,[Constraint::Length(1), Constraint::Min(0)])
|
||||
/// .split_array(area);
|
||||
/// # }
|
||||
pub fn split_array<const N: usize>(self, area: Rect) -> [Rect; N] {
|
||||
self.split(area)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("invalid number of rects")
|
||||
}
|
||||
}
|
||||
|
||||
impl Margin {
|
||||
@@ -747,6 +832,67 @@ impl Constraint {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to create a Length constraint
|
||||
///
|
||||
/// Equivalent to `Constraint::Length(length)`
|
||||
pub fn eq(length: u16) -> Constraint {
|
||||
Constraint::Length(length)
|
||||
}
|
||||
|
||||
/// Helper function to create a Min constraint
|
||||
///
|
||||
/// Equivalent to `Constraint::Min(length)`
|
||||
pub fn ge(length: u16) -> Constraint {
|
||||
Constraint::Min(length)
|
||||
}
|
||||
|
||||
/// Helper function to create a Min constraint
|
||||
///
|
||||
/// Equivalent to `Constraint::Min(length)`
|
||||
pub fn min(length: u16) -> Constraint {
|
||||
Constraint::Min(length)
|
||||
}
|
||||
|
||||
/// Helper function to create a Max constraint
|
||||
///
|
||||
/// Equivalent to `Constraint::Max(length)`
|
||||
pub fn le(length: u16) -> Constraint {
|
||||
Constraint::Max(length)
|
||||
}
|
||||
|
||||
/// Helper function to create a Max constraint
|
||||
///
|
||||
/// Equivalent to `Constraint::Max(length)`
|
||||
pub fn max(length: u16) -> Constraint {
|
||||
Constraint::Max(length)
|
||||
}
|
||||
|
||||
/// Helper function to create a Percentage constraint
|
||||
///
|
||||
/// Equivalent to `Constraint::Percentage(p)`
|
||||
pub fn perc(p: u16) -> Constraint {
|
||||
Constraint::Percentage(p)
|
||||
}
|
||||
|
||||
/// Helper function to create a Ratio constraint
|
||||
///
|
||||
/// Equivalent to `Constraint::Ratio(numerator, denominator)`
|
||||
pub fn ratio(numerator: u32, denominator: u32) -> Constraint {
|
||||
Constraint::Ratio(numerator, denominator)
|
||||
}
|
||||
|
||||
impl From<u16> for Constraint {
|
||||
fn from(length: u16) -> Constraint {
|
||||
Constraint::Length(length)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u32, u32)> for Constraint {
|
||||
fn from((n, d): (u32, u32)) -> Constraint {
|
||||
Constraint::Ratio(n, d)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Constraint> for Constraint {
|
||||
fn as_ref(&self) -> &Constraint {
|
||||
self
|
||||
@@ -878,6 +1024,32 @@ mod tests {
|
||||
assert_eq!(layout.constraints, [Constraint::Min(0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_vertical() {
|
||||
assert_eq!(
|
||||
Layout::vertical([Constraint::Min(0)]),
|
||||
Layout {
|
||||
direction: Direction::Vertical,
|
||||
margin: Margin::new(0, 0),
|
||||
constraints: vec![Constraint::Min(0)],
|
||||
segment_size: LastTakesRemainder,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_horizontal() {
|
||||
assert_eq!(
|
||||
Layout::horizontal([Constraint::Min(0)]),
|
||||
Layout {
|
||||
direction: Direction::Horizontal,
|
||||
margin: Margin::new(0, 0),
|
||||
constraints: vec![Constraint::Min(0)],
|
||||
segment_size: LastTakesRemainder,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// The purpose of this test is to ensure that layout can be constructed with any type that
|
||||
/// implements IntoIterator<Item = AsRef<Constraint>>.
|
||||
#[test]
|
||||
@@ -1713,5 +1885,50 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_array() {
|
||||
let [a, b] = Layout::new(
|
||||
Direction::Horizontal,
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
)
|
||||
.split_array(Rect::new(0, 0, 2, 1));
|
||||
assert_eq!(a, Rect::new(0, 0, 1, 1));
|
||||
assert_eq!(b, Rect::new(1, 0, 1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "invalid number of rects")]
|
||||
fn split_array_invalid_number_of_recs() {
|
||||
let [_a, _b, _c] = Layout::new(
|
||||
Direction::Horizontal,
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
)
|
||||
.split_array(Rect::new(0, 0, 2, 1));
|
||||
}
|
||||
}
|
||||
|
||||
/// A small test that just checks the ideas behind the layout shortcuts
|
||||
#[test]
|
||||
#[allow(unused_variables)]
|
||||
fn constraint_helper_examples() {
|
||||
let constraints = [
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
Constraint::Max(1),
|
||||
Constraint::Percentage(1),
|
||||
Constraint::Ratio(1, 1),
|
||||
eq(1),
|
||||
min(1),
|
||||
ge(1),
|
||||
max(1),
|
||||
le(1),
|
||||
perc(1),
|
||||
ratio(1, 1),
|
||||
];
|
||||
let area = Rect::new(0, 0, 1, 1);
|
||||
let [a, b, c, d] = Layout::horizontal([eq(5), ge(20), le(20), eq(5)]).split_array(area);
|
||||
let [a, b, c, d] = Layout::vertical([1, 2, 3, 4]).split_array(area);
|
||||
let [left, mid, right] = Layout::vertical([(1, 4), (1, 2), (1, 4)]).split_array(area);
|
||||
}
|
||||
}
|
||||
|
||||
229
src/style.rs
229
src/style.rs
@@ -82,6 +82,9 @@ bitflags! {
|
||||
///
|
||||
/// They are bitflags so they can easily be composed.
|
||||
///
|
||||
/// `From<Modifier> for Style` is implemented so you can use `Modifier` anywhere that accepts
|
||||
/// `Into<Style>`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
@@ -138,6 +141,20 @@ impl fmt::Debug for Modifier {
|
||||
///
|
||||
/// For more information about the style shorthands, see the [`Stylize`] trait.
|
||||
///
|
||||
/// We implement conversions from [`Color`] and [`Modifier`] to [`Style`] so you can use them
|
||||
/// anywhere that accepts `Into<Style>`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// Line::styled("hello", Style::new().fg(Color::Red));
|
||||
/// // simplifies to
|
||||
/// Line::styled("hello", Color::Red);
|
||||
///
|
||||
/// Line::styled("hello", Style::new().add_modifier(Modifier::BOLD));
|
||||
/// // simplifies to
|
||||
/// Line::styled("hello", Modifier::BOLD);
|
||||
/// ```
|
||||
///
|
||||
/// Styles represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
|
||||
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
|
||||
/// just S3.
|
||||
@@ -227,10 +244,11 @@ impl Styled for Style {
|
||||
*self
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.patch(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
pub const fn new() -> Style {
|
||||
Style {
|
||||
@@ -366,6 +384,9 @@ impl Style {
|
||||
/// Results in a combined style that is equivalent to applying the two individual styles to
|
||||
/// a style one after the other.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
@@ -378,7 +399,8 @@ impl Style {
|
||||
/// );
|
||||
/// ```
|
||||
#[must_use = "`patch` returns the modified style without modifying the original"]
|
||||
pub fn patch(mut self, other: Style) -> Style {
|
||||
pub fn patch<S: Into<Style>>(mut self, other: S) -> Style {
|
||||
let other = other.into();
|
||||
self.fg = other.fg.or(self.fg);
|
||||
self.bg = other.bg.or(self.bg);
|
||||
|
||||
@@ -396,6 +418,134 @@ impl Style {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Style {
|
||||
/// Creates a new `Style` with the given foreground color.
|
||||
///
|
||||
/// To specify a foreground and background color, use the `from((fg, bg))` constructor.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::from(Color::Red);
|
||||
/// ```
|
||||
fn from(color: Color) -> Self {
|
||||
Self::new().fg(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background
|
||||
/// let style = Style::from((Color::Red, Color::Blue));
|
||||
/// // default foreground, blue background
|
||||
/// let style = Style::from((Color::Reset, Color::Blue));
|
||||
/// ```
|
||||
fn from((fg, bg): (Color, Color)) -> Self {
|
||||
Self::new().fg(fg).bg(bg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Modifier> for Style {
|
||||
/// Creates a new `Style` with the given modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// To specify modifiers to add and remove, use the `from((add_modifier, sub_modifier))`
|
||||
/// constructor.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // add bold and italic
|
||||
/// let style = Style::from(Modifier::BOLD|Modifier::ITALIC);
|
||||
fn from(modifier: Modifier) -> Self {
|
||||
Self::new().add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Modifier, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given modifiers added and removed.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // add bold and italic, remove dim
|
||||
/// let style = Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
|
||||
/// ```
|
||||
fn from((add_modifier, sub_modifier): (Modifier, Modifier)) -> Self {
|
||||
Self::new()
|
||||
.add_modifier(add_modifier)
|
||||
.remove_modifier(sub_modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground color and modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
fn from((fg, modifier): (Color, Modifier)) -> Self {
|
||||
Self::new().fg(fg).add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors and modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
fn from((fg, bg, modifier): (Color, Color, Modifier)) -> Self {
|
||||
Self::new().fg(fg).bg(bg).add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color, Modifier, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors and modifiers added
|
||||
/// and removed.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background, add bold and italic, remove dim
|
||||
/// let style = Style::from((
|
||||
/// Color::Red,
|
||||
/// Color::Blue,
|
||||
/// Modifier::BOLD | Modifier::ITALIC,
|
||||
/// Modifier::DIM,
|
||||
/// ));
|
||||
/// ```
|
||||
fn from((fg, bg, add_modifier, sub_modifier): (Color, Color, Modifier, Modifier)) -> Self {
|
||||
Self::new()
|
||||
.fg(fg)
|
||||
.bg(bg)
|
||||
.add_modifier(add_modifier)
|
||||
.remove_modifier(sub_modifier)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -657,4 +807,79 @@ mod tests {
|
||||
// reset
|
||||
assert_eq!(Style::new().reset(), Style::reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color() {
|
||||
assert_eq!(Style::from(Color::Red), Style::new().fg(Color::Red));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Color::Blue)),
|
||||
Style::new().fg(Color::Red).bg(Color::Blue)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_modifier() {
|
||||
assert_eq!(
|
||||
Style::from(Modifier::BOLD | Modifier::ITALIC),
|
||||
Style::new()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_modifier_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM)),
|
||||
Style::new()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.bg(Color::Blue)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color_modifier_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((
|
||||
Color::Red,
|
||||
Color::Blue,
|
||||
Modifier::BOLD | Modifier::ITALIC,
|
||||
Modifier::DIM
|
||||
)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.bg(Color::Blue)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ use std::{
|
||||
/// - we support `-` and `_` and ` ` as separators for all colors
|
||||
/// - we support both `gray` and `grey` spellings
|
||||
///
|
||||
/// `From<Color> for Style` is implemented by creating a style with the foreground color set to the
|
||||
/// given color. This allows you to use colors anywhere that accepts `Into<Style>`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
@@ -61,7 +64,7 @@ use std::{
|
||||
///
|
||||
/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub enum Color {
|
||||
/// Resets the foreground or background color
|
||||
#[default]
|
||||
@@ -124,6 +127,17 @@ pub enum Color {
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for Color {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
FromStr::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type indicating a failure to parse a color string.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct ParseColorError;
|
||||
@@ -251,6 +265,9 @@ impl Display for Color {
|
||||
mod tests {
|
||||
use std::error::Error;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::de::{Deserialize, IntoDeserializer};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -361,4 +378,46 @@ mod tests {
|
||||
assert_eq!(format!("{}", Color::Rgb(255, 0, 0)), "#FF0000");
|
||||
assert_eq!(format!("{}", Color::Reset), "Reset");
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize() -> Result<(), serde::de::value::Error> {
|
||||
assert_eq!(
|
||||
Color::Black,
|
||||
Color::deserialize("Black".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Magenta,
|
||||
Color::deserialize("magenta".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::LightGreen,
|
||||
Color::deserialize("LightGreen".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::White,
|
||||
Color::deserialize("bright-white".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Indexed(42),
|
||||
Color::deserialize("42".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Rgb(0, 255, 0),
|
||||
Color::deserialize("#00ff00".into_deserializer())?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize_error() {
|
||||
let color: Result<_, serde::de::value::Error> =
|
||||
Color::deserialize("invalid".into_deserializer());
|
||||
assert!(color.is_err());
|
||||
|
||||
let color: Result<_, serde::de::value::Error> =
|
||||
Color::deserialize("#00000000".into_deserializer());
|
||||
assert!(color.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,14 @@ use crate::{
|
||||
pub trait Styled {
|
||||
type Item;
|
||||
|
||||
/// Returns the style of the object.
|
||||
fn style(&self) -> Style;
|
||||
fn set_style(self, style: Style) -> Self::Item;
|
||||
|
||||
/// Sets the style of the object.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item;
|
||||
}
|
||||
|
||||
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
|
||||
@@ -209,7 +215,7 @@ impl<'a> Styled for &'a str {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
@@ -221,7 +227,7 @@ impl Styled for String {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,6 @@
|
||||
//! ]);
|
||||
//! ```
|
||||
|
||||
use crate::style::Style;
|
||||
|
||||
mod grapheme;
|
||||
pub use grapheme::StyledGrapheme;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::style::{Style, Styled};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
|
||||
@@ -12,8 +12,15 @@ pub struct StyledGrapheme<'a> {
|
||||
}
|
||||
|
||||
impl<'a> StyledGrapheme<'a> {
|
||||
pub fn new(symbol: &'a str, style: Style) -> StyledGrapheme<'a> {
|
||||
StyledGrapheme { symbol, style }
|
||||
/// Creates a new `StyledGrapheme` with the given symbol and style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> StyledGrapheme<'a> {
|
||||
StyledGrapheme {
|
||||
symbol,
|
||||
style: style.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +31,8 @@ impl<'a> Styled for StyledGrapheme<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(mut self, style: Style) -> Self::Item {
|
||||
self.style = style;
|
||||
fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -33,7 +40,6 @@ impl<'a> Styled for StyledGrapheme<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
|
||||
461
src/text/line.rs
461
src/text/line.rs
@@ -1,23 +1,106 @@
|
||||
#![deny(missing_docs)]
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{Span, Style, StyledGrapheme};
|
||||
use crate::layout::Alignment;
|
||||
use super::StyledGrapheme;
|
||||
use crate::{prelude::*, widgets::Widget};
|
||||
|
||||
/// A line of text, consisting of one or more [`Span`]s.
|
||||
///
|
||||
/// [`Line`]s are used wherever text is displayed in the terminal and represent a single line of
|
||||
/// text. When a [`Line`] is rendered, it is rendered as a single line of text, with each [`Span`]
|
||||
/// being rendered in order (left to right).
|
||||
///
|
||||
/// [`Line`]s can be created from [`Span`]s, [`String`]s, and [`&str`]s. They can be styled with a
|
||||
/// [`Style`], and have an [`Alignment`].
|
||||
///
|
||||
/// The line's [`Alignment`] is used by the rendering widget to determine how to align the line
|
||||
/// within the available space. If the line is longer than the available space, the alignment is
|
||||
/// ignored and the line is truncated.
|
||||
///
|
||||
/// The line's [`Style`] is used by the rendering widget to determine how to style the line. If the
|
||||
/// line is longer than the available space, the style is applied to the entire line, and the line
|
||||
/// is truncated. Each [`Span`] in the line will be styled with the [`Style`] of the line, and then
|
||||
/// with its own [`Style`].
|
||||
///
|
||||
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`]. Usually
|
||||
/// apps will use the [`Paragraph`] widget instead of rendering a [`Line`] directly as it provides
|
||||
/// more functionality.
|
||||
///
|
||||
/// # Constructor Methods
|
||||
///
|
||||
/// - [`Line::default`] creates a line with empty content and the default style.
|
||||
/// - [`Line::raw`] creates a line with the given content and the default style.
|
||||
/// - [`Line::styled`] creates a line with the given content and style.
|
||||
///
|
||||
/// # Setter Methods
|
||||
///
|
||||
/// These methods are fluent setters. They return a `Line` with the property set.
|
||||
///
|
||||
/// - [`Line::spans`] sets the content of the line.
|
||||
/// - [`Line::style`] sets the style of the line.
|
||||
/// - [`Line::alignment`] sets the alignment of the line.
|
||||
///
|
||||
/// # Other Methods
|
||||
///
|
||||
/// - [`Line::patch_style`] patches the style of the line, adding modifiers from the given style.
|
||||
/// - [`Line::reset_style`] resets the style of the line.
|
||||
/// - [`Line::width`] returns the unicode width of the content held by this line.
|
||||
/// - [`Line::styled_graphemes`] returns an iterator over the graphemes held by this line.
|
||||
///
|
||||
/// # Compatibility Notes
|
||||
///
|
||||
/// Before v0.26.0, [`Line`] did not have a `style` field and instead relied on only the styles that
|
||||
/// were set on each [`Span`] contained in the `spans` field. The [`Line::patch_style`] method was
|
||||
/// the only way to set the overall style for individual lines. For this reason, this field may not
|
||||
/// be supported yet by all widgets (outside of the `ratatui` crate itself).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// Line::raw("unstyled");
|
||||
/// Line::styled("yellow text", Style::new().yellow());
|
||||
/// Line::from("red text").style(Style::new().red());
|
||||
/// Line::from(String::from("unstyled"));
|
||||
/// Line::from(vec![
|
||||
/// Span::styled("Hello", Style::new().blue()),
|
||||
/// Span::raw(" world!"),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Line<'a> {
|
||||
/// The spans that make up this line of text.
|
||||
pub spans: Vec<Span<'a>>,
|
||||
|
||||
/// The style of this line of text.
|
||||
pub style: Style,
|
||||
|
||||
/// The alignment of this line of text.
|
||||
pub alignment: Option<Alignment>,
|
||||
}
|
||||
|
||||
impl<'a> Line<'a> {
|
||||
/// Create a line with the default style.
|
||||
///
|
||||
/// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
|
||||
/// [`Cow<str>`], or your own type that implements [`Into<Cow<str>>`]).
|
||||
///
|
||||
/// A [`Line`] can specify a [`Style`], which will be applied before the style of each [`Span`]
|
||||
/// in the line.
|
||||
///
|
||||
/// Any newlines in the content are removed.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use std::borrow::Cow;
|
||||
/// Line::raw("test content");
|
||||
/// Line::raw(String::from("test content"));
|
||||
/// Line::raw(Cow::from("test content"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Line<'a>
|
||||
where
|
||||
@@ -29,40 +112,120 @@ impl<'a> Line<'a> {
|
||||
.lines()
|
||||
.map(|v| Span::raw(v.to_string()))
|
||||
.collect(),
|
||||
alignment: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a line with a style.
|
||||
/// Create a line with the given style.
|
||||
// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
|
||||
/// [`Cow<str>`], or your own type that implements [`Into<Cow<str>>`]).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Any newlines in the content are removed.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use std::borrow::Cow;
|
||||
/// let style = Style::new().yellow().italic();
|
||||
/// Line::styled("My text", style);
|
||||
/// Line::styled(String::from("My text"), style);
|
||||
/// Line::styled(Cow::from("test content"), style);
|
||||
/// ```
|
||||
pub fn styled<T, S>(content: T, style: S) -> Line<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
Line {
|
||||
spans: content
|
||||
.into()
|
||||
.lines()
|
||||
.map(|v| Span::raw(v.to_string()))
|
||||
.collect(),
|
||||
style: style.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the spans of this line of text.
|
||||
///
|
||||
/// `spans` accepts any iterator that yields items that are convertible to [`Span`] (e.g.
|
||||
/// [`&str`], [`String`], [`Span`], or your own type that implements [`Into<Span>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .add_modifier(Modifier::ITALIC);
|
||||
/// Line::styled("My text", style);
|
||||
/// Line::styled(String::from("My text"), style);
|
||||
/// let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
|
||||
/// let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {}", i)));
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Line<'a>
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn spans<I>(mut self, spans: I) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Span<'a>>,
|
||||
{
|
||||
Line::from(Span::styled(content, style))
|
||||
self.spans = spans.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of this line of text.
|
||||
///
|
||||
/// Defaults to [`Style::default()`].
|
||||
///
|
||||
/// Note: This field was added in v0.26.0. Prior to that, the style of a line was determined
|
||||
/// only by the style of each [`Span`] contained in the line. For this reason, this field may
|
||||
/// not be supported by all widgets (outside of the `ratatui` crate itself).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from("foo").style(Style::new().red());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
///
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from("Hi, what's up?");
|
||||
/// assert_eq!(None, line.alignment);
|
||||
/// assert_eq!(
|
||||
/// Some(Alignment::Right),
|
||||
/// line.alignment(Alignment::Right).alignment
|
||||
/// )
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn alignment(self, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
alignment: Some(alignment),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let line = Line::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// assert_eq!(7, line.width());
|
||||
/// let line = Line::from(vec!["Hello".blue(), " world!".green()]);
|
||||
/// assert_eq!(12, line.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.spans.iter().map(Span::width).sum()
|
||||
@@ -73,7 +236,10 @@ impl<'a> Line<'a> {
|
||||
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
|
||||
/// the resulting [`Style`].
|
||||
///
|
||||
/// ## Examples
|
||||
/// `base_style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`],
|
||||
/// or your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::iter::Iterator;
|
||||
@@ -93,18 +259,26 @@ impl<'a> Line<'a> {
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
pub fn styled_graphemes<S: Into<Style>>(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
base_style: S,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
let style = base_style.into().patch(self.style);
|
||||
self.spans
|
||||
.iter()
|
||||
.flat_map(move |span| span.styled_graphemes(base_style))
|
||||
.flat_map(move |span| span.styled_graphemes(style))
|
||||
}
|
||||
|
||||
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
/// This is useful for when you want to apply a style to a line that already has some styling.
|
||||
/// In contrast to [`Line::style`], this method will not overwrite the existing style, but
|
||||
/// instead will add the given style's modifiers to the existing style of each `Span`.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
@@ -122,16 +296,18 @@ impl<'a> Line<'a> {
|
||||
/// raw_line.patch_style(style);
|
||||
/// assert_eq!(raw_line, styled_line);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
|
||||
let style = style.into();
|
||||
for span in &mut self.spans {
|
||||
span.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of each Span in the Line.
|
||||
///
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
@@ -149,27 +325,6 @@ impl<'a> Line<'a> {
|
||||
span.reset_style();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from("Hi, what's up?");
|
||||
/// assert_eq!(None, line.alignment);
|
||||
/// assert_eq!(
|
||||
/// Some(Alignment::Right),
|
||||
/// line.alignment(Alignment::Right).alignment
|
||||
/// )
|
||||
/// ```
|
||||
pub fn alignment(self, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
alignment: Some(alignment),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Line<'a> {
|
||||
@@ -208,16 +363,118 @@ impl<'a> From<Line<'a>> for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Line<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
buf.set_style(area, self.style);
|
||||
let width = self.width() as u16;
|
||||
let offset = match self.alignment {
|
||||
Some(Alignment::Left) => 0,
|
||||
Some(Alignment::Center) => (area.width.saturating_sub(width)) / 2,
|
||||
Some(Alignment::Right) => area.width.saturating_sub(width),
|
||||
None => 0,
|
||||
};
|
||||
let mut x = area.left().saturating_add(offset);
|
||||
for span in self.spans {
|
||||
let span_width = span.width() as u16;
|
||||
let span_area = Rect {
|
||||
x,
|
||||
width: span_width,
|
||||
..area
|
||||
};
|
||||
span.render(span_area, buf);
|
||||
x = x.saturating_add(span_width);
|
||||
if x >= area.right() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
layout::Alignment,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, StyledGrapheme},
|
||||
};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_width() {
|
||||
fn raw_str() {
|
||||
let line = Line::raw("test content");
|
||||
assert_eq!(line.spans, vec![Span::raw("test content")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
|
||||
let line = Line::raw("a\nb");
|
||||
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_str() {
|
||||
let style = Style::new().yellow();
|
||||
let content = "Hello, world!";
|
||||
let line = Line::styled(content, style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_string() {
|
||||
let style = Style::new().yellow();
|
||||
let content = String::from("Hello, world!");
|
||||
let line = Line::styled(content.clone(), style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_cow() {
|
||||
let style = Style::new().yellow();
|
||||
let content = Cow::from("Hello, world!");
|
||||
let line = Line::styled(content.clone(), style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spans_vec() {
|
||||
let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
vec![
|
||||
Span::styled("Hello", Style::new().blue()),
|
||||
Span::styled(" world!", Style::new().green()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spans_iter() {
|
||||
let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {i}")));
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
vec![
|
||||
Span::raw("Item 1"),
|
||||
Span::raw("Item 2"),
|
||||
Span::raw("Item 3"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let line = Line::default().style(Style::new().red());
|
||||
assert_eq!(line.style, Style::new().red());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alignment() {
|
||||
let line = Line::from("This is left").alignment(Alignment::Left);
|
||||
assert_eq!(Some(Alignment::Left), line.alignment);
|
||||
|
||||
let line = Line::from("This is default");
|
||||
assert_eq!(None, line.alignment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::raw(" text"),
|
||||
@@ -229,7 +486,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patch_style() {
|
||||
fn patch_style() {
|
||||
let style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
@@ -246,7 +503,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_style() {
|
||||
fn reset_style() {
|
||||
let mut line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
@@ -258,21 +515,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
fn from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
fn from_str() {
|
||||
let s = "Hello, world!";
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec() {
|
||||
fn from_vec() {
|
||||
let spans = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
@@ -282,14 +539,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_span() {
|
||||
fn from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
let line = Line::from(span.clone());
|
||||
assert_eq!(vec![span], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_string() {
|
||||
fn into_string() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
@@ -298,15 +555,6 @@ mod tests {
|
||||
assert_eq!("Hello, world!", s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alignment() {
|
||||
let line = Line::from("This is left").alignment(Alignment::Left);
|
||||
assert_eq!(Some(Alignment::Left), line.alignment);
|
||||
|
||||
let line = Line::from("This is default");
|
||||
assert_eq!(None, line.alignment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_graphemes() {
|
||||
const RED: Style = Style::new().fg(Color::Red);
|
||||
@@ -337,14 +585,75 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_str() {
|
||||
let line = Line::raw("test content");
|
||||
assert_eq!(line.spans, vec![Span::raw("test content")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
mod widget {
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
const BLUE: Style = Style::new().fg(Color::Blue);
|
||||
const GREEN: Style = Style::new().fg(Color::Green);
|
||||
const ITALIC: Style = Style::new().add_modifier(Modifier::ITALIC);
|
||||
|
||||
let line = Line::raw("a\nb");
|
||||
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
fn hello_world() -> Line<'static> {
|
||||
Line::from(vec![
|
||||
Span::styled("Hello ", BLUE),
|
||||
Span::styled("world!", GREEN),
|
||||
])
|
||||
.style(ITALIC)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec!["Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_only_styles_line_area() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
|
||||
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec!["Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 11, 1));
|
||||
hello_world().render(Rect::new(0, 0, 11, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec!["Hello world"]);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE.italic());
|
||||
expected.set_style(Rect::new(6, 0, 5, 1), GREEN.italic());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered() {
|
||||
let line = hello_world().alignment(Alignment::Center);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
line.render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![" Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(1, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(7, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_right_aligned() {
|
||||
let line = hello_world().alignment(Alignment::Right);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
line.render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![" Hello world!"]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(3, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(9, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
189
src/text/span.rs
189
src/text/span.rs
@@ -4,7 +4,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::StyledGrapheme;
|
||||
use crate::style::{Style, Styled};
|
||||
use crate::{prelude::*, widgets::Widget};
|
||||
|
||||
/// Represents a part of a line that is contiguous and where all characters share the same style.
|
||||
///
|
||||
@@ -74,7 +74,19 @@ use crate::style::{Style, Styled};
|
||||
/// .italic();
|
||||
/// ```
|
||||
///
|
||||
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Usually
|
||||
/// apps will use the [`Paragraph`] widget instead of rendering `Span` directly, as it handles text
|
||||
/// wrapping and alignment for you.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// # fn render_frame(frame: &mut Frame) {
|
||||
/// frame.render_widget("test content".green().on_yellow().italic(), frame.size());
|
||||
/// # }
|
||||
/// ```
|
||||
/// [`Line`]: crate::text::Line
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
/// [`Cow<str>`]: std::borrow::Cow
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -107,6 +119,12 @@ impl<'a> Span<'a> {
|
||||
|
||||
/// Create a span with the specified style.
|
||||
///
|
||||
/// `content` accepts any type that is convertible to [`Cow<str>`] (e.g. `&str`, `String`,
|
||||
/// `&String`, etc.).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
@@ -115,13 +133,14 @@ impl<'a> Span<'a> {
|
||||
/// Span::styled("test content", style);
|
||||
/// Span::styled(String::from("test content"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Span<'a>
|
||||
pub fn styled<T, S>(content: T, style: S) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style,
|
||||
style: style.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +173,8 @@ impl<'a> Span<'a> {
|
||||
/// In contrast to [`Span::patch_style`], this method replaces the style of the span instead of
|
||||
/// patching it.
|
||||
///
|
||||
/// Accepts any type that can be converted to [`Style`]
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -163,16 +183,16 @@ impl<'a> Span<'a> {
|
||||
/// let mut span = Span::default().style(Style::new().green());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<T>(mut self, style: T) -> Self
|
||||
where
|
||||
T: Into<Style>,
|
||||
{
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Patches the style of the Span, adding modifiers from the given style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
@@ -181,7 +201,7 @@ impl<'a> Span<'a> {
|
||||
/// span.patch_style(Style::new().red().on_yellow().bold());
|
||||
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
|
||||
self.style = self.style.patch(style);
|
||||
}
|
||||
|
||||
@@ -211,6 +231,9 @@ impl<'a> Span<'a> {
|
||||
/// `base_style` is the [`Style`] that will be patched with the `Span`'s `style` to get the
|
||||
/// resulting [`Style`].
|
||||
///
|
||||
/// `base_style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`],
|
||||
/// or your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
@@ -231,18 +254,16 @@ impl<'a> Span<'a> {
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
pub fn styled_graphemes<S: Into<Style>>(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
base_style: S,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
let style = base_style.into().patch(self.style);
|
||||
self.content
|
||||
.as_ref()
|
||||
.graphemes(true)
|
||||
.filter(|g| *g != "\n")
|
||||
.map(move |g| StyledGrapheme {
|
||||
symbol: g,
|
||||
style: base_style.patch(self.style),
|
||||
})
|
||||
.map(move |g| StyledGrapheme { symbol: g, style })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,16 +283,47 @@ impl<'a> Styled for Span<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Span<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let Rect {
|
||||
x: mut current_x,
|
||||
y,
|
||||
width,
|
||||
..
|
||||
} = area;
|
||||
let max_x = Ord::min(current_x.saturating_add(width), buf.area.right());
|
||||
for g in self.styled_graphemes(Style::default()) {
|
||||
let symbol_width = g.symbol.width();
|
||||
let next_x = current_x.saturating_add(symbol_width as u16);
|
||||
if next_x > max_x {
|
||||
break;
|
||||
}
|
||||
buf.get_mut(current_x, y)
|
||||
.set_symbol(g.symbol)
|
||||
.set_style(g.style);
|
||||
|
||||
// multi-width graphemes must clear the cells of characters that are hidden by the
|
||||
// grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
|
||||
// overwritten.
|
||||
for i in (current_x + 1)..next_x {
|
||||
buf.get_mut(i, y).reset();
|
||||
// it may seem odd that the style of the hidden cells are not set to the style of
|
||||
// the grapheme, but this is how the existing buffer.set_span() method works.
|
||||
// buf.get_mut(i, y).set_style(g.style);
|
||||
}
|
||||
current_x = next_x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
@@ -388,4 +440,103 @@ mod tests {
|
||||
assert_eq!(stylized.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, style::Stylize};
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
"test content".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the content of the span is longer than the area passed to render, the content
|
||||
/// should be truncated
|
||||
#[test]
|
||||
fn render_truncates_too_long_content() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected =
|
||||
Buffer::with_lines(vec![Line::from(vec!["test conte".green().on_yellow()])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When there is already a style set on the buffer, the style of the span should be
|
||||
/// patched with the existing style
|
||||
#[test]
|
||||
fn render_patches_existing_style() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
buf.set_style(buf.area, Style::new().italic());
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
"test content".green().on_yellow().italic(),
|
||||
" ".italic(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
|
||||
/// of the hidden characters are cleared.
|
||||
#[test]
|
||||
fn render_multi_width_symbol() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test 😃 content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
// The existing code in buffer.set_line() handles multi-width graphemes by clearing the
|
||||
// cells of the hidden characters. This test ensures that the existing behavior is
|
||||
// preserved.
|
||||
let expected = Buffer::with_lines(vec!["test 😃 content".green().on_yellow()]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme that does not fit in the area passed to
|
||||
/// render, the entire grapheme will be truncated.
|
||||
#[test]
|
||||
fn render_multi_width_symbol_truncates_entire_symbol() {
|
||||
// the 😃 emoji is 2 columns wide so it will be truncated
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test 😃 content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
"test ".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the area passed to render overflows the buffer, the content should be truncated
|
||||
/// to fit the buffer.
|
||||
#[test]
|
||||
fn render_overflowing_area_truncates() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(Rect::new(10, 0, 20, 1), &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
" ".into(),
|
||||
"test ".green().on_yellow(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{Line, Span};
|
||||
use crate::style::Style;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
@@ -60,6 +59,9 @@ impl<'a> Text<'a> {
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
@@ -70,9 +72,10 @@ impl<'a> Text<'a> {
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Text<'a>
|
||||
pub fn styled<T, S>(content: T, style: S) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
let mut text = Text::raw(content);
|
||||
text.patch_style(style);
|
||||
@@ -107,6 +110,9 @@ impl<'a> Text<'a> {
|
||||
|
||||
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
@@ -121,7 +127,8 @@ impl<'a> Text<'a> {
|
||||
/// raw_text.patch_style(style);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
|
||||
let style = style.into();
|
||||
for line in &mut self.lines {
|
||||
line.patch_style(style);
|
||||
}
|
||||
|
||||
@@ -170,11 +170,14 @@ impl<'a> BarChart<'a> {
|
||||
|
||||
/// Set the default style of the bar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// It is also possible to set individually the style of each [`Bar`].
|
||||
/// In this case the default style will be patched by the individual style
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.bar_style = style;
|
||||
pub fn bar_style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
|
||||
self.bar_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -226,6 +229,9 @@ impl<'a> BarChart<'a> {
|
||||
|
||||
/// Set the default value style of the bar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// It is also possible to set individually the value style of each [`Bar`].
|
||||
/// In this case the default value style will be patched by the individual value style
|
||||
///
|
||||
@@ -233,13 +239,16 @@ impl<'a> BarChart<'a> {
|
||||
///
|
||||
/// [Bar::value_style] to set the value style individually.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.value_style = style;
|
||||
pub fn value_style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
|
||||
self.value_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the default label style of the groups and bars.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// It is also possible to set individually the label style of each [`Bar`] or [`BarGroup`].
|
||||
/// In this case the default label style will be patched by the individual label style
|
||||
///
|
||||
@@ -247,8 +256,8 @@ impl<'a> BarChart<'a> {
|
||||
///
|
||||
/// [Bar::label] to set the label style individually.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.label_style = style;
|
||||
pub fn label_style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
|
||||
self.label_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -261,10 +270,13 @@ impl<'a> BarChart<'a> {
|
||||
|
||||
/// Set the style of the entire chart.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// The style will be applied to everything that isn't styled (borders, bars, labels, ...).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -597,7 +609,7 @@ impl<'a> Styled for BarChart<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{buffer::Buffer, prelude::Rect, style::Style, text::Line};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget.
|
||||
///
|
||||
@@ -70,22 +70,27 @@ impl<'a> Bar<'a> {
|
||||
|
||||
/// Set the style of the bar.
|
||||
///
|
||||
/// This will apply to every non-styled element.
|
||||
/// It can be seen and used as a default value.
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will apply to every non-styled element. It can be seen and used as a default value.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> Bar<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Bar<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style of the value.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value`] to set the value.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn value_style(mut self, style: Style) -> Bar<'a> {
|
||||
self.value_style = style;
|
||||
pub fn value_style<S: Into<Style>>(mut self, style: S) -> Bar<'a> {
|
||||
self.value_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,7 @@ use strum::{Display, EnumString};
|
||||
|
||||
pub use self::title::{Position, Title};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
prelude::*,
|
||||
symbols::border,
|
||||
widgets::{Borders, Widget},
|
||||
};
|
||||
@@ -334,10 +332,13 @@ impl<'a> Block<'a> {
|
||||
|
||||
/// Applies the style to all titles.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// If a [`Title`] already has a style, the title's style will add on top of this one.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn title_style(mut self, style: Style) -> Block<'a> {
|
||||
self.titles_style = style;
|
||||
pub fn title_style<S: Into<Style>>(mut self, style: S) -> Block<'a> {
|
||||
self.titles_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -405,6 +406,9 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// If a [`Block::style`] is defined, `border_style` will be applied on top of it.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// This example shows a `Block` with blue borders.
|
||||
@@ -415,8 +419,8 @@ impl<'a> Block<'a> {
|
||||
/// .border_style(Style::new().blue());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn border_style(mut self, style: Style) -> Block<'a> {
|
||||
self.border_style = style;
|
||||
pub fn border_style<S: Into<Style>>(mut self, style: S) -> Block<'a> {
|
||||
self.border_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -426,10 +430,13 @@ impl<'a> Block<'a> {
|
||||
/// more specific style. Elements can be styled further with [`Block::title_style`] and
|
||||
/// [`Block::border_style`].
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will also apply to the widget inside that block, unless the inner widget is styled.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn style(mut self, style: Style) -> Block<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Block<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -797,7 +804,7 @@ impl<'a> Styled for Block<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -1100,15 +1107,60 @@ mod tests {
|
||||
const _DEFAULT_STYLE: Style = Style::new();
|
||||
const _DEFAULT_PADDING: Padding = Padding::uniform(1);
|
||||
const _DEFAULT_BLOCK: Block = Block::new()
|
||||
.title_style(_DEFAULT_STYLE)
|
||||
// the following methods are no longer const because they use Into<Style>
|
||||
// .style(_DEFAULT_STYLE) // no longer const
|
||||
// .border_style(_DEFAULT_STYLE) // no longer const
|
||||
// .title_style(_DEFAULT_STYLE) // no longer const
|
||||
.title_alignment(Alignment::Left)
|
||||
.title_position(Position::Top)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(_DEFAULT_STYLE)
|
||||
.style(_DEFAULT_STYLE)
|
||||
.padding(_DEFAULT_PADDING);
|
||||
}
|
||||
|
||||
/// This test ensures that we have some coverage on the Style::from() implementations
|
||||
#[test]
|
||||
fn block_style() {
|
||||
// nominal style
|
||||
let block = Block::default().style(Style::new().red());
|
||||
assert_eq!(block.style, Style::new().red());
|
||||
|
||||
// auto-convert from Color
|
||||
let block = Block::default().style(Color::Red);
|
||||
assert_eq!(block.style, Style::new().red());
|
||||
|
||||
// auto-convert from (Color, Color)
|
||||
let block = Block::default().style((Color::Red, Color::Blue));
|
||||
assert_eq!(block.style, Style::new().red().on_blue());
|
||||
|
||||
// auto-convert from Modifier
|
||||
let block = Block::default().style(Modifier::BOLD | Modifier::ITALIC);
|
||||
assert_eq!(block.style, Style::new().bold().italic());
|
||||
|
||||
// auto-convert from (Modifier, Modifier)
|
||||
let block = Block::default().style((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
|
||||
assert_eq!(block.style, Style::new().bold().italic().not_dim());
|
||||
|
||||
// auto-convert from (Color, Modifier)
|
||||
let block = Block::default().style((Color::Red, Modifier::BOLD));
|
||||
assert_eq!(block.style, Style::new().red().bold());
|
||||
|
||||
// auto-convert from (Color, Color, Modifier)
|
||||
let block = Block::default().style((Color::Red, Color::Blue, Modifier::BOLD));
|
||||
assert_eq!(block.style, Style::new().red().on_blue().bold());
|
||||
|
||||
// auto-convert from (Color, Color, Modifier, Modifier)
|
||||
let block = Block::default().style((
|
||||
Color::Red,
|
||||
Color::Blue,
|
||||
Modifier::BOLD | Modifier::ITALIC,
|
||||
Modifier::DIM,
|
||||
));
|
||||
assert_eq!(
|
||||
block.style,
|
||||
Style::new().red().on_blue().bold().italic().not_dim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_be_stylized() {
|
||||
let block = Block::default().black().on_white().bold().not_dim();
|
||||
|
||||
@@ -13,18 +13,15 @@ use std::collections::HashMap;
|
||||
use time::{Date, Duration, OffsetDateTime};
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
text::Span,
|
||||
prelude::*,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// Display a month calendar for the month containing `display_date`
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Monthly<'a, S: DateStyler> {
|
||||
pub struct Monthly<'a, DS: DateStyler> {
|
||||
display_date: Date,
|
||||
events: S,
|
||||
events: DS,
|
||||
show_surrounding: Option<Style>,
|
||||
show_weekday: Option<Style>,
|
||||
show_month: Option<Style>,
|
||||
@@ -32,9 +29,9 @@ pub struct Monthly<'a, S: DateStyler> {
|
||||
block: Option<Block<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, S: DateStyler> Monthly<'a, S> {
|
||||
impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
/// Construct a calendar for the `display_date` and highlight the `events`
|
||||
pub fn new(display_date: Date, events: S) -> Self {
|
||||
pub fn new(display_date: Date, events: DS) -> Self {
|
||||
Self {
|
||||
display_date,
|
||||
events,
|
||||
@@ -49,32 +46,44 @@ impl<'a, S: DateStyler> Monthly<'a, S> {
|
||||
/// Fill the calendar slots for days not in the current month also, this causes each line to be
|
||||
/// completely filled. If there is an event style for a date, this style will be patched with
|
||||
/// the event's style
|
||||
pub fn show_surrounding(mut self, style: Style) -> Self {
|
||||
self.show_surrounding = Some(style);
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn show_surrounding<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_surrounding = Some(style.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Display a header containing weekday abbreviations
|
||||
pub fn show_weekdays_header(mut self, style: Style) -> Self {
|
||||
self.show_weekday = Some(style);
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn show_weekdays_header<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_weekday = Some(style.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Display a header containing the month and year
|
||||
pub fn show_month_header(mut self, style: Style) -> Self {
|
||||
self.show_month = Some(style);
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn show_month_header<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_month = Some(style.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// How to render otherwise unstyled dates
|
||||
pub fn default_style(mut self, s: Style) -> Self {
|
||||
self.default_style = s;
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn default_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.default_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Render the calendar within a [Block]
|
||||
pub fn block(mut self, b: Block<'a>) -> Self {
|
||||
self.block = Some(b);
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -108,7 +117,7 @@ impl<'a, S: DateStyler> Monthly<'a, S> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S: DateStyler> Widget for Monthly<'a, S> {
|
||||
impl<'a, DS: DateStyler> Widget for Monthly<'a, DS> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
// Block is used for borders and such
|
||||
// Draw that first, and use the blank area inside the block for our own purposes
|
||||
@@ -178,16 +187,22 @@ pub struct CalendarEventStore(pub HashMap<Date, Style>);
|
||||
|
||||
impl CalendarEventStore {
|
||||
/// Construct a store that has the current date styled.
|
||||
pub fn today(style: Style) -> Self {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn today<S: Into<Style>>(style: S) -> Self {
|
||||
let mut res = Self::default();
|
||||
res.add(OffsetDateTime::now_local().unwrap().date(), style);
|
||||
res.add(OffsetDateTime::now_local().unwrap().date(), style.into());
|
||||
res
|
||||
}
|
||||
|
||||
/// Add a date and style to the store
|
||||
pub fn add(&mut self, date: Date, style: Style) {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn add<S: Into<Style>>(&mut self, date: Date, style: S) {
|
||||
// to simplify style nonsense, last write wins
|
||||
let _ = self.0.insert(date, style);
|
||||
let _ = self.0.insert(date, style.into());
|
||||
}
|
||||
|
||||
/// Helper for trait impls
|
||||
|
||||
@@ -109,6 +109,9 @@ impl<'a> Axis<'a> {
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// [`Axis`] also implements [`Stylize`](crate::style::Stylize) which mean you can style it
|
||||
@@ -119,8 +122,8 @@ impl<'a> Axis<'a> {
|
||||
/// let axis = Axis::default().red();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> Axis<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Axis<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -379,6 +382,9 @@ impl<'a> Dataset<'a> {
|
||||
/// The given style will be used to draw the legend and the data points. Currently the legend
|
||||
/// will use the entire style whereas the data points will only use the foreground.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Example
|
||||
@@ -391,8 +397,8 @@ impl<'a> Dataset<'a> {
|
||||
/// let dataset = Dataset::default().red();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> Dataset<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Dataset<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -547,12 +553,15 @@ impl<'a> Chart<'a> {
|
||||
|
||||
/// Sets the style of the entire chart
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// Styles of [`Axis`] and [`Dataset`] will have priority over this style.
|
||||
///
|
||||
/// 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 style(mut self, style: Style) -> Chart<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Chart<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -1043,7 +1052,7 @@ impl<'a> Styled for Axis<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -1055,7 +1064,7 @@ impl<'a> Styled for Dataset<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -1067,7 +1076,7 @@ impl<'a> Styled for Chart<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,18 +128,24 @@ impl<'a> Gauge<'a> {
|
||||
|
||||
/// Sets the widget style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will style the block (if any non-styled) and background of the widget (everything
|
||||
/// except the bar itself). [`Block`] style set with [`Gauge::block`] takes precedence.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> Gauge<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Gauge<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the bar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn gauge_style(mut self, style: Style) -> Gauge<'a> {
|
||||
self.gauge_style = style;
|
||||
pub fn gauge_style<S: Into<Style>>(mut self, style: S) -> Gauge<'a> {
|
||||
self.gauge_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -323,18 +329,24 @@ impl<'a> LineGauge<'a> {
|
||||
|
||||
/// Sets the widget style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will style everything except the bar itself, so basically the block (if any) and
|
||||
/// background.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the bar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn gauge_style(mut self, style: Style) -> Self {
|
||||
self.gauge_style = style;
|
||||
pub fn gauge_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.gauge_style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -406,7 +418,7 @@ impl<'a> Styled for Gauge<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -418,7 +430,7 @@ impl<'a> Styled for LineGauge<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ use strum::{Display, EnumString};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Corner, Rect},
|
||||
style::{Style, Styled},
|
||||
text::Text,
|
||||
prelude::*,
|
||||
widgets::{Block, HighlightSpacing, StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
@@ -256,6 +253,9 @@ impl<'a> ListItem<'a> {
|
||||
|
||||
/// Sets the item style
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This [`Style`] can be overridden by the [`Style`] of the [`Text`] content.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
@@ -276,8 +276,8 @@ impl<'a> ListItem<'a> {
|
||||
/// let item = ListItem::new("Item 1").red().italic();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> ListItem<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> ListItem<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -523,6 +523,9 @@ impl<'a> List<'a> {
|
||||
|
||||
/// Sets the base style of the widget
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// All text rendered by the widget will use this style, unless overridden by [`Block::style`],
|
||||
/// [`ListItem::style`], or the styles of the [`ListItem`]'s content.
|
||||
///
|
||||
@@ -547,8 +550,8 @@ impl<'a> List<'a> {
|
||||
/// let list = List::new(items).red().italic();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> List<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> List<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -573,6 +576,9 @@ impl<'a> List<'a> {
|
||||
|
||||
/// Set the style of the selected item
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This style will be applied to the entire item, including the
|
||||
/// [highlight symbol](List::highlight_symbol) if it is displayed, and will override any style
|
||||
/// set on the item or on the individual cells.
|
||||
@@ -587,8 +593,8 @@ impl<'a> List<'a> {
|
||||
/// let list = List::new(items).highlight_style(Style::new().red().italic());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn highlight_style(mut self, style: Style) -> List<'a> {
|
||||
self.highlight_style = style;
|
||||
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> List<'a> {
|
||||
self.highlight_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -854,7 +860,7 @@ impl<'a> Styled for List<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -866,7 +872,7 @@ impl<'a> Styled for ListItem<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
text::{StyledGrapheme, Text},
|
||||
prelude::*,
|
||||
text::StyledGrapheme,
|
||||
widgets::{
|
||||
reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine},
|
||||
Block, Widget,
|
||||
@@ -142,6 +140,9 @@ impl<'a> Paragraph<'a> {
|
||||
|
||||
/// Sets the style of the entire widget.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This applies to the entire widget, including the block if one is present. Any style set on
|
||||
/// the block or text will be added to this style.
|
||||
///
|
||||
@@ -152,8 +153,8 @@ impl<'a> Paragraph<'a> {
|
||||
/// let paragraph = Paragraph::new("Hello, world!").style(Style::new().red().on_white());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> Paragraph<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Paragraph<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -355,7 +356,7 @@ impl<'a> Styled for Paragraph<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ use strum::{Display, EnumString};
|
||||
|
||||
use super::StatefulWidget;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
prelude::*,
|
||||
symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
|
||||
};
|
||||
|
||||
@@ -239,9 +237,12 @@ impl<'a> Scrollbar<'a> {
|
||||
}
|
||||
|
||||
/// Sets the style that represents the thumb of the scrollbar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn thumb_style(mut self, thumb_style: Style) -> Self {
|
||||
self.thumb_style = thumb_style;
|
||||
pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
|
||||
self.thumb_style = thumb_style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -253,9 +254,12 @@ impl<'a> Scrollbar<'a> {
|
||||
}
|
||||
|
||||
/// Sets the style that is used for the track of the scrollbar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn track_style(mut self, track_style: Style) -> Self {
|
||||
self.track_style = track_style;
|
||||
pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
|
||||
self.track_style = track_style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -267,9 +271,12 @@ impl<'a> Scrollbar<'a> {
|
||||
}
|
||||
|
||||
/// Sets the style that is used for the beginning of the scrollbar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn begin_style(mut self, begin_style: Style) -> Self {
|
||||
self.begin_style = begin_style;
|
||||
pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
|
||||
self.begin_style = begin_style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -281,9 +288,12 @@ impl<'a> Scrollbar<'a> {
|
||||
}
|
||||
|
||||
/// Sets the style that is used for the end of the scrollbar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn end_style(mut self, end_style: Style) -> Self {
|
||||
self.end_style = end_style;
|
||||
pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
|
||||
self.end_style = end_style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -316,6 +326,10 @@ impl<'a> Scrollbar<'a> {
|
||||
}
|
||||
|
||||
/// Sets the style used for the various parts of the scrollbar from a [`Style`].
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// ```text
|
||||
/// <--▮------->
|
||||
/// ^ ^ ^ ^
|
||||
@@ -325,7 +339,8 @@ impl<'a> Scrollbar<'a> {
|
||||
/// └─────────── begin
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
let style = style.into();
|
||||
self.track_style = style;
|
||||
self.thumb_style = style;
|
||||
self.begin_style = style;
|
||||
|
||||
@@ -4,10 +4,7 @@ use std::cmp::min;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Styled},
|
||||
symbols,
|
||||
prelude::*,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
@@ -89,10 +86,13 @@ impl<'a> Sparkline<'a> {
|
||||
|
||||
/// Sets the style of the entire widget.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// The foreground corresponds to the bars while the background is everything else.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> Sparkline<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Sparkline<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ impl<'a> Styled for Sparkline<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
1816
src/widgets/table.rs
1816
src/widgets/table.rs
File diff suppressed because it is too large
Load Diff
215
src/widgets/table/cell.rs
Normal file
215
src/widgets/table/cell.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
|
||||
///
|
||||
/// You can apply a [`Style`] to the [`Cell`] using [`Cell::style`]. This will set the style for the
|
||||
/// entire area of the cell. Any [`Style`] set on the [`Text`] content will be combined with the
|
||||
/// [`Style`] of the [`Cell`] by adding the [`Style`] of the [`Text`] content to the [`Style`] of
|
||||
/// the [`Cell`]. Styles set on the text content will only affect the content.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// You can create a `Cell` from anything that can be converted to a [`Text`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::borrow::Cow;
|
||||
///
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Cell::from("simple string");
|
||||
/// Cell::from(Span::from("span"));
|
||||
/// Cell::from(Line::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)),
|
||||
/// ]));
|
||||
/// Cell::from(Text::from("a text"));
|
||||
/// Cell::from(Text::from(Cow::Borrowed("hello")));
|
||||
/// ```
|
||||
///
|
||||
/// `Cell` implements [`Styled`] which means you can use style shorthands from the [`Stylize`] trait
|
||||
/// to set the style of the cell concisely.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// Cell::new("Cell 1").red().italic();
|
||||
/// ```
|
||||
///
|
||||
/// [`Row`]: super::Row
|
||||
/// [`Table`]: super::Table
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Cell<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Cell<'a> {
|
||||
/// Creates a new [`Cell`]
|
||||
///
|
||||
/// The `content` parameter accepts any value that can be converted into a [`Text`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Cell::new("simple string");
|
||||
/// Cell::new(Span::from("span"));
|
||||
/// Cell::new(Line::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)),
|
||||
/// ]));
|
||||
/// Cell::new(Text::from("a text"));
|
||||
/// ```
|
||||
pub fn new<T>(content: T) -> Self
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
Self {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the content of the [`Cell`]
|
||||
///
|
||||
/// The `content` parameter accepts any value that can be converted into a [`Text`].
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Cell::default().content("simple string");
|
||||
/// Cell::default().content(Span::from("span"));
|
||||
/// Cell::default().content(Line::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::new().bold()),
|
||||
/// ]));
|
||||
/// Cell::default().content(Text::from("a text"));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn content<T>(mut self, content: T) -> Self
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
self.content = content.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `Style` of this cell
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This `Style` will override the `Style` of the [`Row`] and can be overridden by the `Style`
|
||||
/// of the [`Text`] content.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Cell::new("Cell 1").style(Style::new().red().italic());
|
||||
/// ```
|
||||
///
|
||||
/// `Cell` also implements the [`Styled`] trait, which means you can use style shorthands from
|
||||
/// the [`Stylize`] trait to set the style of the widget more concisely.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Cell::new("Cell 1").red().italic();
|
||||
/// ```
|
||||
///
|
||||
/// [`Row`]: super::Row
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Cell<'_> {
|
||||
pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
for (i, line) in self.content.lines.iter().enumerate() {
|
||||
if i as u16 >= area.height {
|
||||
break;
|
||||
}
|
||||
|
||||
let x_offset = match line.alignment {
|
||||
Some(Alignment::Center) => (area.width / 2).saturating_sub(line.width() as u16 / 2),
|
||||
Some(Alignment::Right) => area.width.saturating_sub(line.width() as u16),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let x = area.x + x_offset;
|
||||
if x >= area.right() {
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.set_line(x, area.y + i as u16, line, area.width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Cell<'a>
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
fn from(content: T) -> Cell<'a> {
|
||||
Cell {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Cell<'a> {
|
||||
type Item = Cell<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Style, Stylize};
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let cell = Cell::new("");
|
||||
assert_eq!(cell.content, Text::from(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content() {
|
||||
let cell = Cell::default().content("");
|
||||
assert_eq!(cell.content, Text::from(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let style = Style::default().red().italic();
|
||||
let cell = Cell::default().style(style);
|
||||
assert_eq!(cell.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
assert_eq!(
|
||||
Cell::from("").black().on_white().bold().not_dim().style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
)
|
||||
}
|
||||
}
|
||||
300
src/widgets/table/row.rs
Normal file
300
src/widgets/table/row.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A single row of data to be displayed in a [`Table`] widget.
|
||||
///
|
||||
/// A `Row` is a collection of [`Cell`]s.
|
||||
///
|
||||
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
|
||||
///
|
||||
/// You can set the style of the entire row using [`Row::style`]. This [`Style`] will be combined
|
||||
/// with the [`Style`] of each individual [`Cell`] by adding the [`Style`] of the [`Cell`] to the
|
||||
/// [`Style`] of the [`Row`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// You can create `Row`s from simple strings.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
|
||||
/// ```
|
||||
///
|
||||
/// If you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// You can also construct a row from any type that can be converted into [`Text`]:
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::borrow::Cow;
|
||||
///
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Row::new(vec![
|
||||
/// Cow::Borrowed("hello"),
|
||||
/// Cow::Owned("world".to_uppercase()),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// `Row` implements [`Styled`] which means you can use style shorthands from the [`Stylize`] trait
|
||||
/// to set the style of the row concisely.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// let cells = vec!["Cell1", "Cell2", "Cell3"];
|
||||
/// Row::new(cells).red().italic();
|
||||
/// ```
|
||||
///
|
||||
/// [`Table`]: super::Table
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Row<'a> {
|
||||
pub(crate) cells: Vec<Cell<'a>>,
|
||||
pub(crate) height: u16,
|
||||
pub(crate) top_margin: u16,
|
||||
pub(crate) bottom_margin: u16,
|
||||
pub(crate) style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Row<'a> {
|
||||
/// Creates a new [`Row`]
|
||||
///
|
||||
/// The `cells` parameter accepts any value that can be converted into an iterator of anything
|
||||
/// that can be converted into a [`Cell`] (e.g. `Vec<&str>`, `&[Cell<'a>]`, `Vec<String>`, etc.)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let row = Row::new(vec!["Cell 1", "Cell 2", "Cell 3"]);
|
||||
/// let row = Row::new(vec![
|
||||
/// Cell::new("Cell 1"),
|
||||
/// Cell::new("Cell 2"),
|
||||
/// Cell::new("Cell 3"),
|
||||
/// ]);
|
||||
/// ```
|
||||
pub fn new<T>(cells: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<Cell<'a>>,
|
||||
{
|
||||
Self {
|
||||
cells: cells.into_iter().map(Into::into).collect(),
|
||||
height: 1,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the cells of the [`Row`]
|
||||
///
|
||||
/// The `cells` parameter accepts any value that can be converted into an iterator of anything
|
||||
/// that can be converted into a [`Cell`] (e.g. `Vec<&str>`, `&[Cell<'a>]`, `Vec<String>`, etc.)
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let row = Row::default().cells(vec!["Cell 1", "Cell 2", "Cell 3"]);
|
||||
/// let row = Row::default().cells(vec![
|
||||
/// Cell::new("Cell 1"),
|
||||
/// Cell::new("Cell 2"),
|
||||
/// Cell::new("Cell 3"),
|
||||
/// ]);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn cells<T>(mut self, cells: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<Cell<'a>>,
|
||||
{
|
||||
self.cells = cells.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the fixed height of the [`Row`]
|
||||
///
|
||||
/// Any [`Cell`] whose content has more lines than this height will see its content truncated.
|
||||
///
|
||||
/// By default, the height is `1`.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let cells = vec!["Cell 1\nline 2", "Cell 2", "Cell 3"];
|
||||
/// let row = Row::new(cells).height(2);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn height(mut self, height: u16) -> Self {
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the top margin. By default, the top margin is `0`.
|
||||
///
|
||||
/// The top margin is the number of blank lines to be displayed before the row.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
|
||||
/// let row = Row::default().top_margin(1);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn top_margin(mut self, margin: u16) -> Self {
|
||||
self.top_margin = margin;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bottom margin. By default, the bottom margin is `0`.
|
||||
///
|
||||
/// The bottom margin is the number of blank lines to be displayed after the row.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
|
||||
/// let row = Row::default().bottom_margin(1);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn bottom_margin(mut self, margin: u16) -> Self {
|
||||
self.bottom_margin = margin;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Style`] of the entire row
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This [`Style`] can be overridden by the [`Style`] of a any individual [`Cell`] or by their
|
||||
/// [`Text`] content.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
|
||||
/// let row = Row::new(cells).style(Style::new().red().italic());
|
||||
/// ```
|
||||
///
|
||||
/// `Row` also implements the [`Styled`] trait, which means you can use style shorthands from
|
||||
/// the [`Stylize`] trait to set the style of the widget more concisely.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
|
||||
/// let row = Row::new(cells).red().italic();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// private methods for rendering
|
||||
impl Row<'_> {
|
||||
/// Returns the total height of the row.
|
||||
pub(crate) fn height_with_margin(&self) -> u16 {
|
||||
self.height
|
||||
.saturating_add(self.top_margin)
|
||||
.saturating_add(self.bottom_margin)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Row<'a> {
|
||||
type Item = Row<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::vec;
|
||||
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Style, Stylize};
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let cells = vec![Cell::from("")];
|
||||
let row = Row::new(cells.clone());
|
||||
assert_eq!(row.cells, cells);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cells() {
|
||||
let cells = vec![Cell::from("")];
|
||||
let row = Row::default().cells(cells.clone());
|
||||
assert_eq!(row.cells, cells);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn height() {
|
||||
let row = Row::default().height(2);
|
||||
assert_eq!(row.height, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_margin() {
|
||||
let row = Row::default().top_margin(1);
|
||||
assert_eq!(row.top_margin, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bottom_margin() {
|
||||
let row = Row::default().bottom_margin(1);
|
||||
assert_eq!(row.bottom_margin, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let style = Style::default().red().italic();
|
||||
let row = Row::default().style(style);
|
||||
assert_eq!(row.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
assert_eq!(
|
||||
Row::new(vec![Cell::from("")])
|
||||
.black()
|
||||
.on_white()
|
||||
.bold()
|
||||
.not_italic()
|
||||
.style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
)
|
||||
}
|
||||
}
|
||||
1423
src/widgets/table/table.rs
Normal file
1423
src/widgets/table/table.rs
Normal file
File diff suppressed because it is too large
Load Diff
238
src/widgets/table/table_state.rs
Normal file
238
src/widgets/table/table_state.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
/// State of a [`Table`] widget
|
||||
///
|
||||
/// This state can be used to scroll through the rows and select one of them. When the table is
|
||||
/// rendered as a stateful widget, the selected row will be highlighted and the table will be
|
||||
/// shifted to ensure that the selected row is visible. This will modify the [`TableState`] object
|
||||
/// passed to the [`Frame::render_stateful_widget`] method.
|
||||
///
|
||||
/// The state consists of two fields:
|
||||
/// - [`offset`]: the index of the first row to be displayed
|
||||
/// - [`selected`]: the index of the selected row, which can be `None` if no row is selected
|
||||
///
|
||||
/// [`offset`]: TableState::offset()
|
||||
/// [`selected`]: TableState::selected()
|
||||
///
|
||||
/// See the [table example] and the recipe and traceroute tabs in the [demo2 example] for a more in
|
||||
/// depth example of the various configuration options and for how to handle state.
|
||||
///
|
||||
/// [table example]: https://github.com/ratatui-org/ratatui/blob/master/examples/table.rs
|
||||
/// [demo2 example]: https://github.com/ratatui-org/ratatui/blob/master/examples/demo2/
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
|
||||
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
|
||||
/// let table = Table::new(rows, widths).widths(widths);
|
||||
///
|
||||
/// // Note: TableState should be stored in your application state (not constructed in your render
|
||||
/// // method) so that the selected row is preserved across renders
|
||||
/// let mut table_state = TableState::default();
|
||||
/// *table_state.offset_mut() = 1; // display the second row and onwards
|
||||
/// table_state.select(Some(3)); // select the forth row (0-indexed)
|
||||
///
|
||||
/// frame.render_stateful_widget(table, area, &mut table_state);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Note that if [`Table::widths`] is not called before rendering, the rendered columns will have
|
||||
/// equal width.
|
||||
///
|
||||
/// [`Table`]: crate::widgets::Table
|
||||
/// [`Table::widths`]: crate::widgets::Table::widths
|
||||
/// [`Frame::render_stateful_widget`]: crate::Frame::render_stateful_widget
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TableState {
|
||||
pub(crate) offset: usize,
|
||||
pub(crate) selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
/// Creates a new [`TableState`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new();
|
||||
/// ```
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Sets the index of the first row to be displayed
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new().with_offset(1);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn with_offset(mut self, offset: usize) -> Self {
|
||||
self.offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the index of the selected row
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new().with_selected(Some(1));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn with_selected<T>(mut self, selected: T) -> Self
|
||||
where
|
||||
T: Into<Option<usize>>,
|
||||
{
|
||||
self.selected = selected.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Index of the first row to be displayed
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new();
|
||||
/// assert_eq!(state.offset(), 0);
|
||||
/// ```
|
||||
pub fn offset(&self) -> usize {
|
||||
self.offset
|
||||
}
|
||||
|
||||
/// Mutable reference to the index of the first row to be displayed
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// *state.offset_mut() = 1;
|
||||
/// ```
|
||||
pub fn offset_mut(&mut self) -> &mut usize {
|
||||
&mut self.offset
|
||||
}
|
||||
|
||||
/// Index of the selected row
|
||||
///
|
||||
/// Returns `None` if no row is selected
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new();
|
||||
/// assert_eq!(state.selected(), None);
|
||||
/// ```
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
|
||||
/// Mutable reference to the index of the selected row
|
||||
///
|
||||
/// Returns `None` if no row is selected
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// *state.selected_mut() = Some(1);
|
||||
/// ```
|
||||
pub fn selected_mut(&mut self) -> &mut Option<usize> {
|
||||
&mut self.selected
|
||||
}
|
||||
|
||||
/// Sets the index of the selected row
|
||||
///
|
||||
/// Set to `None` if no row is selected. This will also reset the offset to `0`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// state.select(Some(1));
|
||||
/// ```
|
||||
pub fn select(&mut self, index: Option<usize>) {
|
||||
self.selected = index;
|
||||
if index.is_none() {
|
||||
self.offset = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let state = TableState::new();
|
||||
assert_eq!(state.offset, 0);
|
||||
assert_eq!(state.selected, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_offset() {
|
||||
let state = TableState::new().with_offset(1);
|
||||
assert_eq!(state.offset, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_selected() {
|
||||
let state = TableState::new().with_selected(Some(1));
|
||||
assert_eq!(state.selected, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset() {
|
||||
let state = TableState::new();
|
||||
assert_eq!(state.offset(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_mut() {
|
||||
let mut state = TableState::new();
|
||||
*state.offset_mut() = 1;
|
||||
assert_eq!(state.offset, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected() {
|
||||
let state = TableState::new();
|
||||
assert_eq!(state.selected(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_mut() {
|
||||
let mut state = TableState::new();
|
||||
*state.selected_mut() = Some(1);
|
||||
assert_eq!(state.selected, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select() {
|
||||
let mut state = TableState::new();
|
||||
state.select(Some(1));
|
||||
assert_eq!(state.selected, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_none() {
|
||||
let mut state = TableState::new().with_selected(Some(1));
|
||||
state.select(None);
|
||||
assert_eq!(state.selected, None);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
#![deny(missing_docs)]
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Modifier, Style, Styled},
|
||||
symbols,
|
||||
text::{Line, Span},
|
||||
prelude::*,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
@@ -118,21 +114,27 @@ impl<'a> Tabs<'a> {
|
||||
|
||||
/// Sets the style of the tabs.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will set the given style on the entire render area.
|
||||
/// More precise style can be applied to the titles by styling the ones given to [`Tabs::new`].
|
||||
/// The selected tab can be styled differently using [`Tabs::highlight_style`].
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style(mut self, style: Style) -> Tabs<'a> {
|
||||
self.style = style;
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style for the highlighted tab.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// Highlighted tab can be selected with [`Tabs::select`].
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn highlight_style(mut self, style: Style) -> Tabs<'a> {
|
||||
self.highlight_style = style;
|
||||
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Tabs<'a> {
|
||||
self.highlight_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -232,7 +234,7 @@ impl<'a> Styled for Tabs<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -307,7 +309,7 @@ impl<'a> Widget for Tabs<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, prelude::*, widgets::Borders};
|
||||
use crate::{assert_buffer_eq, widgets::Borders};
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
|
||||
Reference in New Issue
Block a user