Compare commits
22 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 |
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])
|
||||
|
||||
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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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`].
|
||||
|
||||
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);
|
||||
|
||||
@@ -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
0
src/widgets/list.rs
Executable file → Normal 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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