Compare commits

..

22 Commits

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

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

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

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

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

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

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

---

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

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

---------

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

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

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

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

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

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

Fixes: #1233
2024-07-23 13:13:50 -07:00
Josh McKinney
c34fb77818 feat(text)!: remove unnecessary lifetime from ToText trait (#1234)
BREAKING CHANGE: The ToText trait no longer has a lifetime parameter.
This change simplifies the trait and makes it easier implement.
2024-07-22 04:24:30 -07:00
Josh McKinney
6ce447c4f3 docs(block): add docs about style inheritance (#1190)
Fixes: https://github.com/ratatui-org/ratatui/issues/1129
2024-07-19 20:02:08 -07:00
69 changed files with 860 additions and 193 deletions

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,9 +29,9 @@ use ratatui::{
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
symbols::{self, Marker},
terminal::{Frame, Terminal},
text::Span,
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
Frame, Terminal,
};
#[derive(Clone)]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -963,4 +963,43 @@ mod tests {
]);
assert_eq!(buffer, expected);
}
/// Emojis normally contain various characters which should stay part of the Emoji.
/// This should work fine by utilizing unicode_segmentation but a testcase is probably helpful
/// due to the nature of never perfect Unicode implementations and all of its quirks.
#[rstest]
// Shrug without gender or skintone. Has a width of 2 like all emojis have.
#[case::shrug("🤷", "🤷xxxxx")]
// Technically this is a (brown) bear, a zero-width joiner and a snowflake
// As it is joined its a single emoji and should therefore have a width of 2.
// It's correctly detected as a single grapheme but it's width is 4 for some reason
#[case::polarbear("🐻‍❄️", "🐻xxx")]
// Technically this is an eye, a zero-width joiner and a speech bubble
// Both eye and speech bubble include a 'display as emoji' variation selector
#[case::eye_speechbubble("👁️‍🗨️", "👁🗨xxx")]
fn renders_emoji(#[case] input: &str, #[case] expected: &str) {
use unicode_width::UnicodeWidthChar;
dbg!(input);
dbg!(input.len());
dbg!(input
.graphemes(true)
.map(|symbol| (symbol, symbol.escape_unicode().to_string(), symbol.width()))
.collect::<Vec<_>>());
dbg!(input
.chars()
.map(|char| (
char,
char.escape_unicode().to_string(),
char.width(),
char.is_control()
))
.collect::<Vec<_>>());
let mut buffer = Buffer::filled(Rect::new(0, 0, 7, 1), Cell::new("x"));
buffer.set_string(0, 0, input, Style::new());
let expected = Buffer::with_lines([expected]);
assert_eq!(buffer, expected);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//! [`Text`].
//!
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Line`].

View File

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

View File

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

View File

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

View File

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

View File

@@ -408,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
@@ -740,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));
}
}
@@ -748,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()));
}
}
@@ -1032,7 +1032,7 @@ impl WidgetRef for Chart<'_> {
.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()
@@ -1050,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()

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

View File

@@ -35,7 +35,7 @@ use crate::{
/// - [`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
/// multi-line items
/// - [`List::direction`] sets the list direction
///
/// # Examples

View File

@@ -108,7 +108,7 @@ impl StatefulWidgetRef for List<'_> {
} else {
row_area
};
item.content.clone().render(item_area, buf);
item.content.render_ref(item_area, buf);
for j in 0..item.content.height() {
// if the item is selected, we need to display the highlight symbol:

View File

@@ -87,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,
}
@@ -155,7 +155,7 @@ impl<'a> Paragraph<'a> {
style: Style::default(),
wrap: None,
text: text.into(),
scroll: (0, 0),
scroll: Position::ORIGIN,
alignment: Alignment::Left,
}
}
@@ -223,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
}
@@ -295,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
@@ -315,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
@@ -332,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
@@ -354,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)
}
}
@@ -391,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);
}
}
@@ -406,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();
@@ -416,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;
}
}
@@ -983,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");
@@ -1001,6 +1090,29 @@ mod test {
assert_eq!(paragraph.line_width(), 1200);
}
#[test]
fn widgets_paragraph_line_width_accounts_for_block() {
let block = Block::bordered();
let paragraph = Paragraph::new("Hello World").block(block);
assert_eq!(paragraph.line_width(), 13);
let block = Block::new().borders(Borders::LEFT);
let paragraph = Paragraph::new("Hello World").block(block);
assert_eq!(paragraph.line_width(), 12);
let block = Block::new().borders(Borders::LEFT);
let paragraph = Paragraph::new("Hello World")
.block(block)
.wrap(Wrap { trim: true });
assert_eq!(paragraph.line_width(), 12);
let block = Block::new().borders(Borders::LEFT);
let paragraph = Paragraph::new("Hello World")
.block(block)
.wrap(Wrap { trim: false });
assert_eq!(paragraph.line_width(), 12);
}
#[test]
fn left_aligned() {
let p = Paragraph::new("Hello, world!").left_aligned();

View File

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

View File

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

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

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