Compare commits
29 Commits
v0.27.1-al
...
v0.28.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8857037bff | ||
|
|
e707ff11d1 | ||
|
|
a9fe4284ac | ||
|
|
ffc4300558 | ||
|
|
84cb16483a | ||
|
|
5b89bd04a8 | ||
|
|
32d0695cc2 | ||
|
|
cd93547db8 | ||
|
|
c245c13cc1 | ||
|
|
b2aa843b31 | ||
|
|
3ca920e881 | ||
|
|
b344f95b7c | ||
|
|
b304bb99bd | ||
|
|
be3eb75ea5 | ||
|
|
efef0d0dc0 | ||
|
|
663486f1e8 | ||
|
|
7ddfbc0010 | ||
|
|
3725262ca3 | ||
|
|
84f334163b | ||
|
|
03f3124c1d | ||
|
|
c34fb77818 | ||
|
|
6ce447c4f3 | ||
|
|
272d0591a7 | ||
|
|
e81663bec0 | ||
|
|
7e1bab049b | ||
|
|
379dab9cdb | ||
|
|
5b51018501 | ||
|
|
7bab9f0d80 | ||
|
|
6d210b3b6b |
25
.github/workflows/bench_base.yml
vendored
Normal file
25
.github/workflows/bench_base.yml
vendored
Normal 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
25
.github/workflows/bench_run_fork_pr.yml
vendored
Normal 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 }}
|
||||
75
.github/workflows/bench_track_fork_pr.yml
vendored
Normal file
75
.github/workflows/bench_track_fork_pr.yml
vendored
Normal 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"
|
||||
2
.github/workflows/check-pr.yml
vendored
2
.github/workflows/check-pr.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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])
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -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]]
|
||||
|
||||
20
cliff.toml
20
cliff.toml
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -44,9 +44,9 @@ use ratatui::{
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Color,
|
||||
terminal::Terminal,
|
||||
text::Text,
|
||||
widgets::Widget,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -11,7 +11,7 @@ use ratatui::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
terminal::Terminal,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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};
|
||||
///
|
||||
|
||||
@@ -4,8 +4,8 @@ use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Flex, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
terminal::Frame,
|
||||
widgets::Widget,
|
||||
Frame,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use ratatui::{
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::Rect,
|
||||
terminal::{Terminal, TerminalOptions, Viewport},
|
||||
Terminal, TerminalOptions, Viewport,
|
||||
};
|
||||
|
||||
pub fn init() -> Result<Terminal<impl Backend>> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -27,8 +27,8 @@ use ratatui::{
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
terminal::{Frame, Terminal},
|
||||
widgets::{Block, Clear, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -32,8 +32,8 @@ use ratatui::{
|
||||
},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Style},
|
||||
terminal::{Frame, Terminal},
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
//! A module for the [`Buffer`] and [`Cell`] types.
|
||||
|
||||
mod assert;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod buffer;
|
||||
mod cell;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -4,7 +4,6 @@ mod alignment;
|
||||
mod constraint;
|
||||
mod direction;
|
||||
mod flex;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod layout;
|
||||
mod margin;
|
||||
mod position;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
//! [`Buffer`]: crate::buffer::Buffer
|
||||
|
||||
mod frame;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod terminal;
|
||||
mod viewport;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
121
src/text/line.rs
121
src/text/line.rs
@@ -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));
|
||||
|
||||
@@ -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")])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
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
324
src/widgets/list/item.rs
Normal 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
450
src/widgets/list/list.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
1285
src/widgets/list/rendering.rs
Normal file
1285
src/widgets/list/rendering.rs
Normal file
File diff suppressed because it is too large
Load Diff
289
src/widgets/list/state.rs
Normal file
289
src/widgets/list/state.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
mod cell;
|
||||
mod highlight_spacing;
|
||||
mod row;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod table;
|
||||
mod table_state;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
0
src/widgets/tabs.rs
Executable file → Normal file
0
tests/widgets_table.rs
Executable file → Normal file
0
tests/widgets_table.rs
Executable file → Normal file
Reference in New Issue
Block a user