Compare commits

..

29 Commits

Author SHA1 Message Date
EdJoPaTo
8857037bff docs(terminal): fix imports (#1263) 2024-08-02 21:37:05 +03:00
EdJoPaTo
e707ff11d1 refactor: internally use Position struct (#1256) 2024-08-02 20:55:41 +03:00
EdJoPaTo
a9fe4284ac chore: update cargo-deny config (#1265)
Update `cargo-deny` config (noticed in
https://github.com/ratatui-org/ratatui/pull/1263#pullrequestreview-2215488414)

See https://github.com/EmbarkStudios/cargo-deny/pull/611
2024-08-02 20:53:57 +03:00
EdJoPaTo
ffc4300558 chore: remove executable flag for rs files (#1262) 2024-08-02 05:11:23 -07:00
Josh McKinney
84cb16483a fix(terminal)!: make terminal module private (#1260)
This is a simplification of the public API that is helpful for new users
that are not familiar with how rust re-exports work, and helps avoid
clashes with other modules in the backends that are named terminal.

BREAKING CHANGE: The `terminal` module is now private and can not be
used directly. The types under this module are exported from the root of
the crate.

```diff
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
```

Fixes: https://github.com/ratatui-org/ratatui/issues/1210
2024-08-02 04:18:00 -07:00
EdJoPaTo
5b89bd04a8 feat(layout): add Size::ZERO and Position::ORIGIN constants (#1253) 2024-08-02 03:56:39 -07:00
EdJoPaTo
32d0695cc2 test(buffer): ensure emojis are rendered (#1258) 2024-08-02 11:06:53 +02:00
Alex Saveau
cd93547db8 fix: remove unnecessary synchronization in layout cache (#1245)
Layout::init_cache no longer returns bool and takes a NonZeroUsize instead of usize

The cache is a thread-local, so doesn't make much sense to require
synchronized initialization.
2024-08-01 23:06:49 -07:00
Orhun Parmaksız
c245c13cc1 chore(ci): onboard bencher for tracking benchmarks (#1174)
https://bencher.dev/console/projects/ratatui-org

Closes: #1092
2024-08-01 22:35:51 -07:00
EdJoPaTo
b2aa843b31 feat(layout): enable serde for Margin, Position, Rect, Size (#1255) 2024-08-01 20:51:14 -07:00
EdJoPaTo
3ca920e881 fix(span): prevent panic on rendering out of y bounds (#1257) 2024-08-01 20:17:42 -07:00
Josh McKinney
b344f95b7c fix: only apply style to first line when rendering a Line (#1247)
A `Line` widget should only apply its style to the first line when
rendering and not the entire area. This is because the `Line` widget
should only render a single line of text. This commit fixes the issue by
clamping the area to a single line before rendering the text.
2024-07-27 17:23:21 -07:00
Josh McKinney
b304bb99bd chore(changelog): fixup git-cliff formatting (#1214)
- Replaced backticks surrounding body with blockquotes. This formats
  significantly better.
- Added a few postprocessors to remove junk like the PR template,
  horizontal lines and some trailing whitespace

---

Note: there is extra non-automatically generated stuff in the changelog
that this would remove - the changes to CHANGELOG.md should not be
merged as-is, and this is worth waiting for @orhun to check out.

Compare:
5e7fbe8c32/CHANGELOG.md
To: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-07-27 11:39:00 +03:00
Tayfun Bocek
be3eb75ea5 perf(table): avoid extra allocations when rendering Table (#1242)
When rendering a `Table` the `Text` stored inside of a `Cell` gets
cloned before rendering. This removes the clone and uses `WidgetRef`
instead, saving us from allocating a `Vec<Line<'_>>` inside `Text`. Also
avoids an allocation when rendering the highlight symbol if it contains
an owned value.
2024-07-26 21:48:29 +03:00
Dheepak Krishnamurthy
efef0d0dc0 chore(ci): change label from breaking change to Type: Breaking Change (#1243)
This PR changes the label that is auto attached to a PR with a breaking
change per the conventional commits specification.
2024-07-26 21:48:05 +03:00
Tayfun Bocek
663486f1e8 perf(list): avoid extra allocations when rendering List (#1244)
When rendering a `List`, each `ListItem` would be cloned. Removing the
clone, and replacing `Widget::render` with `WidgetRef::render_ref` saves
us allocations caused by the clone of the `Text<'_>` stored inside of
`ListItem`.

Based on the results of running the "list" benchmark locally;
Performance is improved by %1-3 for all `render` benchmarks for `List`.
2024-07-26 21:47:30 +03:00
Alex Saveau
7ddfbc0010 fix: unnecessary allocations when creating Lines (#1237)
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
2024-07-26 01:55:04 -07:00
Josh McKinney
3725262ca3 feat(text): Add Add and AddAssign implementations for Line, Span, and Text (#1236)
This enables:

```rust
let line = Span::raw("Red").red() + Span::raw("blue").blue();
let line = Line::raw("Red").red() + Span::raw("blue").blue();
let line = Line::raw("Red").red() + Line::raw("Blue").blue();
let text = Line::raw("Red").red() + Line::raw("Blue").blue();
let text = Text::raw("Red").red() + Line::raw("Blue").blue();

let mut line = Line::raw("Red").red();
line += Span::raw("Blue").blue();

let mut text = Text::raw("Red").red();
text += Line::raw("Blue").blue();

line.extend(vec![Span::raw("1"), Span::raw("2"), Span::raw("3")]);
```
2024-07-26 01:37:49 -07:00
Josh McKinney
84f334163b fix: clippy lints from rust 1.80.0 (#1238) 2024-07-26 00:55:07 -07:00
airblast
03f3124c1d fix(paragraph): line_width, and line_count include block borders (#1235)
The `line_width`, and `line_count` methods for `Paragraph` would not
take into account the `Block` if one was set. This will now correctly
calculate the values including the `Block`'s width/height.

Fixes: #1233
2024-07-23 13:13:50 -07:00
Josh McKinney
c34fb77818 feat(text)!: remove unnecessary lifetime from ToText trait (#1234)
BREAKING CHANGE: The ToText trait no longer has a lifetime parameter.
This change simplifies the trait and makes it easier implement.
2024-07-22 04:24:30 -07:00
Josh McKinney
6ce447c4f3 docs(block): add docs about style inheritance (#1190)
Fixes: https://github.com/ratatui-org/ratatui/issues/1129
2024-07-19 20:02:08 -07:00
Josh McKinney
272d0591a7 docs(paragraph): update main docs (#1202) 2024-07-18 15:59:48 -07:00
Josh McKinney
e81663bec0 refactor(list): split up list.rs into smaller modules (#1204) 2024-07-18 15:59:35 -07:00
EdJoPaTo
7e1bab049b fix(buffer): dont render control characters (#1226) 2024-07-17 13:22:00 +02:00
EdJoPaTo
379dab9cdb build: cleanup dev dependencies (#1231) 2024-07-16 04:35:54 -07:00
Josh McKinney
5b51018501 feat(chart): add GraphType::Bar (#1205)
![Demo](https://vhs.charm.sh/vhs-50v7I5n7lQF7tHCb1VCmFc.gif)
2024-07-15 20:47:50 -07:00
Josh McKinney
7bab9f0d80 chore: add more CompactString::const_new instead of new (#1230) 2024-07-15 20:29:32 -07:00
dependabot[bot]
6d210b3b6b chore(deps): update compact_str requirement from 0.7.1 to 0.8.0 (#1229)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

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

---

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

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


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-07-15 14:02:44 -07:00
77 changed files with 3416 additions and 2517 deletions

25
.github/workflows/bench_base.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Run Benchmarks
on:
push:
branches:
- main
jobs:
benchmark_base_branch:
name: Continuous Benchmarking with Bencher
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: bencherdev/bencher@main
- name: Track base branch benchmarks with Bencher
run: |
bencher run \
--project ratatui-org \
--token '${{ secrets.BENCHER_API_TOKEN }}' \
--branch main \
--testbed ubuntu-latest \
--adapter rust_criterion \
--err \
cargo bench

25
.github/workflows/bench_run_fork_pr.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Run and Cache Benchmarks
on:
pull_request:
types: [opened, reopened, edited, synchronize]
jobs:
benchmark_fork_pr_branch:
name: Run Fork PR Benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run Benchmarks
run: cargo bench > benchmark_results.txt
- name: Upload Benchmark Results
uses: actions/upload-artifact@v4
with:
name: benchmark_results.txt
path: ./benchmark_results.txt
- name: Upload GitHub Pull Request Event
uses: actions/upload-artifact@v4
with:
name: event.json
path: ${{ github.event_path }}

View File

@@ -0,0 +1,75 @@
name: Track Benchmarks with Bencher
on:
workflow_run:
workflows: [Run and Cache Benchmarks]
types: [completed]
permissions:
contents: read
pull-requests: write
jobs:
track_fork_pr_branch:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
env:
BENCHMARK_RESULTS: benchmark_results.txt
PR_EVENT: event.json
steps:
- name: Download Benchmark Results
uses: actions/github-script@v7
with:
script: |
async function downloadArtifact(artifactName) {
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == artifactName
})[0];
if (!matchArtifact) {
core.setFailed(`Failed to find artifact: ${artifactName}`);
}
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/${artifactName}.zip`, Buffer.from(download.data));
}
await downloadArtifact(process.env.BENCHMARK_RESULTS);
await downloadArtifact(process.env.PR_EVENT);
- name: Unzip Benchmark Results
run: |
unzip $BENCHMARK_RESULTS.zip
unzip $PR_EVENT.zip
- name: Export PR Event Data
uses: actions/github-script@v7
with:
script: |
let fs = require('fs');
let prEvent = JSON.parse(fs.readFileSync(process.env.PR_EVENT, {encoding: 'utf8'}));
core.exportVariable("PR_HEAD", `${prEvent.number}/merge`);
core.exportVariable("PR_BASE", prEvent.pull_request.base.ref);
core.exportVariable("PR_BASE_SHA", prEvent.pull_request.base.sha);
core.exportVariable("PR_NUMBER", prEvent.number);
- uses: bencherdev/bencher@main
- name: Track Benchmarks with Bencher
run: |
bencher run \
--project ratatui-org \
--token '${{ secrets.BENCHER_API_TOKEN }}' \
--branch '${{ env.PR_HEAD }}' \
--branch-start-point '${{ env.PR_BASE }}' \
--branch-start-point-hash '${{ env.PR_BASE_SHA }}' \
--testbed ubuntu-latest \
--adapter rust_criterion \
--err \
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
--ci-number '${{ env.PR_NUMBER }}' \
--file "$BENCHMARK_RESULTS"

View File

@@ -71,7 +71,7 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['breaking change']
labels: ['Type: Breaking Change']
})
do-not-merge:

View File

@@ -10,6 +10,9 @@ GitHub with a [breaking change] label.
This is a quick summary of the sections below:
- [v0.28.0](#v0280) (unreleased)
- `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize`
- `ratatui::terminal` module is now private
- [v0.27.0](#v0270)
- List no clamps the selected index to list
- Prelude items added / removed
@@ -56,6 +59,30 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## v0.28.0 (unreleased)
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1145])
[#1145]: https://github.com/ratatui-org/ratatui/pull/1145
```diff
- let is_initialized = Layout::init_cache(100);
+ Layout::init_cache(NonZeroUsize::new(100).unwrap());
```
### `ratatui::terminal` module is now private ([#1160])
[#1160]: https://github.com/ratatui-org/ratatui/pull/1160
The `terminal` module is now private and can not be used directly. The types under this module are
exported from the root of the crate. This reduces clashes with other modules in the backends that
are also named terminal, and confusion about module exports for newer Rust users.
```diff
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
```
## [v0.27.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.27.0)
### List no clamps the selected index to list ([#1159])

View File

@@ -27,7 +27,7 @@ rust-version = "1.74.0"
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
compact_str = "0.7.1"
compact_str = "0.8.0"
crossterm = { version = "0.27", optional = true }
document-features = { version = "0.2.7", optional = true }
instability = "0.3.1"
@@ -48,14 +48,12 @@ unicode-width = "0.1.13"
[dev-dependencies]
anyhow = "1.0.71"
argh = "0.1.12"
better-panic = "0.3.0"
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
derive_builder = "0.20.0"
fakeit = "1.1"
font8x8 = "0.3.1"
indoc = "2"
palette = "0.7.3"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
@@ -80,6 +78,10 @@ missing_panics_doc = "allow"
module_name_repetitions = "allow"
must_use_candidate = "allow"
# we often split up a module into multiple files with the main type in a file named after the
# module, so we want to allow this pattern
module_inception = "allow"
# nursery or restricted
as_underscore = "warn"
deref_by_slicing = "warn"
@@ -231,7 +233,7 @@ doc-scrape-examples = false
[[example]]
name = "colors_rgb"
required-features = ["crossterm"]
required-features = ["crossterm", "palette"]
doc-scrape-examples = true
[[example]]
@@ -256,7 +258,7 @@ doc-scrape-examples = false
[[example]]
name = "demo2"
required-features = ["crossterm", "widget-calendar"]
required-features = ["crossterm", "palette", "widget-calendar"]
doc-scrape-examples = true
[[example]]

View File

@@ -31,16 +31,14 @@ body = """
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
{%- if commit.breaking %} [**breaking**]{% endif %}
{%- if commit.body %}
````text {#- 4 backticks escape any backticks in body #}
{{commit.body | indent(prefix=" ") }}
````
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
{%- endif %}
{%- for footer in commit.footers %}
{%- for footer in commit.footers %}\n
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
{{ footer.token | indent(prefix=" ") }}{{ footer.separator }}{{ footer.value }}
>
{{ footer.token | indent(prefix=" > ", first=true, blank=true) }}
{{- footer.separator }}
{{- footer.value| indent(prefix=" > ", first=false, blank=true) }}
{%- endif %}
{%- endfor %}
{% endmacro -%}
@@ -87,6 +85,11 @@ trim = false
footer = """
<!-- generated by git-cliff -->
"""
postprocessors = [
{ pattern = '<!-- Please read CONTRIBUTING.md before submitting any pull request. -->', replace = "" },
{ pattern = '>---+\n', replace = '' },
{ pattern = ' +\n', replace = "\n" },
]
[git]
# parse the commits based on https://www.conventionalcommits.org
@@ -126,6 +129,7 @@ commit_parsers = [
{ message = "^(Buffer|buffer|Frame|frame|Gauge|gauge|Paragraph|paragraph):", group = "<!-- 07 -->Miscellaneous Tasks" },
{ message = "^\\[", group = "<!-- 07 -->Miscellaneous Tasks" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers

View File

@@ -1,9 +1,7 @@
# configuration for https://github.com/EmbarkStudios/cargo-deny
[licenses]
default = "deny"
unlicensed = "deny"
copyleft = "deny"
version = 2
confidence-threshold = 0.8
allow = [
"Apache-2.0",
@@ -16,8 +14,7 @@ allow = [
]
[advisories]
unmaintained = "deny"
yanked = "deny"
version = 2
[bans]
multiple-versions = "allow"

View File

@@ -28,9 +28,9 @@ use ratatui::{
},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
terminal::{Frame, Terminal},
text::{Line, Span},
widgets::{Bar, BarChart, BarGroup, Block, Paragraph},
Frame, Terminal,
};
struct Company<'a> {

View File

@@ -30,12 +30,12 @@ use ratatui::{
},
layout::{Alignment, Constraint, Layout, Rect},
style::{Style, Stylize},
terminal::Frame,
text::Line,
widgets::{
block::{Position, Title},
Block, BorderType, Borders, Padding, Paragraph, Wrap,
},
Frame,
};
// These type aliases are used to make the code more readable by reducing repetition of the generic

View File

@@ -28,11 +28,11 @@ use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Color, Stylize},
symbols::Marker,
terminal::{Frame, Terminal},
widgets::{
canvas::{Canvas, Circle, Map, MapResolution, Rectangle},
Block, Widget,
},
Frame, Terminal,
};
fn main() -> io::Result<()> {

View File

@@ -29,9 +29,9 @@ use ratatui::{
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
symbols::{self, Marker},
terminal::{Frame, Terminal},
text::Span,
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
Frame, Terminal,
};
#[derive(Clone)]
@@ -153,17 +153,18 @@ fn run_app<B: Backend>(
fn ui(frame: &mut Frame, app: &App) {
let area = frame.size();
let vertical = Layout::vertical([Constraint::Percentage(40), Constraint::Percentage(60)]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let [chart1, bottom] = vertical.areas(area);
let [line_chart, scatter] = horizontal.areas(bottom);
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(area);
let [animated_chart, bar_chart] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
render_chart1(frame, chart1, app);
render_animated_chart(frame, animated_chart, app);
render_barchart(frame, bar_chart);
render_line_chart(frame, line_chart);
render_scatter(frame, scatter);
}
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
fn render_animated_chart(f: &mut Frame, area: Rect, app: &App) {
let x_labels = vec![
Span::styled(
format!("{}", app.window[0]),
@@ -189,7 +190,7 @@ fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
];
let chart = Chart::new(datasets)
.block(Block::bordered().title("Chart 1".cyan().bold()))
.block(Block::bordered())
.x_axis(
Axis::default()
.title("X Axis")
@@ -208,6 +209,51 @@ fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
f.render_widget(chart, area);
}
fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
let dataset = Dataset::default()
.marker(symbols::Marker::HalfBlock)
.style(Style::new().fg(Color::Blue))
.graph_type(GraphType::Bar)
// a bell curve
.data(&[
(0., 0.4),
(10., 2.9),
(20., 13.5),
(30., 41.1),
(40., 80.1),
(50., 100.0),
(60., 80.1),
(70., 41.1),
(80., 13.5),
(90., 2.9),
(100., 0.4),
]);
let chart = Chart::new(vec![dataset])
.block(
Block::bordered().title(
Title::default()
.content("Bar chart".cyan().bold())
.alignment(Alignment::Center),
),
)
.x_axis(
Axis::default()
.style(Style::default().gray())
.bounds([0.0, 100.0])
.labels(vec!["0".bold(), "50".into(), "100.0".bold()]),
)
.y_axis(
Axis::default()
.style(Style::default().gray())
.bounds([0.0, 100.0])
.labels(vec!["0".bold(), "50".into(), "100.0".bold()]),
)
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
frame.render_widget(chart, bar_chart);
}
fn render_line_chart(f: &mut Frame, area: Rect) {
let datasets = vec![Dataset::default()
.name("Line from only 2 points".italic())

View File

@@ -33,9 +33,9 @@ use ratatui::{
},
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Style, Stylize},
terminal::{Frame, Terminal},
text::Line,
widgets::{Block, Borders, Paragraph},
Frame, Terminal,
};
type Result<T> = result::Result<T, Box<dyn Error>>;

View File

@@ -44,9 +44,9 @@ use ratatui::{
},
layout::{Constraint, Layout, Rect},
style::Color,
terminal::Terminal,
text::Text,
widgets::Widget,
Terminal,
};
#[derive(Debug, Default)]

View File

@@ -34,9 +34,9 @@ use ratatui::{
Color, Style, Stylize,
},
symbols::{self, line},
terminal::Terminal,
text::{Line, Span, Text},
widgets::{Block, Paragraph, Widget, Wrap},
Terminal,
};
use strum::{Display, EnumIter, FromRepr};

View File

@@ -30,12 +30,12 @@ use ratatui::{
},
style::{palette::tailwind, Color, Modifier, Style, Stylize},
symbols,
terminal::Terminal,
text::Line,
widgets::{
Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
Tabs, Widget,
},
Terminal,
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
@@ -86,9 +86,6 @@ fn main() -> Result<()> {
init_error_hooks()?;
let terminal = init_terminal()?;
// increase the cache size to avoid flickering for indeterminate layouts
Layout::init_cache(100);
App::default().run(terminal)?;
restore_terminal()?;

View File

@@ -28,9 +28,9 @@ use ratatui::{
},
layout::{Constraint, Layout, Rect},
style::{Color, Style},
terminal::{Frame, Terminal},
text::Line,
widgets::{Paragraph, Widget},
Frame, Terminal,
};
/// A custom widget that renders a button with a label, theme and state.

View File

@@ -11,7 +11,7 @@ use ratatui::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
terminal::Terminal,
Terminal,
};
use crate::{app::App, ui};

View File

@@ -2,13 +2,13 @@ use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use ratatui::{
backend::{Backend, TermionBackend},
terminal::Terminal,
termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::IntoAlternateScreen,
},
Terminal,
};
use crate::{app::App, ui};

View File

@@ -5,11 +5,11 @@ use std::{
use ratatui::{
backend::TermwizBackend,
terminal::Terminal,
termwiz::{
input::{InputEvent, KeyCode},
terminal::Terminal as TermwizTerminal,
},
Terminal,
};
use crate::{app::App, ui};

View File

@@ -2,13 +2,13 @@ use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
terminal::Frame,
text::{self, Span},
widgets::{
canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle},
Axis, BarChart, Block, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem, Paragraph,
Row, Sparkline, Table, Tabs, Wrap,
},
Frame,
};
use crate::app::App;

View File

@@ -8,9 +8,9 @@ use ratatui::{
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
layout::{Constraint, Layout, Rect},
style::Color,
terminal::Terminal,
text::{Line, Span},
widgets::{Block, Tabs, Widget},
Terminal,
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};

View File

@@ -26,9 +26,9 @@
//! layout::{self, Alignment, Constraint, Direction, Layout, Margin, Rect},
//! style::{self, Color, Modifier, Style, Styled, Stylize},
//! symbols::{self, Marker},
//! terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport},
//! text::{self, Line, Masked, Span, Text},
//! widgets::{block::BlockExt, StatefulWidget, Widget},
//! CompletedFrame, Frame, Terminal, TerminalOptions, Viewport,
//! };
//! use tui_big_text::{BigTextBuilder, PixelSize};
//!
@@ -100,9 +100,9 @@ pub enum PixelSize {
/// layout::{self, Alignment, Constraint, Direction, Layout, Margin, Rect},
/// style::{self, Color, Modifier, Style, Styled, Stylize},
/// symbols::{self, Marker},
/// terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport},
/// text::{self, Line, Masked, Span, Text},
/// widgets::{block::BlockExt, StatefulWidget, Widget},
/// CompletedFrame, Frame, Terminal, TerminalOptions, Viewport,
/// };
/// use tui_big_text::{BigTextBuilder, PixelSize};
///

View File

@@ -4,8 +4,8 @@ use ratatui::{
buffer::Buffer,
layout::{Flex, Layout, Rect},
style::{Color, Style},
terminal::Frame,
widgets::Widget,
Frame,
};
use unicode_width::UnicodeWidthStr;

View File

@@ -12,7 +12,7 @@ use ratatui::{
ExecutableCommand,
},
layout::Rect,
terminal::{Terminal, TerminalOptions, Viewport},
Terminal, TerminalOptions, Viewport,
};
pub fn init() -> Result<Terminal<impl Backend>> {

View File

@@ -24,9 +24,9 @@ use ratatui::{
},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph},
Frame, Terminal,
};
/// Example code for lib.rs

View File

@@ -13,7 +13,10 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::io::{self, stdout};
use std::{
io::{self, stdout},
num::NonZeroUsize,
};
use color_eyre::{config::HookBuilder, Result};
use ratatui::{
@@ -31,12 +34,12 @@ use ratatui::{
},
style::{palette::tailwind, Color, Modifier, Style, Stylize},
symbols::{self, line},
terminal::Terminal,
text::{Line, Text},
widgets::{
block::Title, Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
StatefulWidget, Tabs, Widget,
},
Terminal,
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
@@ -156,7 +159,9 @@ enum SelectedTab {
fn main() -> Result<()> {
// assuming the user changes spacing about a 100 times or so
Layout::init_cache(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100);
Layout::init_cache(
NonZeroUsize::new(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100).unwrap(),
);
init_error_hooks()?;
let terminal = init_terminal()?;
App::default().run(terminal)?;

View File

@@ -26,9 +26,9 @@ use ratatui::{
},
layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize},
terminal::Terminal,
text::Span,
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph, Widget},
Terminal,
};
const GAUGE1_COLOR: Color = tailwind::RED.c800;

View File

@@ -26,8 +26,8 @@ use ratatui::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
terminal::{Frame, Terminal},
widgets::Paragraph,
Frame, Terminal,
};
/// This is a bare minimum example. There are many approaches to running an application loop, so

View File

@@ -28,10 +28,9 @@ use ratatui::{
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
terminal::{Frame, Terminal, Viewport},
text::{Line, Span},
widgets::{block, Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
TerminalOptions,
Frame, Terminal, TerminalOptions, Viewport,
};
const NUM_DOWNLOADS: usize = 10;

View File

@@ -29,9 +29,9 @@ use ratatui::{
Layout, Rect,
},
style::{Color, Style, Stylize},
terminal::{Frame, Terminal},
text::Line,
widgets::{Block, Paragraph},
Frame, Terminal,
};
fn main() -> Result<(), Box<dyn Error>> {

View File

@@ -26,8 +26,8 @@ use ratatui::{
},
layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize},
terminal::Terminal,
widgets::{block::Title, Block, Borders, LineGauge, Padding, Paragraph, Widget},
Terminal,
};
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;

View File

@@ -26,12 +26,12 @@ use ratatui::{
Color, Modifier, Style, Stylize,
},
symbols,
terminal::Terminal,
text::Line,
widgets::{
Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph,
StatefulWidget, Widget, Wrap,
},
Terminal,
};
const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800);
@@ -304,7 +304,7 @@ mod tui {
},
ExecutableCommand,
},
terminal::Terminal,
Terminal,
};
pub fn init_error_hooks() -> color_eyre::Result<()> {

View File

@@ -35,9 +35,9 @@ use ratatui::{
},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
text::Line,
widgets::Paragraph,
Frame, Terminal,
};
type Result<T> = result::Result<T, Box<dyn Error>>;

View File

@@ -37,9 +37,9 @@ use ratatui::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
terminal::{Frame, Terminal},
text::Line,
widgets::{Block, Paragraph},
Frame, Terminal,
};
type Result<T> = std::result::Result<T, Box<dyn Error>>;

View File

@@ -177,7 +177,7 @@ mod common {
crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
terminal::Terminal,
Terminal,
};
// A simple alias for the terminal type used in this example.

View File

@@ -27,8 +27,8 @@ use ratatui::{
},
layout::{Constraint, Layout, Rect},
style::Stylize,
terminal::{Frame, Terminal},
widgets::{Block, Clear, Paragraph, Wrap},
Frame, Terminal,
};
struct App {

View File

@@ -24,9 +24,8 @@ use itertools::izip;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::terminal::{disable_raw_mode, enable_raw_mode},
terminal::{Terminal, Viewport},
widgets::Paragraph,
TerminalOptions,
Terminal, TerminalOptions, Viewport,
};
/// A fun example of using half block characters to draw a logo

View File

@@ -31,9 +31,9 @@ use ratatui::{
layout::{Alignment, Constraint, Layout, Margin},
style::{Color, Style, Stylize},
symbols::scrollbar,
terminal::{Frame, Terminal},
text::{Line, Masked, Span},
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
Frame, Terminal,
};
#[derive(Default)]

View File

@@ -32,8 +32,8 @@ use ratatui::{
},
layout::{Constraint, Layout},
style::{Color, Style},
terminal::{Frame, Terminal},
widgets::{Block, Borders, Sparkline},
Frame, Terminal,
};
#[derive(Clone)]

View File

@@ -25,12 +25,12 @@ use ratatui::{
},
layout::{Constraint, Layout, Margin, Rect},
style::{self, Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
text::{Line, Text},
widgets::{
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
ScrollbarState, Table, TableState,
},
Frame, Terminal,
};
use style::palette::tailwind;
use unicode_width::UnicodeWidthStr;

View File

@@ -27,9 +27,9 @@ use ratatui::{
layout::{Constraint, Layout, Rect},
style::{palette::tailwind, Color, Stylize},
symbols,
terminal::Terminal,
text::Line,
widgets::{Block, Padding, Paragraph, Tabs, Widget},
Terminal,
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};

View File

@@ -41,8 +41,8 @@ use crossterm::{
};
use ratatui::{
backend::{Backend, CrosstermBackend},
terminal::Terminal,
widgets::{Block, Paragraph},
Terminal,
};
use tracing::{debug, info, instrument, trace, Level};
use tracing_appender::{non_blocking, non_blocking::WorkerGuard};

View File

@@ -38,9 +38,9 @@ use ratatui::{
},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
text::{Line, Span, Text},
widgets::{Block, List, ListItem, Paragraph},
Frame, Terminal,
};
enum InputMode {

View File

@@ -19,8 +19,7 @@ use crate::{
},
terminal::{self, Clear},
},
layout::Size,
prelude::Rect,
layout::{Position, Rect, Size},
style::{Color, Modifier, Style},
};
@@ -155,13 +154,13 @@ where
#[cfg(feature = "underline-color")]
let mut underline_color = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
let mut last_pos: Option<Position> = None;
for (x, y, cell) in content {
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
queue!(self.writer, MoveTo(x, y))?;
}
last_pos = Some((x, y));
last_pos = Some(Position { x, y });
if cell.modifier != modifier {
let diff = ModifierDiff {
from: modifier,

View File

@@ -12,7 +12,7 @@ use std::{
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
prelude::Rect,
layout::{Position, Rect},
style::{Color, Modifier, Style},
termion::{self, color as tcolor, color::Color as _, style as tstyle},
};
@@ -176,13 +176,13 @@ where
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
let mut last_pos: Option<Position> = None;
for (x, y, cell) in content {
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap();
}
last_pos = Some((x, y));
last_pos = Some(Position { x, y });
if cell.modifier != modifier {
write!(
string,

View File

@@ -2,7 +2,6 @@
//! A module for the [`Buffer`] and [`Cell`] types.
mod assert;
#[allow(clippy::module_inception)]
mod buffer;
mod cell;

View File

@@ -192,7 +192,7 @@ impl Buffer {
}
/// Print at most the first n characters of a string if enough space is available
/// until the end of the line.
/// until the end of the line. Skips zero-width graphemes and control characters.
///
/// Use [`Buffer::set_string`] when the maximum amount of characters can be printed.
pub fn set_stringn<T, S>(
@@ -210,6 +210,7 @@ impl Buffer {
let max_width = max_width.try_into().unwrap_or(u16::MAX);
let mut remaining_width = self.area.right().saturating_sub(x).min(max_width);
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true)
.filter(|symbol| !symbol.contains(|char: char| char.is_control()))
.map(|symbol| (symbol, symbol.width() as u16))
.filter(|(_symbol, width)| *width > 0)
.map_while(|(symbol, width)| {
@@ -931,4 +932,74 @@ mod tests {
buf.set_string(0, 1, "bar", Style::new().blue());
assert_eq!(buf, Buffer::with_lines(["foo".red(), "bar".blue()]));
}
#[test]
fn control_sequence_rendered_full() {
let text = "I \x1b[0;36mwas\x1b[0m here!";
let mut buffer = Buffer::filled(Rect::new(0, 0, 25, 3), Cell::new("x"));
buffer.set_string(1, 1, text, Style::new());
let expected = Buffer::with_lines([
"xxxxxxxxxxxxxxxxxxxxxxxxx",
"xI [0;36mwas[0m here!xxxx",
"xxxxxxxxxxxxxxxxxxxxxxxxx",
]);
assert_eq!(buffer, expected);
}
#[test]
fn control_sequence_rendered_partially() {
let text = "I \x1b[0;36mwas\x1b[0m here!";
let mut buffer = Buffer::filled(Rect::new(0, 0, 11, 3), Cell::new("x"));
buffer.set_string(1, 1, text, Style::new());
#[rustfmt::skip]
let expected = Buffer::with_lines([
"xxxxxxxxxxx",
"xI [0;36mwa",
"xxxxxxxxxxx",
]);
assert_eq!(buffer, expected);
}
/// Emojis normally contain various characters which should stay part of the Emoji.
/// This should work fine by utilizing unicode_segmentation but a testcase is probably helpful
/// due to the nature of never perfect Unicode implementations and all of its quirks.
#[rstest]
// Shrug without gender or skintone. Has a width of 2 like all emojis have.
#[case::shrug("🤷", "🤷xxxxx")]
// Technically this is a (brown) bear, a zero-width joiner and a snowflake
// As it is joined its a single emoji and should therefore have a width of 2.
// It's correctly detected as a single grapheme but it's width is 4 for some reason
#[case::polarbear("🐻‍❄️", "🐻xxx")]
// Technically this is an eye, a zero-width joiner and a speech bubble
// Both eye and speech bubble include a 'display as emoji' variation selector
#[case::eye_speechbubble("👁️‍🗨️", "👁🗨xxx")]
fn renders_emoji(#[case] input: &str, #[case] expected: &str) {
use unicode_width::UnicodeWidthChar;
dbg!(input);
dbg!(input.len());
dbg!(input
.graphemes(true)
.map(|symbol| (symbol, symbol.escape_unicode().to_string(), symbol.width()))
.collect::<Vec<_>>());
dbg!(input
.chars()
.map(|char| (
char,
char.escape_unicode().to_string(),
char.width(),
char.is_control()
))
.collect::<Vec<_>>());
let mut buffer = Buffer::filled(Rect::new(0, 0, 7, 1), Cell::new("x"));
buffer.set_string(0, 0, input, Style::new());
let expected = Buffer::with_lines([expected]);
assert_eq!(buffer, expected);
}
}

View File

@@ -41,11 +41,11 @@ impl Cell {
///
/// This works at compile time and puts the symbol onto the stack. Fails to build when the
/// symbol doesnt fit onto the stack and requires to be placed on the heap. Use
/// `Self::default().set_symbol()` in that case. See [`CompactString::new_inline`] for more
/// `Self::default().set_symbol()` in that case. See [`CompactString::const_new`] for more
/// details on this.
pub const fn new(symbol: &str) -> Self {
pub const fn new(symbol: &'static str) -> Self {
Self {
symbol: CompactString::new_inline(symbol),
symbol: CompactString::const_new(symbol),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
@@ -139,7 +139,7 @@ impl Cell {
/// Resets the cell to the empty state.
pub fn reset(&mut self) {
self.symbol = CompactString::new_inline(" ");
self.symbol = CompactString::const_new(" ");
self.fg = Color::Reset;
self.bg = Color::Reset;
#[cfg(feature = "underline-color")]
@@ -167,7 +167,7 @@ mod tests {
assert_eq!(
cell,
Cell {
symbol: CompactString::new_inline(""),
symbol: CompactString::const_new(""),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]

View File

@@ -4,7 +4,6 @@ mod alignment;
mod constraint;
mod direction;
mod flex;
#[allow(clippy::module_inception)]
mod layout;
mod margin;
mod position;

View File

@@ -1,4 +1,4 @@
use std::{cell::RefCell, collections::HashMap, iter, num::NonZeroUsize, rc::Rc, sync::OnceLock};
use std::{cell::RefCell, collections::HashMap, iter, num::NonZeroUsize, rc::Rc};
use cassowary::{
strength::REQUIRED,
@@ -37,7 +37,9 @@ type Cache = LruCache<(Rect, Layout), (Segments, Spacers)>;
const FLOAT_PRECISION_MULTIPLIER: f64 = 100.0;
thread_local! {
static LAYOUT_CACHE: OnceLock<RefCell<Cache>> = const { OnceLock::new() };
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(Cache::new(
NonZeroUsize::new(Layout::DEFAULT_CACHE_SIZE).unwrap(),
));
}
/// A layout is a set of constraints that can be applied to a given area to split it into smaller
@@ -209,22 +211,9 @@ impl Layout {
/// that subsequent calls with the same parameters are faster. The cache is a `LruCache`, and
/// grows until `cache_size` is reached.
///
/// Returns true if the cell's value was set by this call.
/// Returns false if the cell's value was not set by this call, this means that another thread
/// has set this value or that the cache size is already initialized.
///
/// Note that a custom cache size will be set only if this function:
/// * is called before [`Layout::split()`] otherwise, the cache size is
/// [`Self::DEFAULT_CACHE_SIZE`].
/// * is called for the first time, subsequent calls do not modify the cache size.
pub fn init_cache(cache_size: usize) -> bool {
LAYOUT_CACHE
.with(|c| {
c.set(RefCell::new(LruCache::new(
NonZeroUsize::new(cache_size).unwrap(),
)))
})
.is_ok()
/// By default, the cache size is [`Self::DEFAULT_CACHE_SIZE`].
pub fn init_cache(cache_size: NonZeroUsize) {
LAYOUT_CACHE.with_borrow_mut(|c| c.resize(cache_size));
}
/// Set the direction of the layout.
@@ -571,17 +560,10 @@ impl Layout {
/// );
/// ```
pub fn split_with_spacers(&self, area: Rect) -> (Segments, Spacers) {
LAYOUT_CACHE.with(|c| {
c.get_or_init(|| {
RefCell::new(LruCache::new(
NonZeroUsize::new(Self::DEFAULT_CACHE_SIZE).unwrap(),
))
})
.borrow_mut()
.get_or_insert((area, self.clone()), || {
self.try_split(area).expect("failed to split")
})
.clone()
LAYOUT_CACHE.with_borrow_mut(|c| {
let key = (area, self.clone());
c.get_or_insert(key, || self.try_split(area).expect("failed to split"))
.clone()
})
}
@@ -1099,37 +1081,14 @@ mod tests {
}
#[test]
fn custom_cache_size() {
assert!(Layout::init_cache(10));
assert!(!Layout::init_cache(15));
LAYOUT_CACHE.with(|c| {
assert_eq!(c.get().unwrap().borrow().cap().get(), 10);
fn cache_size() {
LAYOUT_CACHE.with_borrow(|c| {
assert_eq!(c.cap().get(), Layout::DEFAULT_CACHE_SIZE);
});
}
#[test]
fn default_cache_size() {
let target = Rect {
x: 2,
y: 2,
width: 10,
height: 10,
};
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(10),
Constraint::Max(5),
Constraint::Min(1),
])
.split(target);
assert!(!Layout::init_cache(15));
LAYOUT_CACHE.with(|c| {
assert_eq!(
c.get().unwrap().borrow().cap().get(),
Layout::DEFAULT_CACHE_SIZE
);
Layout::init_cache(NonZeroUsize::new(10).unwrap());
LAYOUT_CACHE.with_borrow(|c| {
assert_eq!(c.cap().get(), 10);
});
}

View File

@@ -1,6 +1,7 @@
use std::fmt;
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Margin {
pub horizontal: u16,
pub vertical: u16,

View File

@@ -24,6 +24,7 @@ use crate::layout::Rect;
/// let (x, y) = position.into();
/// ```
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Position {
/// The x coordinate of the position
///
@@ -39,6 +40,9 @@ pub struct Position {
}
impl Position {
/// Position at the origin, the top left edge at 0,0
pub const ORIGIN: Self = Self { x: 0, y: 0 };
/// Create a new position
pub const fn new(x: u16, y: u16) -> Self {
Self { x, y }

View File

@@ -32,7 +32,8 @@ pub struct Rect {
/// Positive numbers move to the right/bottom and negative to the left/top.
///
/// See [`Rect::offset`]
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Offset {
/// How much to move on the X axis
pub x: i32,

View File

@@ -8,6 +8,7 @@ use crate::prelude::*;
/// The width and height are stored as `u16` values and represent the number of columns and rows
/// respectively.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Size {
/// The width in columns
pub width: u16,
@@ -16,6 +17,9 @@ pub struct Size {
}
impl Size {
/// A zero sized Size
pub const ZERO: Self = Self::new(0, 0);
/// Create a new `Size` struct
pub const fn new(width: u16, height: u16) -> Self {
Self { width, height }

View File

@@ -348,6 +348,6 @@ pub mod layout;
pub mod prelude;
pub mod style;
pub mod symbols;
pub mod terminal;
mod terminal;
pub mod text;
pub mod widgets;

View File

@@ -30,7 +30,7 @@ pub use crate::{
layout::{self, Alignment, Constraint, Direction, Layout, Margin, Position, Rect, Size},
style::{self, Color, Modifier, Style, Stylize},
symbols::{self},
terminal::{Frame, Terminal},
text::{self, Line, Masked, Span, Text},
widgets::{block::BlockExt, StatefulWidget, Widget},
Frame, Terminal,
};

View File

@@ -32,7 +32,6 @@
//! [`Buffer`]: crate::buffer::Buffer
mod frame;
#[allow(clippy::module_inception)]
mod terminal;
mod viewport;

View File

@@ -16,7 +16,7 @@ pub struct Frame<'a> {
///
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
pub(crate) cursor_position: Option<(u16, u16)>,
pub(crate) cursor_position: Option<Position>,
/// The area of the viewport
pub(crate) viewport_area: Rect,
@@ -167,7 +167,7 @@ impl Frame<'_> {
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
/// with it.
pub fn set_cursor(&mut self, x: u16, y: u16) {
self.cursor_position = Some((x, y));
self.cursor_position = Some(Position { x, y });
}
/// Gets the buffer that this `Frame` draws into as a mutable reference.

View File

@@ -70,7 +70,7 @@ where
last_known_size: Rect,
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
/// and the terminal resized.
last_known_cursor_pos: (u16, u16),
last_known_cursor_pos: Position,
/// Number of frames rendered up until current time.
frame_count: usize,
}
@@ -126,7 +126,7 @@ where
///
/// ```rust
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, backend::TestBackend, terminal::{Viewport, TerminalOptions}};
/// # use ratatui::{prelude::*, backend::TestBackend, Viewport, TerminalOptions};
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
@@ -138,9 +138,9 @@ where
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (size, (0, 0)),
Viewport::Fullscreen => (size, Position::ORIGIN),
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
Viewport::Fixed(area) => (area, (area.left(), area.top())),
Viewport::Fixed(area) => (area, area.as_position()),
};
Ok(Self {
backend,
@@ -188,7 +188,7 @@ where
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
if let Some((col, row, _)) = updates.last() {
self.last_known_cursor_pos = (*col, *row);
self.last_known_cursor_pos = Position { x: *col, y: *row };
}
self.backend.draw(updates.into_iter())
}
@@ -203,7 +203,7 @@ where
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
.1
.y
.saturating_sub(self.viewport_area.top());
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
}
@@ -383,7 +383,7 @@ where
match cursor_position {
None => self.hide_cursor()?,
Some((x, y)) => {
Some(Position { x, y }) => {
self.show_cursor()?;
self.set_cursor(x, y)?;
}
@@ -431,7 +431,7 @@ where
/// Sets the cursor position.
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)?;
self.last_known_cursor_pos = (x, y);
self.last_known_cursor_pos = Position { x, y };
Ok(())
}
@@ -573,9 +573,9 @@ fn compute_inline_size<B: Backend>(
height: u16,
size: Rect,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, (u16, u16))> {
let pos = backend.get_cursor()?;
let mut row = pos.1;
) -> io::Result<(Rect, Position)> {
let pos: Position = backend.get_cursor()?.into();
let mut row = pos.y;
let max_height = size.height.min(height);

View File

@@ -5,7 +5,7 @@
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//! [`Text`].
//!
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Line`].
@@ -56,6 +56,5 @@ pub use masked::Masked;
mod span;
pub use span::{Span, ToSpan};
#[allow(clippy::module_inception)]
mod text;
pub use text::{Text, ToText};

View File

@@ -161,6 +161,13 @@ pub struct Line<'a> {
pub alignment: Option<Alignment>,
}
fn cow_to_spans<'a>(content: impl Into<Cow<'a, str>>) -> Vec<Span<'a>> {
match content.into() {
Cow::Borrowed(s) => s.lines().map(Span::raw).collect(),
Cow::Owned(s) => s.lines().map(|v| Span::raw(v.to_string())).collect(),
}
}
impl<'a> Line<'a> {
/// Create a line with the default style.
///
@@ -186,17 +193,14 @@ impl<'a> Line<'a> {
T: Into<Cow<'a, str>>,
{
Self {
spans: content
.into()
.lines()
.map(|v| Span::raw(v.to_string()))
.collect(),
spans: cow_to_spans(content),
..Default::default()
}
}
/// Create a line with the given style.
// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
///
/// `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
@@ -220,11 +224,7 @@ impl<'a> Line<'a> {
S: Into<Style>,
{
Self {
spans: content
.into()
.lines()
.map(|v| Span::raw(v.to_string()))
.collect(),
spans: cow_to_spans(content),
style: style.into(),
..Default::default()
}
@@ -548,6 +548,37 @@ where
}
}
/// Adds a `Span` to a `Line`, returning a new `Line` with the `Span` added.
impl<'a> std::ops::Add<Span<'a>> for Line<'a> {
type Output = Self;
fn add(mut self, rhs: Span<'a>) -> Self::Output {
self.spans.push(rhs);
self
}
}
/// Adds two `Line`s together, returning a new `Text` with the contents of the two `Line`s.
impl<'a> std::ops::Add<Self> for Line<'a> {
type Output = Text<'a>;
fn add(self, rhs: Self) -> Self::Output {
Text::from(vec![self, rhs])
}
}
impl<'a> std::ops::AddAssign<Span<'a>> for Line<'a> {
fn add_assign(&mut self, rhs: Span<'a>) {
self.spans.push(rhs);
}
}
impl<'a> Extend<Span<'a>> for Line<'a> {
fn extend<T: IntoIterator<Item = Span<'a>>>(&mut self, iter: T) {
self.spans.extend(iter);
}
}
impl Widget for Line<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
@@ -560,6 +591,7 @@ impl WidgetRef for Line<'_> {
if area.is_empty() {
return;
}
let area = Rect { height: 1, ..area };
let line_width = self.width();
if line_width == 0 {
return;
@@ -896,6 +928,62 @@ mod tests {
assert_eq!(line.spans, vec![span],);
}
#[test]
fn add_span() {
assert_eq!(
Line::raw("Red").red() + Span::raw("blue").blue(),
Line {
spans: vec![Span::raw("Red"), Span::raw("blue").blue()],
style: Style::new().red(),
alignment: None,
},
);
}
#[test]
fn add_line() {
assert_eq!(
Line::raw("Red").red() + Line::raw("Blue").blue(),
Text {
lines: vec![Line::raw("Red").red(), Line::raw("Blue").blue()],
style: Style::default(),
alignment: None,
}
);
}
#[test]
fn add_assign_span() {
let mut line = Line::raw("Red").red();
line += Span::raw("Blue").blue();
assert_eq!(
line,
Line {
spans: vec![Span::raw("Red"), Span::raw("Blue").blue()],
style: Style::new().red(),
alignment: None,
},
);
}
#[test]
fn extend() {
let mut line = Line::from("Hello, ");
line.extend(vec![Span::raw("world!")]);
assert_eq!(line.spans, vec![Span::raw("Hello, "), Span::raw("world!")]);
let mut line = Line::from("Hello, ");
line.extend(vec![Span::raw("world! "), Span::raw("How are you?")]);
assert_eq!(
line.spans,
vec![
Span::raw("Hello, "),
Span::raw("world! "),
Span::raw("How are you?")
]
);
}
#[test]
fn into_string() {
let line = Line::from(vec![
@@ -1036,6 +1124,17 @@ mod tests {
assert_eq!(buf, expected);
}
#[test]
fn render_only_styles_first_line() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 2));
hello_world().render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(["Hello world! ", " "]);
expected.set_style(Rect::new(0, 0, 20, 1), ITALIC);
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
assert_eq!(buf, expected);
}
#[test]
fn render_truncates() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));

View File

@@ -342,6 +342,14 @@ where
}
}
impl<'a> std::ops::Add<Self> for Span<'a> {
type Output = Line<'a>;
fn add(self, rhs: Self) -> Self::Output {
Line::from_iter([self, rhs])
}
}
impl<'a> Styled for Span<'a> {
type Item = Self;
@@ -362,11 +370,15 @@ impl Widget for Span<'_> {
impl WidgetRef for Span<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let Rect { mut x, y, .. } = area.intersection(buf.area);
let area = area.intersection(buf.area);
if area.is_empty() {
return;
}
let Rect { mut x, y, .. } = area;
for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
let symbol_width = grapheme.symbol.width();
let next_x = x.saturating_add(symbol_width as u16);
if next_x > area.intersection(buf.area).right() {
if next_x > area.right() {
break;
}
@@ -621,8 +633,11 @@ mod tests {
}
#[rstest]
fn render_out_of_bounds(mut small_buf: Buffer) {
let out_of_bounds = Rect::new(20, 20, 10, 1);
#[case::x(20, 0)]
#[case::y(0, 20)]
#[case::both(20, 20)]
fn render_out_of_bounds(mut small_buf: Buffer, #[case] x: u16, #[case] y: u16) {
let out_of_bounds = Rect::new(x, y, 10, 1);
Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
assert_eq!(small_buf, Buffer::empty(small_buf.area));
}
@@ -773,4 +788,27 @@ mod tests {
]
);
}
#[test]
fn add() {
assert_eq!(
Span::default() + Span::default(),
Line::from(vec![Span::default(), Span::default()])
);
assert_eq!(
Span::default() + Span::raw("test"),
Line::from(vec![Span::default(), Span::raw("test")])
);
assert_eq!(
Span::raw("test") + Span::default(),
Line::from(vec![Span::raw("test"), Span::default()])
);
assert_eq!(
Span::raw("test") + Span::raw("content"),
Line::from(vec![Span::raw("test"), Span::raw("content")])
);
}
}

View File

@@ -570,6 +570,33 @@ where
}
}
impl<'a> std::ops::Add<Line<'a>> for Text<'a> {
type Output = Self;
fn add(mut self, line: Line<'a>) -> Self::Output {
self.push_line(line);
self
}
}
/// Adds two `Text` together.
///
/// This ignores the style and alignment of the second `Text`.
impl<'a> std::ops::Add<Self> for Text<'a> {
type Output = Self;
fn add(mut self, text: Self) -> Self::Output {
self.lines.extend(text.lines);
self
}
}
impl<'a> std::ops::AddAssign<Line<'a>> for Text<'a> {
fn add_assign(&mut self, line: Line<'a>) {
self.push_line(line);
}
}
impl<'a, T> Extend<T> for Text<'a>
where
T: Into<Line<'a>>,
@@ -587,9 +614,9 @@ where
/// you get the `ToText` implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToText<'a> {
pub trait ToText {
/// Converts the value to a [`Text`].
fn to_text(&self) -> Text<'a>;
fn to_text(&self) -> Text<'_>;
}
/// # Panics
@@ -597,8 +624,8 @@ pub trait ToText<'a> {
/// In this implementation, the `to_text` method panics if the `Display` implementation returns an
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
/// returns an error itself.
impl<'a, T: fmt::Display> ToText<'a> for T {
fn to_text(&self) -> Text<'a> {
impl<T: fmt::Display> ToText for T {
fn to_text(&self) -> Text {
Text::raw(self.to_string())
}
}
@@ -829,6 +856,44 @@ mod tests {
assert_eq!(iter.next(), None);
}
#[test]
fn add_line() {
assert_eq!(
Text::raw("Red").red() + Line::raw("Blue").blue(),
Text {
lines: vec![Line::raw("Red"), Line::raw("Blue").blue()],
style: Style::new().red(),
alignment: None,
}
);
}
#[test]
fn add_text() {
assert_eq!(
Text::raw("Red").red() + Text::raw("Blue").blue(),
Text {
lines: vec![Line::raw("Red"), Line::raw("Blue")],
style: Style::new().red(),
alignment: None,
}
);
}
#[test]
fn add_assign_line() {
let mut text = Text::raw("Red").red();
text += Line::raw("Blue").blue();
assert_eq!(
text,
Text {
lines: vec![Line::raw("Red"), Line::raw("Blue").blue()],
style: Style::new().red(),
alignment: None,
}
);
}
#[test]
fn extend() {
let mut text = Text::from("The first line\nThe second line");

View File

@@ -27,8 +27,8 @@ pub use title::{Position, Title};
/// both centered and non-centered titles are rendered, the centered space is calculated based on
/// the full width of the block, rather than the leftover width.
///
/// Titles are not rendered in the corners of the block unless there is no border on that edge.
/// If the block is too small and multiple titles overlap, the border may get cut off at a corner.
/// Titles are not rendered in the corners of the block unless there is no border on that edge. If
/// the block is too small and multiple titles overlap, the border may get cut off at a corner.
///
/// ```plain
/// ┌With at least a left border───
@@ -60,6 +60,10 @@ pub use title::{Position, Title};
/// # Other Methods
/// - [`Block::inner`] Compute the inner area of a block based on its border visibility rules.
///
/// [`Style`]s are applied first to the entire block, then to the borders, and finally to the
/// titles. If the block is used as a container for another widget, the inner widget can also be
/// styled. See [`Style`] for more information on how merging styles works.
///
/// # Examples
///
/// ```
@@ -252,12 +256,15 @@ impl<'a> Block<'a> {
/// Note: If the block is too small and multiple titles overlap, the border might get cut off at
/// a corner.
///
/// # Example
/// # Examples
///
/// See the [Block example] for a visual representation of how the various borders and styles
/// look when rendered.
///
/// The following example demonstrates:
/// - Default title alignment
/// - Multiple titles (notice "Center" is centered according to the full with of the block, not
/// the leftover space)
/// the leftover space)
/// - Two titles with the same alignment (notice the left titles are separated)
/// ```
/// use ratatui::{
@@ -280,6 +287,8 @@ impl<'a> Block<'a> {
/// - [`Block::title_style`]
/// - [`Block::title_alignment`]
/// - [`Block::title_position`]
///
/// [Block example]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md#block
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title<T>(mut self, title: T) -> Self
where
@@ -347,10 +356,14 @@ impl<'a> Block<'a> {
/// Applies the style to all titles.
///
/// This style will be applied to all titles of the block. If a title has a style set, it will
/// be applied after this style. This style will be applied after any [`Block::style`] or
/// [`Block::border_style`] is applied.
///
/// See [`Style`] for more information on how merging styles works.
///
/// `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 fn title_style<S: Into<Style>>(mut self, style: S) -> Self {
self.titles_style = style.into();
@@ -416,7 +429,10 @@ impl<'a> Block<'a> {
/// Defines the style of the borders.
///
/// If a [`Block::style`] is defined, `border_style` will be applied on top of it.
/// This style is applied only to the areas covered by borders, and is applied to the block
/// after any [`Block::style`] is applied.
///
/// See [`Style`] for more information on how merging styles works.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
@@ -434,16 +450,39 @@ impl<'a> Block<'a> {
self
}
/// Defines the block style.
/// Defines the style of the entire block.
///
/// This is the most generic [`Style`] a block can receive, it will be merged with any other
/// more specific style. Elements can be styled further with [`Block::title_style`] and
/// [`Block::border_style`].
/// more specific styles. Elements can be styled further with [`Block::title_style`] and
/// [`Block::border_style`], which will be applied on top of this style. If the block is used as
/// a container for another widget (e.g. a [`Paragraph`]), then the style of the widget is
/// generally applied before this style.
///
/// See [`Style`] for more information on how merging styles works.
///
/// `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.
/// # Example
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// let block = Block::new().style(Style::new().red().on_black());
///
/// // For border and title you can additionally apply styles on top of the block level style.
/// let block = Block::new()
/// .style(Style::new().red().bold().italic())
/// .border_style(Style::new().not_italic()) // will be red and bold
/// .title_style(Style::new().not_bold()) // will be red and italic
/// .title("Title");
///
/// // To style the inner widget, you can style the widget itself.
/// let paragraph = Paragraph::new("Content")
/// .block(block)
/// .style(Style::new().white().not_bold()); // will be white, and italic
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
#[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();
@@ -871,6 +910,35 @@ impl Block<'_> {
height: 1,
}
}
/// Calculate the left, and right space the [`Block`] will take up.
///
/// The result takes the [`Block`]'s, [`Borders`], and [`Padding`] into account.
pub(crate) fn horizontal_space(&self) -> (u16, u16) {
let left = self
.padding
.left
.saturating_add(u16::from(self.borders.contains(Borders::LEFT)));
let right = self
.padding
.right
.saturating_add(u16::from(self.borders.contains(Borders::RIGHT)));
(left, right)
}
/// Calculate the top, and bottom space that the [`Block`] will take up.
///
/// Takes the [`Padding`], [`Title`]'s position, and the [`Borders`] that are selected into
/// account when calculating the result.
pub(crate) fn vertical_space(&self) -> (u16, u16) {
let has_top =
self.borders.contains(Borders::TOP) || self.has_title_at_position(Position::Top);
let top = self.padding.top + u16::from(has_top);
let has_bottom =
self.borders.contains(Borders::BOTTOM) || self.has_title_at_position(Position::Bottom);
let bottom = self.padding.bottom + u16::from(has_bottom);
(top, bottom)
}
}
/// An extension trait for [`Block`] that provides some convenience methods.
@@ -1023,6 +1091,126 @@ mod tests {
assert!(block.has_title_at_position(Position::Bottom));
}
#[rstest]
#[case::none(Borders::NONE, (0, 0))]
#[case::top(Borders::TOP, (1, 0))]
#[case::right(Borders::RIGHT, (0, 0))]
#[case::bottom(Borders::BOTTOM, (0, 1))]
#[case::left(Borders::LEFT, (0, 0))]
#[case::top_right(Borders::TOP | Borders::RIGHT, (1, 0))]
#[case::top_bottom(Borders::TOP | Borders::BOTTOM, (1, 1))]
#[case::top_left(Borders::TOP | Borders::LEFT, (1, 0))]
#[case::bottom_right(Borders::BOTTOM | Borders::RIGHT, (0, 1))]
#[case::bottom_left(Borders::BOTTOM | Borders::LEFT, (0, 1))]
#[case::left_right(Borders::LEFT | Borders::RIGHT, (0, 0))]
fn vertical_space_takes_into_account_borders(
#[case] borders: Borders,
#[case] vertical_space: (u16, u16),
) {
let block = Block::new().borders(borders);
assert_eq!(block.vertical_space(), vertical_space);
}
#[rstest]
#[case::top_border_top_p1(Borders::TOP, Padding::new(0, 0, 1, 0), (2, 0))]
#[case::right_border_top_p1(Borders::RIGHT, Padding::new(0, 0, 1, 0), (1, 0))]
#[case::bottom_border_top_p1(Borders::BOTTOM, Padding::new(0, 0, 1, 0), (1, 1))]
#[case::left_border_top_p1(Borders::LEFT, Padding::new(0, 0, 1, 0), (1, 0))]
#[case::top_bottom_border_all_p3(Borders::TOP | Borders::BOTTOM, Padding::new(100, 100, 4, 5), (5, 6))]
#[case::no_border(Borders::NONE, Padding::new(100, 100, 10, 13), (10, 13))]
#[case::all(Borders::ALL, Padding::new(100, 100, 1, 3), (2, 4))]
fn vertical_space_takes_into_account_padding(
#[case] borders: Borders,
#[case] padding: Padding,
#[case] vertical_space: (u16, u16),
) {
let block = Block::new().borders(borders).padding(padding);
assert_eq!(block.vertical_space(), vertical_space);
}
#[test]
fn vertical_space_takes_into_account_titles() {
let block = Block::new()
.title_position(Position::Top)
.title(Title::from("Test"));
assert_eq!(block.vertical_space(), (1, 0));
let block = Block::new()
.title_position(Position::Bottom)
.title(Title::from("Test"));
assert_eq!(block.vertical_space(), (0, 1));
}
#[rstest]
#[case::top_border_top_title(Block::new(), Borders::TOP, Position::Top, (1, 0))]
#[case::right_border_top_title(Block::new(), Borders::RIGHT, Position::Top, (1, 0))]
#[case::bottom_border_top_title(Block::new(), Borders::BOTTOM, Position::Top, (1, 1))]
#[case::left_border_top_title(Block::new(), Borders::LEFT, Position::Top, (1, 0))]
#[case::top_border_top_title(Block::new(), Borders::TOP, Position::Bottom, (1, 1))]
#[case::right_border_top_title(Block::new(), Borders::RIGHT, Position::Bottom, (0, 1))]
#[case::bottom_border_top_title(Block::new(), Borders::BOTTOM, Position::Bottom, (0, 1))]
#[case::left_border_top_title(Block::new(), Borders::LEFT, Position::Bottom, (0, 1))]
fn vertical_space_takes_into_account_borders_and_title(
#[case] block: Block,
#[case] borders: Borders,
#[case] pos: Position,
#[case] vertical_space: (u16, u16),
) {
let block = block
.borders(borders)
.title_position(pos)
.title(Title::from("Test"));
assert_eq!(block.vertical_space(), vertical_space);
}
#[test]
fn horizontal_space_takes_into_account_borders() {
let block = Block::bordered();
assert_eq!(block.horizontal_space(), (1, 1));
let block = Block::new().borders(Borders::LEFT);
assert_eq!(block.horizontal_space(), (1, 0));
let block = Block::new().borders(Borders::RIGHT);
assert_eq!(block.horizontal_space(), (0, 1));
}
#[test]
fn horizontal_space_takes_into_account_padding() {
let block = Block::new().padding(Padding::new(1, 1, 100, 100));
assert_eq!(block.horizontal_space(), (1, 1));
let block = Block::new().padding(Padding::new(3, 5, 0, 0));
assert_eq!(block.horizontal_space(), (3, 5));
let block = Block::new().padding(Padding::new(0, 1, 100, 100));
assert_eq!(block.horizontal_space(), (0, 1));
let block = Block::new().padding(Padding::new(1, 0, 100, 100));
assert_eq!(block.horizontal_space(), (1, 0));
}
#[rstest]
#[case::all_bordered_all_padded(Block::bordered(), Padding::new(1, 1, 1, 1), (2, 2))]
#[case::all_bordered_left_padded(Block::bordered(), Padding::new(1, 0, 0, 0), (2, 1))]
#[case::all_bordered_right_padded(Block::bordered(), Padding::new(0, 1, 0, 0), (1, 2))]
#[case::all_bordered_top_padded(Block::bordered(), Padding::new(0, 0, 1, 0), (1, 1))]
#[case::all_bordered_bottom_padded(Block::bordered(), Padding::new(0, 0, 0, 1), (1, 1))]
#[case::left_bordered_left_padded(Block::new().borders(Borders::LEFT), Padding::new(1, 0, 0, 0), (2, 0))]
#[case::left_bordered_right_padded(Block::new().borders(Borders::LEFT), Padding::new(0, 1, 0, 0), (1, 1))]
#[case::right_bordered_right_padded(Block::new().borders(Borders::RIGHT), Padding::new(0, 1, 0, 0), (0, 2))]
#[case::right_bordered_left_padded(Block::new().borders(Borders::RIGHT), Padding::new(1, 0, 0, 0), (1, 1))]
fn horizontal_space_takes_into_account_borders_and_padding(
#[case] block: Block,
#[case] padding: Padding,
#[case] horizontal_space: (u16, u16),
) {
let block = block.padding(padding);
assert_eq!(block.horizontal_space(), horizontal_space);
}
#[test]
const fn border_type_can_be_const() {
const _PLAIN: border::Set = BorderType::border_symbols(BorderType::Plain);

View File

@@ -144,11 +144,15 @@ pub enum GraphType {
/// Draw each point. This is the default.
#[default]
Scatter,
/// Draw a line between each following point.
///
/// The order of the lines will be the same as the order of the points in the dataset, which
/// allows this widget to draw lines both left-to-right and right-to-left
Line,
/// Draw a bar chart. This will draw a bar for each point in the dataset.
Bar,
}
/// Allow users to specify the position of a legend in a [`Chart`]
@@ -362,9 +366,10 @@ impl<'a> Dataset<'a> {
/// Sets how the dataset should be drawn
///
/// [`Chart`] can draw either a [scatter](GraphType::Scatter) or [line](GraphType::Line) charts.
/// A scatter will draw only the points in the dataset while a line will also draw a line
/// between them. See [`GraphType`] for more details
/// [`Chart`] can draw [scatter](GraphType::Scatter), [line](GraphType::Line) or
/// [bar](GraphType::Bar) charts. A scatter chart draws only the points in the dataset, a line
/// char draws a line between each point, and a bar chart draws a line from the x axis to the
/// point. See [`GraphType`] for more details
///
/// 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"]
@@ -403,9 +408,9 @@ impl<'a> Dataset<'a> {
/// labels, legend, ...).
struct ChartLayout {
/// Location of the title of the x axis
title_x: Option<(u16, u16)>,
title_x: Option<Position>,
/// Location of the title of the y axis
title_y: Option<(u16, u16)>,
title_y: Option<Position>,
/// Location of the first label of the x axis
label_x: Option<u16>,
/// Location of the first label of the y axis
@@ -735,7 +740,7 @@ impl<'a> Chart<'a> {
if let Some(ref title) = self.x_axis.title {
let w = title.width() as u16;
if w < graph_area.width && graph_area.height > 2 {
title_x = Some((x + graph_area.width - w, y));
title_x = Some(Position::new(x + graph_area.width - w, y));
}
}
@@ -743,7 +748,7 @@ impl<'a> Chart<'a> {
if let Some(ref title) = self.y_axis.title {
let w = title.width() as u16;
if w + 1 < graph_area.width && graph_area.height > 2 {
title_y = Some((x, area.top()));
title_y = Some(Position::new(x, area.top()));
}
}
@@ -998,22 +1003,36 @@ impl WidgetRef for Chart<'_> {
coords: dataset.data,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
if dataset.graph_type == GraphType::Line {
for data in dataset.data.windows(2) {
ctx.draw(&CanvasLine {
x1: data[0].0,
y1: data[0].1,
x2: data[1].0,
y2: data[1].1,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
match dataset.graph_type {
GraphType::Line => {
for data in dataset.data.windows(2) {
ctx.draw(&CanvasLine {
x1: data[0].0,
y1: data[0].1,
x2: data[1].0,
y2: data[1].1,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
}
}
GraphType::Bar => {
for (x, y) in dataset.data {
ctx.draw(&CanvasLine {
x1: *x,
y1: 0.0,
x2: *x,
y2: *y,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
}
}
GraphType::Scatter => {}
}
})
.render(graph_area, buf);
}
if let Some((x, y)) = layout.title_x {
if let Some(Position { x, y }) = layout.title_x {
let title = self.x_axis.title.as_ref().unwrap();
let width = graph_area
.right()
@@ -1031,7 +1050,7 @@ impl WidgetRef for Chart<'_> {
buf.set_line(x, y, title, width);
}
if let Some((x, y)) = layout.title_y {
if let Some(Position { x, y }) = layout.title_y {
let title = self.y_axis.title.as_ref().unwrap();
let width = graph_area
.right()
@@ -1194,12 +1213,14 @@ mod tests {
fn graph_type_to_string() {
assert_eq!(GraphType::Scatter.to_string(), "Scatter");
assert_eq!(GraphType::Line.to_string(), "Line");
assert_eq!(GraphType::Bar.to_string(), "Bar");
}
#[test]
fn graph_type_from_str() {
assert_eq!("Scatter".parse::<GraphType>(), Ok(GraphType::Scatter));
assert_eq!("Line".parse::<GraphType>(), Ok(GraphType::Line));
assert_eq!("Bar".parse::<GraphType>(), Ok(GraphType::Bar));
assert_eq!("".parse::<GraphType>(), Err(ParseError::VariantNotFound));
}
@@ -1460,4 +1481,39 @@ mod tests {
chart.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines(expected));
}
#[test]
fn bar_chart() {
let data = [
(0.0, 0.0),
(2.0, 1.0),
(4.0, 4.0),
(6.0, 8.0),
(8.0, 9.0),
(10.0, 10.0),
];
let chart = Chart::new(vec![Dataset::default()
.data(&data)
.marker(symbols::Marker::Dot)
.graph_type(GraphType::Bar)])
.x_axis(Axis::default().bounds([0.0, 10.0]))
.y_axis(Axis::default().bounds([0.0, 10.0]));
let area = Rect::new(0, 0, 11, 11);
let mut buffer = Buffer::empty(area);
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
"",
" • •",
" • • •",
" • • •",
" • • •",
" • • •",
" • • • •",
" • • • •",
" • • • •",
" • • • • •",
"• • • • • •",
]);
assert_eq!(buffer, expected);
}
}

2299
src/widgets/list.rs Executable file → Normal file

File diff suppressed because it is too large Load Diff

324
src/widgets/list/item.rs Normal file
View File

@@ -0,0 +1,324 @@
use crate::prelude::*;
/// A single item in a [`List`]
///
/// The item's height is defined by the number of lines it contains. This can be queried using
/// [`ListItem::height`]. Similarly, [`ListItem::width`] will return the maximum width of all
/// lines.
///
/// You can set the style of an item with [`ListItem::style`] or using the [`Stylize`] trait.
/// This [`Style`] will be combined with the [`Style`] of the inner [`Text`]. The [`Style`]
/// of the [`Text`] will be added to the [`Style`] of the [`ListItem`].
///
/// You can also align a `ListItem` by aligning its underlying [`Text`] and [`Line`]s. For that,
/// see [`Text::alignment`] and [`Line::alignment`]. On a multiline `Text`, one `Line` can override
/// the alignment by setting it explicitly.
///
/// # Examples
///
/// You can create [`ListItem`]s from simple `&str`
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1");
/// ```
///
/// Anything that can be converted to [`Text`] can be a [`ListItem`].
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item1: ListItem = "Item 1".into();
/// let item2: ListItem = Line::raw("Item 2").into();
/// ```
///
/// A [`ListItem`] styled with [`Stylize`]
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1").red().on_white();
/// ```
///
/// If you need more control over the item's style, you can explicitly style the underlying
/// [`Text`]
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut text = Text::default();
/// text.extend(["Item".blue(), Span::raw(" "), "1".bold().red()]);
/// let item = ListItem::new(text);
/// ```
///
/// A right-aligned `ListItem`
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// ListItem::new(Text::from("foo").alignment(Alignment::Right));
/// ```
///
/// [`List`]: crate::widgets::List
/// [`Stylize`]: crate::style::Stylize
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ListItem<'a> {
pub(crate) content: Text<'a>,
pub(crate) style: Style,
}
impl<'a> ListItem<'a> {
/// Creates a new [`ListItem`]
///
/// The `content` parameter accepts any value that can be converted into [`Text`].
///
/// # Examples
///
/// You can create [`ListItem`]s from simple `&str`
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1");
/// ```
///
/// Anything that can be converted to [`Text`] can be a [`ListItem`].
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item1: ListItem = "Item 1".into();
/// let item2: ListItem = Line::raw("Item 2").into();
/// ```
///
/// You can also create multilines item
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Multi-line\nitem");
/// ```
///
/// # See also
///
/// - [`List::new`](crate::widgets::List::new) to create a list of items that can be converted
/// to [`ListItem`]
pub fn new<T>(content: T) -> Self
where
T: Into<Text<'a>>,
{
Self {
content: content.into(),
style: Style::default(),
}
}
/// 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
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1").style(Style::new().red().italic());
/// ```
///
/// `ListItem` also implements the [`Styled`] trait, which means you can use style shorthands
/// from the [`Stylize`](crate::style::Stylize) trait to set the style of the widget more
/// concisely.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1").red().italic();
/// ```
///
/// [`Styled`]: crate::style::Styled
/// [`ListState`]: crate::widgets::list::ListState
#[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
}
/// Returns the item height
///
/// # Examples
///
/// One line item
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1");
/// assert_eq!(item.height(), 1);
/// ```
///
/// Two lines item (note the `\n`)
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Multi-line\nitem");
/// assert_eq!(item.height(), 2);
/// ```
pub fn height(&self) -> usize {
self.content.height()
}
/// Returns the max width of all the lines
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("12345");
/// assert_eq!(item.width(), 5);
/// ```
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("12345\n1234567");
/// assert_eq!(item.width(), 7);
/// ```
pub fn width(&self) -> usize {
self.content.width()
}
}
impl<'a, T> From<T> for ListItem<'a>
where
T: Into<Text<'a>>,
{
fn from(value: T) -> Self {
Self::new(value)
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn new_from_str() {
let item = ListItem::new("Test item");
assert_eq!(item.content, Text::from("Test item"));
assert_eq!(item.style, Style::default());
}
#[test]
fn new_from_string() {
let item = ListItem::new("Test item".to_string());
assert_eq!(item.content, Text::from("Test item"));
assert_eq!(item.style, Style::default());
}
#[test]
fn new_from_cow_str() {
let item = ListItem::new(Cow::Borrowed("Test item"));
assert_eq!(item.content, Text::from("Test item"));
assert_eq!(item.style, Style::default());
}
#[test]
fn new_from_span() {
let span = Span::styled("Test item", Style::default().fg(Color::Blue));
let item = ListItem::new(span.clone());
assert_eq!(item.content, Text::from(span));
assert_eq!(item.style, Style::default());
}
#[test]
fn new_from_spans() {
let spans = Line::from(vec![
Span::styled("Test ", Style::default().fg(Color::Blue)),
Span::styled("item", Style::default().fg(Color::Red)),
]);
let item = ListItem::new(spans.clone());
assert_eq!(item.content, Text::from(spans));
assert_eq!(item.style, Style::default());
}
#[test]
fn new_from_vec_spans() {
let lines = vec![
Line::from(vec![
Span::styled("Test ", Style::default().fg(Color::Blue)),
Span::styled("item", Style::default().fg(Color::Red)),
]),
Line::from(vec![
Span::styled("Second ", Style::default().fg(Color::Green)),
Span::styled("line", Style::default().fg(Color::Yellow)),
]),
];
let item = ListItem::new(lines.clone());
assert_eq!(item.content, Text::from(lines));
assert_eq!(item.style, Style::default());
}
#[test]
fn str_into_list_item() {
let s = "Test item";
let item: ListItem = s.into();
assert_eq!(item.content, Text::from(s));
assert_eq!(item.style, Style::default());
}
#[test]
fn string_into_list_item() {
let s = String::from("Test item");
let item: ListItem = s.clone().into();
assert_eq!(item.content, Text::from(s));
assert_eq!(item.style, Style::default());
}
#[test]
fn span_into_list_item() {
let s = Span::from("Test item");
let item: ListItem = s.clone().into();
assert_eq!(item.content, Text::from(s));
assert_eq!(item.style, Style::default());
}
#[test]
fn vec_lines_into_list_item() {
let lines = vec![Line::raw("l1"), Line::raw("l2")];
let item: ListItem = lines.clone().into();
assert_eq!(item.content, Text::from(lines));
assert_eq!(item.style, Style::default());
}
#[test]
fn style() {
let item = ListItem::new("Test item").style(Style::default().bg(Color::Red));
assert_eq!(item.content, Text::from("Test item"));
assert_eq!(item.style, Style::default().bg(Color::Red));
}
#[test]
fn height() {
let item = ListItem::new("Test item");
assert_eq!(item.height(), 1);
let item = ListItem::new("Test item\nSecond line");
assert_eq!(item.height(), 2);
}
#[test]
fn width() {
let item = ListItem::new("Test item");
assert_eq!(item.width(), 9);
}
#[test]
fn can_be_stylized() {
assert_eq!(
ListItem::new("").black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
);
}
}

450
src/widgets/list/list.rs Normal file
View File

@@ -0,0 +1,450 @@
use strum::{Display, EnumString};
use super::ListItem;
use crate::{
prelude::*,
style::Styled,
widgets::{Block, HighlightSpacing},
};
/// A widget to display several items among which one can be selected (optional)
///
/// A list is a collection of [`ListItem`]s.
///
/// This is different from a [`Table`] because it does not handle columns, headers or footers and
/// the item's height is automatically determined. A `List` can also be put in reverse order (i.e.
/// *bottom to top*) whereas a [`Table`] cannot.
///
/// [`Table`]: crate::widgets::Table
///
/// List items can be aligned using [`Text::alignment`], for more details see [`ListItem`].
///
/// [`List`] implements [`Widget`] and so it can be drawn using
/// [`Frame::render_widget`](crate::terminal::Frame::render_widget).
///
/// [`List`] is also a [`StatefulWidget`], which means you can use it with [`ListState`] to allow
/// the user to [scroll] through items and [select] one of them.
///
/// See the list in the [Examples] directory for a more in depth example of the various
/// configuration options and for how to handle state.
///
/// [Examples]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
///
/// # Fluent setters
///
/// - [`List::highlight_style`] sets the style of the selected item.
/// - [`List::highlight_symbol`] sets the symbol to be displayed in front of the selected item.
/// - [`List::repeat_highlight_symbol`] sets whether to repeat the symbol and style over selected
/// multi-line items
/// - [`List::direction`] sets the list direction
///
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// let items = ["Item 1", "Item 2", "Item 3"];
/// let list = List::new(items)
/// .block(Block::bordered().title("List"))
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
/// .highlight_symbol(">>")
/// .repeat_highlight_symbol(true)
/// .direction(ListDirection::BottomToTop);
///
/// frame.render_widget(list, area);
/// # }
/// ```
///
/// # Stateful example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// // This should be stored outside of the function in your application state.
/// let mut state = ListState::default();
/// let items = ["Item 1", "Item 2", "Item 3"];
/// let list = List::new(items)
/// .block(Block::bordered().title("List"))
/// .highlight_style(Style::new().add_modifier(Modifier::REVERSED))
/// .highlight_symbol(">>")
/// .repeat_highlight_symbol(true);
///
/// frame.render_stateful_widget(list, area, &mut state);
/// # }
/// ```
///
/// In addition to `List::new`, any iterator whose element is convertible to `ListItem` can be
/// collected into `List`.
///
/// ```
/// use ratatui::widgets::List;
///
/// (0..5).map(|i| format!("Item{i}")).collect::<List>();
/// ```
///
/// [`ListState`]: crate::widgets::list::ListState
/// [scroll]: crate::widgets::list::ListState::offset
/// [select]: crate::widgets::list::ListState::select
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct List<'a> {
/// An optional block to wrap the widget in
pub(crate) block: Option<Block<'a>>,
/// The items in the list
pub(crate) items: Vec<ListItem<'a>>,
/// Style used as a base style for the widget
pub(crate) style: Style,
/// List display direction
pub(crate) direction: ListDirection,
/// Style used to render selected item
pub(crate) highlight_style: Style,
/// Symbol in front of the selected item (Shift all items to the right)
pub(crate) highlight_symbol: Option<&'a str>,
/// Whether to repeat the highlight symbol for each line of the selected item
pub(crate) repeat_highlight_symbol: bool,
/// Decides when to allocate spacing for the selection symbol
pub(crate) highlight_spacing: HighlightSpacing,
/// How many items to try to keep visible before and after the selected item
pub(crate) scroll_padding: usize,
}
/// Defines the direction in which the list will be rendered.
///
/// If there are too few items to fill the screen, the list will stick to the starting edge.
///
/// See [`List::direction`].
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ListDirection {
/// The first value is on the top, going to the bottom
#[default]
TopToBottom,
/// The first value is on the bottom, going to the top.
BottomToTop,
}
impl<'a> List<'a> {
/// Creates a new list from [`ListItem`]s
///
/// The `items` parameter accepts any value that can be converted into an iterator of
/// [`Into<ListItem>`]. This includes arrays of [`&str`] or [`Vec`]s of [`Text`].
///
/// # Example
///
/// From a slice of [`&str`]
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// let list = List::new(["Item 1", "Item 2"]);
/// ```
///
/// From [`Text`]
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// let list = List::new([
/// Text::styled("Item 1", Style::default().red()),
/// Text::styled("Item 2", Style::default().red()),
/// ]);
/// ```
///
/// You can also create an empty list using the [`Default`] implementation and use the
/// [`List::items`] fluent setter.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let empty_list = List::default();
/// let filled_list = empty_list.items(["Item 1"]);
/// ```
pub fn new<T>(items: T) -> Self
where
T: IntoIterator,
T::Item: Into<ListItem<'a>>,
{
Self {
block: None,
style: Style::default(),
items: items.into_iter().map(Into::into).collect(),
direction: ListDirection::default(),
..Self::default()
}
}
/// Set the items
///
/// The `items` parameter accepts any value that can be converted into an iterator of
/// [`Into<ListItem>`]. This includes arrays of [`&str`] or [`Vec`]s of [`Text`].
///
/// This is a fluent setter method which must be chained or used as it consumes self.
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let list = List::default().items(["Item 1", "Item 2"]);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn items<T>(mut self, items: T) -> Self
where
T: IntoIterator,
T::Item: Into<ListItem<'a>>,
{
self.items = items.into_iter().map(Into::into).collect();
self
}
/// Wraps the list with a custom [`Block`] widget.
///
/// The `block` parameter holds the specified [`Block`] to be created around the [`List`]
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// let block = Block::bordered().title("List");
/// let list = List::new(items).block(block);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
/// 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.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// let list = List::new(items).style(Style::new().red().italic());
/// ```
///
/// `List` 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.
///
/// [`Stylize`]: crate::style::Stylize
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// let list = List::new(items).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
}
/// Set the symbol to be displayed in front of the selected item
///
/// By default there are no highlight symbol.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1", "Item 2"];
/// let list = List::new(items).highlight_symbol(">>");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
self.highlight_symbol = Some(highlight_symbol);
self
}
/// 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.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1", "Item 2"];
/// 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<S: Into<Style>>(mut self, style: S) -> Self {
self.highlight_style = style.into();
self
}
/// Set whether to repeat the highlight symbol and style over selected multi-line items
///
/// This is `false` by default.
///
/// 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 const fn repeat_highlight_symbol(mut self, repeat: bool) -> Self {
self.repeat_highlight_symbol = repeat;
self
}
/// Set when to show the highlight spacing
///
/// The highlight spacing is the spacing that is allocated for the selection symbol (if enabled)
/// and is used to shift the list when an item is selected. This method allows you to configure
/// when this spacing is allocated.
///
/// - [`HighlightSpacing::Always`] will always allocate the spacing, regardless of whether an
/// item is selected or not. This means that the table will never change size, regardless of
/// if an item is selected or not.
/// - [`HighlightSpacing::WhenSelected`] will only allocate the spacing if an item is selected.
/// This means that the table will shift when an item is selected. This is the default setting
/// for backwards compatibility, but it is recommended to use `HighlightSpacing::Always` for a
/// better user experience.
/// - [`HighlightSpacing::Never`] will never allocate the spacing, regardless of whether an item
/// is selected or not. This means that the highlight symbol will never be drawn.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// let list = List::new(items).highlight_spacing(HighlightSpacing::Always);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
self.highlight_spacing = value;
self
}
/// Defines the list direction (up or down)
///
/// Defines if the `List` is displayed *top to bottom* (default) or *bottom to top*.
/// If there is too few items to fill the screen, the list will stick to the starting edge.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
///
/// Bottom to top
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// let list = List::new(items).direction(ListDirection::BottomToTop);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn direction(mut self, direction: ListDirection) -> Self {
self.direction = direction;
self
}
/// Sets the number of items around the currently selected item that should be kept visible
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
///
/// A padding value of 1 will keep 1 item above and 1 item bellow visible if possible
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// let list = List::new(items).scroll_padding(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn scroll_padding(mut self, padding: usize) -> Self {
self.scroll_padding = padding;
self
}
/// Returns the number of [`ListItem`]s in the list
pub fn len(&self) -> usize {
self.items.len()
}
/// Returns true if the list contains no elements.
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
impl<'a> Styled for List<'a> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl<'a> Styled for ListItem<'a> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl<'a, Item> FromIterator<Item> for List<'a>
where
Item: Into<ListItem<'a>>,
{
fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
Self::new(iter)
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn collect_list_from_iterator() {
let collected: List = (0..3).map(|i| format!("Item{i}")).collect();
let expected = List::new(["Item0", "Item1", "Item2"]);
assert_eq!(collected, expected);
}
#[test]
fn can_be_stylized() {
assert_eq!(
List::new::<Vec<&str>>(vec![])
.black()
.on_white()
.bold()
.not_dim()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
);
}
}

File diff suppressed because it is too large Load Diff

289
src/widgets/list/state.rs Normal file
View File

@@ -0,0 +1,289 @@
/// State of the [`List`] widget
///
/// This state can be used to scroll through items and select one. When the list is rendered as a
/// stateful widget, the selected item will be highlighted and the list will be shifted to ensure
/// that the selected item is visible. This will modify the [`ListState`] object passed to the
/// [`Frame::render_stateful_widget`](crate::terminal::Frame::render_stateful_widget) method.
///
/// The state consists of two fields:
/// - [`offset`]: the index of the first item to be displayed
/// - [`selected`]: the index of the selected item, which can be `None` if no item is selected
///
/// [`offset`]: ListState::offset()
/// [`selected`]: ListState::selected()
///
/// See the list in the [Examples] directory for a more in depth example of the various
/// configuration options and for how to handle state.
///
/// [Examples]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// # let items = ["Item 1"];
/// let list = List::new(items);
///
/// // This should be stored outside of the function in your application state.
/// let mut state = ListState::default();
///
/// *state.offset_mut() = 1; // display the second item and onwards
/// state.select(Some(3)); // select the forth item (0-indexed)
///
/// frame.render_stateful_widget(list, area, &mut state);
/// # }
/// ```
///
/// [`List`]: crate::widgets::List
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ListState {
pub(crate) offset: usize,
pub(crate) selected: Option<usize>,
}
impl ListState {
/// Sets the index of the first item 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 = ListState::default().with_offset(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn with_offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
/// Sets the index of the selected item
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = ListState::default().with_selected(Some(1));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn with_selected(mut self, selected: Option<usize>) -> Self {
self.selected = selected;
self
}
/// Index of the first item to be displayed
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = ListState::default();
/// assert_eq!(state.offset(), 0);
/// ```
pub const fn offset(&self) -> usize {
self.offset
}
/// Mutable reference to the index of the first item to be displayed
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// *state.offset_mut() = 1;
/// ```
pub fn offset_mut(&mut self) -> &mut usize {
&mut self.offset
}
/// Index of the selected item
///
/// Returns `None` if no item is selected
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::default();
/// assert_eq!(state.selected(), None);
/// ```
pub const fn selected(&self) -> Option<usize> {
self.selected
}
/// Mutable reference to the index of the selected item
///
/// Returns `None` if no item is selected
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// *state.selected_mut() = Some(1);
/// ```
pub fn selected_mut(&mut self) -> &mut Option<usize> {
&mut self.selected
}
/// Sets the index of the selected item
///
/// Set to `None` if no item is selected. This will also reset the offset to `0`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// state.select(Some(1));
/// ```
pub fn select(&mut self, index: Option<usize>) {
self.selected = index;
if index.is_none() {
self.offset = 0;
}
}
/// Selects the next item or the first one if no item is selected
///
/// Note: until the list is rendered, the number of items is not known, so the index is set to
/// `0` and will be corrected when the list is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// state.select_next();
/// ```
pub fn select_next(&mut self) {
let next = self.selected.map_or(0, |i| i.saturating_add(1));
self.select(Some(next));
}
/// Selects the previous item or the last one if no item is selected
///
/// Note: until the list is rendered, the number of items is not known, so the index is set to
/// `usize::MAX` and will be corrected when the list is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// state.select_previous();
/// ```
pub fn select_previous(&mut self) {
let previous = self.selected.map_or(usize::MAX, |i| i.saturating_sub(1));
self.select(Some(previous));
}
/// Selects the first item
///
/// Note: until the list is rendered, the number of items is not known, so the index is set to
/// `0` and will be corrected when the list is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// state.select_first();
/// ```
pub fn select_first(&mut self) {
self.select(Some(0));
}
/// Selects the last item
///
/// Note: until the list is rendered, the number of items is not known, so the index is set to
/// `usize::MAX` and will be corrected when the list is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// state.select_last();
/// ```
pub fn select_last(&mut self) {
self.select(Some(usize::MAX));
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::widgets::ListState;
#[test]
fn selected() {
let mut state = ListState::default();
assert_eq!(state.selected(), None);
state.select(Some(1));
assert_eq!(state.selected(), Some(1));
state.select(None);
assert_eq!(state.selected(), None);
}
#[test]
fn select() {
let mut state = ListState::default();
assert_eq!(state.selected, None);
assert_eq!(state.offset, 0);
state.select(Some(2));
assert_eq!(state.selected, Some(2));
assert_eq!(state.offset, 0);
state.select(None);
assert_eq!(state.selected, None);
assert_eq!(state.offset, 0);
}
#[test]
fn state_navigation() {
let mut state = ListState::default();
state.select_first();
assert_eq!(state.selected, Some(0));
state.select_previous(); // should not go below 0
assert_eq!(state.selected, Some(0));
state.select_next();
assert_eq!(state.selected, Some(1));
state.select_previous();
assert_eq!(state.selected, Some(0));
state.select_last();
assert_eq!(state.selected, Some(usize::MAX));
state.select_next(); // should not go above usize::MAX
assert_eq!(state.selected, Some(usize::MAX));
state.select_previous();
assert_eq!(state.selected, Some(usize::MAX - 1));
state.select_next();
assert_eq!(state.selected, Some(usize::MAX));
let mut state = ListState::default();
state.select_next();
assert_eq!(state.selected, Some(0));
let mut state = ListState::default();
state.select_previous();
assert_eq!(state.selected, Some(usize::MAX));
}
}

View File

@@ -20,6 +20,42 @@ const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Align
/// A widget to display some text.
///
/// It is used to display a block of text. The text can be styled and aligned. It can also be
/// wrapped to the next line if it is too long to fit in the given area.
///
/// The text can be any type that can be converted into a [`Text`]. By default, the text is styled
/// with [`Style::default()`], not wrapped, and aligned to the left.
///
/// The text can be wrapped to the next line if it is too long to fit in the given area. The
/// wrapping can be configured with the [`wrap`] method. For more complex wrapping, consider using
/// the [Textwrap crate].
///
/// The text can be aligned to the left, right, or center. The alignment can be configured with the
/// [`alignment`] method or with the [`left_aligned`], [`right_aligned`], and [`centered`] methods.
///
/// The text can be scrolled to show a specific part of the text. The scroll offset can be set with
/// the [`scroll`] method.
///
/// The text can be surrounded by a [`Block`] with a title and borders. The block can be configured
/// with the [`block`] method.
///
/// The style of the text can be set with the [`style`] method. This style will be applied 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. See the [`Style`] type for more information on how styles are combined.
///
/// Note: If neither wrapping or a block is needed, consider rendering the [`Text`], [`Line`], or
/// [`Span`] widgets directly.
///
/// [Textwrap crate]: https://crates.io/crates/textwrap
/// [`wrap`]: Self::wrap
/// [`alignment`]: Self::alignment
/// [`left_aligned`]: Self::left_aligned
/// [`right_aligned`]: Self::right_aligned
/// [`centered`]: Self::centered
/// [`scroll`]: Self::scroll
/// [`block`]: Self::block
/// [`style`]: Self::style
///
/// # Example
///
/// ```
@@ -51,7 +87,7 @@ pub struct Paragraph<'a> {
/// The text to display
text: Text<'a>,
/// Scroll
scroll: (u16, u16),
scroll: Position,
/// Alignment of the text
alignment: Alignment,
}
@@ -119,7 +155,7 @@ impl<'a> Paragraph<'a> {
style: Style::default(),
wrap: None,
text: text.into(),
scroll: (0, 0),
scroll: Position::ORIGIN,
alignment: Alignment::Left,
}
}
@@ -187,7 +223,10 @@ impl<'a> Paragraph<'a> {
/// Scrollable Widgets](https://github.com/ratatui-org/ratatui/issues/174) on GitHub.
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn scroll(mut self, offset: (Vertical, Horizontal)) -> Self {
self.scroll = offset;
self.scroll = Position {
x: offset.1,
y: offset.0,
};
self
}
@@ -259,6 +298,8 @@ impl<'a> Paragraph<'a> {
/// need in order to be fully rendered. For paragraphs that do not use wrapping, this count is
/// simply the number of lines present in the paragraph.
///
/// This method will also account for the [`Block`] if one is set through [`Self::block`].
///
/// Note: The design for text wrapping is not stable and might affect this API.
///
/// # Example
@@ -279,7 +320,13 @@ impl<'a> Paragraph<'a> {
return 0;
}
if let Some(Wrap { trim }) = self.wrap {
let (top, bottom) = self
.block
.as_ref()
.map(Block::vertical_space)
.unwrap_or_default();
let count = if let Some(Wrap { trim }) = self.wrap {
let styled = self.text.iter().map(|line| {
let graphemes = line
.spans
@@ -296,11 +343,17 @@ impl<'a> Paragraph<'a> {
count
} else {
self.text.height()
}
};
count
.saturating_add(top as usize)
.saturating_add(bottom as usize)
}
/// Calculates the shortest line width needed to avoid any word being wrapped or truncated.
///
/// Accounts for the [`Block`] if a block is set through [`Self::block`].
///
/// Note: The design for text wrapping is not stable and might affect this API.
///
/// # Example
@@ -318,7 +371,16 @@ impl<'a> Paragraph<'a> {
issue = "https://github.com/ratatui-org/ratatui/issues/293"
)]
pub fn line_width(&self) -> usize {
self.text.iter().map(Line::width).max().unwrap_or_default()
let width = self.text.iter().map(Line::width).max().unwrap_or_default();
let (left, right) = self
.block
.as_ref()
.map(Block::horizontal_space)
.unwrap_or_default();
width
.saturating_add(left as usize)
.saturating_add(right as usize)
}
}
@@ -355,7 +417,7 @@ impl Paragraph<'_> {
self.render_text(line_composer, text_area, buf);
} else {
let mut line_composer = LineTruncator::new(styled, text_area.width);
line_composer.set_horizontal_offset(self.scroll.1);
line_composer.set_horizontal_offset(self.scroll.x);
self.render_text(line_composer, text_area, buf);
}
}
@@ -370,7 +432,7 @@ impl<'a> Paragraph<'a> {
alignment: current_line_alignment,
}) = composer.next_line()
{
if y >= self.scroll.0 {
if y >= self.scroll.y {
let mut x = get_line_offset(current_line_width, area.width, current_line_alignment);
for StyledGrapheme { symbol, style } in current_line {
let width = symbol.width();
@@ -380,14 +442,14 @@ impl<'a> Paragraph<'a> {
// If the symbol is empty, the last char which rendered last time will
// leave on the line. It's a quick fix.
let symbol = if symbol.is_empty() { " " } else { symbol };
buf.get_mut(area.left() + x, area.top() + y - self.scroll.0)
buf.get_mut(area.left() + x, area.top() + y - self.scroll.y)
.set_symbol(symbol)
.set_style(*style);
x += width as u16;
}
}
y += 1;
if y >= area.height + self.scroll.0 {
if y >= area.height + self.scroll.y {
break;
}
}
@@ -947,6 +1009,69 @@ mod test {
assert_eq!(paragraph.line_count(6), 200);
}
#[test]
fn widgets_paragraph_rendered_line_count_accounts_block() {
let block = Block::new();
let paragraph = Paragraph::new("Hello World").block(block);
assert_eq!(paragraph.line_count(20), 1);
assert_eq!(paragraph.line_count(10), 1);
let block = Block::new().borders(Borders::TOP);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(20), 2);
assert_eq!(paragraph.line_count(10), 2);
let block = Block::new().borders(Borders::BOTTOM);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(20), 2);
assert_eq!(paragraph.line_count(10), 2);
let block = Block::new().borders(Borders::TOP | Borders::BOTTOM);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(20), 3);
assert_eq!(paragraph.line_count(10), 3);
let block = Block::bordered();
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(20), 3);
assert_eq!(paragraph.line_count(10), 3);
let block = Block::bordered();
let paragraph = paragraph.block(block).wrap(Wrap { trim: true });
assert_eq!(paragraph.line_count(20), 3);
assert_eq!(paragraph.line_count(10), 4);
let block = Block::bordered();
let paragraph = paragraph.block(block).wrap(Wrap { trim: false });
assert_eq!(paragraph.line_count(20), 3);
assert_eq!(paragraph.line_count(10), 4);
let text = "Hello World ".repeat(100);
let block = Block::new();
let paragraph = Paragraph::new(text.trim()).block(block);
assert_eq!(paragraph.line_count(11), 1);
let block = Block::bordered();
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(11), 3);
assert_eq!(paragraph.line_count(6), 3);
let block = Block::new().borders(Borders::TOP);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(11), 2);
assert_eq!(paragraph.line_count(6), 2);
let block = Block::new().borders(Borders::BOTTOM);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(11), 2);
assert_eq!(paragraph.line_count(6), 2);
let block = Block::new().borders(Borders::LEFT | Borders::RIGHT);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(11), 1);
assert_eq!(paragraph.line_count(6), 1);
}
#[test]
fn widgets_paragraph_line_width() {
let paragraph = Paragraph::new("Hello World");
@@ -965,6 +1090,29 @@ mod test {
assert_eq!(paragraph.line_width(), 1200);
}
#[test]
fn widgets_paragraph_line_width_accounts_for_block() {
let block = Block::bordered();
let paragraph = Paragraph::new("Hello World").block(block);
assert_eq!(paragraph.line_width(), 13);
let block = Block::new().borders(Borders::LEFT);
let paragraph = Paragraph::new("Hello World").block(block);
assert_eq!(paragraph.line_width(), 12);
let block = Block::new().borders(Borders::LEFT);
let paragraph = Paragraph::new("Hello World")
.block(block)
.wrap(Wrap { trim: true });
assert_eq!(paragraph.line_width(), 12);
let block = Block::new().borders(Borders::LEFT);
let paragraph = Paragraph::new("Hello World")
.block(block)
.wrap(Wrap { trim: false });
assert_eq!(paragraph.line_width(), 12);
}
#[test]
fn left_aligned() {
let p = Paragraph::new("Hello, world!").left_aligned();

View File

@@ -1,7 +1,6 @@
mod cell;
mod highlight_spacing;
mod row;
#[allow(clippy::module_inception)]
mod table;
mod table_state;

View File

@@ -134,7 +134,7 @@ impl<'a> Cell<'a> {
impl Cell<'_> {
pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
self.content.clone().render(area, buf);
self.content.render_ref(area, buf);
}
}

View File

@@ -722,7 +722,7 @@ impl Table<'_> {
..row_area
};
buf.set_style(selection_area, row.style);
highlight_symbol.clone().render(selection_area, buf);
highlight_symbol.render_ref(selection_area, buf);
};
for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) {
cell.render(
@@ -927,6 +927,8 @@ mod tests {
let table = Table::default().widths([Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);
// ensure that code that uses &[] continues to work as there is a large amount of code that
// uses this pattern
#[allow(clippy::needless_borrows_for_generic_args)]
let table = Table::default().widths(&[Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);
@@ -934,6 +936,9 @@ mod tests {
let table = Table::default().widths(vec![Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);
// ensure that code that uses &some_vec continues to work as there is a large amount of code
// that uses this pattern
#[allow(clippy::needless_borrows_for_generic_args)]
let table = Table::default().widths(&vec![Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);

0
src/widgets/tabs.rs Executable file → Normal file
View File

0
tests/widgets_table.rs Executable file → Normal file
View File