Compare commits

...

34 Commits

Author SHA1 Message Date
Florian Dehau
54b841fab6 feat(widgets/paragraph): improve scrolling
Add optional callback to `Wrap` to compute the scroll offsets given the wrapped lines. This let
users compute the offsets dynamically.
2020-08-03 00:49:27 +02:00
Florian Dehau
ecb482f297 fix(backend): move the cursor when first diff is on second cell
Both termion and crossterm backends were not moving the cursor if the first diff to draw was on the
second cell. The condition triggering the cursor move has been updated to fix this. In addition, two
tests have been added to avoid future regressions.
2020-08-02 21:10:44 +02:00
Florian Dehau
641f391137 feat(terminal): add unstable api to use a fixed viewport
There was now way to avoid the autoresize behavior of `Terminal`. While it was fine for most users,
it made the testing experience painful as it was impossible to avoid the calls to `Backend::size()`.
Indeed they trigger the following error: "Inappropriate ioctl for device" since we are not running
the tests in a real terminal (at least in the CI).

This commit introduces a new api to create a `Terminal` with a fixed viewport.
2020-08-02 20:46:57 +02:00
Florian Dehau
dc26f7ba9f chore: document rustc min version supported
- Add section to README
- Run ci tests with this min version in addition of stable to track changes that would require a min
rustc version bump.
2020-08-02 16:37:34 +02:00
Florian Dehau
6504930888 Release v0.10.0 2020-07-18 18:03:32 +02:00
Florian Dehau
6b52c91257 chore: update CHANGELOG 2020-07-17 22:05:37 +02:00
Florian Dehau
0ffea495b1 refactor: implement cascading styles
- merge `Style` and `StyleDiff` together. `Style` now is used to activate or deactivate certain
style rules not to overidden all of them.
- update all impacted widgets, examples and tests.
2020-07-17 20:58:20 +02:00
Florian Dehau
72ba4ff2d4 refactor(examples): remove unecessary terminal.hide_cursor calls 2020-07-10 23:56:46 +02:00
Florian Dehau
88c4b191fb feat(text): add new text primitives 2020-07-10 22:59:24 +02:00
Brooks Rady
112d2a65f6 feat(widgets/paragraph): add option to preserve indentation when the text is wrapped (#327) 2020-07-07 00:10:24 +02:00
Xiaopeng Li
d999c1b434 feat(widgets/paragraph): Add horizontal scroll (#329)
* `Paragraph:scroll` takes a tuple of offsets instead of a single vertical offset.
* `LineTruncator` takes this new horizontal offset into account to let the paragraph scroll horizontally.
2020-07-06 23:47:52 +02:00
Mikko Rantanen
3aa8b9a259 Implement patch between two StyleDiff 2020-07-05 18:32:33 +02:00
Florian Dehau
fdbea9e2ee fix(widgets/canvas): avoid panic on zero-width bounds 2020-07-05 14:02:11 +02:00
Priime
6204eddade fix(readme): typo in demo section
There was a very small typo in the README on line 40, which cited the
`examples` folder as `exmples`. This resolves that issue.
2020-07-05 11:08:19 +02:00
Aram Drevekenin
e789c671b0 Update README.md 2020-06-28 15:23:22 +02:00
Alexander Batischev
8c2ee0ed85 feat(terminal): Add after-draw() cursor control to Frame (#91) (#309) 2020-06-15 22:57:23 +02:00
Cokemonkey11
2b48409cfd fix(examples): remove typo in demo text 2020-06-15 22:46:45 +02:00
Florian Dehau
7251186762 feat(style): add StyleDiff 2020-06-08 23:55:23 +02:00
Florian Dehau
82fda4ac0e doc(style): improve documentation of Style 2020-06-08 23:55:23 +02:00
Kenta Iwasaki
1d12ddbdfc layout: add vertical split constraint test on height 2020-06-08 23:05:08 +02:00
Kenta Iwasaki
f474c76e19 layout: force constraint that width and height are non-negative 2020-06-08 23:05:08 +02:00
Florian Dehau
ac99104114 feat(style): add support to serialize and deserialize Style using serde
* Add serde as an optional dependency.
* Add feature-gated derives to Color, Modifier and Style.
2020-05-28 01:08:40 +02:00
Paul Horn
0bb9b388f7 Borrow layout for splitting instead of moving it
This allows for layouts to be saved and reused and does not require an additional clone
2020-05-28 00:06:47 +02:00
Florian Dehau
b59e4bb808 feat(examples): enable mouse capture to make crossterm demo on par with termion 2020-05-27 23:56:46 +02:00
Florian Dehau
4fe647df0a refactor(tests): rename integration tests to be able to call group of tests 2020-05-21 21:59:39 +02:00
Florian Dehau
a00350ab54 refactor(tests): rename test files and use the new TestBackend::assert_buffer method 2020-05-21 21:59:39 +02:00
Florian Dehau
96c6b4efcb refactor(tests): move test utilities to TestBackend
* Remove custom Debug implementation of Buffer
* Add `TestBackend::assert_buffer` to compare buffers in integration tests. When
the assertion fails, the output now show the list of differences in addition
of the views of the computed and expected buffers. This effectively replaces
the table of debug code for colors and modifiers as it is easier to read.
2020-05-21 21:59:39 +02:00
Florian Dehau
18714caa60 Release v0.9.5 2020-05-21 20:59:34 +02:00
Stephan Dilly
7110fe0159 fix panic on narrow buffers (fixes #293) 2020-05-21 19:14:12 +02:00
Florian Dehau
5a590bca74 chore: enable clippy on all targets and all features
- Remove deny warnings in lib.rs. This allows easier iteration when developing
new features. The warnings will make the CI fails anyway on the clippy CI
stage.
- Run clippy on all targets (including tests and examples) and all features.
- Fail CI on clippy warnings.
2020-05-17 01:07:49 +02:00
Florian Dehau
963f11a6b1 Release v0.9.4 2020-05-12 21:25:28 +02:00
Florian Dehau
a7761fe55d fix(buffer): ignore zero-width graphemes 2020-05-12 21:16:16 +02:00
Florian Dehau
10cf9305f1 Release v0.9.3 2020-05-11 00:02:12 +02:00
Clement Tsang
b72ced4511 fix(widgets/chart): remove overflow when dataset if empty (#274)
* docs: Fix missing code block fence
* use slice::windows to deal with underflow issue
* add test for empty dataset and lines
2020-05-10 23:48:12 +02:00
61 changed files with 2938 additions and 1511 deletions

View File

@@ -6,6 +6,9 @@ jobs:
linux:
name: Linux
runs-on: ubuntu-latest
strategy:
matrix:
rust: ["1.44.0", "stable"]
steps:
- name: "Install dependencies"
run: sudo apt-get install libncurses5-dev
@@ -13,7 +16,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: default
toolchain: stable
toolchain: ${{ matrix.rust }}
override: true
- name: "Format"
uses: actions-rs/cargo@v1
@@ -44,19 +47,25 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
env:
RUST_BACKTRACE: full
- name: "Clippy"
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --all-features -- -D warnings
windows:
name: Windows
runs-on: windows-latest
strategy:
matrix:
rust: ["1.44.0", "stable"]
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: default
toolchain: stable
toolchain: ${{ matrix.rust }}
override: true
- name: "Check (crossterm)"
uses: actions-rs/cargo@v1
@@ -68,3 +77,5 @@ jobs:
with:
command: test
args: --no-default-features --features=crossterm --tests --examples
env:
RUST_BACKTRACE: full

View File

@@ -2,6 +2,186 @@
## To be released
### Fixes
* Fix incorrect output when the first diff to draw is on the second cell of the terminal (#347).
## v0.10.0 - 2020-07-17
### Breaking changes
#### Easier cursor management
A new method has been added to `Frame` called `set_cursor`. It lets you specify where the cursor
should be placed after the draw call. Furthermore like any other widgets, if you do not set a cursor
position during a draw call, the cursor is automatically hidden.
For example:
```rust
fn draw_input(f: &mut Frame, app: &App) {
if app.editing {
let input_width = app.input.width() as u16;
// The cursor will be placed just after the last character of the input
f.set_cursor((input_width + 1, 0));
} else {
// We are no longer editing, the cursor does not have to be shown, set_cursor is not called and
// thus automatically hidden.
}
}
```
In order to make this possible, the draw closure takes in input `&mut Frame` instead of `mut Frame`.
#### Advanced text styling
It has been reported several times that the text styling capabilities were somewhat limited in many
places of the crate. To solve the issue, this release includes a new set of text primitives that are
now used by a majority of widgets to provide flexible text styling.
`Text` is replaced by the following types:
- `Span`: a string with a unique style.
- `Spans`: a string with multiple styles.
- `Text`: a multi-lines string with multiple styles.
However, you do not always need this complexity so the crate provides `From` implementations to
let you use simple strings as a default and switch to the previous primitives when you need
additional styling capabilities.
For example, the title of a `Block` can be set in the following ways:
```rust
// A title with no styling
Block::default().title("My title");
// A yellow title
Block::default().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
// A title where "My" is bold and "title" is a simple string
Block::default().title(vec![
Span::styled("My", Style::default().add_modifier(Modifier::BOLD)),
Span::from("title")
]);
```
- `Buffer::set_spans` and `Buffer::set_span` were added.
- `Paragraph::new` expects an input that can be converted to a `Text`.
- `Block::title_style` is deprecated.
- `Block::title` expects a `Spans`.
- `Tabs` expects a list of `Spans`.
- `Gauge` custom label is now a `Span`.
- `Axis` title and labels are `Spans` (as a consequence `Chart` no longer has generic bounds).
#### Incremental styling
Previously `Style` was used to represent an exhaustive set of style rules to be applied to an UI
element. It implied that whenever you wanted to change even only one property you had to provide the
complete style. For example, if you had a `Block` where you wanted to have a green background and
a title in bold, you had to do the following:
```rust
let style = Style::default().bg(Color::Green);
Block::default()
.style(style)
.title("My title")
// Here we reused the style otherwise the background color would have been reset
.title_style(style.modifier(Modifier::BOLD));
```
In this new release, you may now write this as:
```rust
Block::default()
.style(Style::default().bg(Color::Green))
// The style is not overidden anymore, we simply add new style rule for the title.
.title(Span::styled("My title", Style::default().add_modifier(Modifier::BOLD)))
```
In addition, the crate now provides a method `patch` to combine two styles into a new set of style
rules:
```rust
let style = Style::default().modifer(Modifier::BOLD);
let style = style.patch(Style::default().add_modifier(Modifier::ITALIC));
// style.modifer == Modifier::BOLD | Modifier::ITALIC, the modifier has been enriched not overidden
```
- `Style::modifier` has been removed in favor of `Style::add_modifier` and `Style::remove_modifier`.
- `Buffer::set_style` has been added. `Buffer::set_background` is deprecated.
- `BarChart::style` no longer set the style of the bars. Use `BarChart::bar_style` in replacement.
- `Gauge::style` no longer set the style of the gauge. Use `Gauge::gauge_style` in replacement.
#### List with item on multiple lines
The `List` widget has been refactored once again to support items with variable heights and complex
styling.
- `List::new` expects an input that can be converted to a `Vec<ListItem>` where `ListItem` is a
wrapper around the item content to provide additional styling capabilities. `ListItem` contains a
`Text`.
- `List::items` has been removed.
```rust
// Before
let items = vec![
"Item1",
"Item2",
"Item3"
];
List::default().items(items.iters());
// After
let items = vec![
ListItem::new("Item1"),
ListItem::new("Item2"),
ListItem::new("Item3"),
];
List::new(items);
```
See the examples for more advanced usages.
#### More wrapping options
`Paragraph::wrap` expects `Wrap` instead of `bool` to let users decided whether they want to trim
whitespaces when the text is wrapped.
```rust
// before
Paragraph::new(text).wrap(true)
// after
Paragraph::new(text).wrap(Wrap { trim: true }) // to have the same behavior
Paragraph::new(text).wrap(Wrap { trim: false }) // to use the new behavior
```
#### Horizontal scrolling in paragraph
You can now scroll horizontally in `Paragraph`. The argument of `Paragraph::scroll` has thus be
changed from `u16` to `(u16, u16)`.
### Features
#### Serialization of style
You can now serialize and de-serialize `Style` using the optional `serde` feature.
## v0.9.5 - 2020-05-21
### Bug Fixes
* Fix out of bounds panic in `widgets::Tabs` when the widget is rendered on
small areas.
## v0.9.4 - 2020-05-12
### Bug Fixes
* Ignore zero-width graphemes in `Buffer::set_stringn`.
## v0.9.3 - 2020-05-11
### Bug Fixes
* Fix usize overflows in `widgets::Chart` when a dataset is empty.
## v0.9.2 - 2020-05-10
### Bug Fixes
@@ -226,15 +406,15 @@ from an enum to a bitflags struct.
So instead of writing:
```rust
let style = Style::default().modifier(Modifier::Italic);
let style = Style::default().add_modifier(Modifier::Italic);
```
one should use:
```rust
let style = Style::default().modifier(Modifier::ITALIC);
let style = Style::default().add_modifier(Modifier::ITALIC);
// or
let style = Style::default().modifier(Modifier::ITALIC | Modifier::BOLD);
let style = Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD);
```
### Bug Fixes

View File

@@ -1,11 +1,11 @@
[package]
name = "tui"
version = "0.9.2"
version = "0.10.0"
authors = ["Florian Dehau <work@fdehau.com>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
documentation = "https://docs.rs/tui/0.9.2/tui/"
documentation = "https://docs.rs/tui/0.10.0/tui/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/fdehau/tui-rs"
license = "MIT"
@@ -22,8 +22,6 @@ curses = ["easycurses", "pancurses"]
[dependencies]
bitflags = "1.0"
cassowary = "0.3"
itertools = "0.9"
either = "1.5"
unicode-segmentation = "1.2"
unicode-width = "0.1"
termion = { version = "1.5", optional = true }
@@ -31,6 +29,7 @@ rustbox = { version = "0.11", optional = true }
crossterm = { version = "0.17", optional = true }
easycurses = { version = "0.12.2", optional = true }
pancurses = { version = "0.16.1", optional = true, features = ["win32a"] }
serde = { version = "1", "optional" = true, features = ["derive"]}
[dev-dependencies]
rand = "0.7"

View File

@@ -6,6 +6,7 @@ SHELL=/bin/bash
RUST_CHANNEL ?= stable
CARGO_FLAGS =
RUSTUP_INSTALLED = $(shell command -v rustup 2> /dev/null)
TEST_FILTER ?=
ifndef RUSTUP_INSTALLED
CARGO = cargo
@@ -52,23 +53,23 @@ fmt: ## Check the format of the source code
.PHONY: clippy
clippy: ## Check the style of the source code and catch common errors
$(CARGO) clippy --all-features
$(CARGO) clippy --all-targets --all-features -- -D warnings
# ================================ Test =======================================
.PHONY: test
test: ## Run the tests
$(CARGO) test --all-features
$(CARGO) test --all-features $(TEST_FILTER)
# =============================== Examples ====================================
.PHONY: build-examples
build-examples: ## Build all examples
@$(CARGO) build --examples --all-features
@$(CARGO) build --release --examples --all-features
.PHONY: run-examples
run-examples: ## Run all examples
run-examples: build-examples ## Run all examples
@for file in examples/*.rs; do \
name=$$(basename $${file/.rs/}); \
$(CARGO) run --all-features --release --example $$name; \
@@ -78,6 +79,7 @@ run-examples: ## Run all examples
.PHONY: doc
doc: RUST_CHANNEL = nightly
doc: ## Build the documentation (available at ./target/doc)
$(CARGO) doc
@@ -95,8 +97,9 @@ watch-test: ## Watch files changes and run the tests if any
watchman-make -p 'src/**/*.rs' 'tests/**/*.rs' 'examples/**/*.rs' -t test
.PHONY: watch-doc
watch-doc: RUST_CHANNEL = nightly
watch-doc: ## Watch file changes and rebuild the documentation if any
watchman-make -p 'src/**/*.rs' -t doc
$(CARGO) watch -x doc -x 'test --doc'
# ================================= Pipelines =================================

View File

@@ -32,12 +32,16 @@ comes from the terminal emulator than the library itself.
Moreover, the library does not provide any input handling nor any event system and
you may rely on the previously cited libraries to achieve such features.
### Rust version requirements
Since version 0.10.0, `tui` requires **rustc version 1.44.0 or greater**.
### [Documentation](https://docs.rs/tui)
### Demo
The demo shown in the gif can be run with all available backends
(`exmples/*_demo.rs` files). For example to see the `termion` version one could
(`examples/*_demo.rs` files). For example to see the `termion` version one could
run:
```
@@ -99,6 +103,7 @@ You can run all examples by running `make run-examples`.
* [gitui](https://github.com/extrawurst/gitui)
* [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli)
* [desed](https://github.com/SoptikHa2/desed)
* [diskonaut](https://github.com/imsnif/diskonaut)
### Alternatives

View File

@@ -61,7 +61,6 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup event handlers
let events = Events::new();
@@ -70,7 +69,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new();
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
@@ -80,7 +79,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
.bar_width(9)
.style(Style::default().fg(Color::Yellow))
.bar_style(Style::default().fg(Color::Yellow))
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
f.render_widget(barchart, chunks[0]);
@@ -94,18 +93,26 @@ fn main() -> Result<(), Box<dyn Error>> {
.data(&app.data)
.bar_width(5)
.bar_gap(3)
.style(Style::default().fg(Color::Green))
.value_style(Style::default().bg(Color::Green).modifier(Modifier::BOLD));
.bar_style(Style::default().fg(Color::Green))
.value_style(
Style::default()
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
);
f.render_widget(barchart, chunks[0]);
let barchart = BarChart::default()
.block(Block::default().title("Data3").borders(Borders::ALL))
.data(&app.data)
.style(Style::default().fg(Color::Red))
.bar_style(Style::default().fg(Color::Red))
.bar_width(7)
.bar_gap(0)
.value_style(Style::default().bg(Color::Red))
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC));
.label_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::ITALIC),
);
f.render_widget(barchart, chunks[1]);
})?;

View File

@@ -8,6 +8,7 @@ use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, BorderType, Borders},
Terminal,
};
@@ -19,13 +20,12 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup event handlers
let events = Events::new();
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
// Wrapping block for a group
// Just draw the block and the group on the same area and build the group
// with at least a margin of 1
@@ -40,48 +40,46 @@ fn main() -> Result<(), Box<dyn Error>> {
.margin(4)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
let block = Block::default()
.title("With background")
.title_style(Style::default().fg(Color::Yellow))
.style(Style::default().bg(Color::Green));
f.render_widget(block, chunks[0]);
let title_style = Style::default()
let top_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
let block = Block::default()
.title(vec![
Span::styled("With", Style::default().fg(Color::Yellow)),
Span::from(" background"),
])
.style(Style::default().bg(Color::Green));
f.render_widget(block, top_chunks[0]);
let block = Block::default().title(Span::styled(
"Styled title",
Style::default()
.fg(Color::White)
.bg(Color::Red)
.modifier(Modifier::BOLD);
let block = Block::default()
.title("Styled title")
.title_style(title_style);
f.render_widget(block, chunks[1]);
}
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let block = Block::default().title("With borders").borders(Borders::ALL);
f.render_widget(block, chunks[0]);
let block = Block::default()
.title("With styled borders and doubled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Double);
f.render_widget(block, chunks[1]);
}
.add_modifier(Modifier::BOLD),
));
f.render_widget(block, top_chunks[1]);
let bottom_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let block = Block::default().title("With borders").borders(Borders::ALL);
f.render_widget(block, bottom_chunks[0]);
let block = Block::default()
.title("With styled borders and doubled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Double);
f.render_widget(block, bottom_chunks[1]);
})?;
match events.next()? {
Event::Input(key) => {
if key == Key::Char('q') {
break;
}
if let Event::Input(key) = events.next()? {
if key == Key::Char('q') {
break;
}
_ => {}
}
}
Ok(())

View File

@@ -79,7 +79,6 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup event handlers
let config = Config {
@@ -92,7 +91,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new();
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())

View File

@@ -12,6 +12,7 @@ use tui::{
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
symbols,
text::Span,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
Terminal,
};
@@ -71,7 +72,6 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
@@ -79,7 +79,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new();
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -92,12 +92,18 @@ fn main() -> Result<(), Box<dyn Error>> {
.as_ref(),
)
.split(size);
let x_labels = [
format!("{}", app.window[0]),
format!("{}", (app.window[0] + app.window[1]) / 2.0),
format!("{}", app.window[1]),
let x_labels = vec![
Span::styled(
format!("{}", app.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
Span::styled(
format!("{}", app.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = [
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
@@ -109,94 +115,118 @@ fn main() -> Result<(), Box<dyn Error>> {
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
];
let chart = Chart::default()
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 1")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.title(Span::styled(
"Chart 1",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds(app.window)
.labels(&x_labels),
.labels(x_labels)
.bounds(app.window),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([-20.0, 20.0])
.labels(&["-20", "0", "20"]),
)
.datasets(&datasets);
.labels(vec![
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("0"),
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
])
.bounds([-20.0, 20.0]),
);
f.render_widget(chart, chunks[0]);
let datasets = [Dataset::default()
let datasets = vec![Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA)];
let chart = Chart::default()
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 2")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.title(Span::styled(
"Chart 2",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5.0"]),
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5.0"]),
)
.datasets(&datasets);
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[1]);
let datasets = [Dataset::default()
let datasets = vec![Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA2)];
let chart = Chart::default()
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 3")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.title(Span::styled(
"Chart 3",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 50.0])
.labels(&["0", "25", "50"]),
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("25"),
Span::styled("50", Style::default().add_modifier(Modifier::BOLD)),
]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5"]),
)
.datasets(&datasets);
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5", Style::default().add_modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[2]);
})?;

View File

@@ -6,7 +6,7 @@ mod util;
use crate::demo::{ui, App};
use argh::FromArgs;
use crossterm::{
event::{self, Event as CEvent, KeyCode},
event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
@@ -41,12 +41,11 @@ fn main() -> Result<(), Box<dyn Error>> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup input handling
let (tx, rx) = mpsc::channel();
@@ -73,12 +72,16 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.clear()?;
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;
match rx.recv()? {
Event::Input(event) => match event.code {
KeyCode::Char('q') => {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
break;
}

View File

@@ -4,7 +4,6 @@ mod util;
use crate::demo::{ui, App};
use argh::FromArgs;
use easycurses;
use std::{
error::Error,
io,
@@ -26,7 +25,8 @@ struct Cli {
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let mut backend = CursesBackend::new().ok_or(io::Error::new(io::ErrorKind::Other, ""))?;
let mut backend =
CursesBackend::new().ok_or_else(|| io::Error::new(io::ErrorKind::Other, ""))?;
let curses = backend.get_curses_mut();
curses.set_echo(false);
curses.set_input_timeout(easycurses::TimeoutMode::WaitUpTo(50));
@@ -40,29 +40,26 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(cli.tick_rate);
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
match terminal.backend_mut().get_curses_mut().get_input() {
Some(input) => {
match input {
easycurses::Input::Character(c) => {
app.on_key(c);
}
easycurses::Input::KeyUp => {
app.on_up();
}
easycurses::Input::KeyDown => {
app.on_down();
}
easycurses::Input::KeyLeft => {
app.on_left();
}
easycurses::Input::KeyRight => {
app.on_right();
}
_ => {}
};
}
_ => {}
terminal.draw(|f| ui::draw(f, &mut app))?;
if let Some(input) = terminal.backend_mut().get_curses_mut().get_input() {
match input {
easycurses::Input::Character(c) => {
app.on_key(c);
}
easycurses::Input::KeyUp => {
app.on_up();
}
easycurses::Input::KeyDown => {
app.on_down();
}
easycurses::Input::KeyLeft => {
app.on_left();
}
easycurses::Input::KeyRight => {
app.on_right();
}
_ => {}
};
};
terminal.backend_mut().get_curses_mut().flush_input();
if last_tick.elapsed() > tick_rate {

View File

@@ -42,19 +42,16 @@ fn main() -> Result<(), Box<dyn Error>> {
let events = Events::new();
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let size = f.size();
let label = Label::default().text("Test");
f.render_widget(label, size);
})?;
match events.next()? {
Event::Input(key) => {
if key == Key::Char('q') {
break;
}
if let Event::Input(key) = events.next()? {
if key == Key::Char('q') {
break;
}
_ => {}
}
}

View File

@@ -1,12 +1,12 @@
use crate::util::{RandomSignal, SinSignal, StatefulList, TabsState};
const TASKS: [&'static str; 24] = [
const TASKS: [&str; 24] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
"Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17", "Item18", "Item19",
"Item20", "Item21", "Item22", "Item23", "Item24",
];
const LOGS: [(&'static str, &'static str); 26] = [
const LOGS: [(&str, &str); 26] = [
("Event1", "INFO"),
("Event2", "INFO"),
("Event3", "CRITICAL"),
@@ -35,7 +35,7 @@ const LOGS: [(&'static str, &'static str); 26] = [
("Event26", "INFO"),
];
const EVENTS: [(&'static str, u64); 24] = [
const EVENTS: [(&str, u64); 24] = [
("B1", 9),
("B2", 12),
("B3", 5),

View File

@@ -1,26 +1,30 @@
use crate::demo::App;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans},
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
widgets::{
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Paragraph, Row, Sparkline,
Table, Tabs, Text,
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, ListItem, Paragraph, Row,
Sparkline, Table, Tabs, Wrap,
},
Frame,
};
use crate::demo::App;
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size());
let tabs = Tabs::default()
let titles = app
.tabs
.titles
.iter()
.map(|t| Spans::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title(app.title))
.titles(&app.tabs.titles)
.style(Style::default().fg(Color::Green))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index);
f.render_widget(tabs, chunks[0]);
@@ -64,13 +68,13 @@ where
let label = format!("{:.2}%", app.progress * 100.0);
let gauge = Gauge::default()
.block(Block::default().title("Gauge:"))
.style(
.gauge_style(
Style::default()
.fg(Color::Magenta)
.bg(Color::Black)
.modifier(Modifier::ITALIC | Modifier::BOLD),
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
)
.label(&label)
.label(label)
.ratio(app.progress);
f.render_widget(gauge, chunks[0]);
@@ -110,29 +114,41 @@ where
.split(chunks[0]);
// Draw tasks
let tasks = app.tasks.items.iter().map(|i| Text::raw(*i));
let tasks: Vec<ListItem> = app
.tasks
.items
.iter()
.map(|i| ListItem::new(vec![Spans::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks)
.block(Block::default().borders(Borders::ALL).title("List"))
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::BOLD))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> ");
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
// Draw logs
let info_style = Style::default().fg(Color::White);
let info_style = Style::default().fg(Color::Blue);
let warning_style = Style::default().fg(Color::Yellow);
let error_style = Style::default().fg(Color::Magenta);
let critical_style = Style::default().fg(Color::Red);
let logs = app.logs.items.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
let logs: Vec<ListItem> = app
.logs
.items
.iter()
.map(|&(evt, level)| {
let s = match level {
"ERROR" => error_style,
"CRITICAL" => critical_style,
"WARNING" => warning_style,
_ => info_style,
},
)
});
};
let content = vec![Spans::from(vec![
Span::styled(format!("{:<9}", level), s),
Span::raw(evt),
])];
ListItem::new(content)
})
.collect();
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
}
@@ -151,19 +167,28 @@ where
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.modifier(Modifier::ITALIC),
.add_modifier(Modifier::ITALIC),
)
.label_style(Style::default().fg(Color::Yellow))
.style(Style::default().fg(Color::Green));
.bar_style(Style::default().fg(Color::Green));
f.render_widget(barchart, chunks[1]);
}
if app.show_chart {
let x_labels = [
format!("{}", app.signals.window[0]),
format!("{}", (app.signals.window[0] + app.signals.window[1]) / 2.0),
format!("{}", app.signals.window[1]),
let x_labels = vec![
Span::styled(
format!("{}", app.signals.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!(
"{}",
(app.signals.window[0] + app.signals.window[1]) / 2.0
)),
Span::styled(
format!("{}", app.signals.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = [
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
@@ -179,30 +204,35 @@ where
.style(Style::default().fg(Color::Yellow))
.data(&app.signals.sin2.points),
];
let chart = Chart::default()
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds(app.signals.window)
.labels(&x_labels),
.labels(x_labels),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([-20.0, 20.0])
.labels(&["-20", "0", "20"]),
)
.datasets(&datasets);
.labels(vec![
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("0"),
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[1]);
}
}
@@ -211,28 +241,40 @@ fn draw_text<B>(f: &mut Frame<B>, area: Rect)
where
B: Backend,
{
let text = [
Text::raw("This is a paragraph with several lines. You can change style your text the way you want.\n\nFox example: "),
Text::styled("under", Style::default().fg(Color::Red)),
Text::raw(" "),
Text::styled("the", Style::default().fg(Color::Green)),
Text::raw(" "),
Text::styled("rainbow", Style::default().fg(Color::Blue)),
Text::raw(".\nOh and if you didn't "),
Text::styled("notice", Style::default().modifier(Modifier::ITALIC)),
Text::raw(" you can "),
Text::styled("automatically", Style::default().modifier(Modifier::BOLD)),
Text::raw(" "),
Text::styled("wrap", Style::default().modifier(Modifier::REVERSED)),
Text::raw(" your "),
Text::styled("text", Style::default().modifier(Modifier::UNDERLINED)),
Text::raw(".\nOne more thing is that it should display unicode characters: 10€")
let text = vec![
Spans::from("This is a paragraph with several lines. You can change style your text the way you want"),
Spans::from(""),
Spans::from(vec![
Span::from("For example: "),
Span::styled("under", Style::default().fg(Color::Red)),
Span::raw(" "),
Span::styled("the", Style::default().fg(Color::Green)),
Span::raw(" "),
Span::styled("rainbow", Style::default().fg(Color::Blue)),
Span::raw("."),
]),
Spans::from(vec![
Span::raw("Oh and if you didn't "),
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
Span::raw(" you can "),
Span::styled("automatically", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" "),
Span::styled("wrap", Style::default().add_modifier(Modifier::REVERSED)),
Span::raw(" your "),
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
Span::raw(".")
]),
Spans::from(
"One more thing is that it should display unicode characters: 10€"
),
];
let block = Block::default()
.borders(Borders::ALL)
.title("Footer")
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::BOLD));
let paragraph = Paragraph::new(text.iter()).block(block).wrap(true);
let block = Block::default().borders(Borders::ALL).title(Span::styled(
"Footer",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
let paragraph = Paragraph::new(text).block(block).wrap(Wrap::default());
f.render_widget(paragraph, area);
}
@@ -247,7 +289,7 @@ where
let up_style = Style::default().fg(Color::Green);
let failure_style = Style::default()
.fg(Color::Red)
.modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
let header = ["Server", "Location", "Status"];
let rows = app.servers.iter().map(|s| {
let style = if s.status == "Up" {

View File

@@ -56,14 +56,13 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
let mut app = App::new();
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
@@ -80,30 +79,34 @@ fn main() -> Result<(), Box<dyn Error>> {
let gauge = Gauge::default()
.block(Block::default().title("Gauge1").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow))
.gauge_style(Style::default().fg(Color::Yellow))
.percent(app.progress1);
f.render_widget(gauge, chunks[0]);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge2").borders(Borders::ALL))
.style(Style::default().fg(Color::Magenta).bg(Color::Green))
.gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green))
.percent(app.progress2)
.label(&label);
.label(label);
f.render_widget(gauge, chunks[1]);
let gauge = Gauge::default()
.block(Block::default().title("Gauge3").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow))
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(app.progress3);
f.render_widget(gauge, chunks[2]);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge4").borders(Borders::ALL))
.style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC))
.block(Block::default().title("Gauge4"))
.gauge_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::ITALIC),
)
.percent(app.progress4)
.label(&label);
.label(label);
f.render_widget(gauge, chunks[3]);
})?;

View File

@@ -18,12 +18,11 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
@@ -42,13 +41,10 @@ fn main() -> Result<(), Box<dyn Error>> {
f.render_widget(block, chunks[2]);
})?;
match events.next()? {
Event::Input(input) => {
if let Key::Char('q') = input {
break;
}
if let Event::Input(input) = events.next()? {
if let Key::Char('q') = input {
break;
}
_ => {}
}
}

View File

@@ -11,26 +11,45 @@ use tui::{
backend::TermionBackend,
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, List, Text},
text::{Span, Spans},
widgets::{Block, Borders, List, ListItem},
Terminal,
};
struct App<'a> {
items: StatefulList<&'a str>,
items: StatefulList<(&'a str, usize)>,
events: Vec<(&'a str, &'a str)>,
info_style: Style,
warning_style: Style,
error_style: Style,
critical_style: Style,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
items: StatefulList::with_items(vec![
"Item0", "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8",
"Item9", "Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16",
"Item17", "Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
("Item0", 1),
("Item1", 2),
("Item2", 1),
("Item3", 3),
("Item4", 1),
("Item5", 4),
("Item6", 1),
("Item7", 3),
("Item8", 1),
("Item9", 6),
("Item10", 1),
("Item11", 3),
("Item12", 1),
("Item13", 2),
("Item14", 1),
("Item15", 1),
("Item16", 4),
("Item17", 1),
("Item18", 5),
("Item19", 4),
("Item20", 1),
("Item21", 2),
("Item22", 1),
("Item23", 3),
("Item24", 1),
]),
events: vec![
("Event1", "INFO"),
@@ -60,10 +79,6 @@ impl<'a> App<'a> {
("Event25", "INFO"),
("Event26", "INFO"),
],
info_style: Style::default().fg(Color::White),
warning_style: Style::default().fg(Color::Yellow),
error_style: Style::default().fg(Color::Magenta),
critical_style: Style::default().fg(Color::Red),
}
}
@@ -80,7 +95,6 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
@@ -88,33 +102,65 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new();
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let style = Style::default().fg(Color::Black).bg(Color::White);
let items = app.items.items.iter().map(|i| Text::raw(*i));
let items: Vec<ListItem> = app
.items
.items
.iter()
.map(|i| {
let mut lines = vec![Spans::from(i.0)];
for _ in 0..i.1 {
lines.push(Spans::from(Span::styled(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
Style::default().add_modifier(Modifier::ITALIC),
)));
}
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
})
.collect();
let items = List::new(items)
.block(Block::default().borders(Borders::ALL).title("List"))
.style(style)
.highlight_style(style.fg(Color::LightGreen).modifier(Modifier::BOLD))
.highlight_symbol(">");
.highlight_style(
Style::default()
.bg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
let events = app.events.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
"ERROR" => app.error_style,
"CRITICAL" => app.critical_style,
"WARNING" => app.warning_style,
_ => app.info_style,
},
)
});
let events: Vec<ListItem> = app
.events
.iter()
.map(|&(evt, level)| {
let s = match level {
"CRITICAL" => Style::default().fg(Color::Red),
"ERROR" => Style::default().fg(Color::Magenta),
"WARNING" => Style::default().fg(Color::Yellow),
"INFO" => Style::default().fg(Color::Blue),
_ => Style::default(),
};
let header = Spans::from(vec![
Span::styled(format!("{:<9}", level), s),
Span::raw(" "),
Span::styled(
"2020-01-01 10:00:00",
Style::default().add_modifier(Modifier::ITALIC),
),
]);
let log = Spans::from(vec![Span::raw(evt)]);
ListItem::new(vec![
Spans::from("-".repeat(chunks[1].width as usize)),
header,
Spans::from(""),
log,
])
})
.collect();
let events_list = List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.start_corner(Corner::BottomLeft);

View File

@@ -8,7 +8,8 @@ use tui::{
backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Paragraph, Text},
text::{Span, Spans},
widgets::{Block, Borders, Paragraph, Wrap},
Terminal,
};
@@ -19,13 +20,12 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
let mut scroll: u16 = 0;
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.
@@ -34,7 +34,7 @@ fn main() -> Result<(), Box<dyn Error>> {
long_line.push('\n');
let block = Block::default()
.style(Style::default().bg(Color::White));
.style(Style::default().bg(Color::White).fg(Color::Black));
f.render_widget(block, size);
let chunks = Layout::default()
@@ -51,56 +51,60 @@ fn main() -> Result<(), Box<dyn Error>> {
)
.split(size);
let text = [
Text::raw("This is a line \n"),
Text::styled("This is a line \n", Style::default().fg(Color::Red)),
Text::styled("This is a line\n", Style::default().bg(Color::Blue)),
Text::styled(
"This is a longer line\n",
Style::default().modifier(Modifier::CROSSED_OUT),
),
Text::styled(&long_line, Style::default().bg(Color::Green)),
Text::styled(
"This is a line\n",
Style::default().fg(Color::Green).modifier(Modifier::ITALIC),
),
let text = vec![
Spans::from("This is a line "),
Spans::from(Span::styled("This is a line ", Style::default().fg(Color::Red))),
Spans::from(Span::styled("This is a line", Style::default().bg(Color::Blue))),
Spans::from(Span::styled(
"This is a longer line",
Style::default().add_modifier(Modifier::CROSSED_OUT),
)),
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
Spans::from(Span::styled(
"This is a line",
Style::default().fg(Color::Green).add_modifier(Modifier::ITALIC),
)),
];
let block = Block::default()
.borders(Borders::ALL)
.title_style(Style::default().modifier(Modifier::BOLD));
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Left, no wrap"))
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.style(Style::default().bg(Color::White).fg(Color::Black))
.title(Span::styled(title, Style::default().add_modifier(Modifier::BOLD)))
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Left, no wrap"))
.alignment(Alignment::Left);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Left, wrap"))
let paragraph = Paragraph::new(text.clone())
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Left, wrap"))
.alignment(Alignment::Left)
.wrap(true);
.wrap(Wrap::default());
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Center, wrap"))
let paragraph = Paragraph::new(text.clone())
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Center, wrap"))
.alignment(Alignment::Center)
.wrap(true)
.scroll(scroll);
.wrap(Wrap::default())
.scroll((scroll, 0));
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Right, wrap"))
let paragraph = Paragraph::new(text)
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Right, wrap"))
.alignment(Alignment::Right)
.wrap(true);
.wrap(Wrap::default());
f.render_widget(paragraph, chunks[3]);
})?;
scroll += 1;
scroll %= 10;
match events.next()? {
Event::Input(key) => {
if key == Key::Char('q') {
break;
}
if let Event::Input(key) = events.next()? {
if key == Key::Char('q') {
break;
}
_ => {}
}
}
Ok(())

81
examples/paragraph2.rs Normal file
View File

@@ -0,0 +1,81 @@
#[allow(dead_code)]
mod util;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Alignment, Margin},
style::{Color, Style},
text::{Span, Spans},
widgets::{Block, Borders, Paragraph, Wrap},
Terminal,
};
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua";
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let events = Events::new();
let mut i = 0;
let mut lines = Vec::with_capacity(100);
while i < 100 {
lines.push((i, format!("{}: {}", i, LOREM_IPSUM)));
i += 1;
}
loop {
terminal.draw(|f| {
let size = f.size();
let text: Vec<Spans> = lines
.iter()
.cloned()
.map(|(j, l)| {
let span = if i == j + 1 {
Span::styled(l, Style::default().bg(Color::Yellow))
} else {
Span::raw(l)
};
Spans::from(span)
})
.collect();
let mut wrap = Wrap::default();
wrap.scroll_callback = Some(Box::new(|text_area, lines| {
let len = lines.len() as u16;
(len.saturating_sub(text_area.height), 0)
}));
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(wrap)
.alignment(Alignment::Left);
f.render_widget(
paragraph,
size.inner(&Margin {
vertical: 2,
horizontal: 2,
}),
);
})?;
match events.next()? {
Event::Tick => {
lines.push((i, format!("{}: {}", i, LOREM_IPSUM)));
lines.remove(0);
i += 1;
}
Event::Input(key) => {
if key == Key::Char('q') {
break;
}
}
}
}
Ok(())
}

View File

@@ -4,13 +4,12 @@ mod util;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::layout::Rect;
use tui::widgets::Clear;
use tui::{
backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Paragraph, Text},
text::{Span, Spans},
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Terminal,
};
@@ -49,12 +48,11 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let size = f.size();
let chunks = Layout::default()
@@ -66,29 +64,29 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut long_line = s.repeat(usize::from(size.width)*usize::from(size.height)/300);
long_line.push('\n');
let text = [
Text::raw("This is a line \n"),
Text::styled("This is a line \n", Style::default().fg(Color::Red)),
Text::styled("This is a line\n", Style::default().bg(Color::Blue)),
Text::styled(
let text = vec![
Spans::from("This is a line "),
Spans::from(Span::styled("This is a line ", Style::default().fg(Color::Red))),
Spans::from(Span::styled("This is a line", Style::default().bg(Color::Blue))),
Spans::from(Span::styled(
"This is a longer line\n",
Style::default().modifier(Modifier::CROSSED_OUT),
),
Text::styled(&long_line, Style::default().bg(Color::Green)),
Text::styled(
Style::default().add_modifier(Modifier::CROSSED_OUT),
)),
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
Spans::from(Span::styled(
"This is a line\n",
Style::default().fg(Color::Green).modifier(Modifier::ITALIC),
),
Style::default().fg(Color::Green).add_modifier(Modifier::ITALIC),
)),
];
let paragraph = Paragraph::new(text.iter())
let paragraph = Paragraph::new(text.clone())
.block(Block::default().title("Left Block").borders(Borders::ALL))
.alignment(Alignment::Left).wrap(true);
.alignment(Alignment::Left).wrap(Wrap::default());
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.iter())
let paragraph = Paragraph::new(text)
.block(Block::default().title("Right Block").borders(Borders::ALL))
.alignment(Alignment::Left).wrap(true);
.alignment(Alignment::Left).wrap(Wrap::default());
f.render_widget(paragraph, chunks[1]);
let block = Block::default().title("Popup").borders(Borders::ALL);
@@ -97,13 +95,10 @@ fn main() -> Result<(), Box<dyn Error>> {
f.render_widget(block, area);
})?;
match events.next()? {
Event::Input(input) => {
if let Key::Char('q') = input {
break;
}
if let Event::Input(input) = events.next()? {
if let Key::Char('q') = input {
break;
}
_ => {}
}
}

View File

@@ -27,16 +27,17 @@ fn main() -> Result<(), Box<dyn Error>> {
let backend = RustboxBackend::new()?;
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Rustbox demo", cli.enhanced_graphics);
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(cli.tick_rate);
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
match terminal.backend().rustbox().peek_event(tick_rate, false) {
Ok(rustbox::Event::KeyEvent(key)) => match key {
terminal.draw(|f| ui::draw(f, &mut app))?;
if let Ok(rustbox::Event::KeyEvent(key)) =
terminal.backend().rustbox().peek_event(tick_rate, false)
{
match key {
Key::Char(c) => {
app.on_key(c);
}
@@ -53,8 +54,7 @@ fn main() -> Result<(), Box<dyn Error>> {
app.on_right();
}
_ => {}
},
_ => {}
}
}
if last_tick.elapsed() > tick_rate {
app.on_tick();

View File

@@ -56,7 +56,6 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup event handlers
let events = Events::new();
@@ -65,7 +64,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new();
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)

View File

@@ -80,7 +80,6 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
@@ -88,19 +87,21 @@ fn main() -> Result<(), Box<dyn Error>> {
// Input
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let rects = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.margin(5)
.split(f.size());
let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::BOLD);
let selected_style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD);
let normal_style = Style::default().fg(Color::White);
let header = ["Header1", "Header2", "Header3"];
let rows = table
.items
.iter()
.map(|i| Row::StyledData(i.into_iter(), normal_style));
.map(|i| Row::StyledData(i.iter(), normal_style));
let t = Table::new(header.iter(), rows)
.block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
@@ -113,8 +114,8 @@ fn main() -> Result<(), Box<dyn Error>> {
f.render_stateful_widget(t, rects[0], &mut table.state);
})?;
match events.next()? {
Event::Input(key) => match key {
if let Event::Input(key) = events.next()? {
match key {
Key::Char('q') => {
break;
}
@@ -125,8 +126,7 @@ fn main() -> Result<(), Box<dyn Error>> {
table.previous();
}
_ => {}
},
_ => {}
}
};
}

View File

@@ -10,7 +10,8 @@ use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::Altern
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Style},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, Tabs},
Terminal,
};
@@ -26,7 +27,6 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
@@ -37,7 +37,7 @@ fn main() -> Result<(), Box<dyn Error>> {
// Main loop
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -45,14 +45,29 @@ fn main() -> Result<(), Box<dyn Error>> {
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(size);
let block = Block::default().style(Style::default().bg(Color::White));
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
f.render_widget(block, size);
let tabs = Tabs::default()
let titles = app
.tabs
.titles
.iter()
.map(|t| {
let (first, rest) = t.split_at(1);
Spans::from(vec![
Span::styled(first, Style::default().fg(Color::Yellow)),
Span::styled(rest, Style::default().fg(Color::Green)),
])
})
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.titles(&app.tabs.titles)
.select(app.tabs.index)
.style(Style::default().fg(Color::Cyan))
.highlight_style(Style::default().fg(Color::Yellow));
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Black),
);
f.render_widget(tabs, chunks[0]);
let inner = match app.tabs.index {
0 => Block::default().title("Inner 0").borders(Borders::ALL),
@@ -64,16 +79,15 @@ fn main() -> Result<(), Box<dyn Error>> {
f.render_widget(inner, chunks[1]);
})?;
match events.next()? {
Event::Input(input) => match input {
if let Event::Input(input) = events.next()? {
match input {
Key::Char('q') => {
break;
}
Key::Right => app.tabs.next(),
Key::Left => app.tabs.previous(),
_ => {}
},
_ => {}
}
}
}
Ok(())

View File

@@ -35,11 +35,10 @@ fn main() -> Result<(), Box<dyn Error>> {
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Termion demo", cli.enhanced_graphics);
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;
match events.next()? {
Event::Input(key) => match key {

View File

@@ -14,18 +14,14 @@
mod util;
use crate::util::event::{Event, Events};
use std::{
error::Error,
io::{self, Write},
};
use termion::{
cursor::Goto, event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen,
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, List, Paragraph, Text},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, Borders, List, ListItem, Paragraph},
Terminal,
};
use unicode_width::UnicodeWidthStr;
@@ -71,7 +67,7 @@ fn main() -> Result<(), Box<dyn Error>> {
loop {
// Draw UI
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
@@ -85,41 +81,73 @@ fn main() -> Result<(), Box<dyn Error>> {
)
.split(f.size());
let msg = match app.input_mode {
InputMode::Normal => "Press q to exit, e to start editing.",
InputMode::Editing => "Press Esc to stop editing, Enter to record the message",
let (msg, style) = match app.input_mode {
InputMode::Normal => (
vec![
Span::raw("Press "),
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit, "),
Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to start editing."),
],
Style::default().add_modifier(Modifier::RAPID_BLINK),
),
InputMode::Editing => (
vec![
Span::raw("Press "),
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to stop editing, "),
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to record the message"),
],
Style::default(),
),
};
let text = [Text::raw(msg)];
let help_message = Paragraph::new(text.iter());
let mut text = Text::from(Spans::from(msg));
text.patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[0]);
let text = [Text::raw(&app.input)];
let input = Paragraph::new(text.iter())
.style(Style::default().fg(Color::Yellow))
let input = Paragraph::new(app.input.as_ref())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, chunks[1]);
let messages = app
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
{}
InputMode::Editing => {
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
f.set_cursor(
// Put cursor past the end of the input text
chunks[1].x + app.input.width() as u16 + 1,
// Move one line down, from the border to the input line
chunks[1].y + 1,
)
}
}
let messages: Vec<ListItem> = app
.messages
.iter()
.enumerate()
.map(|(i, m)| Text::raw(format!("{}: {}", i, m)));
.map(|(i, m)| {
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
ListItem::new(content)
})
.collect();
let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, chunks[2]);
})?;
// Put the cursor back inside the input box
write!(
terminal.backend_mut(),
"{}",
Goto(4 + app.input.width() as u16, 5)
)?;
// stdout is buffered, flush it to see the effect immediately when hitting backspace
io::stdout().flush().ok();
// Handle input
match events.next()? {
Event::Input(input) => match app.input_mode {
if let Event::Input(input) = events.next()? {
match app.input_mode {
InputMode::Normal => match input {
Key::Char('e') => {
app.input_mode = InputMode::Editing;
@@ -146,8 +174,7 @@ fn main() -> Result<(), Box<dyn Error>> {
}
_ => {}
},
},
_ => {}
}
}
}
Ok(())

View File

@@ -53,28 +53,24 @@ impl Events {
thread::spawn(move || {
let stdin = io::stdin();
for evt in stdin.keys() {
match evt {
Ok(key) => {
if let Err(_) = tx.send(Event::Input(key)) {
return;
}
if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key {
return;
}
if let Ok(key) = evt {
if let Err(err) = tx.send(Event::Input(key)) {
eprintln!("{}", err);
return;
}
if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key {
return;
}
Err(_) => {}
}
}
})
};
let tick_handle = {
let tx = tx.clone();
thread::spawn(move || {
let tx = tx.clone();
loop {
tx.send(Event::Tick).unwrap();
thread::sleep(config.tick_rate);
thread::spawn(move || loop {
if tx.send(Event::Tick).is_err() {
break;
}
thread::sleep(config.tick_rate);
})
};
Events {

View File

@@ -93,7 +93,7 @@ impl<T> StatefulList<T> {
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items: items,
items,
}
}

View File

@@ -1,8 +1,9 @@
use std::{
fmt,
io::{self, Write},
use crate::{
backend::Backend,
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
};
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
@@ -12,10 +13,10 @@ use crossterm::{
},
terminal::{self, Clear, ClearType},
};
use crate::backend::Backend;
use crate::style::{Color, Modifier};
use crate::{buffer::Cell, layout::Rect, style};
use std::{
fmt,
io::{self, Write},
};
pub struct CrosstermBackend<W: Write> {
buffer: W,
@@ -54,41 +55,36 @@ where
use fmt::Write;
let mut string = String::with_capacity(content.size_hint().0 * 3);
let mut style = style::Style::default();
let mut last_y = 0;
let mut last_x = 0;
let mut inst = 0;
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
for (x, y, cell) in content {
if y != last_y || x != last_x + 1 || inst == 0 {
// 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) {
map_error(queue!(string, MoveTo(x, y)))?;
}
last_x = x;
last_y = y;
if cell.style.modifier != style.modifier {
last_pos = Some((x, y));
if cell.modifier != modifier {
let diff = ModifierDiff {
from: style.modifier,
to: cell.style.modifier,
from: modifier,
to: cell.modifier,
};
diff.queue(&mut string)?;
inst += 1;
style.modifier = cell.style.modifier;
modifier = cell.modifier;
}
if cell.style.fg != style.fg {
let color = CColor::from(cell.style.fg);
if cell.fg != fg {
let color = CColor::from(cell.fg);
map_error(queue!(string, SetForegroundColor(color)))?;
style.fg = cell.style.fg;
inst += 1;
fg = cell.fg;
}
if cell.style.bg != style.bg {
let color = CColor::from(cell.style.bg);
if cell.bg != bg {
let color = CColor::from(cell.bg);
map_error(queue!(string, SetBackgroundColor(color)))?;
style.bg = cell.style.bg;
inst += 1;
bg = cell.bg;
}
string.push_str(&cell.symbol);
inst += 1;
}
map_error(queue!(

View File

@@ -3,7 +3,7 @@ use std::io;
use crate::backend::Backend;
use crate::buffer::Cell;
use crate::layout::Rect;
use crate::style::{Color, Modifier, Style};
use crate::style::{Color, Modifier};
use crate::symbols::{bar, block};
#[cfg(unix)]
use crate::symbols::{line, DOT};
@@ -41,44 +41,41 @@ impl Backend for CursesBackend {
{
let mut last_col = 0;
let mut last_row = 0;
let mut style = Style {
fg: Color::Reset,
bg: Color::Reset,
modifier: Modifier::empty(),
};
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut modifier = Modifier::empty();
let mut curses_style = CursesStyle {
fg: easycurses::Color::White,
bg: easycurses::Color::Black,
};
let mut update_color = false;
for (col, row, cell) in content {
// eprintln!("{:?}", cell);
if row != last_row || col != last_col + 1 {
self.curses.move_rc(i32::from(row), i32::from(col));
}
last_col = col;
last_row = row;
if cell.style.modifier != style.modifier {
apply_modifier_diff(&mut self.curses.win, style.modifier, cell.style.modifier);
style.modifier = cell.style.modifier;
if cell.modifier != modifier {
apply_modifier_diff(&mut self.curses.win, modifier, cell.modifier);
modifier = cell.modifier;
};
if cell.style.fg != style.fg {
if cell.fg != fg {
update_color = true;
if let Some(ccolor) = cell.style.fg.into() {
style.fg = cell.style.fg;
if let Some(ccolor) = cell.fg.into() {
fg = cell.fg;
curses_style.fg = ccolor;
} else {
style.fg = Color::White;
fg = Color::White;
curses_style.fg = easycurses::Color::White;
}
};
if cell.style.bg != style.bg {
if cell.bg != bg {
update_color = true;
if let Some(ccolor) = cell.style.bg.into() {
style.bg = cell.style.bg;
if let Some(ccolor) = cell.bg.into() {
bg = cell.bg;
curses_style.bg = ccolor;
} else {
style.bg = Color::Black;
bg = Color::Black;
curses_style.bg = easycurses::Color::Black;
}
};

View File

@@ -34,9 +34,9 @@ impl Backend for RustboxBackend {
self.rustbox.print(
x as usize,
y as usize,
cell.style.modifier.into(),
cell.style.fg.into(),
cell.style.bg.into(),
cell.modifier.into(),
cell.fg.into(),
cell.bg.into(),
&cell.symbol,
);
}

View File

@@ -1,11 +1,13 @@
use std::fmt;
use std::io;
use std::io::Write;
use super::Backend;
use crate::buffer::Cell;
use crate::layout::Rect;
use crate::style;
use crate::{
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
};
use std::{
fmt,
io::{self, Write},
};
pub struct TermionBackend<W>
where
@@ -77,49 +79,44 @@ where
use std::fmt::Write;
let mut string = String::with_capacity(content.size_hint().0 * 3);
let mut style = style::Style::default();
let mut last_y = 0;
let mut last_x = 0;
let mut inst = 0;
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
for (x, y, cell) in content {
if y != last_y || x != last_x + 1 || inst == 0 {
// 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) {
write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap();
inst += 1;
}
last_x = x;
last_y = y;
if cell.style.modifier != style.modifier {
last_pos = Some((x, y));
if cell.modifier != modifier {
write!(
string,
"{}",
ModifierDiff {
from: style.modifier,
to: cell.style.modifier
from: modifier,
to: cell.modifier
}
)
.unwrap();
style.modifier = cell.style.modifier;
inst += 1;
modifier = cell.modifier;
}
if cell.style.fg != style.fg {
write!(string, "{}", Fg(cell.style.fg)).unwrap();
style.fg = cell.style.fg;
inst += 1;
if cell.fg != fg {
write!(string, "{}", Fg(cell.fg)).unwrap();
fg = cell.fg;
}
if cell.style.bg != style.bg {
write!(string, "{}", Bg(cell.style.bg)).unwrap();
style.bg = cell.style.bg;
inst += 1;
if cell.bg != bg {
write!(string, "{}", Bg(cell.bg)).unwrap();
bg = cell.bg;
}
string.push_str(&cell.symbol);
inst += 1;
}
write!(
self.stdout,
"{}{}{}{}",
string,
Fg(style::Color::Reset),
Bg(style::Color::Reset),
Fg(Color::Reset),
Bg(Color::Reset),
termion::style::Reset,
)
}
@@ -135,64 +132,64 @@ where
}
}
struct Fg(style::Color);
struct Fg(Color);
struct Bg(style::Color);
struct Bg(Color);
struct ModifierDiff {
from: style::Modifier,
to: style::Modifier,
from: Modifier,
to: Modifier,
}
impl fmt::Display for Fg {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use termion::color::Color;
use termion::color::Color as TermionColor;
match self.0 {
style::Color::Reset => termion::color::Reset.write_fg(f),
style::Color::Black => termion::color::Black.write_fg(f),
style::Color::Red => termion::color::Red.write_fg(f),
style::Color::Green => termion::color::Green.write_fg(f),
style::Color::Yellow => termion::color::Yellow.write_fg(f),
style::Color::Blue => termion::color::Blue.write_fg(f),
style::Color::Magenta => termion::color::Magenta.write_fg(f),
style::Color::Cyan => termion::color::Cyan.write_fg(f),
style::Color::Gray => termion::color::White.write_fg(f),
style::Color::DarkGray => termion::color::LightBlack.write_fg(f),
style::Color::LightRed => termion::color::LightRed.write_fg(f),
style::Color::LightGreen => termion::color::LightGreen.write_fg(f),
style::Color::LightBlue => termion::color::LightBlue.write_fg(f),
style::Color::LightYellow => termion::color::LightYellow.write_fg(f),
style::Color::LightMagenta => termion::color::LightMagenta.write_fg(f),
style::Color::LightCyan => termion::color::LightCyan.write_fg(f),
style::Color::White => termion::color::LightWhite.write_fg(f),
style::Color::Indexed(i) => termion::color::AnsiValue(i).write_fg(f),
style::Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_fg(f),
Color::Reset => termion::color::Reset.write_fg(f),
Color::Black => termion::color::Black.write_fg(f),
Color::Red => termion::color::Red.write_fg(f),
Color::Green => termion::color::Green.write_fg(f),
Color::Yellow => termion::color::Yellow.write_fg(f),
Color::Blue => termion::color::Blue.write_fg(f),
Color::Magenta => termion::color::Magenta.write_fg(f),
Color::Cyan => termion::color::Cyan.write_fg(f),
Color::Gray => termion::color::White.write_fg(f),
Color::DarkGray => termion::color::LightBlack.write_fg(f),
Color::LightRed => termion::color::LightRed.write_fg(f),
Color::LightGreen => termion::color::LightGreen.write_fg(f),
Color::LightBlue => termion::color::LightBlue.write_fg(f),
Color::LightYellow => termion::color::LightYellow.write_fg(f),
Color::LightMagenta => termion::color::LightMagenta.write_fg(f),
Color::LightCyan => termion::color::LightCyan.write_fg(f),
Color::White => termion::color::LightWhite.write_fg(f),
Color::Indexed(i) => termion::color::AnsiValue(i).write_fg(f),
Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_fg(f),
}
}
}
impl fmt::Display for Bg {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use termion::color::Color;
use termion::color::Color as TermionColor;
match self.0 {
style::Color::Reset => termion::color::Reset.write_bg(f),
style::Color::Black => termion::color::Black.write_bg(f),
style::Color::Red => termion::color::Red.write_bg(f),
style::Color::Green => termion::color::Green.write_bg(f),
style::Color::Yellow => termion::color::Yellow.write_bg(f),
style::Color::Blue => termion::color::Blue.write_bg(f),
style::Color::Magenta => termion::color::Magenta.write_bg(f),
style::Color::Cyan => termion::color::Cyan.write_bg(f),
style::Color::Gray => termion::color::White.write_bg(f),
style::Color::DarkGray => termion::color::LightBlack.write_bg(f),
style::Color::LightRed => termion::color::LightRed.write_bg(f),
style::Color::LightGreen => termion::color::LightGreen.write_bg(f),
style::Color::LightBlue => termion::color::LightBlue.write_bg(f),
style::Color::LightYellow => termion::color::LightYellow.write_bg(f),
style::Color::LightMagenta => termion::color::LightMagenta.write_bg(f),
style::Color::LightCyan => termion::color::LightCyan.write_bg(f),
style::Color::White => termion::color::LightWhite.write_bg(f),
style::Color::Indexed(i) => termion::color::AnsiValue(i).write_bg(f),
style::Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_bg(f),
Color::Reset => termion::color::Reset.write_bg(f),
Color::Black => termion::color::Black.write_bg(f),
Color::Red => termion::color::Red.write_bg(f),
Color::Green => termion::color::Green.write_bg(f),
Color::Yellow => termion::color::Yellow.write_bg(f),
Color::Blue => termion::color::Blue.write_bg(f),
Color::Magenta => termion::color::Magenta.write_bg(f),
Color::Cyan => termion::color::Cyan.write_bg(f),
Color::Gray => termion::color::White.write_bg(f),
Color::DarkGray => termion::color::LightBlack.write_bg(f),
Color::LightRed => termion::color::LightRed.write_bg(f),
Color::LightGreen => termion::color::LightGreen.write_bg(f),
Color::LightBlue => termion::color::LightBlue.write_bg(f),
Color::LightYellow => termion::color::LightYellow.write_bg(f),
Color::LightMagenta => termion::color::LightMagenta.write_bg(f),
Color::LightCyan => termion::color::LightCyan.write_bg(f),
Color::White => termion::color::LightWhite.write_bg(f),
Color::Indexed(i) => termion::color::AnsiValue(i).write_bg(f),
Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_bg(f),
}
}
}
@@ -200,63 +197,61 @@ impl fmt::Display for Bg {
impl fmt::Display for ModifierDiff {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let remove = self.from - self.to;
if remove.contains(style::Modifier::REVERSED) {
if remove.contains(Modifier::REVERSED) {
write!(f, "{}", termion::style::NoInvert)?;
}
if remove.contains(style::Modifier::BOLD) {
if remove.contains(Modifier::BOLD) {
// XXX: the termion NoBold flag actually enables double-underline on ECMA-48 compliant
// terminals, and NoFaint additionally disables bold... so we use this trick to get
// the right semantics.
write!(f, "{}", termion::style::NoFaint)?;
if self.to.contains(style::Modifier::DIM) {
if self.to.contains(Modifier::DIM) {
write!(f, "{}", termion::style::Faint)?;
}
}
if remove.contains(style::Modifier::ITALIC) {
if remove.contains(Modifier::ITALIC) {
write!(f, "{}", termion::style::NoItalic)?;
}
if remove.contains(style::Modifier::UNDERLINED) {
if remove.contains(Modifier::UNDERLINED) {
write!(f, "{}", termion::style::NoUnderline)?;
}
if remove.contains(style::Modifier::DIM) {
if remove.contains(Modifier::DIM) {
write!(f, "{}", termion::style::NoFaint)?;
// XXX: the NoFaint flag additionally disables bold as well, so we need to re-enable it
// here if we want it.
if self.to.contains(style::Modifier::BOLD) {
if self.to.contains(Modifier::BOLD) {
write!(f, "{}", termion::style::Bold)?;
}
}
if remove.contains(style::Modifier::CROSSED_OUT) {
if remove.contains(Modifier::CROSSED_OUT) {
write!(f, "{}", termion::style::NoCrossedOut)?;
}
if remove.contains(style::Modifier::SLOW_BLINK)
|| remove.contains(style::Modifier::RAPID_BLINK)
{
if remove.contains(Modifier::SLOW_BLINK) || remove.contains(Modifier::RAPID_BLINK) {
write!(f, "{}", termion::style::NoBlink)?;
}
let add = self.to - self.from;
if add.contains(style::Modifier::REVERSED) {
if add.contains(Modifier::REVERSED) {
write!(f, "{}", termion::style::Invert)?;
}
if add.contains(style::Modifier::BOLD) {
if add.contains(Modifier::BOLD) {
write!(f, "{}", termion::style::Bold)?;
}
if add.contains(style::Modifier::ITALIC) {
if add.contains(Modifier::ITALIC) {
write!(f, "{}", termion::style::Italic)?;
}
if add.contains(style::Modifier::UNDERLINED) {
if add.contains(Modifier::UNDERLINED) {
write!(f, "{}", termion::style::Underline)?;
}
if add.contains(style::Modifier::DIM) {
if add.contains(Modifier::DIM) {
write!(f, "{}", termion::style::Faint)?;
}
if add.contains(style::Modifier::CROSSED_OUT) {
if add.contains(Modifier::CROSSED_OUT) {
write!(f, "{}", termion::style::CrossedOut)?;
}
if add.contains(style::Modifier::SLOW_BLINK) || add.contains(style::Modifier::RAPID_BLINK) {
if add.contains(Modifier::SLOW_BLINK) || add.contains(Modifier::RAPID_BLINK) {
write!(f, "{}", termion::style::Blink)?;
}

View File

@@ -1,8 +1,12 @@
use crate::backend::Backend;
use crate::buffer::{Buffer, Cell};
use crate::layout::Rect;
use std::io;
use crate::{
backend::Backend,
buffer::{Buffer, Cell},
layout::Rect,
};
use std::{fmt::Write, io};
use unicode_width::UnicodeWidthStr;
/// A backend used for the integration tests.
#[derive(Debug)]
pub struct TestBackend {
width: u16,
@@ -12,6 +16,35 @@ pub struct TestBackend {
pos: (u16, u16),
}
/// Returns a string representation of the given buffer for debugging purpose.
fn buffer_view(buffer: &Buffer) -> String {
let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
for cells in buffer.content.chunks(buffer.area.width as usize) {
let mut overwritten = vec![];
let mut skip: usize = 0;
view.push('"');
for (x, c) in cells.iter().enumerate() {
if skip == 0 {
view.push_str(&c.symbol);
} else {
overwritten.push((x, &c.symbol))
}
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
}
view.push('"');
if !overwritten.is_empty() {
write!(
&mut view,
" Hidden by multi-width symbols: {:?}",
overwritten
)
.unwrap();
}
view.push_str("\n");
}
view
}
impl TestBackend {
pub fn new(width: u16, height: u16) -> TestBackend {
TestBackend {
@@ -26,6 +59,44 @@ impl TestBackend {
pub fn buffer(&self) -> &Buffer {
&self.buffer
}
pub fn assert_buffer(&self, expected: &Buffer) {
assert_eq!(expected.area, self.buffer.area);
let diff = expected.diff(&self.buffer);
if diff.is_empty() {
return;
}
let mut debug_info = String::from("Buffers are not equal");
debug_info.push_str("\n");
debug_info.push_str("Expected:");
debug_info.push_str("\n");
let expected_view = buffer_view(expected);
debug_info.push_str(&expected_view);
debug_info.push_str("\n");
debug_info.push_str("Got:");
debug_info.push_str("\n");
let view = buffer_view(&self.buffer);
debug_info.push_str(&view);
debug_info.push_str("\n");
debug_info.push_str("Diff:");
debug_info.push_str("\n");
let nice_diff = diff
.iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = expected.get(*x, *y);
format!(
"{}: at ({}, {}) expected {:?} got {:?}",
i, x, y, expected_cell, cell
)
})
.collect::<Vec<String>>()
.join("\n");
debug_info.push_str(&nice_diff);
panic!(debug_info);
}
}
impl Backend for TestBackend {
@@ -35,32 +106,38 @@ impl Backend for TestBackend {
{
for (x, y, c) in content {
let cell = self.buffer.get_mut(x, y);
cell.symbol = c.symbol.clone();
cell.style = c.style;
*cell = c.clone();
}
Ok(())
}
fn hide_cursor(&mut self) -> Result<(), io::Error> {
self.cursor = false;
Ok(())
}
fn show_cursor(&mut self) -> Result<(), io::Error> {
self.cursor = true;
Ok(())
}
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
Ok(self.pos)
}
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
self.pos = (x, y);
Ok(())
}
fn clear(&mut self) -> Result<(), io::Error> {
Ok(())
}
fn size(&self) -> Result<Rect, io::Error> {
Ok(Rect::new(0, 0, self.width, self.height))
}
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}

View File

@@ -1,18 +1,19 @@
use crate::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Span, Spans},
};
use std::cmp::min;
use std::fmt;
use std::usize;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::layout::Rect;
use crate::style::{Color, Modifier, Style};
/// A buffer cell
#[derive(Debug, Clone, PartialEq)]
pub struct Cell {
pub symbol: String,
pub style: Style,
pub fg: Color,
pub bg: Color,
pub modifier: Modifier,
}
impl Cell {
@@ -29,29 +30,33 @@ impl Cell {
}
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
self.style.fg = color;
self.fg = color;
self
}
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
self.style.bg = color;
self
}
pub fn set_modifier(&mut self, modifier: Modifier) -> &mut Cell {
self.style.modifier = modifier;
self.bg = color;
self
}
pub fn set_style(&mut self, style: Style) -> &mut Cell {
self.style = style;
if let Some(c) = style.fg {
self.fg = c;
}
if let Some(c) = style.bg {
self.bg = c;
}
self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
self
}
pub fn reset(&mut self) {
self.symbol.clear();
self.symbol.push(' ');
self.style.reset();
self.fg = Color::Reset;
self.bg = Color::Reset;
self.modifier = Modifier::empty();
}
}
@@ -59,7 +64,9 @@ impl Default for Cell {
fn default() -> Cell {
Cell {
symbol: " ".into(),
style: Default::default(),
fg: Color::Reset,
bg: Color::Reset,
modifier: Modifier::empty(),
}
}
}
@@ -84,15 +91,14 @@ impl Default for Cell {
/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
/// assert_eq!(buf.get(5, 0), &Cell{
/// symbol: String::from("r"),
/// style: Style {
/// fg: Color::Red,
/// bg: Color::White,
/// modifier: Modifier::empty()
/// }});
/// fg: Color::Red,
/// bg: Color::White,
/// modifier: Modifier::empty()
/// });
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol, "x");
/// ```
#[derive(Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
@@ -110,49 +116,6 @@ impl Default for Buffer {
}
}
impl fmt::Debug for Buffer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Buffer: {:?}", self.area)?;
f.write_str("Content (quoted lines):\n")?;
for cells in self.content.chunks(self.area.width as usize) {
let mut line = String::new();
let mut overwritten = vec![];
let mut skip: usize = 0;
for (x, c) in cells.iter().enumerate() {
if skip == 0 {
line.push_str(&c.symbol);
} else {
overwritten.push((x, &c.symbol))
}
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
}
f.write_fmt(format_args!("{:?},", line))?;
if !overwritten.is_empty() {
f.write_fmt(format_args!(
" Hidden by multi-width symbols: {:?}",
overwritten
))?;
}
f.write_str("\n")?;
}
f.write_str("Style:\n")?;
for cells in self.content.chunks(self.area.width as usize) {
f.write_str("|")?;
for cell in cells {
write!(
f,
"{} {} {}|",
cell.style.fg.code(),
cell.style.bg.code(),
cell.style.modifier.code()
)?;
}
f.write_str("\n")?;
}
Ok(())
}
}
impl Buffer {
/// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer {
@@ -322,6 +285,9 @@ impl Buffer {
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
for s in graphemes {
let width = s.width();
if width == 0 {
continue;
}
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
// change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
if width > max_offset.saturating_sub(x_offset) {
@@ -340,6 +306,35 @@ impl Buffer {
(x_offset as u16, y)
}
pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &spans.0 {
if remaining_width == 0 {
break;
}
let pos = self.set_stringn(
x,
y,
span.content.as_ref(),
remaining_width as usize,
span.style,
);
let w = pos.0.saturating_sub(x);
x = pos.0;
remaining_width = remaining_width.saturating_sub(w);
}
(x, y)
}
pub fn set_span<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) {
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
}
#[deprecated(
since = "0.10.0",
note = "You should use styling capabilities of `Buffer::set_style`"
)]
pub fn set_background(&mut self, area: Rect, color: Color) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
@@ -348,6 +343,14 @@ impl Buffer {
}
}
pub fn set_style(&mut self, area: Rect, style: Style) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
self.get_mut(x, y).set_style(style);
}
}
}
/// Resize the buffer so that the mapped area matches the given area and that the buffer
/// length is equal to area.width * area.height
pub fn resize(&mut self, area: Rect) {
@@ -520,6 +523,22 @@ mod tests {
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
}
#[test]
fn buffer_set_string_zero_width() {
let area = Rect::new(0, 0, 1, 1);
let mut buffer = Buffer::empty(area);
// Leading grapheme with zero width
let s = "\u{1}a";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
// Trailing grapheme with zero with
let s = "a\u{1}";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
}
#[test]
fn buffer_set_string_double_width() {
let area = Rect::new(0, 0, 5, 1);

View File

@@ -174,12 +174,12 @@ impl Layout {
/// ]
/// );
/// ```
pub fn split(self, area: Rect) -> Vec<Rect> {
pub fn split(&self, area: Rect) -> Vec<Rect> {
// TODO: Maybe use a fixed size cache ?
LAYOUT_CACHE.with(|c| {
c.borrow_mut()
.entry((area, self.clone()))
.or_insert_with(|| split(area, &self))
.or_insert_with(|| split(area, self))
.clone()
})
}
@@ -209,6 +209,8 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
let mut ccs: Vec<CassowaryConstraint> =
Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6);
for elt in &elements {
ccs.push(elt.width | GE(REQUIRED) | 0f64);
ccs.push(elt.height | GE(REQUIRED) | 0f64);
ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
@@ -461,6 +463,31 @@ impl Rect {
mod tests {
use super::*;
#[test]
fn test_vertical_split_by_height() {
let target = Rect {
x: 2,
y: 2,
width: 10,
height: 10,
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Max(5),
Constraint::Min(1),
]
.as_ref(),
)
.split(target);
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
}
#[test]
fn test_rect_size_truncation() {
for width in 256u16..300u16 {

View File

@@ -9,7 +9,7 @@
//!
//! ```toml
//! [dependencies]
//! tui = "0.9"
//! tui = "0.10"
//! termion = "1.5"
//! ```
//!
@@ -20,7 +20,7 @@
//! ```toml
//! [dependencies]
//! crossterm = "0.17"
//! tui = { version = "0.9", default-features = false, features = ['crossterm'] }
//! tui = { version = "0.10", default-features = false, features = ['crossterm'] }
//! ```
//!
//! The same logic applies for all other available backends.
@@ -88,7 +88,7 @@
//! let stdout = io::stdout().into_raw_mode()?;
//! let backend = TermionBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! terminal.draw(|mut f| {
//! terminal.draw(|f| {
//! let size = f.size();
//! let block = Block::default()
//! .title("Block")
@@ -116,7 +116,7 @@
//! let stdout = io::stdout().into_raw_mode()?;
//! let backend = TermionBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! terminal.draw(|mut f| {
//! terminal.draw(|f| {
//! let chunks = Layout::default()
//! .direction(Direction::Vertical)
//! .margin(1)
@@ -145,14 +145,13 @@
//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
//! corresponding area.
#![deny(warnings)]
pub mod backend;
pub mod buffer;
pub mod layout;
pub mod style;
pub mod symbols;
pub mod terminal;
pub mod text;
pub mod widgets;
pub use self::terminal::{Frame, Terminal};
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};

View File

@@ -1,6 +1,9 @@
//! `style` contains the primitives used to control how your user interface will look.
use bitflags::bitflags;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Color {
Reset,
Black,
@@ -23,35 +26,19 @@ pub enum Color {
Indexed(u8),
}
impl Color {
/// Returns a short code associated with the color, used for debug purpose
/// only
pub(crate) fn code(self) -> &'static str {
match self {
Color::Reset => "X",
Color::Black => "b",
Color::Red => "r",
Color::Green => "c",
Color::Yellow => "y",
Color::Blue => "b",
Color::Magenta => "m",
Color::Cyan => "c",
Color::Gray => "w",
Color::DarkGray => "B",
Color::LightRed => "R",
Color::LightGreen => "G",
Color::LightYellow => "Y",
Color::LightBlue => "B",
Color::LightMagenta => "M",
Color::LightCyan => "C",
Color::White => "W",
Color::Indexed(_) => "i",
Color::Rgb(_, _, _) => "o",
}
}
}
bitflags! {
/// Modifier changes the way a piece of text is displayed.
///
/// They are bitflags so they can easily be composed.
///
/// ## Examples
///
/// ```rust
/// # use tui::style::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Modifier: u16 {
const BOLD = 0b0000_0000_0001;
const DIM = 0b0000_0000_0010;
@@ -65,83 +52,168 @@ bitflags! {
}
}
impl Modifier {
/// Returns a short code associated with the color, used for debug purpose
/// only
pub(crate) fn code(self) -> String {
use std::fmt::Write;
let mut result = String::new();
if self.contains(Modifier::BOLD) {
write!(result, "BO").unwrap();
}
if self.contains(Modifier::DIM) {
write!(result, "DI").unwrap();
}
if self.contains(Modifier::ITALIC) {
write!(result, "IT").unwrap();
}
if self.contains(Modifier::UNDERLINED) {
write!(result, "UN").unwrap();
}
if self.contains(Modifier::SLOW_BLINK) {
write!(result, "SL").unwrap();
}
if self.contains(Modifier::RAPID_BLINK) {
write!(result, "RA").unwrap();
}
if self.contains(Modifier::REVERSED) {
write!(result, "RE").unwrap();
}
if self.contains(Modifier::HIDDEN) {
write!(result, "HI").unwrap();
}
if self.contains(Modifier::CROSSED_OUT) {
write!(result, "CR").unwrap();
}
result
}
}
/// Style let you control the main characteristics of the displayed elements.
///
/// ## Examples
///
/// ```rust
/// # use tui::style::{Color, Modifier, Style};
/// Style::default()
/// .fg(Color::Black)
/// .bg(Color::Green)
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Color,
pub bg: Color,
pub modifier: Modifier,
pub fg: Option<Color>,
pub bg: Option<Color>,
pub add_modifier: Modifier,
pub sub_modifier: Modifier,
}
impl Default for Style {
fn default() -> Style {
Style::new()
Style {
fg: None,
bg: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
}
}
impl Style {
pub const fn new() -> Self {
Style {
fg: Color::Reset,
bg: Color::Reset,
modifier: Modifier::empty(),
}
}
pub fn reset(&mut self) {
self.fg = Color::Reset;
self.bg = Color::Reset;
self.modifier = Modifier::empty();
/// Changes the foreground color.
///
/// ## Examples
///
/// ```rust
/// # use tui::style::{Color, Style};
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
/// ```
pub fn fg(mut self, color: Color) -> Style {
self.fg = Some(color);
self
}
pub const fn fg(mut self, color: Color) -> Style {
self.fg = color;
/// Changes the background color.
///
/// ## Examples
///
/// ```rust
/// # use tui::style::{Color, Style};
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
/// ```
pub fn bg(mut self, color: Color) -> Style {
self.bg = Some(color);
self
}
pub const fn bg(mut self, color: Color) -> Style {
self.bg = color;
/// Changes the text emphasis.
///
/// When applied, it adds the given modifier to the `Style` modifiers.
///
/// ## Examples
///
/// ```rust
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD);
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
/// assert_eq!(patched.sub_modifier, Modifier::empty());
/// ```
pub fn add_modifier(mut self, modifier: Modifier) -> Style {
self.sub_modifier.remove(modifier);
self.add_modifier.insert(modifier);
self
}
pub const fn modifier(mut self, modifier: Modifier) -> Style {
self.modifier = modifier;
/// Changes the text emphasis.
///
/// When applied, it removes the given modifier from the `Style` modifiers.
///
/// ## Examples
///
/// ```rust
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
/// ```
pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
self.add_modifier.remove(modifier);
self.sub_modifier.insert(modifier);
self
}
/// Results in a combined style that is equivalent to applying the two individual styles to
/// a style one after the other.
///
/// ## Examples
/// ```
/// # use tui::style::{Color, Modifier, Style};
/// let style_1 = Style::default().fg(Color::Yellow);
/// let style_2 = Style::default().bg(Color::Red);
/// let combined = style_1.patch(style_2);
/// assert_eq!(
/// Style::default().patch(style_1).patch(style_2),
/// Style::default().patch(combined));
/// ```
pub fn patch(mut self, other: Style) -> Style {
self.fg = other.fg.or(self.fg);
self.bg = other.bg.or(self.bg);
self.add_modifier.remove(other.sub_modifier);
self.add_modifier.insert(other.add_modifier);
self.sub_modifier.remove(other.add_modifier);
self.sub_modifier.insert(other.sub_modifier);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
fn styles() -> Vec<Style> {
vec![
Style::default(),
Style::default().fg(Color::Yellow),
Style::default().bg(Color::Yellow),
Style::default().add_modifier(Modifier::BOLD),
Style::default().remove_modifier(Modifier::BOLD),
Style::default().add_modifier(Modifier::ITALIC),
Style::default().remove_modifier(Modifier::ITALIC),
Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
]
}
#[test]
fn combined_patch_gives_same_result_as_individual_patch() {
let styles = styles();
for &a in &styles {
for &b in &styles {
for &c in &styles {
for &d in &styles {
let combined = a.patch(b.patch(c.patch(d)));
assert_eq!(
Style::default().patch(a).patch(b).patch(c).patch(d),
Style::default().patch(combined)
);
}
}
}
}
}
}

View File

@@ -1,9 +1,41 @@
use crate::{
backend::Backend,
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
};
use std::io;
use crate::backend::Backend;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::widgets::{StatefulWidget, Widget};
#[derive(Debug, Clone, PartialEq)]
/// UNSTABLE
enum ResizeBehavior {
Fixed,
Auto,
}
#[derive(Debug, Clone, PartialEq)]
/// UNSTABLE
pub struct Viewport {
area: Rect,
resize_behavior: ResizeBehavior,
}
impl Viewport {
/// UNSTABLE
pub fn fixed(area: Rect) -> Viewport {
Viewport {
area,
resize_behavior: ResizeBehavior::Fixed,
}
}
}
#[derive(Debug, Clone, PartialEq)]
/// Options to pass to [`Terminal::draw_with_options`]
pub struct TerminalOptions {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
/// Interface to the terminal backed by Termion
#[derive(Debug)]
@@ -19,8 +51,8 @@ where
current: usize,
/// Whether the cursor is currently hidden
hidden_cursor: bool,
/// Terminal size used for rendering.
known_size: Rect,
/// Viewport
viewport: Viewport,
}
/// Represents a consistent terminal interface for rendering.
@@ -29,6 +61,12 @@ where
B: Backend,
{
terminal: &'a mut Terminal<B>,
/// Where should the cursor be after drawing this frame?
///
/// 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()`.
cursor_position: Option<(u16, u16)>,
}
impl<'a, B> Frame<'a, B>
@@ -37,7 +75,7 @@ where
{
/// Terminal size, guaranteed not to change when rendering.
pub fn size(&self) -> Rect {
self.terminal.known_size
self.terminal.viewport.area
}
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
@@ -77,14 +115,17 @@ where
/// # use tui::Terminal;
/// # use tui::backend::TermionBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::{List, ListState, Text};
/// # use tui::widgets::{List, ListItem, ListState};
/// # let stdout = io::stdout();
/// # let backend = TermionBackend::new(stdout);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let mut state = ListState::default();
/// state.select(Some(1));
/// let items = vec![Text::raw("Item 1"), Text::raw("Item 2")];
/// let list = List::new(items.into_iter());
/// let items = vec![
/// ListItem::new("Item 1"),
/// ListItem::new("Item 2"),
/// ];
/// let list = List::new(items);
/// let area = Rect::new(0, 0, 5, 5);
/// let mut frame = terminal.get_frame();
/// frame.render_stateful_widget(list, area, &mut state);
@@ -95,6 +136,16 @@ where
{
widget.render(area, self.terminal.current_buffer_mut(), state);
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
/// `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));
}
}
impl<B> Drop for Terminal<B>
@@ -115,22 +166,41 @@ impl<B> Terminal<B>
where
B: Backend,
{
/// Wrapper around Termion initialization. Each buffer is initialized with a blank string and
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
/// default colors for the foreground and the background
pub fn new(backend: B) -> io::Result<Terminal<B>> {
let size = backend.size()?;
Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport {
area: size,
resize_behavior: ResizeBehavior::Auto,
},
},
)
}
/// UNSTABLE
pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
Ok(Terminal {
backend,
buffers: [Buffer::empty(size), Buffer::empty(size)],
buffers: [
Buffer::empty(options.viewport.area),
Buffer::empty(options.viewport.area),
],
current: 0,
hidden_cursor: false,
known_size: size,
viewport: options.viewport,
})
}
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
pub fn get_frame(&mut self) -> Frame<B> {
Frame { terminal: self }
Frame {
terminal: self,
cursor_position: None,
}
}
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
@@ -161,16 +231,18 @@ where
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].reset();
self.buffers[1 - self.current].resize(area);
self.known_size = area;
self.viewport.area = area;
self.backend.clear()
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
let size = self.size()?;
if self.known_size != size {
self.resize(size)?;
}
if self.viewport.resize_behavior == ResizeBehavior::Auto {
let size = self.size()?;
if size != self.viewport.area {
self.resize(size)?;
}
};
Ok(())
}
@@ -178,17 +250,30 @@ where
/// and prepares for the next draw call.
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
where
F: FnOnce(Frame<B>),
F: FnOnce(&mut Frame<B>),
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;
f(self.get_frame());
let mut frame = self.get_frame();
f(&mut frame);
// We can't change the cursor position right away because we have to flush the frame to
// stdout first. But we also can't keep the frame around, since it holds a &mut to
// Terminal. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;
// Draw to stdout
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some((x, y)) => {
self.show_cursor()?;
self.set_cursor(x, y)?;
}
}
// Swap buffers
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
@@ -203,20 +288,25 @@ where
self.hidden_cursor = true;
Ok(())
}
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
}
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)
}
pub fn clear(&mut self) -> io::Result<()> {
self.backend.clear()
}
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Rect> {
self.backend.size()

333
src/text.rs Normal file
View File

@@ -0,0 +1,333 @@
//! Primitives for styled text.
//!
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
//! those strings may be associated to a set of styles. `tui` has three ways to represent them:
//! - 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 [`Spans`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//!
//! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Spans`].
//!
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
//! supported for their properties. Moreover, `tui` provides convenient `From` implementations so
//! that you can start by using simple `String` or `&str` and then promote them to the previous
//! primitives when you need additional styling capabilities.
//!
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
//! its `title` property (which is a [`Spans`] under the hood):
//!
//! ```rust
//! # use tui::widgets::Block;
//! # use tui::text::{Span, Spans};
//! # use tui::style::{Color, Style};
//! // A simple string with no styling.
//! // Converted to Spans(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
//! // ])
//! let block = Block::default().title("My title");
//!
//! // A simple string with a unique style.
//! // Converted to Spans(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
//! // ])
//! let block = Block::default().title(
//! Span::styled("My title", Style::default().fg(Color::Yellow))
//! );
//!
//! // A string with multiple styles.
//! // Converted to Spans(vec![
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
//! // Span { content: Cow::Borrowed(" title"), .. }
//! // ])
//! let block = Block::default().title(vec![
//! Span::styled("My", Style::default().fg(Color::Yellow)),
//! Span::raw(" title"),
//! ]);
//! ```
use crate::style::Style;
use std::{borrow::Cow, cmp::max};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
/// A grapheme associated to a style.
#[derive(Debug, Clone, PartialEq)]
pub struct StyledGrapheme<'a> {
pub symbol: &'a str,
pub style: Style,
}
/// A string where all graphemes have the same style.
#[derive(Debug, Clone, PartialEq)]
pub struct Span<'a> {
pub content: Cow<'a, str>,
pub style: Style,
}
impl<'a> Span<'a> {
/// Create a span with no style.
///
/// ## Examples
///
/// ```rust
/// # use tui::text::Span;
/// Span::raw("My text");
/// Span::raw(String::from("My text"));
/// ```
pub fn raw<T>(content: T) -> Span<'a>
where
T: Into<Cow<'a, str>>,
{
Span {
content: content.into(),
style: Style::default(),
}
}
/// Create a span with a style.
///
/// # Examples
///
/// ```rust
/// # use tui::text::Span;
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Span::styled("My text", style);
/// Span::styled(String::from("My text"), style);
/// ```
pub fn styled<T>(content: T, style: Style) -> Span<'a>
where
T: Into<Cow<'a, str>>,
{
Span {
content: content.into(),
style,
}
}
/// Returns the width of the content held by this span.
pub fn width(&self) -> usize {
self.content.width()
}
/// Returns an iterator over the graphemes held by this span.
///
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
/// the resulting [`Style`].
///
/// ## Examples
///
/// ```rust
/// # use tui::text::{Span, StyledGrapheme};
/// # use tui::style::{Color, Modifier, Style};
/// # use std::iter::Iterator;
/// let style = Style::default().fg(Color::Yellow);
/// let span = Span::styled("Text", style);
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
/// let styled_graphemes = span.styled_graphemes(style);
/// assert_eq!(
/// vec![
/// StyledGrapheme {
/// symbol: "T",
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// },
/// StyledGrapheme {
/// symbol: "e",
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// },
/// StyledGrapheme {
/// symbol: "x",
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// },
/// StyledGrapheme {
/// symbol: "t",
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// },
/// ],
/// styled_graphemes.collect::<Vec<StyledGrapheme>>()
/// );
/// ```
pub fn styled_graphemes(
&'a self,
base_style: Style,
) -> impl Iterator<Item = StyledGrapheme<'a>> {
UnicodeSegmentation::graphemes(self.content.as_ref(), true)
.map(move |g| StyledGrapheme {
symbol: g,
style: base_style.patch(self.style),
})
.filter(|s| s.symbol != "\n")
}
}
impl<'a> From<String> for Span<'a> {
fn from(s: String) -> Span<'a> {
Span::raw(s)
}
}
impl<'a> From<&'a str> for Span<'a> {
fn from(s: &'a str) -> Span<'a> {
Span::raw(s)
}
}
/// A string composed of clusters of graphemes, each with their own style.
#[derive(Debug, Clone, PartialEq)]
pub struct Spans<'a>(pub Vec<Span<'a>>);
impl<'a> Default for Spans<'a> {
fn default() -> Spans<'a> {
Spans(Vec::new())
}
}
impl<'a> Spans<'a> {
/// Returns the width of the underlying string.
///
/// ## Examples
///
/// ```rust
/// # use tui::text::{Span, Spans};
/// # use tui::style::{Color, Style};
/// let spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
/// ]);
/// assert_eq!(7, spans.width());
/// ```
pub fn width(&self) -> usize {
self.0.iter().fold(0, |acc, s| acc + s.width())
}
}
impl<'a> From<String> for Spans<'a> {
fn from(s: String) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<&'a str> for Spans<'a> {
fn from(s: &'a str) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
Spans(spans)
}
}
impl<'a> From<Span<'a>> for Spans<'a> {
fn from(span: Span<'a>) -> Spans<'a> {
Spans(vec![span])
}
}
impl<'a> From<Spans<'a>> for String {
fn from(line: Spans<'a>) -> String {
line.0.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s.content.as_ref());
acc
})
}
}
/// A string split over multiple lines where each line is composed of several clusters, each with
/// their own style.
#[derive(Debug, Clone, PartialEq)]
pub struct Text<'a> {
pub lines: Vec<Spans<'a>>,
}
impl<'a> Default for Text<'a> {
fn default() -> Text<'a> {
Text { lines: Vec::new() }
}
}
impl<'a> Text<'a> {
/// Returns the max width of all the lines.
///
/// ## Examples
///
/// ```rust
/// use tui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(15, text.width());
/// ```
pub fn width(&self) -> usize {
self.lines.iter().fold(0, |acc, l| max(acc, l.width()))
}
/// Returns the height.
///
/// ## Examples
///
/// ```rust
/// use tui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
/// ```
pub fn height(&self) -> usize {
self.lines.len()
}
pub fn patch_style(&mut self, style: Style) {
for line in &mut self.lines {
for span in &mut line.0 {
span.style = span.style.patch(style);
}
}
}
}
impl<'a> From<&'a str> for Text<'a> {
fn from(s: &'a str) -> Text<'a> {
Text {
lines: s.lines().map(Spans::from).collect(),
}
}
}
impl<'a> From<Span<'a>> for Text<'a> {
fn from(span: Span<'a>) -> Text<'a> {
Text {
lines: vec![Spans::from(span)],
}
}
}
impl<'a> From<Spans<'a>> for Text<'a> {
fn from(spans: Spans<'a>) -> Text<'a> {
Text { lines: vec![spans] }
}
}
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
Text { lines }
}
}

View File

@@ -19,8 +19,8 @@ use unicode_width::UnicodeWidthStr;
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .bar_width(3)
/// .bar_gap(1)
/// .style(Style::default().fg(Color::Yellow).bg(Color::Red))
/// .value_style(Style::default().fg(Color::Red).modifier(Modifier::BOLD))
/// .bar_style(Style::default().fg(Color::Yellow).bg(Color::Red))
/// .value_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
/// .label_style(Style::default().fg(Color::White))
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .max(4);
@@ -35,6 +35,8 @@ pub struct BarChart<'a> {
bar_gap: u16,
/// Set of symbols used to display the data
bar_set: symbols::bar::Set,
/// Style of the bars
bar_style: Style,
/// Style of the values printed at the bottom of each bar
value_style: Style,
/// Style of the labels printed under each bar
@@ -57,6 +59,7 @@ impl<'a> Default for BarChart<'a> {
max: None,
data: &[],
values: Vec::new(),
bar_style: Style::default(),
bar_width: 1,
bar_gap: 1,
bar_set: symbols::bar::NINE_LEVELS,
@@ -87,6 +90,11 @@ impl<'a> BarChart<'a> {
self
}
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
self.bar_style = style;
self
}
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
self.bar_width = width;
self
@@ -120,10 +128,13 @@ impl<'a> BarChart<'a> {
impl<'a> Widget for BarChart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let chart_area = match self.block {
Some(ref mut b) => {
buf.set_style(area, self.style);
let chart_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
@@ -132,8 +143,6 @@ impl<'a> Widget for BarChart<'a> {
return;
}
buf.set_background(chart_area, self.style.bg);
let max = self
.max
.unwrap_or_else(|| self.data.iter().fold(0, |acc, &(_, v)| max(v, acc)));
@@ -172,7 +181,7 @@ impl<'a> Widget for BarChart<'a> {
chart_area.top() + j,
)
.set_symbol(symbol)
.set_style(self.style);
.set_style(self.bar_style);
}
if d.1 > 8 {

View File

@@ -1,8 +1,11 @@
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::symbols::line;
use crate::widgets::{Borders, Widget};
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols::line,
text::{Span, Spans},
widgets::{Borders, Widget},
};
#[derive(Debug, Clone, Copy)]
pub enum BorderType {
@@ -33,18 +36,15 @@ impl BorderType {
/// # use tui::style::{Style, Color};
/// Block::default()
/// .title("Block")
/// .title_style(Style::default().fg(Color::Red))
/// .borders(Borders::LEFT | Borders::RIGHT)
/// .border_style(Style::default().fg(Color::White))
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// ```
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub struct Block<'a> {
/// Optional title place on the upper left of the block
title: Option<&'a str>,
/// Title style
title_style: Style,
title: Option<Spans<'a>>,
/// Visible borders
borders: Borders,
/// Border style
@@ -60,7 +60,6 @@ impl<'a> Default for Block<'a> {
fn default() -> Block<'a> {
Block {
title: None,
title_style: Default::default(),
borders: Borders::NONE,
border_style: Default::default(),
border_type: BorderType::Plain,
@@ -70,13 +69,23 @@ impl<'a> Default for Block<'a> {
}
impl<'a> Block<'a> {
pub fn title(mut self, title: &'a str) -> Block<'a> {
self.title = Some(title);
pub fn title<T>(mut self, title: T) -> Block<'a>
where
T: Into<Spans<'a>>,
{
self.title = Some(title.into());
self
}
#[deprecated(
since = "0.10.0",
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
)]
pub fn title_style(mut self, style: Style) -> Block<'a> {
self.title_style = style;
if let Some(t) = self.title {
let title = String::from(t);
self.title = Some(Spans::from(Span::styled(title, style)));
}
self
}
@@ -126,12 +135,12 @@ impl<'a> Block<'a> {
impl<'a> Widget for Block<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
if area.width < 2 || area.height < 2 {
return;
}
buf.set_background(area, self.style.bg);
let symbols = BorderType::line_symbols(self.border_type);
// Sides
if self.borders.intersects(Borders::LEFT) {
@@ -199,13 +208,7 @@ impl<'a> Widget for Block<'a> {
0
};
let width = area.width - lx - rx;
buf.set_stringn(
area.left() + lx,
area.top(),
title,
width as usize,
self.title_style,
);
buf.set_spans(area.left() + lx, area.top(), &title, width);
}
}
}

View File

@@ -206,6 +206,9 @@ impl<'a, 'b> Painter<'a, 'b> {
}
let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs();
let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs();
if width == 0.0 || height == 0.0 {
return None;
}
let x = ((x - left) * self.resolution.0 / width) as usize;
let y = ((top - y) * self.resolution.1 / height) as usize;
Some((x, y))
@@ -416,10 +419,11 @@ where
F: Fn(&mut Context),
{
fn render(mut self, area: Rect, buf: &mut Buffer) {
let canvas_area = match self.block {
Some(ref mut b) => {
let canvas_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};

View File

@@ -1,8 +1,9 @@
use crate::{
buffer::Buffer,
layout::{Constraint, Rect},
style::Style,
style::{Color, Style},
symbols,
text::{Span, Spans},
widgets::{
canvas::{Canvas, Line, Points},
Block, Borders, Widget,
@@ -13,70 +14,60 @@ use unicode_width::UnicodeWidthStr;
/// An X or Y axis for the chart widget
#[derive(Debug, Clone)]
pub struct Axis<'a, L>
where
L: AsRef<str> + 'a,
{
pub struct Axis<'a> {
/// Title displayed next to axis end
title: Option<&'a str>,
/// Style of the title
title_style: Style,
title: Option<Spans<'a>>,
/// Bounds for the axis (all data points outside these limits will not be represented)
bounds: [f64; 2],
/// A list of labels to put to the left or below the axis
labels: Option<&'a [L]>,
/// The labels' style
labels_style: Style,
labels: Option<Vec<Span<'a>>>,
/// The style used to draw the axis itself
style: Style,
}
impl<'a, L> Default for Axis<'a, L>
where
L: AsRef<str>,
{
fn default() -> Axis<'a, L> {
impl<'a> Default for Axis<'a> {
fn default() -> Axis<'a> {
Axis {
title: None,
title_style: Default::default(),
bounds: [0.0, 0.0],
labels: None,
labels_style: Default::default(),
style: Default::default(),
}
}
}
impl<'a, L> Axis<'a, L>
where
L: AsRef<str>,
{
pub fn title(mut self, title: &'a str) -> Axis<'a, L> {
self.title = Some(title);
impl<'a> Axis<'a> {
pub fn title<T>(mut self, title: T) -> Axis<'a>
where
T: Into<Spans<'a>>,
{
self.title = Some(title.into());
self
}
pub fn title_style(mut self, style: Style) -> Axis<'a, L> {
self.title_style = style;
#[deprecated(
since = "0.10.0",
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
)]
pub fn title_style(mut self, style: Style) -> Axis<'a> {
if let Some(t) = self.title {
let title = String::from(t);
self.title = Some(Spans::from(Span::styled(title, style)));
}
self
}
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a, L> {
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
self.bounds = bounds;
self
}
pub fn labels(mut self, labels: &'a [L]) -> Axis<'a, L> {
pub fn labels(mut self, labels: Vec<Span<'a>>) -> Axis<'a> {
self.labels = Some(labels);
self
}
pub fn labels_style(mut self, style: Style) -> Axis<'a, L> {
self.labels_style = style;
self
}
pub fn style(mut self, style: Style) -> Axis<'a, L> {
pub fn style(mut self, style: Style) -> Axis<'a> {
self.style = style;
self
}
@@ -193,103 +184,83 @@ impl Default for ChartLayout {
/// # use tui::symbols;
/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
/// # use tui::style::{Style, Color};
/// Chart::default()
/// # use tui::text::Span;
/// let datasets = vec![
/// Dataset::default()
/// .name("data1")
/// .marker(symbols::Marker::Dot)
/// .graph_type(GraphType::Scatter)
/// .style(Style::default().fg(Color::Cyan))
/// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
/// Dataset::default()
/// .name("data2")
/// .marker(symbols::Marker::Braille)
/// .graph_type(GraphType::Line)
/// .style(Style::default().fg(Color::Magenta))
/// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)]),
/// ];
/// Chart::new(datasets)
/// .block(Block::default().title("Chart"))
/// .x_axis(Axis::default()
/// .title("X Axis")
/// .title_style(Style::default().fg(Color::Red))
/// .title(Span::styled("X Axis", Style::default().fg(Color::Red)))
/// .style(Style::default().fg(Color::White))
/// .bounds([0.0, 10.0])
/// .labels(&["0.0", "5.0", "10.0"]))
/// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()))
/// .y_axis(Axis::default()
/// .title("Y Axis")
/// .title_style(Style::default().fg(Color::Red))
/// .title(Span::styled("Y Axis", Style::default().fg(Color::Red)))
/// .style(Style::default().fg(Color::White))
/// .bounds([0.0, 10.0])
/// .labels(&["0.0", "5.0", "10.0"]))
/// .datasets(&[Dataset::default()
/// .name("data1")
/// .marker(symbols::Marker::Dot)
/// .graph_type(GraphType::Scatter)
/// .style(Style::default().fg(Color::Cyan))
/// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
/// Dataset::default()
/// .name("data2")
/// .marker(symbols::Marker::Braille)
/// .graph_type(GraphType::Line)
/// .style(Style::default().fg(Color::Magenta))
/// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)])]);
/// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()));
/// ```
#[derive(Debug, Clone)]
pub struct Chart<'a, LX, LY>
where
LX: AsRef<str> + 'a,
LY: AsRef<str> + 'a,
{
pub struct Chart<'a> {
/// A block to display around the widget eventually
block: Option<Block<'a>>,
/// The horizontal axis
x_axis: Axis<'a, LX>,
x_axis: Axis<'a>,
/// The vertical axis
y_axis: Axis<'a, LY>,
y_axis: Axis<'a>,
/// A reference to the datasets
datasets: &'a [Dataset<'a>],
datasets: Vec<Dataset<'a>>,
/// The widget base style
style: Style,
/// Constraints used to determine whether the legend should be shown or
/// not
/// Constraints used to determine whether the legend should be shown or not
hidden_legend_constraints: (Constraint, Constraint),
}
impl<'a, LX, LY> Default for Chart<'a, LX, LY>
where
LX: AsRef<str>,
LY: AsRef<str>,
{
fn default() -> Chart<'a, LX, LY> {
impl<'a> Chart<'a> {
pub fn new(datasets: Vec<Dataset<'a>>) -> Chart<'a> {
Chart {
block: None,
x_axis: Axis::default(),
y_axis: Axis::default(),
style: Default::default(),
datasets: &[],
datasets,
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
}
}
}
impl<'a, LX, LY> Chart<'a, LX, LY>
where
LX: AsRef<str>,
LY: AsRef<str>,
{
pub fn block(mut self, block: Block<'a>) -> Chart<'a, LX, LY> {
pub fn block(mut self, block: Block<'a>) -> Chart<'a> {
self.block = Some(block);
self
}
pub fn style(mut self, style: Style) -> Chart<'a, LX, LY> {
pub fn style(mut self, style: Style) -> Chart<'a> {
self.style = style;
self
}
pub fn x_axis(mut self, axis: Axis<'a, LX>) -> Chart<'a, LX, LY> {
pub fn x_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
self.x_axis = axis;
self
}
pub fn y_axis(mut self, axis: Axis<'a, LY>) -> Chart<'a, LX, LY> {
pub fn y_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
self.y_axis = axis;
self
}
pub fn datasets(mut self, datasets: &'a [Dataset<'a>]) -> Chart<'a, LX, LY> {
self.datasets = datasets;
self
}
/// Set the constraints used to determine whether the legend should be shown or
/// not.
/// Set the constraints used to determine whether the legend should be shown or not.
///
/// # Examples
///
@@ -302,12 +273,10 @@ where
/// );
/// // Hide the legend when either its width is greater than 33% of the total widget width
/// // or if its height is greater than 25% of the total widget height.
/// let _chart: Chart<String, String> = Chart::default()
/// let _chart: Chart = Chart::new(vec![])
/// .hidden_legend_constraints(constraints);
pub fn hidden_legend_constraints(
mut self,
constraints: (Constraint, Constraint),
) -> Chart<'a, LX, LY> {
/// ```
pub fn hidden_legend_constraints(mut self, constraints: (Constraint, Constraint)) -> Chart<'a> {
self.hidden_legend_constraints = constraints;
self
}
@@ -327,14 +296,14 @@ where
y -= 1;
}
if let Some(y_labels) = self.y_axis.labels {
if let Some(ref y_labels) = self.y_axis.labels {
let mut max_width = y_labels
.iter()
.fold(0, |acc, l| max(l.as_ref().width(), acc))
.fold(0, |acc, l| max(l.content.width(), acc))
as u16;
if let Some(x_labels) = self.x_axis.labels {
if let Some(ref x_labels) = self.x_axis.labels {
if !x_labels.is_empty() {
max_width = max(max_width, x_labels[0].as_ref().width() as u16);
max_width = max(max_width, x_labels[0].content.width() as u16);
}
}
if x + max_width < area.right() {
@@ -357,14 +326,14 @@ where
layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
}
if let Some(title) = self.x_axis.title {
if let Some(ref title) = self.x_axis.title {
let w = title.width() as u16;
if w < layout.graph_area.width && layout.graph_area.height > 2 {
layout.title_x = Some((x + layout.graph_area.width - w, y));
}
}
if let Some(title) = self.y_axis.title {
if let Some(ref title) = self.y_axis.title {
let w = title.width() as u16;
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
layout.title_y = Some((x + 1, area.top()));
@@ -398,16 +367,14 @@ where
}
}
impl<'a, LX, LY> Widget for Chart<'a, LX, LY>
where
LX: AsRef<str>,
LY: AsRef<str>,
{
impl<'a> Widget for Chart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let chart_area = match self.block {
Some(ref mut b) => {
buf.set_style(area, self.style);
let chart_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
@@ -418,30 +385,28 @@ where
return;
}
buf.set_background(chart_area, self.style.bg);
if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap();
buf.set_string(x, y, title, self.x_axis.title_style);
buf.set_spans(x, y, &title, graph_area.right().saturating_sub(x));
}
if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap();
buf.set_string(x, y, title, self.y_axis.title_style);
buf.set_spans(x, y, &title, graph_area.right().saturating_sub(x));
}
if let Some(y) = layout.label_x {
let labels = self.x_axis.labels.unwrap();
let total_width = labels.iter().fold(0, |acc, l| l.as_ref().width() + acc) as u16;
let total_width = labels.iter().fold(0, |acc, l| l.content.width() + acc) as u16;
let labels_len = labels.len() as u16;
if total_width < graph_area.width && labels_len > 1 {
for (i, label) in labels.iter().enumerate() {
buf.set_string(
buf.set_span(
graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1)
- label.as_ref().width() as u16,
- label.content.width() as u16,
y,
label.as_ref(),
self.x_axis.labels_style,
label,
label.width() as u16,
);
}
}
@@ -453,12 +418,7 @@ where
for (i, label) in labels.iter().enumerate() {
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
if dy < graph_area.bottom() {
buf.set_string(
x,
graph_area.bottom() - 1 - dy,
label.as_ref(),
self.y_axis.labels_style,
);
buf.set_span(x, graph_area.bottom() - 1 - dy, label, label.width() as u16);
}
}
}
@@ -487,25 +447,25 @@ where
}
}
for dataset in self.datasets {
for dataset in &self.datasets {
Canvas::default()
.background_color(self.style.bg)
.background_color(self.style.bg.unwrap_or(Color::Reset))
.x_bounds(self.x_axis.bounds)
.y_bounds(self.y_axis.bounds)
.marker(dataset.marker)
.paint(|ctx| {
ctx.draw(&Points {
coords: dataset.data,
color: dataset.style.fg,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
if let GraphType::Line = dataset.graph_type {
for i in 0..dataset.data.len() - 1 {
for data in dataset.data.windows(2) {
ctx.draw(&Line {
x1: dataset.data[i].0,
y1: dataset.data[i].1,
x2: dataset.data[i + 1].0,
y2: dataset.data[i + 1].1,
color: dataset.style.fg,
x1: data[0].0,
y1: data[0].1,
x2: data[1].0,
y2: data[1].1,
color: dataset.style.fg.unwrap_or(Color::Reset),
})
}
}
@@ -542,12 +502,6 @@ mod tests {
#[test]
fn it_should_hide_the_legend() {
let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)];
let datasets = (0..10)
.map(|i| {
let name = format!("Dataset #{}", i);
Dataset::default().name(name).data(&data)
})
.collect::<Vec<_>>();
let cases = [
LegendTestCase {
chart_area: Rect::new(0, 0, 100, 100),
@@ -561,11 +515,16 @@ mod tests {
},
];
for case in &cases {
let chart: Chart<String, String> = Chart::default()
let datasets = (0..10)
.map(|i| {
let name = format!("Dataset #{}", i);
Dataset::default().name(name).data(&data)
})
.collect::<Vec<_>>();
let chart = Chart::new(datasets)
.x_axis(Axis::default().title("X axis"))
.y_axis(Axis::default().title("Y axis"))
.hidden_legend_constraints(case.hidden_legend_constraints)
.datasets(datasets.as_slice());
.hidden_legend_constraints(case.hidden_legend_constraints);
let layout = chart.layout(case.chart_area);
assert_eq!(layout.legend_area, case.legend_area);
}

View File

@@ -1,9 +1,10 @@
use unicode_width::UnicodeWidthStr;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::{Color, Style};
use crate::widgets::{Block, Widget};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
text::Span,
widgets::{Block, Widget},
};
/// A widget to display a task progress.
///
@@ -14,15 +15,16 @@ use crate::widgets::{Block, Widget};
/// # use tui::style::{Style, Color, Modifier};
/// Gauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .style(Style::default().fg(Color::White).bg(Color::Black).modifier(Modifier::ITALIC))
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::ITALIC))
/// .percent(20);
/// ```
#[derive(Debug, Clone)]
pub struct Gauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
label: Option<&'a str>,
label: Option<Span<'a>>,
style: Style,
gauge_style: Style,
}
impl<'a> Default for Gauge<'a> {
@@ -31,7 +33,8 @@ impl<'a> Default for Gauge<'a> {
block: None,
ratio: 0.0,
label: None,
style: Default::default(),
style: Style::default(),
gauge_style: Style::default(),
}
}
}
@@ -61,8 +64,11 @@ impl<'a> Gauge<'a> {
self
}
pub fn label(mut self, string: &'a str) -> Gauge<'a> {
self.label = Some(string);
pub fn label<T>(mut self, label: T) -> Gauge<'a>
where
T: Into<Span<'a>>,
{
self.label = Some(label.into());
self
}
@@ -70,28 +76,37 @@ impl<'a> Gauge<'a> {
self.style = style;
self
}
pub fn gauge_style(mut self, style: Style) -> Gauge<'a> {
self.gauge_style = style;
self
}
}
impl<'a> Widget for Gauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let gauge_area = match self.block {
Some(ref mut b) => {
buf.set_style(area, self.style);
let gauge_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
buf.set_style(gauge_area, self.gauge_style);
if gauge_area.height < 1 {
return;
}
if self.style.bg != Color::Reset {
buf.set_background(gauge_area, self.style.bg);
}
let center = gauge_area.height / 2 + gauge_area.top();
let width = (f64::from(gauge_area.width) * self.ratio).round() as u16;
let end = gauge_area.left() + width;
// Label
let ratio = self.ratio;
let label = self
.label
.unwrap_or_else(|| Span::from(format!("{}%", (ratio * 100.0).round())));
for y in gauge_area.top()..gauge_area.bottom() {
// Gauge
for x in gauge_area.left()..end {
@@ -99,19 +114,16 @@ impl<'a> Widget for Gauge<'a> {
}
if y == center {
// Label
let precent_label = format!("{}%", (self.ratio * 100.0).round());
let label = self.label.unwrap_or(&precent_label);
let label_width = label.width() as u16;
let middle = (gauge_area.width - label_width) / 2 + gauge_area.left();
buf.set_string(middle, y, label, self.style);
buf.set_span(middle, y, &label, gauge_area.right() - middle);
}
// Fix colors
for x in gauge_area.left()..end {
buf.get_mut(x, y)
.set_fg(self.style.bg)
.set_bg(self.style.fg);
.set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
.set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
}
}
}

View File

@@ -1,12 +1,13 @@
use crate::{
buffer::Buffer,
layout::{Corner, Rect},
style::Style,
text::Text,
widgets::{Block, StatefulWidget, Widget},
};
use std::iter::{self, Iterator};
use unicode_width::UnicodeWidthStr;
use crate::buffer::Buffer;
use crate::layout::{Corner, Rect};
use crate::style::Style;
use crate::widgets::{Block, StatefulWidget, Text, Widget};
#[derive(Debug, Clone)]
pub struct ListState {
offset: usize,
@@ -35,112 +36,111 @@ impl ListState {
}
}
#[derive(Debug, Clone)]
pub struct ListItem<'a> {
content: Text<'a>,
style: Style,
}
impl<'a> ListItem<'a> {
pub fn new<T>(content: T) -> ListItem<'a>
where
T: Into<Text<'a>>,
{
ListItem {
content: content.into(),
style: Style::default(),
}
}
pub fn style(mut self, style: Style) -> ListItem<'a> {
self.style = style;
self
}
pub fn height(&self) -> usize {
self.content.height()
}
}
/// A widget to display several items among which one can be selected (optional)
///
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, List, Text};
/// # use tui::widgets::{Block, Borders, List, ListItem};
/// # use tui::style::{Style, Color, Modifier};
/// let items = ["Item 1", "Item 2", "Item 3"].iter().map(|i| Text::raw(*i));
/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")];
/// List::new(items)
/// .block(Block::default().title("List").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().modifier(Modifier::ITALIC))
/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
/// .highlight_symbol(">>");
/// ```
#[derive(Debug, Clone)]
pub struct List<'b, L>
where
L: Iterator<Item = Text<'b>>,
{
block: Option<Block<'b>>,
items: L,
start_corner: Corner,
/// Base style of the widget
pub struct List<'a> {
block: Option<Block<'a>>,
items: Vec<ListItem<'a>>,
/// Style used as a base style for the widget
style: Style,
start_corner: Corner,
/// Style used to render selected item
highlight_style: Style,
/// Symbol in front of the selected item (Shift all items to the right)
highlight_symbol: Option<&'b str>,
highlight_symbol: Option<&'a str>,
}
impl<'b, L> Default for List<'b, L>
where
L: Iterator<Item = Text<'b>> + Default,
{
fn default() -> List<'b, L> {
impl<'a> List<'a> {
pub fn new<T>(items: T) -> List<'a>
where
T: Into<Vec<ListItem<'a>>>,
{
List {
block: None,
items: L::default(),
style: Default::default(),
start_corner: Corner::TopLeft,
highlight_style: Style::default(),
highlight_symbol: None,
}
}
}
impl<'b, L> List<'b, L>
where
L: Iterator<Item = Text<'b>>,
{
pub fn new(items: L) -> List<'b, L> {
List {
block: None,
items,
style: Default::default(),
style: Style::default(),
items: items.into(),
start_corner: Corner::TopLeft,
highlight_style: Style::default(),
highlight_symbol: None,
}
}
pub fn block(mut self, block: Block<'b>) -> List<'b, L> {
pub fn block(mut self, block: Block<'a>) -> List<'a> {
self.block = Some(block);
self
}
pub fn items<I>(mut self, items: I) -> List<'b, L>
where
I: IntoIterator<Item = Text<'b>, IntoIter = L>,
{
self.items = items.into_iter();
self
}
pub fn style(mut self, style: Style) -> List<'b, L> {
pub fn style(mut self, style: Style) -> List<'a> {
self.style = style;
self
}
pub fn highlight_symbol(mut self, highlight_symbol: &'b str) -> List<'b, L> {
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> List<'a> {
self.highlight_symbol = Some(highlight_symbol);
self
}
pub fn highlight_style(mut self, highlight_style: Style) -> List<'b, L> {
self.highlight_style = highlight_style;
pub fn highlight_style(mut self, style: Style) -> List<'a> {
self.highlight_style = style;
self
}
pub fn start_corner(mut self, corner: Corner) -> List<'b, L> {
pub fn start_corner(mut self, corner: Corner) -> List<'a> {
self.start_corner = corner;
self
}
}
impl<'b, L> StatefulWidget for List<'b, L>
where
L: Iterator<Item = Text<'b>>,
{
impl<'a> StatefulWidget for List<'a> {
type State = ListState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let list_area = match self.block {
Some(ref mut b) => {
buf.set_style(area, self.style);
let list_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
@@ -149,81 +149,99 @@ where
return;
}
if self.items.is_empty() {
return;
}
let list_height = list_area.height as usize;
buf.set_background(list_area, self.style.bg);
let mut start = state.offset;
let mut end = state.offset;
let mut height = 0;
for item in self.items.iter().skip(state.offset) {
if height + item.height() > list_height {
break;
}
height += item.height();
end += 1;
}
let selected = state.selected.unwrap_or(0).min(self.items.len() - 1);
while selected >= end {
height = height.saturating_add(self.items[end].height());
end += 1;
while height > list_height {
height = height.saturating_sub(self.items[start].height());
start += 1;
}
}
while selected < start {
start -= 1;
height = height.saturating_add(self.items[start].height());
while height > list_height {
end -= 1;
height = height.saturating_sub(self.items[end].height());
}
}
state.offset = start;
// Use highlight_style only if something is selected
let (selected, highlight_style) = match state.selected {
Some(i) => (Some(i), self.highlight_style),
None => (None, self.style),
};
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
// Make sure the list show the selected item
state.offset = if let Some(selected) = selected {
if selected >= list_height + state.offset - 1 {
selected + 1 - list_height
} else if selected < state.offset {
selected
} else {
state.offset
}
} else {
0
};
let mut current_height = 0;
let has_selection = state.selected.is_some();
for (i, item) in self
.items
.skip(state.offset)
.iter_mut()
.enumerate()
.take(list_area.height as usize)
.skip(state.offset)
.take(end - start)
{
let (x, y) = match self.start_corner {
Corner::TopLeft => (list_area.left(), list_area.top() + i as u16),
Corner::BottomLeft => (list_area.left(), list_area.bottom() - (i + 1) as u16),
// Not supported
_ => (list_area.left(), list_area.top() + i as u16),
};
let (elem_x, style) = if let Some(s) = selected {
if s == i + state.offset {
let (x, _) = buf.set_stringn(
x,
y,
highlight_symbol,
list_area.width as usize,
highlight_style,
);
(x, Some(highlight_style))
} else {
let (x, _) =
buf.set_stringn(x, y, &blank_symbol, list_area.width as usize, self.style);
(x, None)
Corner::BottomLeft => {
current_height += item.height() as u16;
(list_area.left(), list_area.bottom() - current_height)
}
_ => {
let pos = (list_area.left(), list_area.top() + current_height);
current_height += item.height() as u16;
pos
}
} else {
(x, None)
};
let area = Rect {
x,
y,
width: list_area.width,
height: item.height() as u16,
};
let item_style = self.style.patch(item.style);
buf.set_style(area, item_style);
let max_element_width = (list_area.width - (elem_x - x)) as usize;
match item {
Text::Raw(ref v) => {
buf.set_stringn(elem_x, y, v, max_element_width, style.unwrap_or(self.style));
}
Text::Styled(ref v, s) => {
buf.set_stringn(elem_x, y, v, max_element_width, style.unwrap_or(s));
}
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
let elem_x = if has_selection {
let symbol = if is_selected {
highlight_symbol
} else {
&blank_symbol
};
let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style);
x
} else {
x
};
let max_element_width = (list_area.width - (elem_x - x)) as usize;
for (j, line) in item.content.lines.iter().enumerate() {
buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
}
if is_selected {
buf.set_style(area, self.highlight_style);
}
}
}
}
impl<'b, L> Widget for List<'b, L>
where
L: Iterator<Item = Text<'b>>,
{
impl<'a> Widget for List<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = ListState::default();
StatefulWidget::render(self, area, buf, &mut state);

View File

@@ -15,9 +15,6 @@
//! - [`Sparkline`]
//! - [`Clear`]
use bitflags::bitflags;
use std::borrow::Cow;
mod barchart;
mod block;
pub mod canvas;
@@ -36,15 +33,14 @@ pub use self::block::{Block, BorderType};
pub use self::chart::{Axis, Chart, Dataset, GraphType};
pub use self::clear::Clear;
pub use self::gauge::Gauge;
pub use self::list::{List, ListState};
pub use self::paragraph::Paragraph;
pub use self::list::{List, ListItem, ListState};
pub use self::paragraph::{Paragraph, Wrap};
pub use self::sparkline::Sparkline;
pub use self::table::{Row, Table, TableState};
pub use self::tabs::Tabs;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::{buffer::Buffer, layout::Rect};
use bitflags::bitflags;
bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
@@ -64,22 +60,6 @@ bitflags! {
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Text<'b> {
Raw(Cow<'b, str>),
Styled(Cow<'b, str>, Style),
}
impl<'b> Text<'b> {
pub fn raw<D: Into<Cow<'b, str>>>(data: D) -> Text<'b> {
Text::Raw(data.into())
}
pub fn styled<D: Into<Cow<'b, str>>>(data: D, style: Style) -> Text<'b> {
Text::Styled(data.into(), style)
}
}
/// Base requirements for a Widget
pub trait Widget {
/// Draws the current state of the widget in the given buffer. That the only method required to
@@ -108,7 +88,7 @@ pub trait Widget {
/// # use std::io;
/// # use tui::Terminal;
/// # use tui::backend::{Backend, TermionBackend};
/// # use tui::widgets::{Widget, List, ListState, Text};
/// # use tui::widgets::{Widget, List, ListItem, ListState};
///
/// // Let's say we have some events to display.
/// struct Events {
@@ -184,10 +164,10 @@ pub trait Widget {
/// ]);
///
/// loop {
/// terminal.draw(|mut f| {
/// terminal.draw(|f| {
/// // The items managed by the application are transformed to something
/// // that is understood by tui.
/// let items = events.items.iter().map(Text::raw);
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_ref())).collect();
/// // The `List` widget is then built with those items.
/// let list = List::new(items);
/// // Finally the widget is rendered using the associated state. `events.state` is

View File

@@ -1,13 +1,16 @@
use either::Either;
use unicode_segmentation::UnicodeSegmentation;
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::Style,
text::{StyledGrapheme, Text},
widgets::{
reflow::{LineComposer, LineTruncator, WordWrapper},
Block, Widget,
},
};
use std::iter;
use unicode_width::UnicodeWidthStr;
use crate::buffer::Buffer;
use crate::layout::{Alignment, Rect};
use crate::style::Style;
use crate::widgets::reflow::{LineComposer, LineTruncator, Styled, WordWrapper};
use crate::widgets::{Block, Text, Widget};
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
match alignment {
Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
@@ -21,96 +24,168 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, Paragraph, Text};
/// # use tui::style::{Style, Color};
/// # use tui::text::{Text, Spans, Span};
/// # use tui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use tui::style::{Style, Color, Modifier};
/// # use tui::layout::{Alignment};
/// let text = [
/// Text::raw("First line\n"),
/// Text::styled("Second line\n", Style::default().fg(Color::Red))
/// let text = vec![
/// Spans::from(vec![
/// Span::raw("First"),
/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)),
/// Span::raw("."),
/// ]),
/// Spans::from(Span::styled("Second line", Style::default().fg(Color::Red))),
/// ];
/// Paragraph::new(text.iter())
/// Paragraph::new(text)
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White).bg(Color::Black))
/// .alignment(Alignment::Center)
/// .wrap(true);
/// .wrap(Wrap::default());
/// ```
#[derive(Debug, Clone)]
pub struct Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
pub struct Paragraph<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
/// Widget style
style: Style,
/// Wrap the text or not
wrapping: bool,
/// How to wrap the text
wrap: Option<Wrap>,
/// The text to display
text: T,
/// Should we parse the text for embedded commands
raw: bool,
text: Text<'a>,
/// Scroll
scroll: u16,
/// Aligenment of the text
scroll: (u16, u16),
/// Alignment of the text
alignment: Alignment,
}
impl<'a, 't, T> Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
pub fn new(text: T) -> Paragraph<'a, 't, T> {
/// Describes how to wrap text across lines.
///
/// ## Examples
///
/// ```
/// # use tui::widgets::{Paragraph, Wrap};
/// # use tui::text::Text;
/// let bullet_points = Text::from(r#"Some indented points:
/// - First thing goes here and is long so that it wraps
/// - Here is another point that is long enough to wrap"#);
///
/// // With leading spaces trimmed (window width of 30 chars):
/// Paragraph::new(bullet_points.clone()).wrap(Wrap::default());
/// // Some indented points:
/// // - First thing goes here and is
/// // long so that it wraps
/// // - Here is another point that
/// // is long enough to wrap
///
/// // But without trimming, indentation is preserved:
/// Paragraph::new(bullet_points).wrap(Wrap { trim: false, ..Wrap::default() });
/// // Some indented points:
/// // - First thing goes here
/// // and is long so that it wraps
/// // - Here is another point
/// // that is long enough to wrap
/// ```
pub struct Wrap {
/// Should leading whitespace be trimmed
pub trim: bool,
pub scroll_callback: Option<Box<ScrollCallback>>,
}
impl Default for Wrap {
fn default() -> Wrap {
Wrap {
trim: true,
scroll_callback: None,
}
}
}
pub type ScrollCallback = dyn FnOnce(Rect, &[(Vec<StyledGrapheme<'_>>, u16)]) -> (u16, u16);
impl<'a> Paragraph<'a> {
pub fn new<T>(text: T) -> Paragraph<'a>
where
T: Into<Text<'a>>,
{
Paragraph {
block: None,
style: Default::default(),
wrapping: false,
raw: false,
text,
scroll: 0,
wrap: None,
text: text.into(),
scroll: (0, 0),
alignment: Alignment::Left,
}
}
pub fn block(mut self, block: Block<'a>) -> Paragraph<'a, 't, T> {
pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> {
self.block = Some(block);
self
}
pub fn style(mut self, style: Style) -> Paragraph<'a, 't, T> {
pub fn style(mut self, style: Style) -> Paragraph<'a> {
self.style = style;
self
}
pub fn wrap(mut self, flag: bool) -> Paragraph<'a, 't, T> {
self.wrapping = flag;
pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
self.wrap = Some(wrap);
self
}
pub fn raw(mut self, flag: bool) -> Paragraph<'a, 't, T> {
self.raw = flag;
self
}
pub fn scroll(mut self, offset: u16) -> Paragraph<'a, 't, T> {
pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
self.scroll = offset;
self
}
pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a, 't, T> {
pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> {
self.alignment = alignment;
self
}
fn draw_lines<'b, T>(
&self,
text_area: Rect,
buf: &mut Buffer,
mut line_composer: T,
scroll: (u16, u16),
) where
T: LineComposer<'b>,
{
let mut y = 0;
let mut i = 0;
while let Some((current_line, current_line_width)) = line_composer.next_line() {
if i >= scroll.0 {
let cell_y = text_area.top().saturating_add(y);
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
for StyledGrapheme { symbol, style } in current_line {
buf.get_mut(text_area.left() + x, cell_y)
.set_symbol(if symbol.is_empty() {
// If the symbol is empty, the last char which rendered last time will
// leave on the line. It's a quick fix.
" "
} else {
symbol
})
.set_style(*style);
x += symbol.width() as u16;
}
y += 1;
}
i += 1;
if y >= text_area.height {
break;
}
}
}
}
impl<'a, 't, 'b, T> Widget for Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
impl<'a> Widget for Paragraph<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let text_area = match self.block {
Some(ref mut b) => {
buf.set_style(area, self.style);
let text_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
@@ -119,40 +194,64 @@ where
return;
}
buf.set_background(text_area, self.style.bg);
let style = self.style;
let mut styled = self.text.by_ref().flat_map(|t| match *t {
Text::Raw(ref d) => {
let data: &'t str = d; // coerce to &str
Either::Left(UnicodeSegmentation::graphemes(data, true).map(|g| Styled(g, style)))
}
Text::Styled(ref d, s) => {
let data: &'t str = d; // coerce to &str
Either::Right(UnicodeSegmentation::graphemes(data, true).map(move |g| Styled(g, s)))
}
let mut styled = self.text.lines.iter().flat_map(|spans| {
spans
.0
.iter()
.flat_map(|span| span.styled_graphemes(style))
// Required given the way composers work but might be refactored out if we change
// composers to operate on lines instead of a stream of graphemes.
.chain(iter::once(StyledGrapheme {
symbol: "\n",
style,
}))
});
let mut line_composer: Box<dyn LineComposer> = if self.wrapping {
Box::new(WordWrapper::new(&mut styled, text_area.width))
} else {
Box::new(LineTruncator::new(&mut styled, text_area.width))
};
let mut y = 0;
while let Some((current_line, current_line_width)) = line_composer.next_line() {
if y >= self.scroll {
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
for Styled(symbol, style) in current_line {
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll)
.set_symbol(symbol)
.set_style(*style);
x += symbol.width() as u16;
match self.wrap {
None => {
let mut line_composer = LineTruncator::new(&mut styled, text_area.width);
if let Alignment::Left = self.alignment {
line_composer.set_horizontal_offset(self.scroll.1);
}
self.draw_lines(text_area, buf, line_composer, self.scroll);
}
y += 1;
if y >= text_area.height + self.scroll {
break;
Some(Wrap {
trim,
scroll_callback: None,
}) => {
let line_composer = WordWrapper::new(&mut styled, text_area.width, trim);
self.draw_lines(text_area, buf, line_composer, self.scroll);
}
}
Some(Wrap {
trim,
ref mut scroll_callback,
}) => {
let mut line_composer = WordWrapper::new(&mut styled, text_area.width, trim);
let mut lines = Vec::new();
while let Some((current_line, current_line_width)) = line_composer.next_line() {
lines.push((Vec::from(current_line), current_line_width));
}
let f = scroll_callback.take().unwrap();
let scroll = f(text_area, lines.as_ref());
self.draw_lines(text_area, buf, WrappedLines { lines, index: 0 }, scroll);
}
};
}
}
struct WrappedLines<'a> {
lines: Vec<(Vec<StyledGrapheme<'a>>, u16)>,
index: usize,
}
impl<'a> LineComposer<'a> for WrappedLines<'a> {
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
if self.index >= self.lines.len() {
return None;
}
let (line, width) = &self.lines[self.index];
self.index += 1;
Some((&line, *width))
}
}

View File

@@ -1,42 +1,44 @@
use crate::style::Style;
use crate::text::StyledGrapheme;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
const NBSP: &str = "\u{00a0}";
#[derive(Copy, Clone, Debug)]
pub struct Styled<'a>(pub &'a str, pub Style);
/// A state machine to pack styled symbols into lines.
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
/// iterators for that).
pub trait LineComposer<'a> {
fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)>;
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>;
}
/// A state machine that wraps lines on word boundaries.
pub struct WordWrapper<'a, 'b> {
symbols: &'b mut dyn Iterator<Item = Styled<'a>>,
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16,
current_line: Vec<Styled<'a>>,
next_line: Vec<Styled<'a>>,
current_line: Vec<StyledGrapheme<'a>>,
next_line: Vec<StyledGrapheme<'a>>,
/// Removes the leading whitespace from lines
trim: bool,
}
impl<'a, 'b> WordWrapper<'a, 'b> {
pub fn new(
symbols: &'b mut dyn Iterator<Item = Styled<'a>>,
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16,
trim: bool,
) -> WordWrapper<'a, 'b> {
WordWrapper {
symbols,
max_line_width,
current_line: vec![],
next_line: vec![],
trim,
}
}
}
impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> {
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
if self.max_line_width == 0 {
return None;
}
@@ -46,21 +48,21 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
let mut current_line_width = self
.current_line
.iter()
.map(|Styled(c, _)| c.width() as u16)
.map(|StyledGrapheme { symbol, .. }| symbol.width() as u16)
.sum();
let mut symbols_to_last_word_end: usize = 0;
let mut width_to_last_word_end: u16 = 0;
let mut prev_whitespace = false;
let mut symbols_exhausted = true;
for Styled(symbol, style) in &mut self.symbols {
for StyledGrapheme { symbol, style } in &mut self.symbols {
symbols_exhausted = false;
let symbol_whitespace = symbol.chars().all(&char::is_whitespace);
// Ignore characters wider that the total max width.
if symbol.width() as u16 > self.max_line_width
// Skip leading whitespace.
|| symbol_whitespace && symbol != "\n" && current_line_width == 0
// Skip leading whitespace when trim is enabled.
|| self.trim && symbol_whitespace && symbol != "\n" && current_line_width == 0
{
continue;
}
@@ -80,7 +82,7 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
width_to_last_word_end = current_line_width;
}
self.current_line.push(Styled(symbol, style));
self.current_line.push(StyledGrapheme { symbol, style });
current_line_width += symbol.width() as u16;
if current_line_width > self.max_line_width {
@@ -94,9 +96,10 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
// Push the remainder to the next line but strip leading whitespace:
{
let remainder = &self.current_line[truncate_at..];
if let Some(remainder_nonwhite) = remainder
.iter()
.position(|Styled(c, _)| !c.chars().all(&char::is_whitespace))
if let Some(remainder_nonwhite) =
remainder.iter().position(|StyledGrapheme { symbol, .. }| {
!symbol.chars().all(&char::is_whitespace)
})
{
self.next_line
.extend_from_slice(&remainder[remainder_nonwhite..]);
@@ -121,26 +124,33 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
/// A state machine that truncates overhanging lines.
pub struct LineTruncator<'a, 'b> {
symbols: &'b mut dyn Iterator<Item = Styled<'a>>,
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16,
current_line: Vec<Styled<'a>>,
current_line: Vec<StyledGrapheme<'a>>,
/// Record the offet to skip render
horizontal_offset: u16,
}
impl<'a, 'b> LineTruncator<'a, 'b> {
pub fn new(
symbols: &'b mut dyn Iterator<Item = Styled<'a>>,
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16,
) -> LineTruncator<'a, 'b> {
LineTruncator {
symbols,
max_line_width,
horizontal_offset: 0,
current_line: vec![],
}
}
pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) {
self.horizontal_offset = horizontal_offset;
}
}
impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> {
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
if self.max_line_width == 0 {
return None;
}
@@ -150,7 +160,8 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
let mut skip_rest = false;
let mut symbols_exhausted = true;
for Styled(symbol, style) in &mut self.symbols {
let mut horizontal_offset = self.horizontal_offset as usize;
for StyledGrapheme { symbol, style } in &mut self.symbols {
symbols_exhausted = false;
// Ignore characters wider that the total max width.
@@ -169,12 +180,25 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
break;
}
let symbol = if horizontal_offset == 0 {
symbol
} else {
let w = symbol.width();
if w > horizontal_offset {
let t = trim_offset(symbol, horizontal_offset);
horizontal_offset = 0;
t
} else {
horizontal_offset -= w;
""
}
};
current_line_width += symbol.width() as u16;
self.current_line.push(Styled(symbol, style));
self.current_line.push(StyledGrapheme { symbol, style });
}
if skip_rest {
for Styled(symbol, _) in &mut self.symbols {
for StyledGrapheme { symbol, .. } in &mut self.symbols {
if symbol == "\n" {
break;
}
@@ -189,21 +213,40 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
}
}
/// This function will return a str slice which start at specified offset.
/// As src is a unicode str, start offset has to be calculated with each character.
fn trim_offset(src: &str, mut offset: usize) -> &str {
let mut start = 0;
for c in UnicodeSegmentation::graphemes(src, true) {
let w = c.width();
if w <= offset {
offset -= w;
start += c.len();
} else {
break;
}
}
&src[start..]
}
#[cfg(test)]
mod test {
use super::*;
use unicode_segmentation::UnicodeSegmentation;
enum Composer {
WordWrapper,
WordWrapper { trim: bool },
LineTruncator,
}
fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
let style = Default::default();
let mut styled = UnicodeSegmentation::graphemes(text, true).map(|g| Styled(g, style));
let mut styled =
UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
let mut composer: Box<dyn LineComposer> = match which {
Composer::WordWrapper => Box::new(WordWrapper::new(&mut styled, text_area_width)),
Composer::WordWrapper { trim } => {
Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
}
Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
};
let mut lines = vec![];
@@ -211,7 +254,7 @@ mod test {
while let Some((styled, width)) = composer.next_line() {
let line = styled
.iter()
.map(|Styled(g, _style)| *g)
.map(|StyledGrapheme { symbol, .. }| *symbol)
.collect::<String>();
assert!(width <= text_area_width);
lines.push(line);
@@ -225,7 +268,8 @@ mod test {
let width = 40;
for i in 1..width {
let text = "a".repeat(i);
let (word_wrapper, _) = run_composer(Composer::WordWrapper, &text, width as u16);
let (word_wrapper, _) =
run_composer(Composer::WordWrapper { trim: true }, &text, width as u16);
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
let expected = vec![text];
assert_eq!(word_wrapper, expected);
@@ -238,7 +282,7 @@ mod test {
let width = 20;
let text =
"abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
let wrapped: Vec<&str> = text.split('\n').collect();
@@ -250,7 +294,8 @@ mod test {
fn line_composer_long_word() {
let width = 20;
let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width as u16);
let (word_wrapper, _) =
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
let wrapped = vec![
@@ -261,7 +306,7 @@ mod test {
];
assert_eq!(
word_wrapper, wrapped,
"WordWrapper should deect the line cannot be broken on word boundary and \
"WordWrapper should detect the line cannot be broken on word boundary and \
break it at line width limit."
);
assert_eq!(line_truncator, vec![&text[..width]]);
@@ -276,9 +321,12 @@ mod test {
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
m n o";
let (word_wrapper_single_space, _) =
run_composer(Composer::WordWrapper, text, width as u16);
let (word_wrapper_multi_space, _) =
run_composer(Composer::WordWrapper, text_multi_space, width as u16);
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
let (word_wrapper_multi_space, _) = run_composer(
Composer::WordWrapper { trim: true },
text_multi_space,
width as u16,
);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
let word_wrapped = vec![
@@ -298,7 +346,7 @@ mod test {
fn line_composer_zero_width() {
let width = 0;
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
let expected: Vec<&str> = Vec::new();
@@ -310,7 +358,7 @@ mod test {
fn line_composer_max_line_width_of_1() {
let width = 1;
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
@@ -325,7 +373,7 @@ mod test {
let width = 1;
let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
両端点では、";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
assert_eq!(line_truncator, vec!["", "a"]);
@@ -336,7 +384,7 @@ mod test {
fn line_composer_word_wrapper_mixed_length() {
let width = 20;
let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
assert_eq!(
word_wrapper,
vec![
@@ -354,7 +402,8 @@ mod test {
let width = 20;
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
では、";
let (word_wrapper, word_wrapper_width) = run_composer(Composer::WordWrapper, &text, width);
let (word_wrapper, word_wrapper_width) =
run_composer(Composer::WordWrapper { trim: true }, &text, width);
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
let wrapped = vec![
@@ -372,7 +421,7 @@ mod test {
fn line_composer_leading_whitespace_removal() {
let width = 20;
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
@@ -383,7 +432,7 @@ mod test {
fn line_composer_lots_of_spaces() {
let width = 20;
let text = " ";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(word_wrapper, vec![""]);
assert_eq!(line_truncator, vec![" "]);
@@ -395,7 +444,7 @@ mod test {
fn line_composer_char_plus_lots_of_spaces() {
let width = 20;
let text = "a ";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
// What's happening below is: the first line gets consumed, trailing spaces discarded,
// after 20 of which a word break occurs (probably shouldn't). The second line break
@@ -414,7 +463,8 @@ mod test {
// hiragana and katakana...
// This happens to also be a test case for mixed width because regular spaces are single width.
let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
let (word_wrapper, word_wrapper_width) = run_composer(Composer::WordWrapper, text, width);
let (word_wrapper, word_wrapper_width) =
run_composer(Composer::WordWrapper { trim: true }, text, width);
assert_eq!(
word_wrapper,
vec![
@@ -435,12 +485,50 @@ mod test {
fn line_composer_word_wrapper_nbsp() {
let width = 20;
let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
// Ensure that if the character was a regular space, it would be wrapped differently.
let text_space = text.replace("\u{00a0}", " ");
let (word_wrapper_space, _) = run_composer(Composer::WordWrapper, &text_space, width);
let (word_wrapper_space, _) =
run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
}
#[test]
fn line_composer_word_wrapper_preserve_indentation() {
let width = 20;
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
}
#[test]
fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
let width = 10;
let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
assert_eq!(
word_wrapper,
vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"]
);
}
#[test]
fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
let width = 10;
let text = " 4 Indent\n must wrap!";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
assert_eq!(
word_wrapper,
vec![
" ",
" 4",
"Indent",
" ",
" must",
"wrap!"
]
);
}
}

View File

@@ -76,10 +76,11 @@ impl<'a> Sparkline<'a> {
impl<'a> Widget for Sparkline<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let spark_area = match self.block {
Some(ref mut b) => {
let spark_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
@@ -120,8 +121,7 @@ impl<'a> Widget for Sparkline<'a> {
};
buf.get_mut(spark_area.left() + i as u16, spark_area.top() + j)
.set_symbol(symbol)
.set_fg(self.style.fg)
.set_bg(self.style.bg);
.set_style(self.style);
if *d > 8 {
*d -= 8;

View File

@@ -220,17 +220,18 @@ where
type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
buf.set_style(area, self.style);
// Render block if necessary and get the drawing area
let table_area = match self.block {
Some(ref mut b) => {
let table_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
buf.set_background(table_area, self.style.bg);
let mut solver = Solver::new();
let mut var_indices = HashMap::new();
let mut ccs = Vec::new();

View File

@@ -1,10 +1,11 @@
use unicode_width::UnicodeWidthStr;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::symbols::line;
use crate::widgets::{Block, Widget};
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols,
text::{Span, Spans},
widgets::{Block, Widget},
};
/// A widget to display available tabs in a multiple panels context.
///
@@ -13,93 +14,80 @@ use crate::widgets::{Block, Widget};
/// ```
/// # use tui::widgets::{Block, Borders, Tabs};
/// # use tui::style::{Style, Color};
/// # use tui::text::{Spans};
/// # use tui::symbols::{DOT};
/// Tabs::default()
/// let titles = ["Tab1", "Tab2", "Tab3", "Tab4"].iter().cloned().map(Spans::from).collect();
/// Tabs::new(titles)
/// .block(Block::default().title("Tabs").borders(Borders::ALL))
/// .titles(&["Tab1", "Tab2", "Tab3", "Tab4"])
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().fg(Color::Yellow))
/// .divider(DOT);
/// ```
#[derive(Debug, Clone)]
pub struct Tabs<'a, T>
where
T: AsRef<str> + 'a,
{
pub struct Tabs<'a> {
/// A block to wrap this widget in if necessary
block: Option<Block<'a>>,
/// One title for each tab
titles: &'a [T],
titles: Vec<Spans<'a>>,
/// The index of the selected tabs
selected: usize,
/// The style used to draw the text
style: Style,
/// The style used to display the selected item
/// Style to apply to the selected item
highlight_style: Style,
/// Tab divider
divider: &'a str,
divider: Span<'a>,
}
impl<'a, T> Default for Tabs<'a, T>
where
T: AsRef<str>,
{
fn default() -> Tabs<'a, T> {
impl<'a> Tabs<'a> {
pub fn new(titles: Vec<Spans<'a>>) -> Tabs<'a> {
Tabs {
block: None,
titles: &[],
titles,
selected: 0,
style: Default::default(),
highlight_style: Default::default(),
divider: line::VERTICAL,
divider: Span::raw(symbols::line::VERTICAL),
}
}
}
impl<'a, T> Tabs<'a, T>
where
T: AsRef<str>,
{
pub fn block(mut self, block: Block<'a>) -> Tabs<'a, T> {
pub fn block(mut self, block: Block<'a>) -> Tabs<'a> {
self.block = Some(block);
self
}
pub fn titles(mut self, titles: &'a [T]) -> Tabs<'a, T> {
self.titles = titles;
self
}
pub fn select(mut self, selected: usize) -> Tabs<'a, T> {
pub fn select(mut self, selected: usize) -> Tabs<'a> {
self.selected = selected;
self
}
pub fn style(mut self, style: Style) -> Tabs<'a, T> {
pub fn style(mut self, style: Style) -> Tabs<'a> {
self.style = style;
self
}
pub fn highlight_style(mut self, style: Style) -> Tabs<'a, T> {
pub fn highlight_style(mut self, style: Style) -> Tabs<'a> {
self.highlight_style = style;
self
}
pub fn divider(mut self, divider: &'a str) -> Tabs<'a, T> {
self.divider = divider;
pub fn divider<T>(mut self, divider: T) -> Tabs<'a>
where
T: Into<Span<'a>>,
{
self.divider = divider.into();
self
}
}
impl<'a, T> Widget for Tabs<'a, T>
where
T: AsRef<str>,
{
impl<'a> Widget for Tabs<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let tabs_area = match self.block {
Some(ref mut b) => {
buf.set_style(area, self.style);
let tabs_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
@@ -108,32 +96,34 @@ where
return;
}
buf.set_background(tabs_area, self.style.bg);
let mut x = tabs_area.left();
let titles_length = self.titles.len();
let divider_width = self.divider.width() as u16;
for (title, style, last_title) in self.titles.iter().enumerate().map(|(i, t)| {
let lt = i + 1 == titles_length;
if i == self.selected {
(t, self.highlight_style, lt)
} else {
(t, self.style, lt)
}
}) {
x += 1;
if x > tabs_area.right() {
for (i, title) in self.titles.into_iter().enumerate() {
let last_title = titles_length - 1 == i;
x = x.saturating_add(1);
let remaining_width = tabs_area.right().saturating_sub(x);
if remaining_width == 0 {
break;
} else {
buf.set_string(x, tabs_area.top(), title.as_ref(), style);
x += title.as_ref().width() as u16 + 1;
if x >= tabs_area.right() || last_title {
break;
} else {
buf.set_string(x, tabs_area.top(), self.divider, self.style);
x += divider_width;
}
}
let pos = buf.set_spans(x, tabs_area.top(), &title, remaining_width);
if i == self.selected {
buf.set_style(
Rect {
x,
y: tabs_area.top(),
width: pos.0.saturating_sub(x),
height: 1,
},
self.highlight_style,
);
}
x = pos.0.saturating_add(1);
let remaining_width = tabs_area.right().saturating_sub(x);
if remaining_width == 0 || last_title {
break;
}
let pos = buf.set_span(x, tabs_area.top(), &self.divider, remaining_width);
x = pos.0;
}
}
}

63
tests/backend_termion.rs Normal file
View File

@@ -0,0 +1,63 @@
#[cfg(feature = "termion")]
#[test]
fn backend_termion_should_only_write_diffs() -> Result<(), Box<dyn std::error::Error>> {
use std::{fmt::Write, io::Cursor};
let mut bytes = Vec::new();
let mut stdout = Cursor::new(&mut bytes);
{
use tui::{
backend::TermionBackend, layout::Rect, widgets::Paragraph, Terminal, TerminalOptions,
Viewport,
};
let backend = TermionBackend::new(&mut stdout);
let area = Rect::new(0, 0, 3, 1);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::fixed(area),
},
)?;
terminal.draw(|f| {
f.render_widget(Paragraph::new("a"), area);
})?;
terminal.draw(|f| {
f.render_widget(Paragraph::new("ab"), area);
})?;
terminal.draw(|f| {
f.render_widget(Paragraph::new("abc"), area);
})?;
}
let expected = {
use termion::{color, cursor, style};
let mut s = String::new();
// First draw
write!(s, "{}", cursor::Goto(1, 1))?;
s.push_str("a");
write!(s, "{}", color::Fg(color::Reset))?;
write!(s, "{}", color::Bg(color::Reset))?;
write!(s, "{}", style::Reset)?;
write!(s, "{}", cursor::Hide)?;
// Second draw
write!(s, "{}", cursor::Goto(2, 1))?;
s.push_str("b");
write!(s, "{}", color::Fg(color::Reset))?;
write!(s, "{}", color::Bg(color::Reset))?;
write!(s, "{}", style::Reset)?;
write!(s, "{}", cursor::Hide)?;
// Third draw
write!(s, "{}", cursor::Goto(3, 1))?;
s.push_str("c");
write!(s, "{}", color::Fg(color::Reset))?;
write!(s, "{}", color::Bg(color::Reset))?;
write!(s, "{}", style::Reset)?;
write!(s, "{}", cursor::Hide)?;
// Terminal drop
write!(s, "{}", cursor::Show)?;
s
};
assert_eq!(std::str::from_utf8(&bytes)?, expected);
Ok(())
}

View File

@@ -1,74 +0,0 @@
use tui::{
backend::TestBackend,
layout::Rect,
style::{Color, Style},
symbols,
widgets::{Axis, Block, Borders, Chart, Dataset},
Terminal,
};
#[test]
fn zero_axes_ok() {
let backend = TestBackend::new(100, 100);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
let datasets = [Dataset::default()
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta))
.data(&[(0.0, 0.0)])];
let chart = Chart::default()
.block(Block::default().title("Plot").borders(Borders::ALL))
.x_axis(Axis::default().bounds([0.0, 0.0]).labels(&["0.0", "1.0"]))
.y_axis(Axis::default().bounds([0.0, 1.0]).labels(&["0.0", "1.0"]))
.datasets(&datasets);
f.render_widget(
chart,
Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
);
})
.unwrap();
}
#[test]
fn handles_overflow() {
let backend = TestBackend::new(80, 30);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
let datasets = [Dataset::default()
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta))
.data(&[
(1588298471.0, 1.0),
(1588298473.0, 0.0),
(1588298496.0, 1.0),
])];
let chart = Chart::default()
.block(Block::default().title("Plot").borders(Borders::ALL))
.x_axis(
Axis::default()
.bounds([1588298471.0, 1588992600.0])
.labels(&["1588298471.0", "1588992600.0"]),
)
.y_axis(Axis::default().bounds([0.0, 1.0]).labels(&["0.0", "1.0"]))
.datasets(&datasets);
f.render_widget(
chart,
Rect {
x: 0,
y: 0,
width: 80,
height: 30,
},
);
})
.unwrap();
}

View File

@@ -1,8 +1,10 @@
use tui::backend::{Backend, TestBackend};
use tui::Terminal;
use tui::{
backend::{Backend, TestBackend},
Terminal,
};
#[test]
fn buffer_size_limited() {
fn terminal_buffer_size_should_be_limited() {
let backend = TestBackend::new(400, 400);
let terminal = Terminal::new(backend).unwrap();
let size = terminal.backend().size().unwrap();

View File

@@ -1,20 +1,22 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders};
use tui::Terminal;
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
style::{Color, Style},
text::Span,
widgets::{Block, Borders},
Terminal,
};
#[test]
fn it_draws_a_block() {
fn widgets_block_renders() {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let block = Block::default()
.title("Title")
.borders(Borders::ALL)
.title_style(Style::default().fg(Color::LightBlue));
.title(Span::styled("Title", Style::default().fg(Color::LightBlue)))
.borders(Borders::ALL);
f.render_widget(
block,
Rect {
@@ -41,5 +43,5 @@ fn it_draws_a_block() {
for x in 1..=5 {
expected.get_mut(x, 0).set_fg(Color::LightBlue);
}
assert_eq!(&expected, terminal.backend().buffer());
terminal.backend().assert_buffer(&expected);
}

126
tests/widgets_chart.rs Normal file
View File

@@ -0,0 +1,126 @@
use tui::{
backend::TestBackend,
layout::Rect,
style::{Color, Style},
symbols,
text::Span,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType::Line},
Terminal,
};
fn create_labels<'a>(labels: &'a [&'a str]) -> Vec<Span<'a>> {
labels.iter().map(|l| Span::from(*l)).collect()
}
#[test]
fn widgets_chart_can_have_axis_with_zero_length_bounds() {
let backend = TestBackend::new(100, 100);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let datasets = vec![Dataset::default()
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta))
.data(&[(0.0, 0.0)])];
let chart = Chart::new(datasets)
.block(Block::default().title("Plot").borders(Borders::ALL))
.x_axis(
Axis::default()
.bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
);
f.render_widget(
chart,
Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
);
})
.unwrap();
}
#[test]
fn widgets_chart_handles_overflows() {
let backend = TestBackend::new(80, 30);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let datasets = vec![Dataset::default()
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta))
.data(&[
(1_588_298_471.0, 1.0),
(1_588_298_473.0, 0.0),
(1_588_298_496.0, 1.0),
])];
let chart = Chart::new(datasets)
.block(Block::default().title("Plot").borders(Borders::ALL))
.x_axis(
Axis::default()
.bounds([1_588_298_471.0, 1_588_992_600.0])
.labels(create_labels(&["1588298471.0", "1588992600.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 1.0])
.labels(create_labels(&["0.0", "1.0"])),
);
f.render_widget(
chart,
Rect {
x: 0,
y: 0,
width: 80,
height: 30,
},
);
})
.unwrap();
}
#[test]
fn widgets_chart_can_have_empty_datasets() {
let backend = TestBackend::new(100, 100);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let datasets = vec![Dataset::default().data(&[]).graph_type(Line)];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Empty Dataset With Line")
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 1.0])
.labels(create_labels(&["0.0", "1.0"])),
);
f.render_widget(
chart,
Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
);
})
.unwrap();
}

View File

@@ -1,15 +1,17 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::{Constraint, Direction, Layout};
use tui::widgets::{Block, Borders, Gauge};
use tui::Terminal;
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders, Gauge},
Terminal,
};
#[test]
fn gauge_render() {
fn widgets_gauge_renders() {
let backend = TestBackend::new(40, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
@@ -38,5 +40,5 @@ fn gauge_render() {
" ",
" ",
]);
assert_eq!(&expected, terminal.backend().buffer());
terminal.backend().assert_buffer(&expected);
}

View File

@@ -4,86 +4,85 @@ use tui::{
layout::Rect,
style::{Color, Style},
symbols,
widgets::{Block, Borders, List, ListState, Text},
widgets::{Block, Borders, List, ListItem, ListState},
Terminal,
};
#[test]
fn it_should_highlight_the_selected_item() {
fn widgets_list_should_highlight_the_selected_item() {
let backend = TestBackend::new(10, 3);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
state.select(Some(1));
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let items = vec![
Text::raw("Item 1"),
Text::raw("Item 2"),
Text::raw("Item 3"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
];
let list = List::new(items.into_iter())
let list = List::new(items)
.highlight_style(Style::default().bg(Color::Yellow))
.highlight_symbol(">> ");
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![" Item 1 ", ">> Item 2 ", " Item 3 "]);
for x in 0..9 {
for x in 0..10 {
expected.get_mut(x, 1).set_bg(Color::Yellow);
}
assert_eq!(*terminal.backend().buffer(), expected);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn it_should_truncate_items() {
fn widgets_list_should_truncate_items() {
let backend = TestBackend::new(10, 2);
let mut terminal = Terminal::new(backend).unwrap();
struct TruncateTestCase<'a> {
name: &'a str,
selected: Option<usize>,
items: Vec<Text<'a>>,
items: Vec<ListItem<'a>>,
expected: Buffer,
}
let cases = vec![
// An item is selected
TruncateTestCase {
name: "an item is selected",
selected: Some(0),
items: vec![Text::raw("A very long line"), Text::raw("A very long line")],
items: vec![
ListItem::new("A very long line"),
ListItem::new("A very long line"),
],
expected: Buffer::with_lines(vec![
format!(">> A ve{} ", symbols::line::VERTICAL),
format!(" A ve{} ", symbols::line::VERTICAL),
]),
},
// No item is selected
TruncateTestCase {
name: "no item is selected",
selected: None,
items: vec![Text::raw("A very long line"), Text::raw("A very long line")],
items: vec![
ListItem::new("A very long line"),
ListItem::new("A very long line"),
],
expected: Buffer::with_lines(vec![
format!("A very {} ", symbols::line::VERTICAL),
format!("A very {} ", symbols::line::VERTICAL),
]),
},
];
for mut case in cases {
for case in cases {
let mut state = ListState::default();
state.select(case.selected);
let items = case.items.drain(..);
terminal
.draw(|mut f| {
let list = List::new(items.into_iter())
.draw(|f| {
let list = List::new(case.items.clone())
.block(Block::default().borders(Borders::RIGHT))
.highlight_symbol(">> ");
f.render_stateful_widget(list, Rect::new(0, 0, 8, 2), &mut state);
})
.unwrap();
assert_eq!(
*terminal.backend().buffer(),
case.expected,
"Failed to assert the buffer matches the expected one when {}",
case.name
);
terminal.backend().assert_buffer(&case.expected);
}
}

View File

@@ -1,8 +1,11 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::Alignment;
use tui::widgets::{Block, Borders, Paragraph, Text};
use tui::Terminal;
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::Alignment,
text::{Spans, Text},
widgets::{Block, Borders, Paragraph, Wrap},
Terminal,
};
const SAMPLE_STRING: &str = "The library is based on the principle of immediate rendering with \
intermediate buffers. This means that at each new frame you should build all widgets that are \
@@ -10,27 +13,27 @@ const SAMPLE_STRING: &str = "The library is based on the principle of immediate
interactive UI, this may introduce overhead for highly dynamic content.";
#[test]
fn paragraph_render_wrap() {
let render = |alignment| {
fn widgets_paragraph_can_wrap_its_content() {
let test_case = |alignment, expected| {
let backend = TestBackend::new(20, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let text = [Text::raw(SAMPLE_STRING)];
let paragraph = Paragraph::new(text.iter())
let text = vec![Spans::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.alignment(alignment)
.wrap(true);
.wrap(Wrap::default());
f.render_widget(paragraph, size);
})
.unwrap();
terminal.backend().buffer().clone()
terminal.backend().assert_buffer(&expected);
};
assert_eq!(
render(Alignment::Left),
test_case(
Alignment::Left,
Buffer::with_lines(vec![
"┌──────────────────┐",
"│The library is │",
@@ -42,10 +45,10 @@ fn paragraph_render_wrap() {
"│buffers. This │",
"│means that at each│",
"└──────────────────┘",
])
]),
);
assert_eq!(
render(Alignment::Right),
test_case(
Alignment::Right,
Buffer::with_lines(vec![
"┌──────────────────┐",
"│ The library is│",
@@ -57,10 +60,10 @@ fn paragraph_render_wrap() {
"│ buffers. This│",
"│means that at each│",
"└──────────────────┘",
])
]),
);
assert_eq!(
render(Alignment::Center),
test_case(
Alignment::Center,
Buffer::with_lines(vec![
"┌──────────────────┐",
"│ The library is │",
@@ -72,23 +75,23 @@ fn paragraph_render_wrap() {
"│ buffers. This │",
"│means that at each│",
"└──────────────────┘",
])
]),
);
}
#[test]
fn paragraph_render_double_width() {
fn widgets_paragraph_renders_double_width_graphemes() {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend).unwrap();
let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、";
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let text = [Text::raw(s)];
let paragraph = Paragraph::new(text.iter())
let text = vec![Spans::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(true);
.wrap(Wrap::default());
f.render_widget(paragraph, size);
})
.unwrap();
@@ -105,22 +108,22 @@ fn paragraph_render_double_width() {
"│を行う場│",
"└────────┘",
]);
assert_eq!(&expected, terminal.backend().buffer());
terminal.backend().assert_buffer(&expected);
}
#[test]
fn paragraph_render_mixed_width() {
fn widgets_paragraph_renders_mixed_width_graphemes() {
let backend = TestBackend::new(10, 7);
let mut terminal = Terminal::new(backend).unwrap();
let s = "aコンピュータ上で文字を扱う場合、";
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let text = [Text::raw(s)];
let paragraph = Paragraph::new(text.iter())
let text = vec![Spans::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(true);
.wrap(Wrap::default());
f.render_widget(paragraph, size);
})
.unwrap();
@@ -135,5 +138,62 @@ fn paragraph_render_mixed_width() {
"│、 │",
"└────────┘",
]);
assert_eq!(&expected, terminal.backend().buffer());
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_paragraph_can_scroll_horizontally() {
let test_case = |alignment, scroll, expected| {
let backend = TestBackend::new(20, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let text = Text::from(
"段落现在可以水平滚动了!\nParagraph can scroll horizontally!\nShort line",
);
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.alignment(alignment)
.scroll(scroll);
f.render_widget(paragraph, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
test_case(
Alignment::Left,
(0, 7),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│在可以水平滚动了!│",
"│ph can scroll hori│",
"│ine │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└──────────────────┘",
]),
);
// only support Alignment::Left
test_case(
Alignment::Right,
(0, 7),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│段落现在可以水平滚│",
"│Paragraph can scro│",
"│ Short line│",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└──────────────────┘",
]),
);
}

View File

@@ -5,13 +5,13 @@ use tui::widgets::{Block, Borders, Row, Table};
use tui::Terminal;
#[test]
fn table_column_spacing() {
let render = |column_spacing| {
fn widgets_table_column_spacing_can_be_changed() {
let test_case = |column_spacing, expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
@@ -33,12 +33,12 @@ fn table_column_spacing() {
f.render_widget(table, size);
})
.unwrap();
terminal.backend().buffer().clone()
terminal.backend().assert_buffer(&expected);
};
// no space between columns
assert_eq!(
render(0),
test_case(
0,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1Head2Head3 │",
@@ -50,12 +50,12 @@ fn table_column_spacing() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
// one space between columns
assert_eq!(
render(1),
test_case(
1,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
@@ -67,12 +67,12 @@ fn table_column_spacing() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
// enough space to just not hide the third column
assert_eq!(
render(6),
test_case(
6,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
@@ -84,12 +84,12 @@ fn table_column_spacing() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
// enough space to hide part of the third column
assert_eq!(
render(7),
test_case(
7,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head│",
@@ -101,18 +101,18 @@ fn table_column_spacing() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
}
#[test]
fn table_widths() {
let render = |widths| {
fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
let test_case = |widths, expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
@@ -129,16 +129,16 @@ fn table_widths() {
f.render_widget(table, size);
})
.unwrap();
terminal.backend().buffer().clone()
terminal.backend().assert_buffer(&expected);
};
// columns of zero width show nothing
assert_eq!(
render(&[
test_case(
&[
Constraint::Length(0),
Constraint::Length(0),
Constraint::Length(0)
]),
Constraint::Length(0),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ │",
@@ -150,16 +150,16 @@ fn table_widths() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
// columns of 1 width trim
assert_eq!(
render(&[
test_case(
&[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1)
]),
Constraint::Length(1),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│H H H │",
@@ -171,16 +171,16 @@ fn table_widths() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
// columns of large width just before pushing a column off
assert_eq!(
render(&[
test_case(
&[
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(8)
]),
Constraint::Length(8),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
@@ -192,18 +192,18 @@ fn table_widths() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
}
#[test]
fn table_percentage_widths() {
let render = |widths| {
fn widgets_table_columns_widths_can_use_percentage_constraints() {
let test_case = |widths, expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
@@ -221,16 +221,16 @@ fn table_percentage_widths() {
f.render_widget(table, size);
})
.unwrap();
terminal.backend().buffer().clone()
terminal.backend().assert_buffer(&expected);
};
// columns of zero width show nothing
assert_eq!(
render(&[
test_case(
&[
Constraint::Percentage(0),
Constraint::Percentage(0),
Constraint::Percentage(0)
]),
Constraint::Percentage(0),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ │",
@@ -242,16 +242,16 @@ fn table_percentage_widths() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
// columns of not enough width trims the data
assert_eq!(
render(&[
test_case(
&[
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10)
]),
Constraint::Percentage(10),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│HeaHeaHea │",
@@ -263,16 +263,16 @@ fn table_percentage_widths() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
// columns of large width just before pushing a column off
assert_eq!(
render(&[
test_case(
&[
Constraint::Percentage(30),
Constraint::Percentage(30),
Constraint::Percentage(30)
]),
Constraint::Percentage(30),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
@@ -284,12 +284,12 @@ fn table_percentage_widths() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
// percentages summing to 100 should give equal widths
assert_eq!(
render(&[Constraint::Percentage(50), Constraint::Percentage(50)]),
test_case(
&[Constraint::Percentage(50), Constraint::Percentage(50)],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 │",
@@ -301,18 +301,18 @@ fn table_percentage_widths() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
}
#[test]
fn table_mixed_widths() {
let render = |widths| {
fn widgets_table_columns_widths_can_use_mixed_constraints() {
let test_case = |widths, expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
@@ -329,16 +329,16 @@ fn table_mixed_widths() {
f.render_widget(table, size);
})
.unwrap();
terminal.backend().buffer().clone()
terminal.backend().assert_buffer(&expected);
};
// columns of zero width show nothing
assert_eq!(
render(&[
test_case(
&[
Constraint::Percentage(0),
Constraint::Length(0),
Constraint::Percentage(0)
]),
Constraint::Percentage(0),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ │",
@@ -350,16 +350,16 @@ fn table_mixed_widths() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
// columns of not enough width trims the data
assert_eq!(
render(&[
test_case(
&[
Constraint::Percentage(10),
Constraint::Length(20),
Constraint::Percentage(10)
]),
Constraint::Percentage(10),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Hea Head2 Hea│",
@@ -371,16 +371,16 @@ fn table_mixed_widths() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
// columns of large width just before pushing a column off
assert_eq!(
render(&[
test_case(
&[
Constraint::Percentage(30),
Constraint::Length(10),
Constraint::Percentage(30)
]),
Constraint::Percentage(30),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
@@ -392,16 +392,16 @@ fn table_mixed_widths() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
// columns of large size (>100% total) hide the last column
assert_eq!(
render(&[
test_case(
&[
Constraint::Percentage(60),
Constraint::Length(10),
Constraint::Percentage(60)
]),
Constraint::Percentage(60),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 │",
@@ -413,6 +413,6 @@ fn table_mixed_widths() {
"│ │",
"│ │",
"└────────────────────────────┘",
])
]),
);
}

48
tests/widgets_tabs.rs Normal file
View File

@@ -0,0 +1,48 @@
use tui::{
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Spans, widgets::Tabs,
Terminal,
};
#[test]
fn widgets_tabs_should_not_panic_on_narrow_areas() {
let backend = TestBackend::new(1, 1);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Spans::from).collect());
f.render_widget(
tabs,
Rect {
x: 0,
y: 0,
width: 1,
height: 1,
},
);
})
.unwrap();
let expected = Buffer::with_lines(vec![" "]);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_tabs_should_truncate_the_last_item() {
let backend = TestBackend::new(10, 1);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Spans::from).collect());
f.render_widget(
tabs,
Rect {
x: 0,
y: 0,
width: 9,
height: 1,
},
);
})
.unwrap();
let expected = Buffer::with_lines(vec![format!(" Tab1 {} T ", symbols::line::VERTICAL)]);
terminal.backend().assert_buffer(&expected);
}