Compare commits
132 Commits
v0.9.5
...
653/master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f32fb022e0 | ||
|
|
12e4f0d921 | ||
|
|
727e13d876 | ||
|
|
71835a0a52 | ||
|
|
a6b25a4877 | ||
|
|
90d8cb6526 | ||
|
|
e71faa988e | ||
|
|
ed0ae81aae | ||
|
|
a61b078dea | ||
|
|
85939306e3 | ||
|
|
cf2d9c2c1d | ||
|
|
853d9047b0 | ||
|
|
6069d89dee | ||
|
|
d25e263b8e | ||
|
|
d05e696d45 | ||
|
|
ef583cead9 | ||
|
|
90c4da4e68 | ||
|
|
8032191366 | ||
|
|
c8c03294e1 | ||
|
|
e00df22588 | ||
|
|
9806217a6a | ||
|
|
1be5cf2d90 | ||
|
|
ca68bae4ed | ||
|
|
8c1f58079f | ||
|
|
4845c03eec | ||
|
|
532a595c41 | ||
|
|
25ce5bc90b | ||
|
|
80a929ccc6 | ||
|
|
3797863e14 | ||
|
|
7870793b4b | ||
|
|
a7c21a9729 | ||
|
|
914d54e672 | ||
|
|
a68e38e59e | ||
|
|
e870e5d8a5 | ||
|
|
29387e785c | ||
|
|
8eb6336f5e | ||
|
|
34a2be6458 | ||
|
|
fbd834469f | ||
|
|
8da5f740af | ||
|
|
38dcddb126 | ||
|
|
92948d2394 | ||
|
|
a3a0a80a02 | ||
|
|
a5f7019b2a | ||
|
|
e05b80cec1 | ||
|
|
23d5fbde56 | ||
|
|
a346704cdc | ||
|
|
24396d97ed | ||
|
|
703e41cd49 | ||
|
|
975c4165d0 | ||
|
|
dbf38d847a | ||
|
|
91a2519cc3 | ||
|
|
a1c3ba2088 | ||
|
|
d47565be5c | ||
|
|
1028d39db0 | ||
|
|
b250825c38 | ||
|
|
90a6a8f2d6 | ||
|
|
414386e797 | ||
|
|
3a843d5074 | ||
|
|
4e76bfa2ca | ||
|
|
8832281dcf | ||
|
|
853f3d9200 | ||
|
|
67e996c5f4 | ||
|
|
f09863faa0 | ||
|
|
eb1e3be722 | ||
|
|
4ec902b96f | ||
|
|
74243394d9 | ||
|
|
e7f263efa7 | ||
|
|
0991145c58 | ||
|
|
01d2a8588a | ||
|
|
45431a2649 | ||
|
|
0b78fb9201 | ||
|
|
9cdff275cb | ||
|
|
77c6e106e4 | ||
|
|
efdd6bfb19 | ||
|
|
117098d2d2 | ||
|
|
f933d892aa | ||
|
|
5ea54792c0 | ||
|
|
23a9280db7 | ||
|
|
79e27b1778 | ||
|
|
0a05579a1c | ||
|
|
0030eb4a13 | ||
|
|
5bf40343eb | ||
|
|
1e35f983c4 | ||
|
|
a15ac8870b | ||
|
|
8a27036a54 | ||
|
|
8543523f18 | ||
|
|
5a9b59866b | ||
|
|
dc76956215 | ||
|
|
98fb5e4bbd | ||
|
|
25ff2e5e61 | ||
|
|
5050f1ce1c | ||
|
|
51b691e7ac | ||
|
|
c4cd0a5f31 | ||
|
|
41142732ec | ||
|
|
62495c3bd1 | ||
|
|
d00184a7c3 | ||
|
|
ce32d5537d | ||
|
|
25921fa91a | ||
|
|
932a496c3c | ||
|
|
57862eeda6 | ||
|
|
11df94d601 | ||
|
|
0abaa20de9 | ||
|
|
c35a1dd79f | ||
|
|
e0b2572eba | ||
|
|
aada695b3f | ||
|
|
90f3858eff | ||
|
|
ecb482f297 | ||
|
|
641f391137 | ||
|
|
dc26f7ba9f | ||
|
|
6504930888 | ||
|
|
6b52c91257 | ||
|
|
0ffea495b1 | ||
|
|
72ba4ff2d4 | ||
|
|
88c4b191fb | ||
|
|
112d2a65f6 | ||
|
|
d999c1b434 | ||
|
|
3aa8b9a259 | ||
|
|
fdbea9e2ee | ||
|
|
6204eddade | ||
|
|
e789c671b0 | ||
|
|
8c2ee0ed85 | ||
|
|
2b48409cfd | ||
|
|
7251186762 | ||
|
|
82fda4ac0e | ||
|
|
1d12ddbdfc | ||
|
|
f474c76e19 | ||
|
|
ac99104114 | ||
|
|
0bb9b388f7 | ||
|
|
b59e4bb808 | ||
|
|
4fe647df0a | ||
|
|
a00350ab54 | ||
|
|
96c6b4efcb |
60
.github/ISSUE_TEMPLATE/bug_report.md
vendored
60
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,30 +1,60 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
about: Create an issue about a bug you encountered
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!--
|
||||
Hi there, sorry `tui` is not working as expected.
|
||||
Please fill this bug report conscientiously.
|
||||
A detailed and complete issue is more likely to be processed quickly.
|
||||
-->
|
||||
|
||||
## Description
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
**To Reproduce**
|
||||
If possible include a code sample exhibiting the problem.
|
||||
|
||||
**Expected behavior**
|
||||
## To Reproduce
|
||||
<!--
|
||||
Try to reduce the issue to a simple code sample exhibiting the problem.
|
||||
Ideally, fork the project and add a test or an example.
|
||||
-->
|
||||
|
||||
|
||||
## Expected behavior
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Linux,Windows]
|
||||
- Terminal Emulator [e.g xterm, Konsole, Terminal, iTerm2, ConEmu]
|
||||
- Font [e.g Inconsolata, Monospace]
|
||||
- Crate version [e.g. 0.7]
|
||||
- Backend [e.g termion, crossterm]
|
||||
## Screenshots
|
||||
<!--
|
||||
If applicable, add screenshots, gifs or videos to help explain your problem.
|
||||
-->
|
||||
|
||||
**Additional context**
|
||||
|
||||
## Environment
|
||||
<!--
|
||||
Add a description of the systems where you are observing the issue. For example:
|
||||
- OS: Linux
|
||||
- Terminal Emulator: xterm
|
||||
- Font: Inconsolata (Patched)
|
||||
- Crate version: 0.7
|
||||
- Backend: termion
|
||||
-->
|
||||
|
||||
- OS:
|
||||
- Terminal Emulator:
|
||||
- Font:
|
||||
- Crate version:
|
||||
- Backend:
|
||||
|
||||
## Additional context
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
If you already looked into the issue, include all the leads you have explored.
|
||||
-->
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,14 +7,26 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
## Problem
|
||||
<!--
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
-->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
## Solution
|
||||
<!--
|
||||
A clear and concise description of what you want to happen.
|
||||
Things to consider:
|
||||
- backward compatibility
|
||||
- ease of use of the API (https://rust-lang.github.io/api-guidelines/)
|
||||
- consistency with the rest of the crate
|
||||
-->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
## Alternatives
|
||||
<!--
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
**Additional context**
|
||||
## Additional context
|
||||
<!--
|
||||
Add any other context or screenshots about the feature request here.
|
||||
-->
|
||||
|
||||
17
.github/pull_request_template.md
vendored
Normal file
17
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
## Description
|
||||
<!--
|
||||
A clear and concise description of what this PR changes.
|
||||
-->
|
||||
|
||||
## Testing guidelines
|
||||
<!--
|
||||
A clear and concise description of how the changes can be tested.
|
||||
For example, you can include a command to run the relevant tests or examples.
|
||||
You can also include screenshots of the expected behavior.
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] I have read the [contributing guidelines](../CONTRIBUTING.md).
|
||||
* [ ] I have added relevant tests.
|
||||
* [ ] I have documented all new additions.
|
||||
108
.github/workflows/ci.yml
vendored
108
.github/workflows/ci.yml
vendored
@@ -1,71 +1,71 @@
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
name: CI
|
||||
|
||||
env:
|
||||
CI_CARGO_MAKE_VERSION: 0.35.8
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.56.1", "stable"]
|
||||
steps:
|
||||
- name: "Install dependencies"
|
||||
run: sudo apt-get install libncurses5-dev
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
with:
|
||||
rust-version: ${{ matrix.rust }}
|
||||
components: rustfmt,clippy
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: "Get cargo bin directory"
|
||||
id: cargo-bin-dir
|
||||
run: echo "::set-output name=dir::$HOME/.cargo/bin"
|
||||
- name: "Cache cargo make"
|
||||
id: cache-cargo-make
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
profile: default
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: "Format"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
- name: "Check"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --examples
|
||||
- name: "Check (crossterm)"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --no-default-features --features=crossterm --example crossterm_demo
|
||||
- name: "Check (rustbox)"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --no-default-features --features=rustbox --example rustbox_demo
|
||||
- name: "Check (curses)"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --no-default-features --features=curses --example curses_demo
|
||||
- name: "Test"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
- name: "Clippy"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-targets --all-features -- -D warnings
|
||||
path: ${{ steps.cargo-bin-dir.outputs.dir }}/cargo-make
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
windows:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.56.1", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
with:
|
||||
profile: default
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: "Check (crossterm)"
|
||||
uses: actions-rs/cargo@v1
|
||||
rust-version: ${{ matrix.rust }}
|
||||
components: rustfmt,clippy
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Get cargo bin directory"
|
||||
id: cargo-bin-dir
|
||||
run: echo "::set-output name=dir::$HOME\.cargo\bin"
|
||||
- name: "Cache cargo make"
|
||||
id: cache-cargo-make
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
command: check
|
||||
args: --no-default-features --features=crossterm --example crossterm_demo
|
||||
- name: "Test (crossterm)"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --no-default-features --features=crossterm --tests --examples
|
||||
path: ${{ steps.cargo-bin-dir.outputs.dir }}\cargo-make.exe
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
346
CHANGELOG.md
346
CHANGELOG.md
@@ -2,6 +2,346 @@
|
||||
|
||||
## To be released
|
||||
|
||||
## v0.18.0 - 2022-04-24
|
||||
|
||||
### Features
|
||||
|
||||
* Update `crossterm` to `0.23`
|
||||
|
||||
## v0.17.0 - 2022-01-22
|
||||
|
||||
### Features
|
||||
|
||||
* Add option to `widgets::List` to repeat the hightlight symbol for each line of multi-line items (#533).
|
||||
* Add option to control the alignment of `Axis` labels in the `Chart` widget (#568).
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The minimum supported rust version is now `1.56.1`.
|
||||
|
||||
#### New default backend and consolidated backend options (#553)
|
||||
|
||||
* `crossterm` is now the default backend.
|
||||
If you are already using the `crossterm` backend, you can simplify your dependency specification in `Cargo.toml`:
|
||||
```diff
|
||||
- tui = { version = "0.16", default-features = false, features = ["crossterm"] }
|
||||
+ tui = "0.17"
|
||||
```
|
||||
If you are using the `termion` backend, your `Cargo` is now a bit more verbose:
|
||||
```diff
|
||||
- tui = "0.16"
|
||||
+ tui = { version = "0.17", default-features = false, features = ["termion"] }
|
||||
```
|
||||
|
||||
`crossterm` has also been bumped to version `0.22`.
|
||||
|
||||
Because of their apparent low usage, `curses` and `rustbox` backends have been removed.
|
||||
If you are using one of them, you can import their last implementation in your own project:
|
||||
* [curses](https://github.com/fdehau/tui-rs/blob/v0.16.0/src/backend/curses.rs)
|
||||
* [rustbox](https://github.com/fdehau/tui-rs/blob/v0.16.0/src/backend/rustbox.rs)
|
||||
|
||||
#### Canvas labels (#543)
|
||||
|
||||
* Labels of the `Canvas` widget are now `text::Spans`.
|
||||
The signature of `widgets::canvas::Context::print` has thus been updated:
|
||||
```diff
|
||||
- ctx.print(x, y, "Some text", Color::Yellow);
|
||||
+ ctx.print(x, y, Span::styled("Some text", Style::default().fg(Color::Yellow)))
|
||||
```
|
||||
|
||||
## v0.16.0 - 2021-08-01
|
||||
|
||||
### Features
|
||||
|
||||
* Update `crossterm` to `0.20`.
|
||||
* Add `From<Cow<str>>` implementation for `text::Text` (#471).
|
||||
* Add option to right or center align the title of a `widgets::Block` (#462).
|
||||
|
||||
### Fixes
|
||||
|
||||
* Apply label style in `widgets::Gauge` and avoid panics because of overflows with long labels (#494).
|
||||
* Avoid panics because of overflows with long axis labels in `widgets::Chart` (#512).
|
||||
* Fix computation of column widths in `widgets::Table` (#514).
|
||||
* Fix panics because of invalid offset when input changes between two frames in `widgets::List` and
|
||||
`widgets::Chart` (#516).
|
||||
|
||||
## v0.15.0 - 2021-05-02
|
||||
|
||||
### Features
|
||||
|
||||
* Update `crossterm` to `0.19`.
|
||||
* Update `rand` to `0.8`.
|
||||
* Add a read-only view of the terminal state after the draw call (#440).
|
||||
|
||||
### Fixes
|
||||
|
||||
* Remove compile warning in `TestBackend::assert_buffer` (#466).
|
||||
|
||||
## v0.14.0 - 2021-01-01
|
||||
|
||||
### Breaking changes
|
||||
|
||||
#### New API for the Table widget
|
||||
|
||||
The `Table` widget got a lot of improvements that should make it easier to work with:
|
||||
* It should not longer panic when rendered on small areas.
|
||||
* `Row`s are now a collection of `Cell`s, themselves wrapping a `Text`. This means you can style
|
||||
the entire `Table`, an entire `Row`, an entire `Cell` and rely on the styling capabilities of
|
||||
`Text` to get full control over the look of your `Table`.
|
||||
* `Row`s can have multiple lines.
|
||||
* The header is now optional and is just another `Row` always visible at the top.
|
||||
* `Row`s can have a bottom margin.
|
||||
* The header alignment is no longer off when an item is selected.
|
||||
|
||||
Taking the example of the code in `examples/demo/ui.rs`, this is what you may have to change:
|
||||
```diff
|
||||
let failure_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.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" {
|
||||
up_style
|
||||
} else {
|
||||
failure_style
|
||||
};
|
||||
- Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
|
||||
+ Row::new(vec![s.name, s.location, s.status]).style(style)
|
||||
});
|
||||
- let table = Table::new(header.iter(), rows)
|
||||
+ let table = Table::new(rows)
|
||||
+ .header(
|
||||
+ Row::new(vec!["Server", "Location", "Status"])
|
||||
+ .style(Style::default().fg(Color::Yellow))
|
||||
+ .bottom_margin(1),
|
||||
+ )
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
- .header_style(Style::default().fg(Color::Yellow))
|
||||
.widths(&[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
```
|
||||
Here, we had to:
|
||||
- Change the way we construct [`Row`](https://docs.rs/tui/*/tui/widgets/struct.Row.html) which is no
|
||||
longer an `enum` but a `struct`. It accepts anything that can be converted to an iterator of things
|
||||
that can be converted to a [`Cell`](https://docs.rs/tui/*/tui/widgets/struct.Cell.html)
|
||||
- The header is no longer a required parameter so we use
|
||||
[`Table::header`](https://docs.rs/tui/*/tui/widgets/struct.Table.html#method.header) to set it.
|
||||
`Table::header_style` has been removed since the style can be directly set using
|
||||
[`Row::style`](https://docs.rs/tui/*/tui/widgets/struct.Row.html#method.style). In addition, we want
|
||||
to preserve the old margin between the header and the rest of the rows so we add a bottom margin to
|
||||
the header using
|
||||
[`Row::bottom_margin`](https://docs.rs/tui/*/tui/widgets/struct.Row.html#method.bottom_margin).
|
||||
|
||||
You may want to look at the documentation of the different types to get a better understanding:
|
||||
- [`Table`](https://docs.rs/tui/*/tui/widgets/struct.Table.html)
|
||||
- [`Row`](https://docs.rs/tui/*/tui/widgets/struct.Row.html)
|
||||
- [`Cell`](https://docs.rs/tui/*/tui/widgets/struct.Cell.html)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix handling of Non Breaking Space (NBSP) in wrapped text in `Paragraph` widget.
|
||||
|
||||
### Features
|
||||
|
||||
- Add `Style::reset` to create a `Style` resetting all styling properties when applied.
|
||||
- Add an option to render the `Gauge` widget with unicode blocks.
|
||||
- Manage common project tasks with `cargo-make` rather than `make` for easier on-boarding.
|
||||
|
||||
## v0.13.0 - 2020-11-14
|
||||
|
||||
### Features
|
||||
|
||||
* Add `LineGauge` widget which is a more compact variant of the existing `Gauge`.
|
||||
* Bump `crossterm` to 0.18
|
||||
|
||||
### Fixes
|
||||
|
||||
* Take into account the borders of the `Table` widget when the widths of columns is controlled by
|
||||
`Percentage` and `Ratio` constraints.
|
||||
|
||||
## v0.12.0 - 2020-09-27
|
||||
|
||||
### Features
|
||||
|
||||
* Make it easier to work with string with multiple lines in `Text` (#361).
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix a style leak in `Graph` so components drawn on top of the plotted data (i.e legend and axis
|
||||
titles) are not affected by the style of the `Dataset`s (#388).
|
||||
* Make sure `BarChart` shows bars with the max height only when the plotted data is actually equal
|
||||
to the max (#383).
|
||||
|
||||
## v0.11.0 - 2020-09-20
|
||||
|
||||
### Features
|
||||
|
||||
* Add the dot character as a new type of canvas marker (#350).
|
||||
* Support more style modifiers on Windows (#368).
|
||||
|
||||
### Fixes
|
||||
|
||||
* Clearing the terminal through `Terminal::clear` will cause the whole UI to be redrawn (#380).
|
||||
* 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
|
||||
@@ -245,15 +585,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
|
||||
|
||||
33
CONTRIBUTING.md
Normal file
33
CONTRIBUTING.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Contributing
|
||||
|
||||
## Building
|
||||
|
||||
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
|
||||
|
||||
`tui` is an ordinary Rust project where common tasks are managed with [cargo-make].
|
||||
It wraps common `cargo` commands with sane defaults depending on your platform of choice.
|
||||
Building the project should be as easy as running `cargo make build`.
|
||||
|
||||
## :hammer_and_wrench: Pull requests
|
||||
|
||||
All contributions are obviously welcome.
|
||||
Please include as many details as possible in your PR description to help the reviewer (follow the provided template).
|
||||
Make sure to highlight changes which may need additional attention or you are uncertain about.
|
||||
Any idea with a large scale impact on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
We use Github Actions for the CI where we perform the following checks:
|
||||
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
|
||||
- The tests (docs, lib, tests and examples) should pass.
|
||||
- The code should conform to the default format enforced by `rustfmt`.
|
||||
- The code should not contain common style issues `clippy`.
|
||||
|
||||
You can also check most of those things yourself locally using `cargo make ci` which will offer you a shorter feedback loop.
|
||||
|
||||
## Tests
|
||||
|
||||
The test coverage of the crate is far from being ideal but we already have a fair amount of tests in place.
|
||||
Beside the usual doc and unit tests, one of the most valuable test you can write for `tui` is a test again the `TestBackend`.
|
||||
It allows you to assert the content of the output buffer that would have been flushed to the terminal after a given draw call.
|
||||
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.
|
||||
162
Cargo.toml
162
Cargo.toml
@@ -1,127 +1,93 @@
|
||||
[package]
|
||||
name = "tui"
|
||||
version = "0.9.5"
|
||||
version = "0.18.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.5/tui/"
|
||||
documentation = "https://docs.rs/tui/0.18.0/tui/"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/fdehau/tui-rs"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["assets/*", ".github"]
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
autoexamples = true
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[badges]
|
||||
|
||||
[features]
|
||||
default = ["termion"]
|
||||
curses = ["easycurses", "pancurses"]
|
||||
default = ["crossterm"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.0"
|
||||
bitflags = "1.3"
|
||||
cassowary = "0.3"
|
||||
itertools = "0.9"
|
||||
either = "1.5"
|
||||
unicode-segmentation = "1.2"
|
||||
unicode-width = "0.1"
|
||||
termion = { version = "1.5", optional = true }
|
||||
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"] }
|
||||
crossterm = { version = "0.23", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"]}
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.7"
|
||||
rand = "0.8"
|
||||
argh = "0.1"
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
path = "examples/canvas.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
path = "examples/user_input.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
path = "examples/gauge.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
path = "examples/barchart.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "chart"
|
||||
path = "examples/chart.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "paragraph"
|
||||
path = "examples/paragraph.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
path = "examples/list.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "table"
|
||||
path = "examples/table.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "tabs"
|
||||
path = "examples/tabs.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
path = "examples/custom_widget.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
path = "examples/layout.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "popup"
|
||||
path = "examples/popup.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
path = "examples/block.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "sparkline"
|
||||
path = "examples/sparkline.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "termion_demo"
|
||||
path = "examples/termion_demo.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "rustbox_demo"
|
||||
path = "examples/rustbox_demo.rs"
|
||||
required-features = ["rustbox"]
|
||||
|
||||
[[example]]
|
||||
name = "crossterm_demo"
|
||||
path = "examples/crossterm_demo.rs"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "curses_demo"
|
||||
path = "examples/curses_demo.rs"
|
||||
required-features = ["curses"]
|
||||
name = "block"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "chart"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "panic"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "paragraph"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "popup"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "sparkline"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "table"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "tabs"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
113
Makefile
113
Makefile
@@ -1,113 +0,0 @@
|
||||
SHELL=/bin/bash
|
||||
|
||||
# ================================ Cargo ======================================
|
||||
|
||||
|
||||
RUST_CHANNEL ?= stable
|
||||
CARGO_FLAGS =
|
||||
RUSTUP_INSTALLED = $(shell command -v rustup 2> /dev/null)
|
||||
|
||||
ifndef RUSTUP_INSTALLED
|
||||
CARGO = cargo
|
||||
else
|
||||
ifdef CI
|
||||
CARGO = cargo
|
||||
else
|
||||
CARGO = rustup run $(RUST_CHANNEL) cargo
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
# ================================ Help =======================================
|
||||
|
||||
.PHONY: help
|
||||
help: ## Print all the available commands
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
|
||||
# =============================== Build =======================================
|
||||
|
||||
.PHONY: check
|
||||
check: ## Validate the project code
|
||||
$(CARGO) check
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build the project in debug mode
|
||||
$(CARGO) build $(CARGO_FLAGS)
|
||||
|
||||
.PHONY: release
|
||||
release: CARGO_FLAGS += --release
|
||||
release: build ## Build the project in release mode
|
||||
|
||||
|
||||
# ================================ Lint =======================================
|
||||
|
||||
.PHONY: lint
|
||||
lint: fmt clippy ## Lint project files
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: ## Check the format of the source code
|
||||
cargo fmt --all -- --check
|
||||
|
||||
.PHONY: clippy
|
||||
clippy: ## Check the style of the source code and catch common errors
|
||||
$(CARGO) clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
|
||||
# ================================ Test =======================================
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run the tests
|
||||
$(CARGO) test --all-features
|
||||
|
||||
# =============================== Examples ====================================
|
||||
|
||||
.PHONY: build-examples
|
||||
build-examples: ## Build all examples
|
||||
@$(CARGO) build --examples --all-features
|
||||
|
||||
.PHONY: run-examples
|
||||
run-examples: ## Run all examples
|
||||
@for file in examples/*.rs; do \
|
||||
name=$$(basename $${file/.rs/}); \
|
||||
$(CARGO) run --all-features --release --example $$name; \
|
||||
done;
|
||||
|
||||
# ================================ Doc ========================================
|
||||
|
||||
|
||||
.PHONY: doc
|
||||
doc: ## Build the documentation (available at ./target/doc)
|
||||
$(CARGO) doc
|
||||
|
||||
|
||||
# ================================= Watch =====================================
|
||||
|
||||
# Requires watchman and watchman-make (https://facebook.github.io/watchman/docs/install.html)
|
||||
|
||||
.PHONY: watch
|
||||
watch: ## Watch file changes and build the project if any
|
||||
watchman-make -p 'src/**/*.rs' -t check build
|
||||
|
||||
.PHONY: watch-test
|
||||
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: ## Watch file changes and rebuild the documentation if any
|
||||
watchman-make -p 'src/**/*.rs' -t doc
|
||||
|
||||
# ================================= Pipelines =================================
|
||||
|
||||
.PHONY: stable
|
||||
stable: RUST_CHANNEL = stable
|
||||
stable: build lint test ## Run build and tests for stable
|
||||
|
||||
.PHONY: beta
|
||||
beta: RUST_CHANNEL = beta
|
||||
beta: build lint test ## Run build and tests for beta
|
||||
|
||||
.PHONY: nightly
|
||||
nightly: RUST_CHANNEL = nightly
|
||||
nightly: build lint test ## Run build, lint and tests for nightly
|
||||
160
Makefile.toml
Normal file
160
Makefile.toml
Normal file
@@ -0,0 +1,160 @@
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[tasks.ci]
|
||||
run_task = [
|
||||
{ name = "ci-unix", condition = { platforms = ["linux", "mac"] } },
|
||||
{ name = "ci-windows", condition = { platforms = ["windows"] } },
|
||||
]
|
||||
|
||||
[tasks.ci-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"fmt",
|
||||
"check-crossterm",
|
||||
"check-termion",
|
||||
"test-crossterm",
|
||||
"test-termion",
|
||||
"clippy-crossterm",
|
||||
"clippy-termion",
|
||||
"test-doc",
|
||||
]
|
||||
|
||||
[tasks.ci-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"fmt",
|
||||
"check-crossterm",
|
||||
"test-crossterm",
|
||||
"clippy-crossterm",
|
||||
"test-doc",
|
||||
]
|
||||
|
||||
[tasks.fmt]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"fmt",
|
||||
"--all",
|
||||
"--",
|
||||
"--check",
|
||||
]
|
||||
|
||||
[tasks.check-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "check"
|
||||
|
||||
[tasks.check-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "check"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"check",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
]
|
||||
|
||||
[tasks.build-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "build"
|
||||
|
||||
[tasks.build-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "build"
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"build",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
]
|
||||
|
||||
[tasks.clippy-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "clippy"
|
||||
|
||||
[tasks.clippy-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "clippy"
|
||||
|
||||
[tasks.clippy]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.test-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "test"
|
||||
|
||||
[tasks.test-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "test"
|
||||
|
||||
[tasks.test]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"test",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--lib",
|
||||
"--tests",
|
||||
"--examples",
|
||||
]
|
||||
|
||||
[tasks.test-doc]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--doc",
|
||||
]
|
||||
|
||||
[tasks.run-example]
|
||||
private = true
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
|
||||
command = "cargo"
|
||||
args = [
|
||||
"run",
|
||||
"--release",
|
||||
"--example",
|
||||
"${TUI_EXAMPLE_NAME}"
|
||||
]
|
||||
|
||||
[tasks.build-examples]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"build",
|
||||
"--examples",
|
||||
"--release"
|
||||
]
|
||||
|
||||
[tasks.run-examples]
|
||||
dependencies = ["build-examples"]
|
||||
script = '''
|
||||
#!@duckscript
|
||||
files = glob_array ./examples/*.rs
|
||||
for file in ${files}
|
||||
name = basename ${file}
|
||||
name = substring ${name} -3
|
||||
set_env TUI_EXAMPLE_NAME ${name}
|
||||
cm_run_task run-example
|
||||
end
|
||||
'''
|
||||
84
README.md
84
README.md
@@ -11,15 +11,9 @@ user interfaces and dashboards. It is heavily inspired by the `Javascript`
|
||||
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
|
||||
`Go` library [termui](https://github.com/gizak/termui).
|
||||
|
||||
The library itself supports four different backends to draw to the terminal. You
|
||||
can either choose from:
|
||||
|
||||
The library supports multiple backends:
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||
- [termion](https://github.com/ticki/termion)
|
||||
- [rustbox](https://github.com/gchp/rustbox)
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm)
|
||||
- [pancurses](https://github.com/ihalila/pancurses)
|
||||
|
||||
However, some features may only be available in one of the four.
|
||||
|
||||
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
|
||||
@@ -32,56 +26,55 @@ 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.17.0, `tui` requires **rustc version 1.56.1 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
|
||||
run:
|
||||
The demo shown in the gif can be run with all available backends.
|
||||
|
||||
```
|
||||
cargo run --example termion_demo --release -- --tick-rate 200
|
||||
# crossterm
|
||||
cargo run --example demo --release -- --tick-rate 200
|
||||
# termion
|
||||
cargo run --example demo --no-default-features --features=termion --release -- --tick-rate 200
|
||||
```
|
||||
|
||||
where `tick-rate` is the UI refresh rate in ms.
|
||||
|
||||
The UI code is in [examples/demo/ui.rs](examples/demo/ui.rs) while the
|
||||
application state is in [examples/demo/app.rs](examples/demo/app.rs).
|
||||
|
||||
Beware that the `termion_demo` only works on Unix platforms. If you are a Windows user,
|
||||
you can see the same demo using the `crossterm` backend with the following command:
|
||||
|
||||
```
|
||||
cargo run --example crossterm_demo --no-default-features --features="crossterm" --release -- --tick-rate 200
|
||||
```
|
||||
The UI code is in [examples/demo/ui.rs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/demo/ui.rs) while the
|
||||
application state is in [examples/demo/app.rs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/demo/app.rs).
|
||||
|
||||
If the user interface contains glyphs that are not displayed correctly by your terminal, you may want to run
|
||||
the demo without those symbols:
|
||||
|
||||
```
|
||||
cargo run --example crossterm_demo --no-default-features --features="crossterm" --release -- --tick-rate 200 --enhanced-graphics false
|
||||
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
|
||||
```
|
||||
|
||||
### Widgets
|
||||
|
||||
The library comes with the following list of widgets:
|
||||
|
||||
* [Block](examples/block.rs)
|
||||
* [Gauge](examples/gauge.rs)
|
||||
* [Sparkline](examples/sparkline.rs)
|
||||
* [Chart](examples/chart.rs)
|
||||
* [BarChart](examples/barchart.rs)
|
||||
* [List](examples/list.rs)
|
||||
* [Table](examples/table.rs)
|
||||
* [Paragraph](examples/paragraph.rs)
|
||||
* [Canvas (with line, point cloud, map)](examples/canvas.rs)
|
||||
* [Tabs](examples/tabs.rs)
|
||||
* [Block](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/block.rs)
|
||||
* [Gauge](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/gauge.rs)
|
||||
* [Sparkline](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/sparkline.rs)
|
||||
* [Chart](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/chart.rs)
|
||||
* [BarChart](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/barchart.rs)
|
||||
* [List](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/list.rs)
|
||||
* [Table](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/table.rs)
|
||||
* [Paragraph](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/paragraph.rs)
|
||||
* [Canvas (with line, point cloud, map)](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/canvas.rs)
|
||||
* [Tabs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/tabs.rs)
|
||||
|
||||
Click on each item to see the source of the example. Run the examples with with
|
||||
cargo (e.g. to run the demo `cargo run --example demo`), and quit by pressing `q`.
|
||||
cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
|
||||
|
||||
You can run all examples by running `make run-examples`.
|
||||
You can run all examples by running `cargo make run-examples` (require
|
||||
`cargo-make` that can be installed with `cargo install cargo-make`).
|
||||
|
||||
### Third-party widgets
|
||||
|
||||
@@ -92,6 +85,7 @@ You can run all examples by running `make run-examples`.
|
||||
* [spotify-tui](https://github.com/Rigellute/spotify-tui)
|
||||
* [bandwhich](https://github.com/imsnif/bandwhich)
|
||||
* [kmon](https://github.com/orhun/kmon)
|
||||
* [gpg-tui](https://github.com/orhun/gpg-tui)
|
||||
* [ytop](https://github.com/cjbassi/ytop)
|
||||
* [zenith](https://github.com/bvaisvil/zenith)
|
||||
* [bottom](https://github.com/ClementTsang/bottom)
|
||||
@@ -99,6 +93,28 @@ 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)
|
||||
* [tickrs](https://github.com/tarkah/tickrs)
|
||||
* [rusty-krab-manager](https://github.com/aryakaul/rusty-krab-manager)
|
||||
* [termchat](https://github.com/lemunozm/termchat)
|
||||
* [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui)
|
||||
* [gping](https://github.com/orf/gping/)
|
||||
* [Vector](https://vector.dev)
|
||||
* [KDash](https://github.com/kdash-rs/kdash)
|
||||
* [xplr](https://github.com/sayanarijit/xplr)
|
||||
* [minesweep](https://github.com/cpcloud/minesweep-rs)
|
||||
* [Battleship.rs](https://github.com/deepu105/battleship-rs)
|
||||
* [termscp](https://github.com/veeso/termscp)
|
||||
* [joshuto](https://github.com/kamiyaa/joshuto)
|
||||
* [adsb_deku/radar](https://github.com/wcampbell0x2a/adsb_deku#radar-tui)
|
||||
* [hoard](https://github.com/Hyde46/hoard)
|
||||
* [tokio-console](https://github.com/tokio-rs/console): a diagnostics and debugging tool for asynchronous Rust programs.
|
||||
* [hwatch](https://github.com/blacknon/hwatch): a alternative watch command that records the result of command execution and can display its history and diffs.
|
||||
* [ytui-music](https://github.com/sudipghimire533/ytui-music): listen to music from youtube inside your terminal.
|
||||
* [mqttui](https://github.com/EdJoPaTo/mqttui): subscribe or publish to a MQTT Topic quickly from the terminal.
|
||||
* [meteo-tui](https://github.com/16arpi/meteo-tui): french weather via the command line.
|
||||
* [picterm](https://github.com/ksk001100/picterm): preview images in your terminal.
|
||||
* [gobang](https://github.com/TaKO8Ki/gobang): a cross-platform TUI database management tool.
|
||||
|
||||
### Alternatives
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
#[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 crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{BarChart, Block, Borders},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
@@ -48,78 +52,110 @@ impl<'a> App<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.data.pop().unwrap();
|
||||
self.data.insert(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
|
||||
f.render_widget(barchart, chunks[0]);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data2").borders(Borders::ALL))
|
||||
.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));
|
||||
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_width(7)
|
||||
.bar_gap(0)
|
||||
.value_style(Style::default().bg(Color::Red))
|
||||
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC));
|
||||
f.render_widget(barchart, chunks[1]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.bar_style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
|
||||
f.render_widget(barchart, chunks[0]);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data2").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(5)
|
||||
.bar_gap(3)
|
||||
.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)
|
||||
.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)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
);
|
||||
f.render_widget(barchart, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,85 +1,119 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, BorderType, Borders},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|mut 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
|
||||
let size = f.size();
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Main block with round corners")
|
||||
.border_type(BorderType::Rounded);
|
||||
f.render_widget(block, size);
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.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()
|
||||
.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]);
|
||||
}
|
||||
})?;
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if let Event::Input(key) = events.next()? {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
// 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
|
||||
let size = f.size();
|
||||
|
||||
// Surrounding block
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Main block with round corners")
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_type(BorderType::Rounded);
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(4)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Top two inner blocks
|
||||
let top_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
|
||||
// Top left inner block with green background
|
||||
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]);
|
||||
|
||||
// Top right inner block with styled title aligned to the right
|
||||
let block = Block::default()
|
||||
.title(Span::styled(
|
||||
"Styled title",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.title_alignment(Alignment::Right);
|
||||
f.render_widget(block, top_chunks[1]);
|
||||
|
||||
// Bottom two inner blocks
|
||||
let bottom_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
// Bottom left block with all default borders
|
||||
let block = Block::default().title("With borders").borders(Borders::ALL);
|
||||
f.render_widget(block, bottom_chunks[0]);
|
||||
|
||||
// Bottom right block with styled left and right border
|
||||
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]);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Config, Event, Events};
|
||||
use std::{error::Error, io, time::Duration};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::Color,
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{
|
||||
canvas::{Canvas, Map, MapResolution, Rectangle},
|
||||
Block, Borders,
|
||||
},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
@@ -46,7 +51,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
if self.ball.x < self.playground.left() as f64
|
||||
|| self.ball.x + self.ball.width > self.playground.right() as f64
|
||||
{
|
||||
@@ -73,77 +78,103 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
// Setup event handlers
|
||||
let config = Config {
|
||||
tick_rate: Duration::from_millis(250),
|
||||
..Default::default()
|
||||
};
|
||||
let events = Events::with_config(config);
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(app.x, -app.y, "You are here", Color::Yellow);
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0]);
|
||||
f.render_widget(canvas, chunks[0]);
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&app.ball);
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0]);
|
||||
f.render_widget(canvas, chunks[1]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => match input {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
Key::Down => {
|
||||
app.y += 1.0;
|
||||
}
|
||||
Key::Up => {
|
||||
app.y -= 1.0;
|
||||
}
|
||||
Key::Right => {
|
||||
app.x += 1.0;
|
||||
}
|
||||
Key::Left => {
|
||||
app.x -= 1.0;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
}
|
||||
KeyCode::Down => {
|
||||
app.y += 1.0;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
app.y -= 1.0;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.x += 1.0;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
app.x -= 1.0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(
|
||||
app.x,
|
||||
-app.y,
|
||||
Span::styled("You are here", Style::default().fg(Color::Yellow)),
|
||||
);
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0]);
|
||||
f.render_widget(canvas, chunks[0]);
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&app.ball);
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0]);
|
||||
f.render_widget(canvas, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
SinSignal,
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
|
||||
@@ -27,6 +29,34 @@ const DATA2: [(f64, f64); 7] = [
|
||||
(60.0, 3.0),
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
signal1: SinSignal,
|
||||
data1: Vec<(f64, f64)>,
|
||||
@@ -50,7 +80,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..5 {
|
||||
self.data1.remove(0);
|
||||
}
|
||||
@@ -65,152 +95,207 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]
|
||||
.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 datasets = [
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data1),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data2),
|
||||
];
|
||||
let chart = Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 1")
|
||||
.title_style(Style::default().fg(Color::Cyan).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),
|
||||
)
|
||||
.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);
|
||||
f.render_widget(chart, chunks[0]);
|
||||
|
||||
let datasets = [Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA)];
|
||||
let chart = Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 2")
|
||||
.title_style(Style::default().fg(Color::Cyan).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"]),
|
||||
)
|
||||
.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);
|
||||
f.render_widget(chart, chunks[1]);
|
||||
|
||||
let datasets = [Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA2)];
|
||||
let chart = Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 3")
|
||||
.title_style(Style::default().fg(Color::Cyan).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"]),
|
||||
)
|
||||
.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);
|
||||
f.render_widget(chart, chunks[2]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
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 = vec![
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data1),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data2),
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.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(x_labels)
|
||||
.bounds(app.window),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.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 = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.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))
|
||||
.bounds([0.0, 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))
|
||||
.bounds([0.0, 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)),
|
||||
]),
|
||||
);
|
||||
f.render_widget(chart, chunks[1]);
|
||||
|
||||
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::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.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))
|
||||
.bounds([0.0, 50.0])
|
||||
.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))
|
||||
.bounds([0.0, 5.0])
|
||||
.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]);
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
#[allow(dead_code)]
|
||||
mod demo;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::demo::{ui, App};
|
||||
use argh::FromArgs;
|
||||
use crossterm::{
|
||||
event::{self, Event as CEvent, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{stdout, Write},
|
||||
sync::mpsc,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{backend::CrosstermBackend, Terminal};
|
||||
|
||||
enum Event<I> {
|
||||
Input(I),
|
||||
Tick,
|
||||
}
|
||||
|
||||
/// Crossterm demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
|
||||
enable_raw_mode()?;
|
||||
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
// Setup input handling
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||
thread::spawn(move || {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
// poll for tick rate duration, if no events, sent tick event.
|
||||
if event::poll(tick_rate - last_tick.elapsed()).unwrap() {
|
||||
if let CEvent::Key(key) = event::read().unwrap() {
|
||||
tx.send(Event::Input(key)).unwrap();
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut app = App::new("Crossterm Demo", cli.enhanced_graphics);
|
||||
|
||||
terminal.clear()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => match event.code {
|
||||
KeyCode::Char('q') => {
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
break;
|
||||
}
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.on_tick();
|
||||
}
|
||||
}
|
||||
if app.should_quit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
mod demo;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::demo::{ui, App};
|
||||
use argh::FromArgs;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{backend::CursesBackend, Terminal};
|
||||
|
||||
/// Curses demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
|
||||
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));
|
||||
curses.set_input_mode(easycurses::InputMode::RawCharacter);
|
||||
curses.set_keypad_enabled(true);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
let mut app = App::new("Curses 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))?;
|
||||
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 {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend, buffer::Buffer, layout::Rect, style::Style, widgets::Widget, Terminal,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
widgets::Widget,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Label<'a> {
|
||||
text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Default for Label<'a> {
|
||||
fn default() -> Label<'a> {
|
||||
Label { text: "" }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Label<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(area.left(), area.top(), self.text, Style::default());
|
||||
@@ -32,28 +32,46 @@ impl<'a> Label<'a> {
|
||||
}
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let label = Label::default().text("Test");
|
||||
f.render_widget(label, size);
|
||||
})?;
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Event::Input(key) = events.next()? {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
let size = f.size();
|
||||
let label = Label::default().text("Test");
|
||||
f.render_widget(label, size);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use crate::util::{RandomSignal, SinSignal, StatefulList, TabsState};
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use tui::widgets::ListState;
|
||||
|
||||
const TASKS: [&str; 24] = [
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
|
||||
@@ -62,6 +66,120 @@ const EVENTS: [(&str, u64); 24] = [
|
||||
("B24", 5),
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TabsState<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<'a> TabsState<'a> {
|
||||
pub fn new(titles: Vec<&'a str>) -> TabsState {
|
||||
TabsState { titles, index: 0 }
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Signal<S: Iterator> {
|
||||
source: S,
|
||||
pub points: Vec<S::Item>,
|
||||
@@ -129,7 +247,7 @@ impl<'a> App<'a> {
|
||||
App {
|
||||
title,
|
||||
should_quit: false,
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1"]),
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2"]),
|
||||
show_chart: true,
|
||||
progress: 0.0,
|
||||
sparkline: Signal {
|
||||
|
||||
77
examples/demo/crossterm.rs
Normal file
77
examples/demo/crossterm.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use crate::{app::App, ui};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Crossterm Demo", enhanced_graphics);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
31
examples/demo/main.rs
Normal file
31
examples/demo/main.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
mod app;
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
mod ui;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
use crate::crossterm::run;
|
||||
#[cfg(feature = "termion")]
|
||||
use crate::termion::run;
|
||||
use argh::FromArgs;
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
/// Demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||
run(tick_rate, cli.enhanced_graphics)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
mod app;
|
||||
pub mod ui;
|
||||
pub use app::App;
|
||||
80
examples/demo/termion.rs
Normal file
80
examples/demo/termion.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use crate::{app::App, ui};
|
||||
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
|
||||
use termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::AlternateScreen,
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, TermionBackend},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
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)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Termion demo", enhanced_graphics);
|
||||
run_app(&mut terminal, app, tick_rate)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let events = events(tick_rate);
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
match events.recv()? {
|
||||
Event::Input(key) => match key {
|
||||
Key::Char(c) => app.on_key(c),
|
||||
Key::Up => app.on_up(),
|
||||
Key::Down => app.on_down(),
|
||||
Key::Left => app.on_left(),
|
||||
Key::Right => app.on_right(),
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => app.on_tick(),
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let keys_tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for key in stdin.keys().flatten() {
|
||||
if let Err(err) = keys_tx.send(Event::Input(key)) {
|
||||
eprintln!("{}", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
thread::spawn(move || loop {
|
||||
if let Err(err) = tx.send(Event::Tick) {
|
||||
eprintln!("{}", err);
|
||||
break;
|
||||
}
|
||||
thread::sleep(tick_rate);
|
||||
});
|
||||
rx
|
||||
}
|
||||
@@ -1,32 +1,37 @@
|
||||
use crate::app::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, Cell, Chart, Dataset, Gauge, LineGauge, 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]);
|
||||
match app.tabs.index {
|
||||
0 => draw_first_tab(f, app, chunks[1]),
|
||||
1 => draw_second_tab(f, app, chunks[1]),
|
||||
2 => draw_third_tab(f, app, chunks[1]),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
@@ -38,8 +43,8 @@ where
|
||||
let chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(7),
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(7),
|
||||
]
|
||||
.as_ref(),
|
||||
@@ -55,7 +60,14 @@ where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(2), Constraint::Length(3)].as_ref())
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.margin(1)
|
||||
.split(area);
|
||||
let block = Block::default().borders(Borders::ALL).title("Graphs");
|
||||
@@ -64,13 +76,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]);
|
||||
|
||||
@@ -84,6 +96,17 @@ where
|
||||
symbols::bar::THREE_LEVELS
|
||||
});
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
|
||||
let line_gauge = LineGauge::default()
|
||||
.block(Block::default().title("LineGauge:"))
|
||||
.gauge_style(Style::default().fg(Color::Magenta))
|
||||
.line_set(if app.enhanced_graphics {
|
||||
symbols::line::THICK
|
||||
} else {
|
||||
symbols::line::NORMAL
|
||||
})
|
||||
.ratio(app.progress);
|
||||
f.render_widget(line_gauge, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_charts<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
|
||||
@@ -110,29 +133,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 +186,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 +223,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 +260,43 @@ 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 {
|
||||
trim: true,
|
||||
break_words: false,
|
||||
});
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
@@ -247,19 +311,22 @@ where
|
||||
let up_style = Style::default().fg(Color::Green);
|
||||
let failure_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
|
||||
let header = ["Server", "Location", "Status"];
|
||||
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
|
||||
let rows = app.servers.iter().map(|s| {
|
||||
let style = if s.status == "Up" {
|
||||
up_style
|
||||
} else {
|
||||
failure_style
|
||||
};
|
||||
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
|
||||
Row::new(vec![s.name, s.location, s.status]).style(style)
|
||||
});
|
||||
let table = Table::new(header.iter(), rows)
|
||||
let table = Table::new(rows)
|
||||
.header(
|
||||
Row::new(vec!["Server", "Location", "Status"])
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
.header_style(Style::default().fg(Color::Yellow))
|
||||
.widths(&[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
@@ -299,7 +366,11 @@ where
|
||||
} else {
|
||||
Color::Red
|
||||
};
|
||||
ctx.print(server.coords.1, server.coords.0, "X", color);
|
||||
ctx.print(
|
||||
server.coords.1,
|
||||
server.coords.0,
|
||||
Span::styled("X", Style::default().fg(color)),
|
||||
);
|
||||
}
|
||||
})
|
||||
.marker(if app.enhanced_graphics {
|
||||
@@ -311,3 +382,51 @@ where
|
||||
.y_bounds([-90.0, 90.0]);
|
||||
f.render_widget(map, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_third_tab<B>(f: &mut Frame<B>, _app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
|
||||
.split(area);
|
||||
let colors = [
|
||||
Color::Reset,
|
||||
Color::Black,
|
||||
Color::Red,
|
||||
Color::Green,
|
||||
Color::Yellow,
|
||||
Color::Blue,
|
||||
Color::Magenta,
|
||||
Color::Cyan,
|
||||
Color::Gray,
|
||||
Color::DarkGray,
|
||||
Color::LightRed,
|
||||
Color::LightGreen,
|
||||
Color::LightYellow,
|
||||
Color::LightBlue,
|
||||
Color::LightMagenta,
|
||||
Color::LightCyan,
|
||||
Color::White,
|
||||
];
|
||||
let items: Vec<Row> = colors
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let cells = vec![
|
||||
Cell::from(Span::raw(format!("{:?}: ", c))),
|
||||
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
|
||||
Cell::from(Span::styled("Background", Style::default().bg(*c))),
|
||||
];
|
||||
Row::new(cells)
|
||||
})
|
||||
.collect();
|
||||
let table = Table::new(items)
|
||||
.block(Block::default().title("Colors").borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]);
|
||||
f.render_widget(table, chunks[0]);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
#[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 crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Gauge},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
@@ -24,17 +29,17 @@ impl App {
|
||||
App {
|
||||
progress1: 0,
|
||||
progress2: 0,
|
||||
progress3: 0.0,
|
||||
progress3: 0.45,
|
||||
progress4: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
self.progress1 += 5;
|
||||
fn on_tick(&mut self) {
|
||||
self.progress1 += 1;
|
||||
if self.progress1 > 100 {
|
||||
self.progress1 = 0;
|
||||
}
|
||||
self.progress2 += 10;
|
||||
self.progress2 += 2;
|
||||
if self.progress2 > 100 {
|
||||
self.progress2 = 0;
|
||||
}
|
||||
@@ -42,7 +47,7 @@ impl App {
|
||||
if self.progress3 > 1.0 {
|
||||
self.progress3 = 0.0;
|
||||
}
|
||||
self.progress4 += 3;
|
||||
self.progress4 += 1;
|
||||
if self.progress4 > 100 {
|
||||
self.progress4 = 0;
|
||||
}
|
||||
@@ -50,74 +55,112 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge1").borders(Borders::ALL))
|
||||
.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))
|
||||
.percent(app.progress2)
|
||||
.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))
|
||||
.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))
|
||||
.percent(app.progress4)
|
||||
.label(&label);
|
||||
f.render_widget(gauge, chunks[3]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge1").borders(Borders::ALL))
|
||||
.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))
|
||||
.gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green))
|
||||
.percent(app.progress2)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
|
||||
let label = Span::styled(
|
||||
format!("{:.2}%", app.progress3 * 100.0),
|
||||
Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge3").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(app.progress3)
|
||||
.label(label)
|
||||
.use_unicode(true);
|
||||
f.render_widget(gauge, chunks[2]);
|
||||
|
||||
let label = format!("{}/100", app.progress2);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge4"))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)
|
||||
.percent(app.progress4)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[3]);
|
||||
}
|
||||
|
||||
@@ -1,53 +1,70 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
widgets::{Block, Borders},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
let block = Block::default().title("Block").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[0]);
|
||||
let block = Block::default().title("Block 2").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[2]);
|
||||
})?;
|
||||
|
||||
if let Event::Input(input) = events.next()? {
|
||||
if let Key::Char('q') = input {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let block = Block::default().title("Block").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[0]);
|
||||
let block = Block::default().title("Block 2").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[2]);
|
||||
}
|
||||
|
||||
317
examples/list.rs
317
examples/list.rs
@@ -1,36 +1,108 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
StatefulList,
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, List, Text},
|
||||
Terminal,
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is a wrapper
|
||||
/// around `ListState`. Keeping track of the items state let us render the associated widget with its state
|
||||
/// and have access to features such as natural scrolling.
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events.
|
||||
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
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,88 +132,155 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
let event = self.events.pop().unwrap();
|
||||
self.events.insert(0, event);
|
||||
/// Rotate through the event list.
|
||||
/// This only exists to simulate some kind of "progress"
|
||||
fn on_tick(&mut self) {
|
||||
let event = self.events.remove(0);
|
||||
self.events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut 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 = List::new(items)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.style(style)
|
||||
.highlight_style(style.fg(Color::LightGreen).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_list = List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.start_corner(Corner::BottomLeft);
|
||||
f.render_widget(events_list, chunks[1]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => match input {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
Key::Left => {
|
||||
app.items.unselect();
|
||||
}
|
||||
Key::Down => {
|
||||
app.items.next();
|
||||
}
|
||||
Key::Up => {
|
||||
app.items.previous();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Left => app.items.unselect(),
|
||||
KeyCode::Down => app.items.next(),
|
||||
KeyCode::Up => app.items.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
// Create two chunks with equal horizontal screen space
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Iterate through all elements in the `items` app and append some debug text to it.
|
||||
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();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one
|
||||
let items = List::new(items)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
// We can now render the item list
|
||||
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
|
||||
|
||||
// Let's do the same for the events.
|
||||
// The event list doesn't have any state and only displays the current state of the list.
|
||||
let events: Vec<ListItem> = app
|
||||
.events
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|&(event, level)| {
|
||||
// Colorcode the level depending on its type
|
||||
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(),
|
||||
};
|
||||
// Add a example datetime and apply proper spacing between them
|
||||
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),
|
||||
),
|
||||
]);
|
||||
// The event gets its own line
|
||||
let log = Spans::from(vec![Span::raw(event)]);
|
||||
|
||||
// Here several things happen:
|
||||
// 1. Add a `---` spacing line above the final list entry
|
||||
// 2. Add the Level + datetime
|
||||
// 3. Add a spacer line
|
||||
// 4. Add the actual event
|
||||
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);
|
||||
f.render_widget(events_list, chunks[1]);
|
||||
}
|
||||
|
||||
142
examples/panic.rs
Normal file
142
examples/panic.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
//! How to use a panic hook to reset the terminal before printing the panic to
|
||||
//! the terminal.
|
||||
//!
|
||||
//! When exiting normally or when handling `Result::Err`, we can reset the
|
||||
//! terminal manually at the end of `main` just before we print the error.
|
||||
//!
|
||||
//! Because a panic interrupts the normal control flow, manually resetting the
|
||||
//! terminal at the end of `main` won't do us any good. Instead, we need to
|
||||
//! make sure to set up a panic hook that first resets the terminal before
|
||||
//! handling the panic. This both reuses the standard panic hook to ensure a
|
||||
//! consistent panic handling UX and properly resets the terminal to not
|
||||
//! distort the output.
|
||||
//!
|
||||
//! That's why this example is set up to show both situations, with and without
|
||||
//! the chained panic hook, to see the difference.
|
||||
|
||||
#![deny(clippy::all)]
|
||||
#![warn(clippy::pedantic, clippy::nursery)]
|
||||
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||
|
||||
use tui::backend::{Backend, CrosstermBackend};
|
||||
use tui::layout::Alignment;
|
||||
use tui::text::Spans;
|
||||
use tui::widgets::{Block, Borders, Paragraph};
|
||||
use tui::{Frame, Terminal};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
hook_enabled: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn chain_hook(&mut self) {
|
||||
let original_hook = std::panic::take_hook();
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic| {
|
||||
reset_terminal().unwrap();
|
||||
original_hook(panic);
|
||||
}));
|
||||
|
||||
self.hook_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = init_terminal()?;
|
||||
|
||||
let mut app = App::default();
|
||||
let res = run_tui(&mut terminal, &mut app);
|
||||
|
||||
reset_terminal()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes the terminal.
|
||||
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
|
||||
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
/// Resets the terminal.
|
||||
fn reset_terminal() -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
crossterm::execute!(io::stdout(), LeaveAlternateScreen)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs the TUI loop.
|
||||
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('p') => {
|
||||
panic!("intentional demo panic");
|
||||
}
|
||||
|
||||
KeyCode::Char('e') => {
|
||||
app.chain_hook();
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the TUI.
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let text = vec![
|
||||
if app.hook_enabled {
|
||||
Spans::from("HOOK IS CURRENTLY **ENABLED**")
|
||||
} else {
|
||||
Spans::from("HOOK IS CURRENTLY **DISABLED**")
|
||||
},
|
||||
Spans::from(""),
|
||||
Spans::from("press `p` to panic"),
|
||||
Spans::from("press `e` to enable the terminal-resetting panic hook"),
|
||||
Spans::from("press any other key to quit without panic"),
|
||||
Spans::from(""),
|
||||
Spans::from("when you panic without the chained hook,"),
|
||||
Spans::from("you will likely have to reset your terminal afterwards"),
|
||||
Spans::from("with the `reset` command"),
|
||||
Spans::from(""),
|
||||
Spans::from("with the chained panic hook enabled,"),
|
||||
Spans::from("you should see the panic report as you would without tui"),
|
||||
Spans::from(""),
|
||||
Spans::from("try first without the panic handler to see the difference"),
|
||||
];
|
||||
|
||||
let b = Block::default()
|
||||
.title("Panic Handler Demo")
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let p = Paragraph::new(text).block(b).alignment(Alignment::Center);
|
||||
|
||||
f.render_widget(p, f.size());
|
||||
}
|
||||
@@ -1,104 +1,180 @@
|
||||
#[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 crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Paragraph, Text},
|
||||
Terminal,
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
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)?;
|
||||
terminal.hide_cursor()?;
|
||||
struct App {
|
||||
scroll: u16,
|
||||
}
|
||||
|
||||
let events = Events::new();
|
||||
|
||||
let mut scroll: u16 = 0;
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default()
|
||||
.style(Style::default().bg(Color::White));
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.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 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"))
|
||||
.alignment(Alignment::Left);
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
let paragraph = Paragraph::new(text.iter())
|
||||
.block(block.clone().title("Left, wrap"))
|
||||
.alignment(Alignment::Left)
|
||||
.wrap(true);
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
let paragraph = Paragraph::new(text.iter())
|
||||
.block(block.clone().title("Center, wrap"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(true)
|
||||
.scroll(scroll);
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
let paragraph = Paragraph::new(text.iter())
|
||||
.block(block.clone().title("Right, wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(true);
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
})?;
|
||||
|
||||
scroll += 1;
|
||||
scroll %= 10;
|
||||
|
||||
if let Event::Input(key) = events.next()? {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { scroll: 0 }
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.scroll += 1;
|
||||
self.scroll %= 10;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
|
||||
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 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.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Left, wrap"))
|
||||
.alignment(Alignment::Left)
|
||||
.wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: false,
|
||||
});
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Center, wrap"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: false,
|
||||
})
|
||||
.scroll((app.scroll, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Right, wrap (break words)"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
});
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,109 @@
|
||||
#[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::layout::Rect;
|
||||
use tui::widgets::Clear;
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Paragraph, Text},
|
||||
Terminal,
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
/// helper function to create a centered rect using up
|
||||
/// certain percentage of the available rect `r`
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
struct App {
|
||||
show_popup: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { show_popup: false }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => app.show_popup = !app.show_popup,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let text = if app.show_popup {
|
||||
"Press p to close the popup"
|
||||
} else {
|
||||
"Press p to show the popup"
|
||||
};
|
||||
let paragraph = Paragraph::new(Span::styled(
|
||||
text,
|
||||
Style::default().add_modifier(Modifier::SLOW_BLINK),
|
||||
))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: false,
|
||||
});
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let block = Block::default()
|
||||
.title("Content")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(Color::Blue));
|
||||
f.render_widget(block, chunks[1]);
|
||||
|
||||
if app.show_popup {
|
||||
let block = Block::default().title("Popup").borders(Borders::ALL);
|
||||
let area = centered_rect(60, 20, size);
|
||||
f.render_widget(Clear, area); //this clears out the background
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
}
|
||||
|
||||
/// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -41,68 +129,3 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
)
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
|
||||
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)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
let events = Events::new();
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let size = f.size();
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
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(
|
||||
"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 paragraph = Paragraph::new(text.iter())
|
||||
.block(Block::default().title("Left Block").borders(Borders::ALL))
|
||||
.alignment(Alignment::Left).wrap(true);
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.iter())
|
||||
.block(Block::default().title("Right Block").borders(Borders::ALL))
|
||||
.alignment(Alignment::Left).wrap(true);
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
|
||||
let block = Block::default().title("Popup").borders(Borders::ALL);
|
||||
let area = centered_rect(60, 20, size);
|
||||
f.render_widget(Clear, area); //this clears out the background
|
||||
f.render_widget(block, area);
|
||||
})?;
|
||||
|
||||
if let Event::Input(input) = events.next()? {
|
||||
if let Key::Char('q') = input {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
mod demo;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::demo::{ui, App};
|
||||
use argh::FromArgs;
|
||||
use rustbox::keyboard::Key;
|
||||
use std::{
|
||||
error::Error,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{backend::RustboxBackend, Terminal};
|
||||
|
||||
/// Rustbox demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
|
||||
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))?;
|
||||
if let Ok(rustbox::Event::KeyEvent(key)) =
|
||||
terminal.backend().rustbox().peek_event(tick_rate, false)
|
||||
{
|
||||
match key {
|
||||
Key::Char(c) => {
|
||||
app.on_key(c);
|
||||
}
|
||||
Key::Up => {
|
||||
app.on_up();
|
||||
}
|
||||
Key::Down => {
|
||||
app.on_down();
|
||||
}
|
||||
Key::Left => {
|
||||
app.on_left();
|
||||
}
|
||||
Key::Right => {
|
||||
app.on_right();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() > tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,20 +1,47 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
RandomSignal,
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
signal: RandomSignal,
|
||||
data1: Vec<u64>,
|
||||
@@ -36,7 +63,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data1.pop();
|
||||
self.data1.insert(0, value);
|
||||
@@ -50,76 +77,100 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Create default app state
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(0),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
f.render_widget(sparkline, chunks[0]);
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
// Multiline
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
f.render_widget(sparkline, chunks[2]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(0),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
f.render_widget(sparkline, chunks[0]);
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
// Multiline
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
f.render_widget(sparkline, chunks[2]);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Row, Table, TableState},
|
||||
Terminal,
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
pub struct StatefulTable<'a> {
|
||||
struct App<'a> {
|
||||
state: TableState,
|
||||
items: Vec<Vec<&'a str>>,
|
||||
}
|
||||
|
||||
impl<'a> StatefulTable<'a> {
|
||||
fn new() -> StatefulTable<'a> {
|
||||
StatefulTable {
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
state: TableState::default(),
|
||||
items: vec![
|
||||
vec!["Row11", "Row12", "Row13"],
|
||||
@@ -27,7 +27,7 @@ impl<'a> StatefulTable<'a> {
|
||||
vec!["Row31", "Row32", "Row33"],
|
||||
vec!["Row41", "Row42", "Row43"],
|
||||
vec!["Row51", "Row52", "Row53"],
|
||||
vec!["Row61", "Row62", "Row63"],
|
||||
vec!["Row61", "Row62\nTest", "Row63"],
|
||||
vec!["Row71", "Row72", "Row73"],
|
||||
vec!["Row81", "Row82", "Row83"],
|
||||
vec!["Row91", "Row92", "Row93"],
|
||||
@@ -74,60 +74,82 @@ impl<'a> StatefulTable<'a> {
|
||||
}
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
let mut table = StatefulTable::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
loop {
|
||||
terminal.draw(|mut 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 normal_style = Style::default().fg(Color::White);
|
||||
let header = ["Header1", "Header2", "Header3"];
|
||||
let rows = table
|
||||
.items
|
||||
.iter()
|
||||
.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)
|
||||
.highlight_symbol(">> ")
|
||||
.widths(&[
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Length(30),
|
||||
Constraint::Max(10),
|
||||
]);
|
||||
f.render_stateful_widget(t, rects[0], &mut table.state);
|
||||
})?;
|
||||
|
||||
if let Event::Input(key) = events.next()? {
|
||||
match key {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
Key::Down => {
|
||||
table.next();
|
||||
}
|
||||
Key::Up => {
|
||||
table.previous();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let rects = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.margin(5)
|
||||
.split(f.size());
|
||||
|
||||
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||
let normal_style = Style::default().bg(Color::Blue);
|
||||
let header_cells = ["Header1", "Header2", "Header3"]
|
||||
.iter()
|
||||
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
|
||||
let header = Row::new(header_cells)
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.bottom_margin(1);
|
||||
let rows = app.items.iter().map(|item| {
|
||||
let height = item
|
||||
.iter()
|
||||
.map(|content| content.chars().filter(|c| *c == '\n').count())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 1;
|
||||
let cells = item.iter().map(|c| Cell::from(*c));
|
||||
Row::new(cells).height(height as u16).bottom_margin(1)
|
||||
});
|
||||
let t = Table::new(rows)
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ")
|
||||
.widths(&[
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Length(30),
|
||||
Constraint::Min(10),
|
||||
]);
|
||||
f.render_stateful_widget(t, rects[0], &mut app.state);
|
||||
}
|
||||
|
||||
159
examples/tabs.rs
159
examples/tabs.rs
@@ -1,79 +1,124 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
TabsState,
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Tabs},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
tabs: TabsState<'a>,
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
titles: vec!["Tab0", "Tab1", "Tab2", "Tab3"],
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// App
|
||||
let mut app = App {
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2", "Tab3"]),
|
||||
};
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Main loop
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(size);
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let block = Block::default().style(Style::default().bg(Color::White));
|
||||
f.render_widget(block, size);
|
||||
let tabs = Tabs::default()
|
||||
.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));
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
let inner = match app.tabs.index {
|
||||
0 => Block::default().title("Inner 0").borders(Borders::ALL),
|
||||
1 => Block::default().title("Inner 1").borders(Borders::ALL),
|
||||
2 => Block::default().title("Inner 2").borders(Borders::ALL),
|
||||
3 => Block::default().title("Inner 3").borders(Borders::ALL),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
f.render_widget(inner, chunks[1]);
|
||||
})?;
|
||||
|
||||
if let Event::Input(input) = events.next()? {
|
||||
match input {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
Key::Right => app.tabs.next(),
|
||||
Key::Left => app.tabs.previous(),
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
f.render_widget(block, size);
|
||||
let titles = app
|
||||
.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"))
|
||||
.select(app.index)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Black),
|
||||
);
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
let inner = match app.index {
|
||||
0 => Block::default().title("Inner 0").borders(Borders::ALL),
|
||||
1 => Block::default().title("Inner 1").borders(Borders::ALL),
|
||||
2 => Block::default().title("Inner 2").borders(Borders::ALL),
|
||||
3 => Block::default().title("Inner 3").borders(Borders::ALL),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
f.render_widget(inner, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
mod demo;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::{
|
||||
demo::{ui, App},
|
||||
util::event::{Config, Event, Events},
|
||||
};
|
||||
use argh::FromArgs;
|
||||
use std::{error::Error, io, time::Duration};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{backend::TermionBackend, Terminal};
|
||||
|
||||
/// Termion demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
|
||||
let events = Events::with_config(Config {
|
||||
tick_rate: Duration::from_millis(cli.tick_rate),
|
||||
..Config::default()
|
||||
});
|
||||
|
||||
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)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
let mut app = App::new("Termion demo", cli.enhanced_graphics);
|
||||
loop {
|
||||
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(key) => match key {
|
||||
Key::Char(c) => {
|
||||
app.on_key(c);
|
||||
}
|
||||
Key::Up => {
|
||||
app.on_up();
|
||||
}
|
||||
Key::Down => {
|
||||
app.on_down();
|
||||
}
|
||||
Key::Left => {
|
||||
app.on_left();
|
||||
}
|
||||
Key::Right => {
|
||||
app.on_right();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.on_tick();
|
||||
}
|
||||
}
|
||||
if app.should_quit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -9,24 +9,19 @@
|
||||
/// * Pressing Backspace erases a character
|
||||
/// * Pressing Enter pushes the current input in the history of previous
|
||||
/// messages
|
||||
|
||||
#[allow(dead_code)]
|
||||
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 crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, List, Paragraph, Text},
|
||||
Terminal,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -56,98 +51,142 @@ impl Default for App {
|
||||
}
|
||||
|
||||
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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Setup event handlers
|
||||
let mut events = Events::new();
|
||||
// create app and run it
|
||||
let app = App::default();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// Create default app state
|
||||
let mut app = App::default();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
// Draw UI
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
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 text = [Text::raw(msg)];
|
||||
let help_message = Paragraph::new(text.iter());
|
||||
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))
|
||||
.block(Block::default().borders(Borders::ALL).title("Input"));
|
||||
f.render_widget(input, chunks[1]);
|
||||
let messages = app
|
||||
.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| Text::raw(format!("{}: {}", i, m)));
|
||||
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
|
||||
if let Event::Input(input) = events.next()? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match app.input_mode {
|
||||
InputMode::Normal => match input {
|
||||
Key::Char('e') => {
|
||||
InputMode::Normal => match key.code {
|
||||
KeyCode::Char('e') => {
|
||||
app.input_mode = InputMode::Editing;
|
||||
events.disable_exit_key();
|
||||
}
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Editing => match input {
|
||||
Key::Char('\n') => {
|
||||
InputMode::Editing => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.messages.push(app.input.drain(..).collect());
|
||||
}
|
||||
Key::Char(c) => {
|
||||
KeyCode::Char(c) => {
|
||||
app.input.push(c);
|
||||
}
|
||||
Key::Backspace => {
|
||||
KeyCode::Backspace => {
|
||||
app.input.pop();
|
||||
}
|
||||
Key::Esc => {
|
||||
KeyCode::Esc => {
|
||||
app.input_mode = InputMode::Normal;
|
||||
events.enable_exit_key();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
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 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 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]);
|
||||
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)| {
|
||||
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]);
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use termion::event::Key;
|
||||
use termion::input::TermRead;
|
||||
|
||||
pub enum Event<I> {
|
||||
Input(I),
|
||||
Tick,
|
||||
}
|
||||
|
||||
/// A small event handler that wrap termion input and tick events. Each event
|
||||
/// type is handled in its own thread and returned to a common `Receiver`
|
||||
pub struct Events {
|
||||
rx: mpsc::Receiver<Event<Key>>,
|
||||
input_handle: thread::JoinHandle<()>,
|
||||
ignore_exit_key: Arc<AtomicBool>,
|
||||
tick_handle: thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Config {
|
||||
pub exit_key: Key,
|
||||
pub tick_rate: Duration,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
exit_key: Key::Char('q'),
|
||||
tick_rate: Duration::from_millis(250),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Events {
|
||||
pub fn new() -> Events {
|
||||
Events::with_config(Config::default())
|
||||
}
|
||||
|
||||
pub fn with_config(config: Config) -> Events {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let ignore_exit_key = Arc::new(AtomicBool::new(false));
|
||||
let input_handle = {
|
||||
let tx = tx.clone();
|
||||
let ignore_exit_key = ignore_exit_key.clone();
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for evt in stdin.keys() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
let tick_handle = {
|
||||
thread::spawn(move || loop {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(config.tick_rate);
|
||||
})
|
||||
};
|
||||
Events {
|
||||
rx,
|
||||
ignore_exit_key,
|
||||
input_handle,
|
||||
tick_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
|
||||
self.rx.recv()
|
||||
}
|
||||
|
||||
pub fn disable_exit_key(&mut self) {
|
||||
self.ignore_exit_key.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn enable_exit_key(&mut self) {
|
||||
self.ignore_exit_key.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
#[cfg(feature = "termion")]
|
||||
pub mod event;
|
||||
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use rand::rngs::ThreadRng;
|
||||
use tui::widgets::ListState;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TabsState<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<'a> TabsState<'a> {
|
||||
pub fn new(titles: Vec<&'a str>) -> TabsState {
|
||||
TabsState { titles, index: 0 }
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn new() -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
}
|
||||
@@ -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,7 @@ use crossterm::{
|
||||
},
|
||||
terminal::{self, Clear, ClearType},
|
||||
};
|
||||
|
||||
use crate::backend::Backend;
|
||||
use crate::style::{Color, Modifier};
|
||||
use crate::{buffer::Cell, layout::Rect, style};
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
buffer: W,
|
||||
@@ -51,49 +49,40 @@ where
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
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 {
|
||||
map_error(queue!(string, MoveTo(x, y)))?;
|
||||
// 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!(self.buffer, 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;
|
||||
diff.queue(&mut self.buffer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.style.fg != style.fg {
|
||||
let color = CColor::from(cell.style.fg);
|
||||
map_error(queue!(string, SetForegroundColor(color)))?;
|
||||
style.fg = cell.style.fg;
|
||||
inst += 1;
|
||||
if cell.fg != fg {
|
||||
let color = CColor::from(cell.fg);
|
||||
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.style.bg != style.bg {
|
||||
let color = CColor::from(cell.style.bg);
|
||||
map_error(queue!(string, SetBackgroundColor(color)))?;
|
||||
style.bg = cell.style.bg;
|
||||
inst += 1;
|
||||
if cell.bg != bg {
|
||||
let color = CColor::from(cell.bg);
|
||||
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
|
||||
bg = cell.bg;
|
||||
}
|
||||
|
||||
string.push_str(&cell.symbol);
|
||||
inst += 1;
|
||||
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
|
||||
}
|
||||
|
||||
map_error(queue!(
|
||||
self.buffer,
|
||||
Print(string),
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset)
|
||||
@@ -169,11 +158,10 @@ struct ModifierDiff {
|
||||
pub to: Modifier,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl ModifierDiff {
|
||||
fn queue<W>(&self, mut w: W) -> io::Result<()>
|
||||
where
|
||||
W: fmt::Write,
|
||||
W: io::Write,
|
||||
{
|
||||
//use crossterm::Attribute;
|
||||
let removed = self.from - self.to;
|
||||
@@ -231,28 +219,3 @@ impl ModifierDiff {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl ModifierDiff {
|
||||
fn queue<W>(&self, mut w: W) -> io::Result<()>
|
||||
where
|
||||
W: fmt::Write,
|
||||
{
|
||||
let removed = self.from - self.to;
|
||||
if removed.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
}
|
||||
if removed.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
|
||||
}
|
||||
|
||||
let added = self.to - self.from;
|
||||
if added.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
|
||||
}
|
||||
if added.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
use crate::backend::Backend;
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::{Color, Modifier, Style};
|
||||
use crate::symbols::{bar, block};
|
||||
#[cfg(unix)]
|
||||
use crate::symbols::{line, DOT};
|
||||
#[cfg(unix)]
|
||||
use pancurses::{chtype, ToChtype};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
pub struct CursesBackend {
|
||||
curses: easycurses::EasyCurses,
|
||||
}
|
||||
|
||||
impl CursesBackend {
|
||||
pub fn new() -> Option<CursesBackend> {
|
||||
let curses = easycurses::EasyCurses::initialize_system()?;
|
||||
Some(CursesBackend { curses })
|
||||
}
|
||||
|
||||
pub fn with_curses(curses: easycurses::EasyCurses) -> CursesBackend {
|
||||
CursesBackend { curses }
|
||||
}
|
||||
|
||||
pub fn get_curses(&self) -> &easycurses::EasyCurses {
|
||||
&self.curses
|
||||
}
|
||||
|
||||
pub fn get_curses_mut(&mut self) -> &mut easycurses::EasyCurses {
|
||||
&mut self.curses
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for CursesBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
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 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.style.fg != style.fg {
|
||||
update_color = true;
|
||||
if let Some(ccolor) = cell.style.fg.into() {
|
||||
style.fg = cell.style.fg;
|
||||
curses_style.fg = ccolor;
|
||||
} else {
|
||||
style.fg = Color::White;
|
||||
curses_style.fg = easycurses::Color::White;
|
||||
}
|
||||
};
|
||||
if cell.style.bg != style.bg {
|
||||
update_color = true;
|
||||
if let Some(ccolor) = cell.style.bg.into() {
|
||||
style.bg = cell.style.bg;
|
||||
curses_style.bg = ccolor;
|
||||
} else {
|
||||
style.bg = Color::Black;
|
||||
curses_style.bg = easycurses::Color::Black;
|
||||
}
|
||||
};
|
||||
if update_color {
|
||||
self.curses
|
||||
.set_color_pair(easycurses::ColorPair::new(curses_style.fg, curses_style.bg));
|
||||
};
|
||||
update_color = false;
|
||||
draw(&mut self.curses, cell.symbol.as_str());
|
||||
}
|
||||
self.curses.win.attrset(pancurses::Attribute::Normal);
|
||||
self.curses.set_color_pair(easycurses::ColorPair::new(
|
||||
easycurses::Color::White,
|
||||
easycurses::Color::Black,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.curses
|
||||
.set_cursor_visibility(easycurses::CursorVisibility::Invisible);
|
||||
Ok(())
|
||||
}
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.curses
|
||||
.set_cursor_visibility(easycurses::CursorVisibility::Visible);
|
||||
Ok(())
|
||||
}
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let (y, x) = self.curses.get_cursor_rc();
|
||||
Ok((x as u16, y as u16))
|
||||
}
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.curses.move_rc(i32::from(y), i32::from(x));
|
||||
Ok(())
|
||||
}
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.curses.clear();
|
||||
// self.curses.refresh();
|
||||
Ok(())
|
||||
}
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
let (nrows, ncols) = self.curses.get_row_col_count();
|
||||
Ok(Rect::new(0, 0, ncols as u16, nrows as u16))
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.curses.refresh();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct CursesStyle {
|
||||
fg: easycurses::Color,
|
||||
bg: easycurses::Color,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
/// Deals with lack of unicode support for ncurses on unix
|
||||
fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) {
|
||||
for grapheme in symbol.graphemes(true) {
|
||||
let ch = match grapheme {
|
||||
line::TOP_RIGHT => pancurses::ACS_URCORNER(),
|
||||
line::VERTICAL => pancurses::ACS_VLINE(),
|
||||
line::HORIZONTAL => pancurses::ACS_HLINE(),
|
||||
line::TOP_LEFT => pancurses::ACS_ULCORNER(),
|
||||
line::BOTTOM_RIGHT => pancurses::ACS_LRCORNER(),
|
||||
line::BOTTOM_LEFT => pancurses::ACS_LLCORNER(),
|
||||
line::VERTICAL_LEFT => pancurses::ACS_RTEE(),
|
||||
line::VERTICAL_RIGHT => pancurses::ACS_LTEE(),
|
||||
line::HORIZONTAL_DOWN => pancurses::ACS_TTEE(),
|
||||
line::HORIZONTAL_UP => pancurses::ACS_BTEE(),
|
||||
block::FULL => pancurses::ACS_BLOCK(),
|
||||
block::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||
block::THREE_QUARTERS => pancurses::ACS_BLOCK(),
|
||||
block::FIVE_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||
block::HALF => pancurses::ACS_BLOCK(),
|
||||
block::THREE_EIGHTHS => ' ' as chtype,
|
||||
block::ONE_QUARTER => ' ' as chtype,
|
||||
block::ONE_EIGHTH => ' ' as chtype,
|
||||
bar::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||
bar::THREE_QUARTERS => pancurses::ACS_BLOCK(),
|
||||
bar::FIVE_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||
bar::HALF => pancurses::ACS_BLOCK(),
|
||||
bar::THREE_EIGHTHS => pancurses::ACS_S9(),
|
||||
bar::ONE_QUARTER => pancurses::ACS_S9(),
|
||||
bar::ONE_EIGHTH => pancurses::ACS_S9(),
|
||||
DOT => pancurses::ACS_BULLET(),
|
||||
unicode_char => {
|
||||
if unicode_char.is_ascii() {
|
||||
let mut chars = unicode_char.chars();
|
||||
if let Some(ch) = chars.next() {
|
||||
ch.to_chtype()
|
||||
} else {
|
||||
pancurses::ACS_BLOCK()
|
||||
}
|
||||
} else {
|
||||
pancurses::ACS_BLOCK()
|
||||
}
|
||||
}
|
||||
};
|
||||
curses.win.addch(ch);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) {
|
||||
for grapheme in symbol.graphemes(true) {
|
||||
let ch = match grapheme {
|
||||
block::SEVEN_EIGHTHS => block::FULL,
|
||||
block::THREE_QUARTERS => block::FULL,
|
||||
block::FIVE_EIGHTHS => block::HALF,
|
||||
block::THREE_EIGHTHS => block::HALF,
|
||||
block::ONE_QUARTER => block::HALF,
|
||||
block::ONE_EIGHTH => " ",
|
||||
bar::SEVEN_EIGHTHS => bar::FULL,
|
||||
bar::THREE_QUARTERS => bar::FULL,
|
||||
bar::FIVE_EIGHTHS => bar::HALF,
|
||||
bar::THREE_EIGHTHS => bar::HALF,
|
||||
bar::ONE_QUARTER => bar::HALF,
|
||||
bar::ONE_EIGHTH => " ",
|
||||
ch => ch,
|
||||
};
|
||||
// curses.win.addch(ch);
|
||||
curses.print(ch);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Option<easycurses::Color> {
|
||||
fn from(color: Color) -> Option<easycurses::Color> {
|
||||
match color {
|
||||
Color::Reset => None,
|
||||
Color::Black => Some(easycurses::Color::Black),
|
||||
Color::Red | Color::LightRed => Some(easycurses::Color::Red),
|
||||
Color::Green | Color::LightGreen => Some(easycurses::Color::Green),
|
||||
Color::Yellow | Color::LightYellow => Some(easycurses::Color::Yellow),
|
||||
Color::Magenta | Color::LightMagenta => Some(easycurses::Color::Magenta),
|
||||
Color::Cyan | Color::LightCyan => Some(easycurses::Color::Cyan),
|
||||
Color::White | Color::Gray | Color::DarkGray => Some(easycurses::Color::White),
|
||||
Color::Blue | Color::LightBlue => Some(easycurses::Color::Blue),
|
||||
Color::Indexed(_) => None,
|
||||
Color::Rgb(_, _, _) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_modifier_diff(win: &mut pancurses::Window, from: Modifier, to: Modifier) {
|
||||
remove_modifier(win, from - to);
|
||||
add_modifier(win, to - from);
|
||||
}
|
||||
|
||||
fn remove_modifier(win: &mut pancurses::Window, remove: Modifier) {
|
||||
if remove.contains(Modifier::BOLD) {
|
||||
win.attroff(pancurses::Attribute::Bold);
|
||||
}
|
||||
if remove.contains(Modifier::DIM) {
|
||||
win.attroff(pancurses::Attribute::Dim);
|
||||
}
|
||||
if remove.contains(Modifier::ITALIC) {
|
||||
win.attroff(pancurses::Attribute::Italic);
|
||||
}
|
||||
if remove.contains(Modifier::UNDERLINED) {
|
||||
win.attroff(pancurses::Attribute::Underline);
|
||||
}
|
||||
if remove.contains(Modifier::SLOW_BLINK) || remove.contains(Modifier::RAPID_BLINK) {
|
||||
win.attroff(pancurses::Attribute::Blink);
|
||||
}
|
||||
if remove.contains(Modifier::REVERSED) {
|
||||
win.attroff(pancurses::Attribute::Reverse);
|
||||
}
|
||||
if remove.contains(Modifier::HIDDEN) {
|
||||
win.attroff(pancurses::Attribute::Invisible);
|
||||
}
|
||||
if remove.contains(Modifier::CROSSED_OUT) {
|
||||
win.attroff(pancurses::Attribute::Strikeout);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_modifier(win: &mut pancurses::Window, add: Modifier) {
|
||||
if add.contains(Modifier::BOLD) {
|
||||
win.attron(pancurses::Attribute::Bold);
|
||||
}
|
||||
if add.contains(Modifier::DIM) {
|
||||
win.attron(pancurses::Attribute::Dim);
|
||||
}
|
||||
if add.contains(Modifier::ITALIC) {
|
||||
win.attron(pancurses::Attribute::Italic);
|
||||
}
|
||||
if add.contains(Modifier::UNDERLINED) {
|
||||
win.attron(pancurses::Attribute::Underline);
|
||||
}
|
||||
if add.contains(Modifier::SLOW_BLINK) || add.contains(Modifier::RAPID_BLINK) {
|
||||
win.attron(pancurses::Attribute::Blink);
|
||||
}
|
||||
if add.contains(Modifier::REVERSED) {
|
||||
win.attron(pancurses::Attribute::Reverse);
|
||||
}
|
||||
if add.contains(Modifier::HIDDEN) {
|
||||
win.attron(pancurses::Attribute::Invisible);
|
||||
}
|
||||
if add.contains(Modifier::CROSSED_OUT) {
|
||||
win.attron(pancurses::Attribute::Strikeout);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,6 @@ use std::io;
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
|
||||
#[cfg(feature = "rustbox")]
|
||||
mod rustbox;
|
||||
#[cfg(feature = "rustbox")]
|
||||
pub use self::rustbox::RustboxBackend;
|
||||
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termion")]
|
||||
@@ -18,11 +13,6 @@ mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
#[cfg(feature = "curses")]
|
||||
mod curses;
|
||||
#[cfg(feature = "curses")]
|
||||
pub use self::curses::CursesBackend;
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
use std::io;
|
||||
|
||||
pub struct RustboxBackend {
|
||||
rustbox: rustbox::RustBox,
|
||||
}
|
||||
|
||||
impl RustboxBackend {
|
||||
pub fn new() -> Result<RustboxBackend, rustbox::InitError> {
|
||||
let rustbox = rustbox::RustBox::init(Default::default())?;
|
||||
Ok(RustboxBackend { rustbox })
|
||||
}
|
||||
|
||||
pub fn with_rustbox(instance: rustbox::RustBox) -> RustboxBackend {
|
||||
RustboxBackend { rustbox: instance }
|
||||
}
|
||||
|
||||
pub fn rustbox(&self) -> &rustbox::RustBox {
|
||||
&self.rustbox
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for RustboxBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, cell) in content {
|
||||
self.rustbox.print(
|
||||
x as usize,
|
||||
y as usize,
|
||||
cell.style.modifier.into(),
|
||||
cell.style.fg.into(),
|
||||
cell.style.bg.into(),
|
||||
&cell.symbol,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
Err(io::Error::from(io::ErrorKind::Other))
|
||||
}
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.rustbox.set_cursor(x as isize, y as isize);
|
||||
Ok(())
|
||||
}
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.rustbox.clear();
|
||||
Ok(())
|
||||
}
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
let term_width = self.rustbox.width();
|
||||
let term_height = self.rustbox.height();
|
||||
let max = u16::max_value();
|
||||
Ok(Rect::new(
|
||||
0,
|
||||
0,
|
||||
if term_width > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_width as u16
|
||||
},
|
||||
if term_height > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_height as u16
|
||||
},
|
||||
))
|
||||
}
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
self.rustbox.present();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb_to_byte(r: u8, g: u8, b: u8) -> u16 {
|
||||
u16::from((r & 0xC0) + ((g & 0xE0) >> 2) + ((b & 0xE0) >> 5))
|
||||
}
|
||||
|
||||
impl Into<rustbox::Color> for Color {
|
||||
fn into(self) -> rustbox::Color {
|
||||
match self {
|
||||
Color::Reset => rustbox::Color::Default,
|
||||
Color::Black | Color::Gray | Color::DarkGray => rustbox::Color::Black,
|
||||
Color::Red | Color::LightRed => rustbox::Color::Red,
|
||||
Color::Green | Color::LightGreen => rustbox::Color::Green,
|
||||
Color::Yellow | Color::LightYellow => rustbox::Color::Yellow,
|
||||
Color::Magenta | Color::LightMagenta => rustbox::Color::Magenta,
|
||||
Color::Cyan | Color::LightCyan => rustbox::Color::Cyan,
|
||||
Color::White => rustbox::Color::White,
|
||||
Color::Blue | Color::LightBlue => rustbox::Color::Blue,
|
||||
Color::Indexed(i) => rustbox::Color::Byte(u16::from(i)),
|
||||
Color::Rgb(r, g, b) => rustbox::Color::Byte(rgb_to_byte(r, g, b)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<rustbox::Style> for Modifier {
|
||||
fn into(self) -> rustbox::Style {
|
||||
let mut result = rustbox::Style::empty();
|
||||
if self.contains(Modifier::BOLD) {
|
||||
result.insert(rustbox::RB_BOLD);
|
||||
}
|
||||
if self.contains(Modifier::UNDERLINED) {
|
||||
result.insert(rustbox::RB_UNDERLINE);
|
||||
}
|
||||
if self.contains(Modifier::REVERSED) {
|
||||
result.insert(rustbox::RB_REVERSE);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -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)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -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('\n');
|
||||
}
|
||||
view
|
||||
}
|
||||
|
||||
impl TestBackend {
|
||||
pub fn new(width: u16, height: u16) -> TestBackend {
|
||||
TestBackend {
|
||||
@@ -26,6 +59,50 @@ impl TestBackend {
|
||||
pub fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u16, height: u16) {
|
||||
self.buffer.resize(Rect::new(0, 0, width, height));
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
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('\n');
|
||||
debug_info.push_str("Expected:");
|
||||
debug_info.push('\n');
|
||||
let expected_view = buffer_view(expected);
|
||||
debug_info.push_str(&expected_view);
|
||||
debug_info.push('\n');
|
||||
debug_info.push_str("Got:");
|
||||
debug_info.push('\n');
|
||||
let view = buffer_view(&self.buffer);
|
||||
debug_info.push_str(&view);
|
||||
debug_info.push('\n');
|
||||
|
||||
debug_info.push_str("Diff:");
|
||||
debug_info.push('\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 +112,39 @@ 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> {
|
||||
self.buffer.reset();
|
||||
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(())
|
||||
}
|
||||
|
||||
156
src/buffer.rs
156
src/buffer.rs
@@ -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,40 @@ 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 style(&self) -> Style {
|
||||
Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.add_modifier(self.modifier)
|
||||
}
|
||||
|
||||
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 +71,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 +98,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, Default)]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
@@ -101,58 +114,6 @@ pub struct Buffer {
|
||||
pub content: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Default for Buffer {
|
||||
fn default() -> Buffer {
|
||||
Buffer {
|
||||
area: Default::default(),
|
||||
content: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -176,9 +137,11 @@ impl Buffer {
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let height = lines.len() as u16;
|
||||
let width = lines.iter().fold(0, |acc, item| {
|
||||
std::cmp::max(acc, item.as_ref().width() as u16)
|
||||
});
|
||||
let width = lines
|
||||
.iter()
|
||||
.map(|i| i.as_ref().width() as u16)
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let mut buffer = Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -343,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() {
|
||||
@@ -351,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) {
|
||||
|
||||
@@ -63,6 +63,9 @@ pub struct Layout {
|
||||
direction: Direction,
|
||||
margin: Margin,
|
||||
constraints: Vec<Constraint>,
|
||||
/// Whether the last chunk of the computed layout should be expanded to fill the available
|
||||
/// space.
|
||||
expand_to_fill: bool,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -78,6 +81,7 @@ impl Default for Layout {
|
||||
vertical: 0,
|
||||
},
|
||||
constraints: Vec::new(),
|
||||
expand_to_fill: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,6 +118,11 @@ impl Layout {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
|
||||
self.expand_to_fill = expand_to_fill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wrapper function around the cassowary-rs solver to be able to split a given
|
||||
/// area into smaller ones based on the preferred widths or heights and the direction.
|
||||
///
|
||||
@@ -174,12 +183,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 +218,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()));
|
||||
@@ -220,11 +231,13 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
|
||||
});
|
||||
}
|
||||
if let Some(last) = elements.last() {
|
||||
ccs.push(match layout.direction {
|
||||
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
|
||||
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
|
||||
});
|
||||
if layout.expand_to_fill {
|
||||
if let Some(last) = elements.last() {
|
||||
ccs.push(match layout.direction {
|
||||
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
|
||||
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
|
||||
});
|
||||
}
|
||||
}
|
||||
match layout.direction {
|
||||
Direction::Horizontal => {
|
||||
@@ -297,14 +310,16 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
}
|
||||
}
|
||||
|
||||
// Fix imprecision by extending the last item a bit if necessary
|
||||
if let Some(last) = results.last_mut() {
|
||||
match layout.direction {
|
||||
Direction::Vertical => {
|
||||
last.height = dest_area.bottom() - last.y;
|
||||
}
|
||||
Direction::Horizontal => {
|
||||
last.width = dest_area.right() - last.x;
|
||||
if layout.expand_to_fill {
|
||||
// Fix imprecision by extending the last item a bit if necessary
|
||||
if let Some(last) = results.last_mut() {
|
||||
match layout.direction {
|
||||
Direction::Vertical => {
|
||||
last.height = dest_area.bottom() - last.y;
|
||||
}
|
||||
Direction::Horizontal => {
|
||||
last.width = dest_area.right() - last.x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,7 +363,7 @@ impl Element {
|
||||
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
|
||||
pub struct Rect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
@@ -356,17 +371,6 @@ pub struct Rect {
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Default for Rect {
|
||||
fn default() -> Rect {
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Creates a new rect, with width and height limited to keep the area under max u16.
|
||||
/// If clipped, aspect ratio will be preserved.
|
||||
@@ -399,7 +403,7 @@ impl Rect {
|
||||
}
|
||||
|
||||
pub fn right(self) -> u16 {
|
||||
self.x + self.width
|
||||
self.x.saturating_add(self.width)
|
||||
}
|
||||
|
||||
pub fn top(self) -> u16 {
|
||||
@@ -407,7 +411,7 @@ impl Rect {
|
||||
}
|
||||
|
||||
pub fn bottom(self) -> u16 {
|
||||
self.y + self.height
|
||||
self.y.saturating_add(self.height)
|
||||
}
|
||||
|
||||
pub fn inner(self, margin: &Margin) -> Rect {
|
||||
@@ -461,6 +465,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 {
|
||||
|
||||
141
src/lib.rs
141
src/lib.rs
@@ -9,18 +9,19 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! tui = "0.9"
|
||||
//! termion = "1.5"
|
||||
//! tui = "0.18"
|
||||
//! crossterm = "0.23"
|
||||
//! ```
|
||||
//!
|
||||
//! The crate is using the `termion` backend by default but if for example you want your
|
||||
//! application to work on Windows, you might want to use the `crossterm` backend instead. This can
|
||||
//! be done by changing your dependencies specification to the following:
|
||||
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
|
||||
//! example you want to use the `termion` backend instead. This can be done by changing your
|
||||
//! dependencies specification to the following:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! crossterm = "0.17"
|
||||
//! tui = { version = "0.9", default-features = false, features = ['crossterm'] }
|
||||
//! termion = "1.5"
|
||||
//! tui = { version = "0.18", default-features = false, features = ['termion'] }
|
||||
//!
|
||||
//! ```
|
||||
//!
|
||||
//! The same logic applies for all other available backends.
|
||||
@@ -33,29 +34,27 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::TermionBackend;
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//! use tui::{backend::CrosstermBackend, Terminal};
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let stdout = io::stdout();
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If you had previously chosen `crossterm` as a backend, the terminal can be created in a similar
|
||||
//! If you had previously chosen `termion` as a backend, the terminal can be created in a similar
|
||||
//! way:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use std::io;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::CrosstermBackend;
|
||||
//! use tui::{backend::TermionBackend, Terminal};
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout();
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
@@ -71,30 +70,53 @@
|
||||
//! implement your own.
|
||||
//!
|
||||
//! Each widget follows a builder pattern API providing a default configuration along with methods
|
||||
//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take
|
||||
//! your widget instance an area to draw to.
|
||||
//! to customize them. The widget is then rendered using [`Frame::render_widget`] which takes
|
||||
//! your widget instance and an area to draw to.
|
||||
//!
|
||||
//! The following example renders a block of the size of the terminal:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::TermionBackend;
|
||||
//! use tui::widgets::{Widget, Block, Borders};
|
||||
//! use tui::layout::{Layout, Constraint, Direction};
|
||||
//! use std::{io, thread, time::Duration};
|
||||
//! use tui::{
|
||||
//! backend::CrosstermBackend,
|
||||
//! widgets::{Widget, Block, Borders},
|
||||
//! layout::{Layout, Constraint, Direction},
|
||||
//! Terminal
|
||||
//! };
|
||||
//! use crossterm::{
|
||||
//! event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
//! execute,
|
||||
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
//! };
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! // setup terminal
|
||||
//! enable_raw_mode()?;
|
||||
//! let mut stdout = io::stdout();
|
||||
//! execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
//! let backend = CrosstermBackend::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")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, size);
|
||||
//! })
|
||||
//! })?;
|
||||
//!
|
||||
//! thread::sleep(Duration::from_millis(5000));
|
||||
//!
|
||||
//! // restore terminal
|
||||
//! disable_raw_mode()?;
|
||||
//! execute!(
|
||||
//! terminal.backend_mut(),
|
||||
//! LeaveAlternateScreen,
|
||||
//! DisableMouseCapture
|
||||
//! )?;
|
||||
//! terminal.show_cursor()?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -105,38 +127,32 @@
|
||||
//! full customization. And `Layout` is no exception:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::TermionBackend;
|
||||
//! use tui::widgets::{Widget, Block, Borders};
|
||||
//! use tui::layout::{Layout, Constraint, Direction};
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! terminal.draw(|mut f| {
|
||||
//! let chunks = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
//! .margin(1)
|
||||
//! .constraints(
|
||||
//! [
|
||||
//! Constraint::Percentage(10),
|
||||
//! Constraint::Percentage(80),
|
||||
//! Constraint::Percentage(10)
|
||||
//! ].as_ref()
|
||||
//! )
|
||||
//! .split(f.size());
|
||||
//! let block = Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[0]);
|
||||
//! let block = Block::default()
|
||||
//! .title("Block 2")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[1]);
|
||||
//! })
|
||||
//! use tui::{
|
||||
//! backend::Backend,
|
||||
//! layout::{Constraint, Direction, Layout},
|
||||
//! widgets::{Block, Borders},
|
||||
//! Frame,
|
||||
//! };
|
||||
//! fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
//! let chunks = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
//! .margin(1)
|
||||
//! .constraints(
|
||||
//! [
|
||||
//! Constraint::Percentage(10),
|
||||
//! Constraint::Percentage(80),
|
||||
//! Constraint::Percentage(10)
|
||||
//! ].as_ref()
|
||||
//! )
|
||||
//! .split(f.size());
|
||||
//! let block = Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[0]);
|
||||
//! let block = Block::default()
|
||||
//! .title("Block 2")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[1]);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -151,6 +167,7 @@ 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};
|
||||
|
||||
306
src/style.rs
306
src/style.rs
@@ -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,230 @@ 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.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
/// .bg(Color::Green)
|
||||
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
/// ```
|
||||
///
|
||||
/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
|
||||
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default().bg(Color::Red),
|
||||
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Red),
|
||||
/// add_modifier: Modifier::BOLD,
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// The default implementation returns a `Style` that does not modify anything. If you wish to
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::reset().fg(Color::Yellow),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Reset),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[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 {
|
||||
/// Returns a `Style` resetting all properties.
|
||||
pub fn reset() -> Style {
|
||||
Style {
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::all(),
|
||||
}
|
||||
}
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,8 +224,10 @@ pub mod braille {
|
||||
/// Marker to use when plotting data points
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Marker {
|
||||
/// One point per cell
|
||||
/// One point per cell in shape of dot
|
||||
Dot,
|
||||
/// One point per cell in shape of a block
|
||||
Block,
|
||||
/// Up to 8 points per cell
|
||||
Braille,
|
||||
}
|
||||
|
||||
172
src/terminal.rs
172
src/terminal.rs
@@ -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::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,21 +75,19 @@ 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`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// ```rust
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::TermionBackend;
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::Block;
|
||||
/// # let stdout = io::stdout();
|
||||
/// # let backend = TermionBackend::new(stdout);
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let block = Block::default();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
@@ -72,19 +108,20 @@ where
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// ```rust
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::TermionBackend;
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::{List, ListState, Text};
|
||||
/// # let stdout = io::stdout();
|
||||
/// # let backend = TermionBackend::new(stdout);
|
||||
/// # use tui::widgets::{List, ListItem, ListState};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # 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 +132,24 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
/// CompletedFrame represents the state of the terminal after all changes performed in the last
|
||||
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
|
||||
/// [`Terminal::draw`].
|
||||
pub struct CompletedFrame<'a> {
|
||||
pub buffer: &'a Buffer,
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
@@ -115,22 +170,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 {
|
||||
@@ -159,43 +233,60 @@ where
|
||||
/// This leads to a full clear of the screen.
|
||||
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.known_size = area;
|
||||
self.backend.clear()
|
||||
self.viewport.area = area;
|
||||
self.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(())
|
||||
}
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
|
||||
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;
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
Ok(())
|
||||
Ok(CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.viewport.area,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
@@ -203,20 +294,29 @@ 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)
|
||||
}
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
self.backend.clear()
|
||||
self.backend.clear()?;
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
|
||||
428
src/text.rs
Normal file
428
src/text.rs
Normal file
@@ -0,0 +1,428 @@
|
||||
//! 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;
|
||||
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, Default)]
|
||||
pub struct Spans<'a>(pub Vec<Span<'a>>);
|
||||
|
||||
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().map(Span::width).sum()
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
///
|
||||
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
|
||||
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
|
||||
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
/// let mut text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
///
|
||||
/// // Adding two more unstyled lines
|
||||
/// text.extend(Text::raw("These are two\nmore lines!"));
|
||||
/// assert_eq!(4, text.height());
|
||||
///
|
||||
/// // Adding a final two styled lines
|
||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
/// assert_eq!(6, text.height());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Text<'a> {
|
||||
pub lines: Vec<Spans<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
/// Create some text (potentially multiple lines) with no style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// Text::raw("The first line\nThe second line");
|
||||
/// Text::raw(String::from("The first line\nThe second line"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Text {
|
||||
lines: match content.into() {
|
||||
Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
|
||||
Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let mut text = Text::raw(content);
|
||||
text.patch_style(style);
|
||||
text
|
||||
}
|
||||
|
||||
/// 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()
|
||||
.map(Spans::width)
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Apply a new style to existing text.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_text = Text::raw("The first line\nThe second line");
|
||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// assert_ne!(raw_text, styled_text);
|
||||
///
|
||||
/// raw_text.patch_style(style);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
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<String> for Text<'a> {
|
||||
fn from(s: String) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Text<'a> {
|
||||
fn from(s: &'a str) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for Text<'a> {
|
||||
fn from(s: Cow<'a, str>) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Text<'a> {
|
||||
type Item = Spans<'a>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.lines.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extend<Spans<'a>> for Text<'a> {
|
||||
fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
|
||||
self.lines.extend(iter);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
symbols,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use std::cmp::{max, min};
|
||||
use std::cmp::min;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// Display multiple bars in a single widgets
|
||||
@@ -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,11 +143,9 @@ 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)));
|
||||
.unwrap_or_else(|| self.data.iter().map(|t| t.1).max().unwrap_or_default());
|
||||
let max_index = min(
|
||||
(chart_area.width / (self.bar_width + self.bar_gap)) as usize,
|
||||
self.data.len(),
|
||||
@@ -148,7 +157,7 @@ impl<'a> Widget for BarChart<'a> {
|
||||
.map(|&(l, v)| {
|
||||
(
|
||||
l,
|
||||
v * u64::from(chart_area.height) * 8 / std::cmp::max(max, 1),
|
||||
v * u64::from(chart_area.height - 1) * 8 / std::cmp::max(max, 1),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(&str, u64)>>();
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
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::{Alignment, Rect},
|
||||
style::Style,
|
||||
symbols::line,
|
||||
text::{Span, Spans},
|
||||
widgets::{Borders, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum BorderType {
|
||||
Plain,
|
||||
Rounded,
|
||||
@@ -33,18 +36,18 @@ 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, PartialEq)]
|
||||
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>>,
|
||||
/// Title alignment. The default is top left of the block, but one can choose to place
|
||||
/// title in the top middle, or top right of the block
|
||||
title_alignment: Alignment,
|
||||
/// Visible borders
|
||||
borders: Borders,
|
||||
/// Border style
|
||||
@@ -60,7 +63,7 @@ impl<'a> Default for Block<'a> {
|
||||
fn default() -> Block<'a> {
|
||||
Block {
|
||||
title: None,
|
||||
title_style: Default::default(),
|
||||
title_alignment: Alignment::Left,
|
||||
borders: Borders::NONE,
|
||||
border_style: Default::default(),
|
||||
border_type: BorderType::Plain,
|
||||
@@ -70,13 +73,28 @@ 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
|
||||
}
|
||||
|
||||
pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
|
||||
self.title_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -102,23 +120,20 @@ impl<'a> Block<'a> {
|
||||
|
||||
/// Compute the inner area of a block based on its border visibility rules.
|
||||
pub fn inner(&self, area: Rect) -> Rect {
|
||||
if area.width < 2 || area.height < 2 {
|
||||
return Rect::default();
|
||||
}
|
||||
let mut inner = area;
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
inner.x += 1;
|
||||
inner.width -= 1;
|
||||
inner.x = inner.x.saturating_add(1).min(inner.right());
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::TOP) || self.title.is_some() {
|
||||
inner.y += 1;
|
||||
inner.height -= 1;
|
||||
inner.y = inner.y.saturating_add(1).min(inner.bottom());
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::RIGHT) {
|
||||
inner.width -= 1;
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::BOTTOM) {
|
||||
inner.height -= 1;
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
}
|
||||
inner
|
||||
}
|
||||
@@ -126,13 +141,12 @@ impl<'a> Block<'a> {
|
||||
|
||||
impl<'a> Widget for Block<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width < 2 || area.height < 2 {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
buf.set_background(area, self.style.bg);
|
||||
|
||||
buf.set_style(area, self.style);
|
||||
let symbols = BorderType::line_symbols(self.border_type);
|
||||
|
||||
// Sides
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
for y in area.top()..area.bottom() {
|
||||
@@ -166,9 +180,9 @@ impl<'a> Widget for Block<'a> {
|
||||
}
|
||||
|
||||
// Corners
|
||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
buf.get_mut(area.left(), area.top())
|
||||
.set_symbol(symbols.top_left)
|
||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
.set_symbol(symbols.bottom_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
||||
@@ -181,31 +195,379 @@ impl<'a> Widget for Block<'a> {
|
||||
.set_symbol(symbols.bottom_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
.set_symbol(symbols.bottom_right)
|
||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
buf.get_mut(area.left(), area.top())
|
||||
.set_symbol(symbols.top_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
|
||||
// Title
|
||||
if let Some(title) = self.title {
|
||||
let lx = if self.borders.intersects(Borders::LEFT) {
|
||||
let left_border_dx = if self.borders.intersects(Borders::LEFT) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let rx = if self.borders.intersects(Borders::RIGHT) {
|
||||
|
||||
let right_border_dx = if self.borders.intersects(Borders::RIGHT) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let width = area.width - lx - rx;
|
||||
buf.set_stringn(
|
||||
area.left() + lx,
|
||||
area.top(),
|
||||
title,
|
||||
width as usize,
|
||||
self.title_style,
|
||||
);
|
||||
|
||||
let title_area_width = area
|
||||
.width
|
||||
.saturating_sub(left_border_dx)
|
||||
.saturating_sub(right_border_dx);
|
||||
|
||||
let title_dx = match self.title_alignment {
|
||||
Alignment::Left => left_border_dx,
|
||||
Alignment::Center => area.width.saturating_sub(title.width() as u16) / 2,
|
||||
Alignment::Right => area
|
||||
.width
|
||||
.saturating_sub(title.width() as u16)
|
||||
.saturating_sub(right_border_dx),
|
||||
};
|
||||
|
||||
let title_x = area.left() + title_dx;
|
||||
let title_y = area.top();
|
||||
|
||||
buf.set_spans(title_x, title_y, &title, title_area_width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::layout::Rect;
|
||||
|
||||
#[test]
|
||||
fn inner_takes_into_account_the_borders() {
|
||||
// No borders
|
||||
assert_eq!(
|
||||
Block::default().inner(Rect::default()),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
"no borders, width=0, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"no borders, width=1, height=1"
|
||||
);
|
||||
|
||||
// Left border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"left, width=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"left, width=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"left, width=2"
|
||||
);
|
||||
|
||||
// Top border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"top, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"top, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 2
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"top, height=2"
|
||||
);
|
||||
|
||||
// Right border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"right, width=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"right, width=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"right, width=2"
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"bottom, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"bottom, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 2
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"bottom, height=2"
|
||||
);
|
||||
|
||||
// All borders
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.inner(Rect::default()),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
"all borders, width=0, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
"all borders, width=1, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
"all borders, width=2, height=2"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 3,
|
||||
height: 3,
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
"all borders, width=3, height=3"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_takes_into_account_the_title() {
|
||||
assert_eq!(
|
||||
Block::default().title("Test").inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.title_alignment(Alignment::Center)
|
||||
.inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.title_alignment(Alignment::Right)
|
||||
.inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Spans,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
@@ -26,10 +27,9 @@ pub trait Shape {
|
||||
/// Label to draw some text on the canvas
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Label<'a> {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub text: &'a str,
|
||||
pub color: Color,
|
||||
x: f64,
|
||||
y: f64,
|
||||
spans: Spans<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -111,26 +111,28 @@ impl Grid for BrailleGrid {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DotGrid {
|
||||
struct CharGrid {
|
||||
width: u16,
|
||||
height: u16,
|
||||
cells: Vec<char>,
|
||||
colors: Vec<Color>,
|
||||
cell_char: char,
|
||||
}
|
||||
|
||||
impl DotGrid {
|
||||
fn new(width: u16, height: u16) -> DotGrid {
|
||||
impl CharGrid {
|
||||
fn new(width: u16, height: u16, cell_char: char) -> CharGrid {
|
||||
let length = usize::from(width * height);
|
||||
DotGrid {
|
||||
CharGrid {
|
||||
width,
|
||||
height,
|
||||
cells: vec![' '; length],
|
||||
colors: vec![Color::Reset; length],
|
||||
cell_char,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid for DotGrid {
|
||||
impl Grid for CharGrid {
|
||||
fn width(&self) -> u16 {
|
||||
self.width
|
||||
}
|
||||
@@ -162,7 +164,7 @@ impl Grid for DotGrid {
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
let index = y * self.width as usize + x;
|
||||
if let Some(c) = self.cells.get_mut(index) {
|
||||
*c = '•';
|
||||
*c = self.cell_char;
|
||||
}
|
||||
if let Some(c) = self.colors.get_mut(index) {
|
||||
*c = color;
|
||||
@@ -206,6 +208,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))
|
||||
@@ -256,7 +261,8 @@ impl<'a> Context<'a> {
|
||||
marker: symbols::Marker,
|
||||
) -> Context<'a> {
|
||||
let grid: Box<dyn Grid> = match marker {
|
||||
symbols::Marker::Dot => Box::new(DotGrid::new(width, height)),
|
||||
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, '•')),
|
||||
symbols::Marker::Block => Box::new(CharGrid::new(width, height, '▄')),
|
||||
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
|
||||
};
|
||||
Context {
|
||||
@@ -287,8 +293,15 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
|
||||
/// Print a string on the canvas at the given position
|
||||
pub fn print(&mut self, x: f64, y: f64, text: &'a str, color: Color) {
|
||||
self.labels.push(Label { x, y, text, color });
|
||||
pub fn print<T>(&mut self, x: f64, y: f64, spans: T)
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
{
|
||||
self.labels.push(Label {
|
||||
x,
|
||||
y,
|
||||
spans: spans.into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Push the last layer if necessary
|
||||
@@ -393,8 +406,8 @@ where
|
||||
}
|
||||
|
||||
/// Change the type of points used to draw the shapes. By default the braille patterns are used
|
||||
/// as they provide a more fine grained result but you might want to use the simple dot instead
|
||||
/// if the targeted terminal does not support those symbols.
|
||||
/// as they provide a more fine grained result but you might want to use the simple dot or
|
||||
/// block instead if the targeted terminal does not support those symbols.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -404,6 +417,8 @@ where
|
||||
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
|
||||
///
|
||||
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
|
||||
///
|
||||
/// Canvas::default().marker(symbols::Marker::Block).paint(|ctx| {});
|
||||
/// ```
|
||||
pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> {
|
||||
self.marker = marker;
|
||||
@@ -416,14 +431,17 @@ 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,
|
||||
};
|
||||
|
||||
buf.set_style(canvas_area, Style::default().bg(self.background_color));
|
||||
|
||||
let width = canvas_area.width as usize;
|
||||
|
||||
let painter = match self.painter {
|
||||
@@ -455,14 +473,12 @@ where
|
||||
let (x, y) = (i % width, i / width);
|
||||
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
|
||||
.set_char(ch)
|
||||
.set_fg(color)
|
||||
.set_bg(self.background_color);
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally draw the labels
|
||||
let style = Style::default().bg(self.background_color);
|
||||
let left = self.x_bounds[0];
|
||||
let right = self.x_bounds[1];
|
||||
let top = self.y_bounds[1];
|
||||
@@ -481,13 +497,7 @@ where
|
||||
{
|
||||
let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
|
||||
let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
|
||||
buf.set_stringn(
|
||||
x,
|
||||
y,
|
||||
label.text,
|
||||
(canvas_area.right() - x) as usize,
|
||||
style.fg(label.color),
|
||||
);
|
||||
buf.set_spans(x, y, &label.spans, canvas_area.right() - x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6122,6 +6122,7 @@ pub static WORLD_LOW_RESOLUTION: [(f64, f64); 1166] = [
|
||||
(120.43, 16.43),
|
||||
(121.72, 18.40),
|
||||
(125.34, 9.79),
|
||||
#[allow(clippy::approx_constant)]
|
||||
(125.56, 6.28),
|
||||
(122.38, 7.00),
|
||||
(125.10, 9.38),
|
||||
|
||||
@@ -1,83 +1,89 @@
|
||||
use std::{borrow::Cow, cmp::max};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::layout::Alignment;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
style::Style,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::{
|
||||
canvas::{Canvas, Line, Points},
|
||||
Block, Borders, Widget,
|
||||
},
|
||||
};
|
||||
use std::{borrow::Cow, cmp::max};
|
||||
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,
|
||||
/// The alignment of the labels of the Axis
|
||||
labels_alignment: Alignment,
|
||||
}
|
||||
|
||||
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(),
|
||||
labels_alignment: Alignment::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
pub fn style(mut self, style: Style) -> Axis<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Axis<'a, L> {
|
||||
self.style = style;
|
||||
/// Defines the alignment of the labels of the axis.
|
||||
/// The alignment behaves differently based on the axis:
|
||||
/// - Y-Axis: The labels are aligned within the area on the left of the axis
|
||||
/// - X-Axis: The first X-axis label is aligned relative to the Y-axis
|
||||
pub fn labels_alignment(mut self, alignment: Alignment) -> Axis<'a> {
|
||||
self.labels_alignment = alignment;
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -150,7 +156,7 @@ impl<'a> Dataset<'a> {
|
||||
|
||||
/// A container that holds all the infos about where to display each elements of the chart (axis,
|
||||
/// labels, legend, ...).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
struct ChartLayout {
|
||||
/// Location of the title of the x axis
|
||||
title_x: Option<(u16, u16)>,
|
||||
@@ -170,21 +176,6 @@ struct ChartLayout {
|
||||
graph_area: Rect,
|
||||
}
|
||||
|
||||
impl Default for ChartLayout {
|
||||
fn default() -> ChartLayout {
|
||||
ChartLayout {
|
||||
title_x: None,
|
||||
title_y: None,
|
||||
label_x: None,
|
||||
label_y: None,
|
||||
axis_x: None,
|
||||
axis_y: None,
|
||||
legend_area: None,
|
||||
graph_area: Rect::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to plot one or more dataset in a cartesian coordinate system
|
||||
///
|
||||
/// # Examples
|
||||
@@ -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,13 +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
|
||||
}
|
||||
@@ -328,21 +296,8 @@ where
|
||||
y -= 1;
|
||||
}
|
||||
|
||||
if let Some(y_labels) = self.y_axis.labels {
|
||||
let mut max_width = y_labels
|
||||
.iter()
|
||||
.fold(0, |acc, l| max(l.as_ref().width(), acc))
|
||||
as u16;
|
||||
if let Some(x_labels) = self.x_axis.labels {
|
||||
if !x_labels.is_empty() {
|
||||
max_width = max(max_width, x_labels[0].as_ref().width() as u16);
|
||||
}
|
||||
}
|
||||
if x + max_width < area.right() {
|
||||
layout.label_y = Some(x);
|
||||
x += max_width;
|
||||
}
|
||||
}
|
||||
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
|
||||
x += self.max_width_of_labels_left_of_y_axis(area, self.y_axis.labels.is_some());
|
||||
|
||||
if self.x_axis.labels.is_some() && y > area.top() {
|
||||
layout.axis_x = Some(y);
|
||||
@@ -358,17 +313,17 @@ 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()));
|
||||
layout.title_y = Some((x, area.top()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,18 +352,161 @@ where
|
||||
}
|
||||
layout
|
||||
}
|
||||
|
||||
fn max_width_of_labels_left_of_y_axis(&self, area: Rect, has_y_axis: bool) -> u16 {
|
||||
let mut max_width = self
|
||||
.y_axis
|
||||
.labels
|
||||
.as_ref()
|
||||
.map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(first_x_label) = self.x_axis.labels.as_ref().and_then(|labels| labels.get(0)) {
|
||||
let first_label_width = first_x_label.content.width() as u16;
|
||||
let width_left_of_y_axis = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => {
|
||||
// The last character of the label should be below the Y-Axis when it exists, not on its left
|
||||
let y_axis_offset = if has_y_axis { 1 } else { 0 };
|
||||
first_label_width.saturating_sub(y_axis_offset)
|
||||
}
|
||||
Alignment::Center => first_label_width / 2,
|
||||
Alignment::Right => 0,
|
||||
};
|
||||
max_width = max(max_width, width_left_of_y_axis);
|
||||
}
|
||||
// labels of y axis and first label of x axis can take at most 1/3rd of the total width
|
||||
max_width.min(area.width / 3)
|
||||
}
|
||||
|
||||
fn render_x_labels(
|
||||
&mut self,
|
||||
buf: &mut Buffer,
|
||||
layout: &ChartLayout,
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) {
|
||||
let y = match layout.label_x {
|
||||
Some(y) => y,
|
||||
None => return,
|
||||
};
|
||||
let labels = self.x_axis.labels.as_ref().unwrap();
|
||||
let labels_len = labels.len() as u16;
|
||||
if labels_len < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let width_between_ticks = graph_area.width / labels_len;
|
||||
|
||||
let label_area = self.first_x_label_area(
|
||||
y,
|
||||
labels.first().unwrap().width() as u16,
|
||||
width_between_ticks,
|
||||
chart_area,
|
||||
graph_area,
|
||||
);
|
||||
|
||||
let label_alignment = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => Alignment::Right,
|
||||
Alignment::Center => Alignment::Center,
|
||||
Alignment::Right => Alignment::Left,
|
||||
};
|
||||
|
||||
Self::render_label(buf, labels.first().unwrap(), label_area, label_alignment);
|
||||
|
||||
for (i, label) in labels[1..labels.len() - 1].iter().enumerate() {
|
||||
// We add 1 to x (and width-1 below) to leave at least one space before each intermediate labels
|
||||
let x = graph_area.left() + (i + 1) as u16 * width_between_ticks + 1;
|
||||
let label_area = Rect::new(x, y, width_between_ticks.saturating_sub(1), 1);
|
||||
|
||||
Self::render_label(buf, label, label_area, Alignment::Center);
|
||||
}
|
||||
|
||||
let x = graph_area.right() - width_between_ticks;
|
||||
let label_area = Rect::new(x, y, width_between_ticks, 1);
|
||||
// The last label should be aligned Right to be at the edge of the graph area
|
||||
Self::render_label(buf, labels.last().unwrap(), label_area, Alignment::Right);
|
||||
}
|
||||
|
||||
fn first_x_label_area(
|
||||
&self,
|
||||
y: u16,
|
||||
label_width: u16,
|
||||
max_width_after_y_axis: u16,
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) -> Rect {
|
||||
let (min_x, max_x) = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => (chart_area.left(), graph_area.left()),
|
||||
Alignment::Center => (
|
||||
chart_area.left(),
|
||||
graph_area.left() + max_width_after_y_axis.min(label_width),
|
||||
),
|
||||
Alignment::Right => (
|
||||
graph_area.left().saturating_sub(1),
|
||||
graph_area.left() + max_width_after_y_axis,
|
||||
),
|
||||
};
|
||||
|
||||
Rect::new(min_x, y, max_x - min_x, 1)
|
||||
}
|
||||
|
||||
fn render_label(buf: &mut Buffer, label: &Span, label_area: Rect, alignment: Alignment) {
|
||||
let label_width = label.width() as u16;
|
||||
let bounded_label_width = label_area.width.min(label_width);
|
||||
|
||||
let x = match alignment {
|
||||
Alignment::Left => label_area.left(),
|
||||
Alignment::Center => label_area.left() + label_area.width / 2 - bounded_label_width / 2,
|
||||
Alignment::Right => label_area.right() - bounded_label_width,
|
||||
};
|
||||
|
||||
buf.set_span(x, label_area.top(), label, bounded_label_width);
|
||||
}
|
||||
|
||||
fn render_y_labels(
|
||||
&mut self,
|
||||
buf: &mut Buffer,
|
||||
layout: &ChartLayout,
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) {
|
||||
let x = match layout.label_y {
|
||||
Some(x) => x,
|
||||
None => return,
|
||||
};
|
||||
let labels = self.y_axis.labels.as_ref().unwrap();
|
||||
let labels_len = labels.len() as u16;
|
||||
for (i, label) in labels.iter().enumerate() {
|
||||
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
|
||||
if dy < graph_area.bottom() {
|
||||
let label_area = Rect::new(
|
||||
x,
|
||||
graph_area.bottom().saturating_sub(1) - dy,
|
||||
(graph_area.left() - chart_area.left()).saturating_sub(1),
|
||||
1,
|
||||
);
|
||||
Self::render_label(buf, label, label_area, self.y_axis.labels_alignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
// Sample the style of the entire widget. This sample will be used to reset the style of
|
||||
// the cells that are part of the components put on top of the grah area (i.e legend and
|
||||
// axis names).
|
||||
let original_style = buf.get(area.left(), area.top()).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,
|
||||
};
|
||||
@@ -419,50 +517,8 @@ 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 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(
|
||||
graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1)
|
||||
- label.as_ref().width() as u16,
|
||||
y,
|
||||
label.as_ref(),
|
||||
self.x_axis.labels_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(x) = layout.label_y {
|
||||
let labels = self.y_axis.labels.unwrap();
|
||||
let labels_len = labels.len() as u16;
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.render_x_labels(buf, &layout, chart_area, graph_area);
|
||||
self.render_y_labels(buf, &layout, chart_area, graph_area);
|
||||
|
||||
if let Some(y) = layout.axis_x {
|
||||
for x in graph_area.left()..graph_area.right() {
|
||||
@@ -488,16 +544,16 @@ 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 data in dataset.data.windows(2) {
|
||||
@@ -506,7 +562,7 @@ where
|
||||
y1: data[0].1,
|
||||
x2: data[1].0,
|
||||
y2: data[1].1,
|
||||
color: dataset.style.fg,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -515,6 +571,7 @@ where
|
||||
}
|
||||
|
||||
if let Some(legend_area) = layout.legend_area {
|
||||
buf.set_style(legend_area, original_style);
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.render(legend_area, buf);
|
||||
@@ -527,6 +584,36 @@ where
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_x {
|
||||
let title = self.x_axis.title.unwrap();
|
||||
let width = graph_area.right().saturating_sub(x);
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height: 1,
|
||||
},
|
||||
original_style,
|
||||
);
|
||||
buf.set_spans(x, y, &title, width);
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_y {
|
||||
let title = self.y_axis.title.unwrap();
|
||||
let width = graph_area.right().saturating_sub(x);
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height: 1,
|
||||
},
|
||||
original_style,
|
||||
);
|
||||
buf.set_spans(x, y, &title, width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,12 +630,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),
|
||||
@@ -562,11 +643,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);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::widgets::Widget;
|
||||
use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
|
||||
/// A widget to to clear/reset a certain area to allow overdrawing (e.g. for popups)
|
||||
/// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups).
|
||||
///
|
||||
/// This widget **cannot be used to clear the terminal on the first render** as `tui` assumes the
|
||||
/// render area is empty. Use [`crate::Terminal::clear`] instead.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// A widget to display a task progress.
|
||||
///
|
||||
@@ -14,15 +16,17 @@ 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>>,
|
||||
use_unicode: bool,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Default for Gauge<'a> {
|
||||
@@ -31,7 +35,9 @@ impl<'a> Default for Gauge<'a> {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
style: Default::default(),
|
||||
use_unicode: false,
|
||||
style: Style::default(),
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,15 +60,18 @@ impl<'a> Gauge<'a> {
|
||||
/// Sets ratio ([0.0, 1.0]) directly.
|
||||
pub fn ratio(mut self, ratio: f64) -> Gauge<'a> {
|
||||
assert!(
|
||||
ratio <= 1.0 && ratio >= 0.0,
|
||||
(0.0..=1.0).contains(&ratio),
|
||||
"Ratio should be between 0 and 1 inclusively."
|
||||
);
|
||||
self.ratio = ratio;
|
||||
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,49 +79,212 @@ impl<'a> Gauge<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gauge_style(mut self, style: Style) -> Gauge<'a> {
|
||||
self.gauge_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn use_unicode(mut self, unicode: bool) -> Gauge<'a> {
|
||||
self.use_unicode = unicode;
|
||||
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);
|
||||
// compute label value and its position
|
||||
// label is put at the center of the gauge_area
|
||||
let label = {
|
||||
let pct = f64::round(self.ratio * 100.0);
|
||||
self.label
|
||||
.unwrap_or_else(|| Span::from(format!("{}%", pct)))
|
||||
};
|
||||
let clamped_label_width = gauge_area.width.min(label.width() as u16);
|
||||
let label_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2;
|
||||
let label_row = gauge_area.top() + gauge_area.height / 2;
|
||||
|
||||
// the gauge will be filled proportionally to the ratio
|
||||
let filled_width = f64::from(gauge_area.width) * self.ratio;
|
||||
let end = if self.use_unicode {
|
||||
gauge_area.left() + filled_width.floor() as u16
|
||||
} else {
|
||||
gauge_area.left() + filled_width.round() as u16
|
||||
};
|
||||
for y in gauge_area.top()..gauge_area.bottom() {
|
||||
// render the filled area (left to end)
|
||||
for x in gauge_area.left()..end {
|
||||
// spaces are needed to apply the background styling
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(" ")
|
||||
.set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
|
||||
.set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
|
||||
}
|
||||
if self.use_unicode && self.ratio < 1.0 {
|
||||
buf.get_mut(end, y)
|
||||
.set_symbol(get_unicode_block(filled_width % 1.0));
|
||||
}
|
||||
}
|
||||
// set the span
|
||||
buf.set_span(label_col, label_row, &label, clamped_label_width);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_unicode_block<'a>(frac: f64) -> &'a str {
|
||||
match (frac * 8.0).round() as u16 {
|
||||
1 => symbols::block::ONE_EIGHTH,
|
||||
2 => symbols::block::ONE_QUARTER,
|
||||
3 => symbols::block::THREE_EIGHTHS,
|
||||
4 => symbols::block::HALF,
|
||||
5 => symbols::block::FIVE_EIGHTHS,
|
||||
6 => symbols::block::THREE_QUARTERS,
|
||||
7 => symbols::block::SEVEN_EIGHTHS,
|
||||
8 => symbols::block::FULL,
|
||||
_ => " ",
|
||||
}
|
||||
}
|
||||
|
||||
/// A compact widget to display a task progress over a single line.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Widget, LineGauge, Block, Borders};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # use tui::symbols;
|
||||
/// LineGauge::default()
|
||||
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
|
||||
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::BOLD))
|
||||
/// .line_set(symbols::line::THICK)
|
||||
/// .ratio(0.4);
|
||||
/// ```
|
||||
pub struct LineGauge<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
ratio: f64,
|
||||
label: Option<Spans<'a>>,
|
||||
line_set: symbols::line::Set,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Default for LineGauge<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
style: Style::default(),
|
||||
line_set: symbols::line::NORMAL,
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LineGauge<'a> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ratio(mut self, ratio: f64) -> Self {
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&ratio),
|
||||
"Ratio should be between 0 and 1 inclusively."
|
||||
);
|
||||
self.ratio = ratio;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn line_set(mut self, set: symbols::line::Set) -> Self {
|
||||
self.line_set = set;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label<T>(mut self, label: T) -> Self
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
{
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gauge_style(mut self, style: Style) -> Self {
|
||||
self.gauge_style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for LineGauge<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
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);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if gauge_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
for y in gauge_area.top()..gauge_area.bottom() {
|
||||
// Gauge
|
||||
for x in gauge_area.left()..end {
|
||||
buf.get_mut(x, y).set_symbol(" ");
|
||||
}
|
||||
let ratio = self.ratio;
|
||||
let label = self
|
||||
.label
|
||||
.unwrap_or_else(move || Spans::from(format!("{:.0}%", ratio * 100.0)));
|
||||
let (col, row) = buf.set_spans(
|
||||
gauge_area.left(),
|
||||
gauge_area.top(),
|
||||
&label,
|
||||
gauge_area.width,
|
||||
);
|
||||
let start = col + 1;
|
||||
if start >= gauge_area.right() {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Fix colors
|
||||
for x in gauge_area.left()..end {
|
||||
buf.get_mut(x, y)
|
||||
.set_fg(self.style.bg)
|
||||
.set_bg(self.style.fg);
|
||||
}
|
||||
let end = start
|
||||
+ (f64::from(gauge_area.right().saturating_sub(start)) * self.ratio).floor() as u16;
|
||||
for col in start..end {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.fg,
|
||||
bg: None,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
}
|
||||
for col in end..gauge_area.right() {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.bg,
|
||||
bg: None,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
use std::iter::{self, Iterator};
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Corner, Rect},
|
||||
style::Style,
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
};
|
||||
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)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ListState {
|
||||
offset: usize,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for ListState {
|
||||
fn default() -> ListState {
|
||||
ListState {
|
||||
offset: 0,
|
||||
selected: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListState {
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
@@ -35,112 +26,157 @@ impl ListState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
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>,
|
||||
/// Whether to repeat the highlight symbol for each line of the selected item
|
||||
repeat_highlight_symbol: bool,
|
||||
}
|
||||
|
||||
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,
|
||||
repeat_highlight_symbol: false,
|
||||
}
|
||||
}
|
||||
|
||||
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 repeat_highlight_symbol(mut self, repeat: bool) -> List<'a> {
|
||||
self.repeat_highlight_symbol = repeat;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_corner(mut self, corner: Corner) -> List<'a> {
|
||||
self.start_corner = corner;
|
||||
self
|
||||
}
|
||||
|
||||
fn get_items_bounds(
|
||||
&self,
|
||||
selected: Option<usize>,
|
||||
offset: usize,
|
||||
max_height: usize,
|
||||
) -> (usize, usize) {
|
||||
let offset = offset.min(self.items.len().saturating_sub(1));
|
||||
let mut start = offset;
|
||||
let mut end = offset;
|
||||
let mut height = 0;
|
||||
for item in self.items.iter().skip(offset) {
|
||||
if height + item.height() > max_height {
|
||||
break;
|
||||
}
|
||||
height += item.height();
|
||||
end += 1;
|
||||
}
|
||||
|
||||
let selected = selected.unwrap_or(0).min(self.items.len() - 1);
|
||||
while selected >= end {
|
||||
height = height.saturating_add(self.items[end].height());
|
||||
end += 1;
|
||||
while height > max_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 > max_height {
|
||||
end -= 1;
|
||||
height = height.saturating_sub(self.items[end].height());
|
||||
}
|
||||
}
|
||||
(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
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 +185,78 @@ 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 (start, end) = self.get_items_bounds(state.selected, state.offset, list_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 blank_symbol = " ".repeat(highlight_symbol.width());
|
||||
|
||||
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);
|
||||
for (j, line) in item.content.lines.iter().enumerate() {
|
||||
// if the item is selected, we need to display the hightlight symbol:
|
||||
// - either for the first line of the item only,
|
||||
// - or for each line of the item if the appropriate option is set
|
||||
let symbol = if is_selected && (j == 0 || self.repeat_highlight_symbol) {
|
||||
highlight_symbol
|
||||
} else {
|
||||
&blank_symbol
|
||||
};
|
||||
let (elem_x, max_element_width) = if has_selection {
|
||||
let (elem_x, _) = buf.set_stringn(
|
||||
x,
|
||||
y + j as u16,
|
||||
symbol,
|
||||
list_area.width as usize,
|
||||
item_style,
|
||||
);
|
||||
(elem_x, (list_area.width - (elem_x - x)) as u16)
|
||||
} else {
|
||||
(x, list_area.width)
|
||||
};
|
||||
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);
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
//! - [`Sparkline`]
|
||||
//! - [`Clear`]
|
||||
|
||||
use bitflags::bitflags;
|
||||
use std::borrow::Cow;
|
||||
|
||||
mod barchart;
|
||||
mod block;
|
||||
pub mod canvas;
|
||||
@@ -35,16 +32,15 @@ pub use self::barchart::BarChart;
|
||||
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::gauge::{Gauge, LineGauge};
|
||||
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::table::{Cell, 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,26 +60,10 @@ 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
|
||||
/// implement a custom widget.
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom widget.
|
||||
fn render(self, area: Rect, buf: &mut Buffer);
|
||||
}
|
||||
|
||||
@@ -107,8 +87,8 @@ pub trait Widget {
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::{Backend, TermionBackend};
|
||||
/// # use tui::widgets::{Widget, List, ListState, Text};
|
||||
/// # use tui::backend::{Backend, TestBackend};
|
||||
/// # use tui::widgets::{Widget, List, ListItem, ListState};
|
||||
///
|
||||
/// // Let's say we have some events to display.
|
||||
/// struct Events {
|
||||
@@ -174,9 +154,8 @@ pub trait Widget {
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let stdout = io::stdout();
|
||||
/// let backend = TermionBackend::new(stdout);
|
||||
/// let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// let mut events = Events::new(vec![
|
||||
/// String::from("Item 1"),
|
||||
@@ -184,10 +163,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
|
||||
|
||||
@@ -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,124 @@ 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 { trim: true, break_words: false });
|
||||
/// ```
|
||||
#[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 { trim: true, break_words: false });
|
||||
/// // 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, break_words: false });
|
||||
/// // Some indented points:
|
||||
/// // - First thing goes here
|
||||
/// // and is long so that it wraps
|
||||
/// // - Here is another point
|
||||
/// // that is long enough to wrap
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Wrap {
|
||||
/// Should leading whitespace be trimmed
|
||||
pub trim: bool,
|
||||
/// Should words at the end of lines be broken
|
||||
pub break_words: bool,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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,38 +150,54 @@ 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: self.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 line_composer: Box<dyn LineComposer> =
|
||||
if let Some(Wrap { trim, break_words }) = self.wrap {
|
||||
Box::new(WordWrapper::new(
|
||||
&mut styled,
|
||||
text_area.width,
|
||||
trim,
|
||||
break_words,
|
||||
))
|
||||
} else {
|
||||
let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
|
||||
if let Alignment::Left = self.alignment {
|
||||
line_composer.set_horizontal_offset(self.scroll.1);
|
||||
}
|
||||
line_composer
|
||||
};
|
||||
let mut y = 0;
|
||||
while let Some((current_line, current_line_width)) = line_composer.next_line() {
|
||||
if y >= self.scroll {
|
||||
if y >= self.scroll.0 {
|
||||
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)
|
||||
for StyledGrapheme { symbol, style } in current_line {
|
||||
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
|
||||
.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;
|
||||
if y >= text_area.height + self.scroll {
|
||||
if y >= text_area.height + self.scroll.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,47 @@
|
||||
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,
|
||||
break_words: 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,
|
||||
break_words: bool,
|
||||
) -> WordWrapper<'a, 'b> {
|
||||
WordWrapper {
|
||||
symbols,
|
||||
max_line_width,
|
||||
current_line: vec![],
|
||||
next_line: vec![],
|
||||
trim,
|
||||
break_words,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +51,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);
|
||||
let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -75,28 +80,30 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
||||
}
|
||||
|
||||
// Mark the previous symbol as word end.
|
||||
if symbol_whitespace && !prev_whitespace && symbol != NBSP {
|
||||
if symbol_whitespace && !prev_whitespace {
|
||||
symbols_to_last_word_end = self.current_line.len();
|
||||
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 {
|
||||
// If there was no word break in the text, wrap at the end of the line.
|
||||
let (truncate_at, truncated_width) = if symbols_to_last_word_end != 0 {
|
||||
(symbols_to_last_word_end, width_to_last_word_end)
|
||||
} else {
|
||||
(self.current_line.len() - 1, self.max_line_width)
|
||||
};
|
||||
let (truncate_at, truncated_width) =
|
||||
if symbols_to_last_word_end != 0 && !self.break_words {
|
||||
(symbols_to_last_word_end, width_to_last_word_end)
|
||||
} else {
|
||||
(self.current_line.len() - 1, self.max_line_width)
|
||||
};
|
||||
|
||||
// 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 +128,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 +164,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 +184,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 +217,43 @@ 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, break_words: 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, break_words } => Box::new(WordWrapper::new(
|
||||
&mut styled,
|
||||
text_area_width,
|
||||
trim,
|
||||
break_words,
|
||||
)),
|
||||
Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
|
||||
};
|
||||
let mut lines = vec![];
|
||||
@@ -211,7 +261,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 +275,14 @@ 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,
|
||||
break_words: 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 +295,14 @@ 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,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let wrapped: Vec<&str> = text.split('\n').collect();
|
||||
@@ -250,7 +314,14 @@ 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,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width as u16,
|
||||
);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let wrapped = vec![
|
||||
@@ -261,7 +332,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]]);
|
||||
@@ -275,10 +346,22 @@ mod test {
|
||||
let text_multi_space =
|
||||
"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);
|
||||
let (word_wrapper_single_space, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width as u16,
|
||||
);
|
||||
let (word_wrapper_multi_space, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text_multi_space,
|
||||
width as u16,
|
||||
);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let word_wrapped = vec![
|
||||
@@ -298,7 +381,14 @@ 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,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = Vec::new();
|
||||
@@ -310,7 +400,14 @@ 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,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
|
||||
@@ -325,7 +422,14 @@ 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,
|
||||
break_words: 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 +440,14 @@ 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,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
@@ -354,8 +465,15 @@ mod test {
|
||||
let width = 20;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
|
||||
では、";
|
||||
let (word_wrapper, word_wrapper_width) = run_composer(Composer::WordWrapper, &text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
|
||||
let (word_wrapper, word_wrapper_width) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
|
||||
let wrapped = vec![
|
||||
"コンピュータ上で文字",
|
||||
@@ -372,7 +490,14 @@ 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,
|
||||
break_words: 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 +508,14 @@ 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,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec![""]);
|
||||
assert_eq!(line_truncator, vec![" "]);
|
||||
@@ -395,7 +527,14 @@ 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,
|
||||
break_words: 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 +553,14 @@ 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,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
@@ -435,12 +581,84 @@ 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,
|
||||
break_words: 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 text_space = text.replace('\u{00a0}', " ");
|
||||
let (word_wrapper_space, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: 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,
|
||||
break_words: true,
|
||||
},
|
||||
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,
|
||||
break_words: true,
|
||||
},
|
||||
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,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
" ",
|
||||
" 4",
|
||||
"Indent",
|
||||
" ",
|
||||
" must",
|
||||
"wrap!"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,36 +1,346 @@
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::Style,
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
};
|
||||
use cassowary::{
|
||||
strength::{MEDIUM, REQUIRED, WEAK},
|
||||
WeightedRelation::*,
|
||||
{Expression, Solver},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
iter::{self, Iterator},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
|
||||
///
|
||||
/// It can be created from anything that can be converted to a [`Text`].
|
||||
/// ```rust
|
||||
/// # use tui::widgets::Cell;
|
||||
/// # use tui::style::{Style, Modifier};
|
||||
/// # use tui::text::{Span, Spans, Text};
|
||||
/// # use std::borrow::Cow;
|
||||
/// Cell::from("simple string");
|
||||
///
|
||||
/// Cell::from(Span::from("span"));
|
||||
///
|
||||
/// Cell::from(Spans::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
|
||||
/// ]));
|
||||
///
|
||||
/// Cell::from(Text::from("a text"));
|
||||
///
|
||||
/// Cell::from(Text::from(Cow::Borrowed("hello")));
|
||||
/// ```
|
||||
///
|
||||
/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
|
||||
/// capabilities of [`Text`].
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Cell<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Cell<'a> {
|
||||
/// Set the `Style` of this cell.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Cell<'a>
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
fn from(content: T) -> Cell<'a> {
|
||||
Cell {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds data to be displayed in a [`Table`] widget.
|
||||
///
|
||||
/// A [`Row`] is a collection of cells. It can be created from simple strings:
|
||||
/// ```rust
|
||||
/// # use tui::widgets::Row;
|
||||
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
|
||||
/// ```
|
||||
///
|
||||
/// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s:
|
||||
/// ```rust
|
||||
/// # use tui::widgets::{Row, Cell};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// You can also construct a row from any type that can be converted into [`Text`]:
|
||||
/// ```rust
|
||||
/// # use std::borrow::Cow;
|
||||
/// # use tui::widgets::Row;
|
||||
/// Row::new(vec![
|
||||
/// Cow::Borrowed("hello"),
|
||||
/// Cow::Owned("world".to_uppercase()),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Row<'a> {
|
||||
cells: Vec<Cell<'a>>,
|
||||
height: u16,
|
||||
style: Style,
|
||||
bottom_margin: u16,
|
||||
}
|
||||
|
||||
impl<'a> Row<'a> {
|
||||
/// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
|
||||
pub fn new<T>(cells: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<Cell<'a>>,
|
||||
{
|
||||
Self {
|
||||
height: 1,
|
||||
cells: cells.into_iter().map(|c| c.into()).collect(),
|
||||
style: Style::default(),
|
||||
bottom_margin: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
|
||||
/// height will see its content truncated.
|
||||
pub fn height(mut self, height: u16) -> Self {
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a
|
||||
/// any individual [`Cell`] or event by their [`Text`] content.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bottom margin. By default, the bottom margin is `0`.
|
||||
pub fn bottom_margin(mut self, margin: u16) -> Self {
|
||||
self.bottom_margin = margin;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the total height of the row.
|
||||
fn total_height(&self) -> u16 {
|
||||
self.height.saturating_add(self.bottom_margin)
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to display data in formatted columns.
|
||||
///
|
||||
/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
|
||||
/// ```rust
|
||||
/// # use tui::widgets::{Block, Borders, Table, Row, Cell};
|
||||
/// # use tui::layout::Constraint;
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # use tui::text::{Text, Spans, Span};
|
||||
/// Table::new(vec![
|
||||
/// // Row can be created from simple strings.
|
||||
/// Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
/// // You can style the entire row.
|
||||
/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
|
||||
/// // If you need more control over the styling you may need to create Cells directly
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Row31"),
|
||||
/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
|
||||
/// Cell::from(Spans::from(vec![
|
||||
/// Span::raw("Row"),
|
||||
/// Span::styled("33", Style::default().fg(Color::Green))
|
||||
/// ])),
|
||||
/// ]),
|
||||
/// // If a Row need to display some content over multiple lines, you just have to change
|
||||
/// // its height.
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Row\n41"),
|
||||
/// Cell::from("Row\n42"),
|
||||
/// Cell::from("Row\n43"),
|
||||
/// ]).height(2),
|
||||
/// ])
|
||||
/// // You can set the style of the entire Table.
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// // It has an optional header, which is simply a Row always visible at the top.
|
||||
/// .header(
|
||||
/// Row::new(vec!["Col1", "Col2", "Col3"])
|
||||
/// .style(Style::default().fg(Color::Yellow))
|
||||
/// // If you want some space between the header and the rest of the rows, you can always
|
||||
/// // specify some margin at the bottom.
|
||||
/// .bottom_margin(1)
|
||||
/// )
|
||||
/// // As any other widget, a Table can be wrapped in a Block.
|
||||
/// .block(Block::default().title("Table"))
|
||||
/// // Columns widths are constrained in the same way as Layout...
|
||||
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
|
||||
/// // ...and they can be separated by a fixed spacing.
|
||||
/// .column_spacing(1)
|
||||
/// // If you wish to highlight a row in any specific way when it is selected...
|
||||
/// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
/// // ...and potentially show a symbol in front of the selection.
|
||||
/// .highlight_symbol(">>");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Table<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Base style for the widget
|
||||
style: Style,
|
||||
/// Width constraints for each column
|
||||
widths: &'a [Constraint],
|
||||
/// Space between each column
|
||||
column_spacing: u16,
|
||||
/// Style used to render the selected row
|
||||
highlight_style: Style,
|
||||
/// Symbol in front of the selected rom
|
||||
highlight_symbol: Option<&'a str>,
|
||||
/// Optional header
|
||||
header: Option<Row<'a>>,
|
||||
/// Data to display in each row
|
||||
rows: Vec<Row<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Table<'a> {
|
||||
pub fn new<T>(rows: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = Row<'a>>,
|
||||
{
|
||||
Self {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
header: None,
|
||||
rows: rows.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header(mut self, header: Row<'a>) -> Self {
|
||||
self.header = Some(header);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
|
||||
let between_0_and_100 = |&w| match w {
|
||||
Constraint::Percentage(p) => p <= 100,
|
||||
_ => true,
|
||||
};
|
||||
assert!(
|
||||
widths.iter().all(between_0_and_100),
|
||||
"Percentages should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
|
||||
self.highlight_symbol = Some(highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, highlight_style: Style) -> Self {
|
||||
self.highlight_style = highlight_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Self {
|
||||
self.column_spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
|
||||
let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
|
||||
if has_selection {
|
||||
let highlight_symbol_width =
|
||||
self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
|
||||
constraints.push(Constraint::Length(highlight_symbol_width));
|
||||
}
|
||||
for constraint in self.widths {
|
||||
constraints.push(*constraint);
|
||||
constraints.push(Constraint::Length(self.column_spacing));
|
||||
}
|
||||
if !self.widths.is_empty() {
|
||||
constraints.pop();
|
||||
}
|
||||
let mut chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(constraints)
|
||||
.expand_to_fill(false)
|
||||
.split(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: max_width,
|
||||
height: 1,
|
||||
});
|
||||
if has_selection {
|
||||
chunks.remove(0);
|
||||
}
|
||||
chunks.iter().step_by(2).map(|c| c.width).collect()
|
||||
}
|
||||
|
||||
fn get_row_bounds(
|
||||
&self,
|
||||
selected: Option<usize>,
|
||||
offset: usize,
|
||||
max_height: u16,
|
||||
) -> (usize, usize) {
|
||||
let offset = offset.min(self.rows.len().saturating_sub(1));
|
||||
let mut start = offset;
|
||||
let mut end = offset;
|
||||
let mut height = 0;
|
||||
for item in self.rows.iter().skip(offset) {
|
||||
if height + item.height > max_height {
|
||||
break;
|
||||
}
|
||||
height += item.total_height();
|
||||
end += 1;
|
||||
}
|
||||
|
||||
let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
|
||||
while selected >= end {
|
||||
height = height.saturating_add(self.rows[end].total_height());
|
||||
end += 1;
|
||||
while height > max_height {
|
||||
height = height.saturating_sub(self.rows[start].total_height());
|
||||
start += 1;
|
||||
}
|
||||
}
|
||||
while selected < start {
|
||||
start -= 1;
|
||||
height = height.saturating_add(self.rows[start].total_height());
|
||||
while height > max_height {
|
||||
end -= 1;
|
||||
height = height.saturating_sub(self.rows[end].total_height());
|
||||
}
|
||||
}
|
||||
(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TableState {
|
||||
offset: usize,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for TableState {
|
||||
fn default() -> TableState {
|
||||
TableState {
|
||||
offset: 0,
|
||||
selected: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
@@ -44,311 +354,130 @@ impl TableState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds data to be displayed in a Table widget
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Row<D>
|
||||
where
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
{
|
||||
Data(D),
|
||||
StyledData(D, Style),
|
||||
}
|
||||
|
||||
/// A widget to display data in formatted columns
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, Borders, Table, Row};
|
||||
/// # use tui::layout::Constraint;
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// let row_style = Style::default().fg(Color::White);
|
||||
/// Table::new(
|
||||
/// ["Col1", "Col2", "Col3"].into_iter(),
|
||||
/// vec![
|
||||
/// Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), row_style),
|
||||
/// Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), row_style),
|
||||
/// Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), row_style),
|
||||
/// Row::Data(["Row41", "Row42", "Row43"].into_iter())
|
||||
/// ].into_iter()
|
||||
/// )
|
||||
/// .block(Block::default().title("Table"))
|
||||
/// .header_style(Style::default().fg(Color::Yellow))
|
||||
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .column_spacing(1);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Table<'a, H, R> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Base style for the widget
|
||||
style: Style,
|
||||
/// Header row for all columns
|
||||
header: H,
|
||||
/// Style for the header
|
||||
header_style: Style,
|
||||
/// Width constraints for each column
|
||||
widths: &'a [Constraint],
|
||||
/// Space between each column
|
||||
column_spacing: u16,
|
||||
/// Space between the header and the rows
|
||||
header_gap: u16,
|
||||
/// Style used to render the selected row
|
||||
highlight_style: Style,
|
||||
/// Symbol in front of the selected rom
|
||||
highlight_symbol: Option<&'a str>,
|
||||
/// Data to display in each row
|
||||
rows: R,
|
||||
}
|
||||
|
||||
impl<'a, H, R> Default for Table<'a, H, R>
|
||||
where
|
||||
H: Iterator + Default,
|
||||
R: Iterator + Default,
|
||||
{
|
||||
fn default() -> Table<'a, H, R> {
|
||||
Table {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
header: H::default(),
|
||||
header_style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
header_gap: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
rows: R::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a, H, D, R> Table<'a, H, R>
|
||||
where
|
||||
H: Iterator,
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
R: Iterator<Item = Row<D>>,
|
||||
{
|
||||
pub fn new(header: H, rows: R) -> Table<'a, H, R> {
|
||||
Table {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
header,
|
||||
header_style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
header_gap: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
rows,
|
||||
}
|
||||
}
|
||||
pub fn block(mut self, block: Block<'a>) -> Table<'a, H, R> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header<II>(mut self, header: II) -> Table<'a, H, R>
|
||||
where
|
||||
II: IntoIterator<Item = H::Item, IntoIter = H>,
|
||||
{
|
||||
self.header = header.into_iter();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header_style(mut self, style: Style) -> Table<'a, H, R> {
|
||||
self.header_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, H, R> {
|
||||
let between_0_and_100 = |&w| match w {
|
||||
Constraint::Percentage(p) => p <= 100,
|
||||
_ => true,
|
||||
};
|
||||
assert!(
|
||||
widths.iter().all(between_0_and_100),
|
||||
"Percentages should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rows<II>(mut self, rows: II) -> Table<'a, H, R>
|
||||
where
|
||||
II: IntoIterator<Item = Row<D>, IntoIter = R>,
|
||||
{
|
||||
self.rows = rows.into_iter();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Table<'a, H, R> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Table<'a, H, R> {
|
||||
self.highlight_symbol = Some(highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, highlight_style: Style) -> Table<'a, H, R> {
|
||||
self.highlight_style = highlight_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Table<'a, H, R> {
|
||||
self.column_spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header_gap(mut self, gap: u16) -> Table<'a, H, R> {
|
||||
self.header_gap = gap;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, H, D, R> StatefulWidget for Table<'a, H, R>
|
||||
where
|
||||
H: Iterator,
|
||||
H::Item: Display,
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
R: Iterator<Item = Row<D>>,
|
||||
{
|
||||
impl<'a> StatefulWidget for Table<'a> {
|
||||
type State = TableState;
|
||||
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
// Render block if necessary and get the drawing area
|
||||
let table_area = match self.block {
|
||||
Some(ref mut b) => {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
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();
|
||||
let mut variables = Vec::new();
|
||||
for i in 0..self.widths.len() {
|
||||
let var = cassowary::Variable::new();
|
||||
variables.push(var);
|
||||
var_indices.insert(var, i);
|
||||
}
|
||||
for (i, constraint) in self.widths.iter().enumerate() {
|
||||
ccs.push(variables[i] | GE(WEAK) | 0.);
|
||||
ccs.push(match *constraint {
|
||||
Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => {
|
||||
variables[i] | EQ(WEAK) | (f64::from(v * area.width) / 100.0)
|
||||
}
|
||||
Constraint::Ratio(n, d) => {
|
||||
variables[i] | EQ(WEAK) | (f64::from(area.width) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
|
||||
Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
|
||||
})
|
||||
}
|
||||
solver
|
||||
.add_constraint(
|
||||
variables
|
||||
.iter()
|
||||
.fold(Expression::from_constant(0.), |acc, v| acc + *v)
|
||||
| LE(REQUIRED)
|
||||
| f64::from(
|
||||
area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1)),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
solver.add_constraints(&ccs).unwrap();
|
||||
let mut solved_widths = vec![0; variables.len()];
|
||||
for &(var, value) in solver.fetch_changes() {
|
||||
let index = var_indices[&var];
|
||||
let value = if value.is_sign_negative() {
|
||||
0
|
||||
} else {
|
||||
value as u16
|
||||
};
|
||||
solved_widths[index] = value
|
||||
}
|
||||
|
||||
let mut y = table_area.top();
|
||||
let mut x = table_area.left();
|
||||
let has_selection = state.selected.is_some();
|
||||
let columns_widths = self.get_columns_widths(table_area.width, has_selection);
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
let blank_symbol = " ".repeat(highlight_symbol.width());
|
||||
let mut current_height = 0;
|
||||
let mut rows_height = table_area.height;
|
||||
|
||||
// Draw header
|
||||
if y < table_area.bottom() {
|
||||
for (w, t) in solved_widths.iter().zip(self.header.by_ref()) {
|
||||
buf.set_stringn(x, y, format!("{}", t), *w as usize, self.header_style);
|
||||
x += *w + self.column_spacing;
|
||||
if let Some(ref header) = self.header {
|
||||
let max_header_height = table_area.height.min(header.total_height());
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x: table_area.left(),
|
||||
y: table_area.top(),
|
||||
width: table_area.width,
|
||||
height: table_area.height.min(header.height),
|
||||
},
|
||||
header.style,
|
||||
);
|
||||
let mut col = table_area.left();
|
||||
if has_selection {
|
||||
col += (highlight_symbol.width() as u16).min(table_area.width);
|
||||
}
|
||||
for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
|
||||
render_cell(
|
||||
buf,
|
||||
cell,
|
||||
Rect {
|
||||
x: col,
|
||||
y: table_area.top(),
|
||||
width: *width,
|
||||
height: max_header_height,
|
||||
},
|
||||
);
|
||||
col += *width + self.column_spacing;
|
||||
}
|
||||
current_height += max_header_height;
|
||||
rows_height = rows_height.saturating_sub(max_header_height);
|
||||
}
|
||||
y += 1 + self.header_gap;
|
||||
|
||||
// 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>();
|
||||
|
||||
// Draw rows
|
||||
let default_style = Style::default();
|
||||
if y < table_area.bottom() {
|
||||
let remaining = (table_area.bottom() - y) as usize;
|
||||
|
||||
// Make sure the table shows the selected item
|
||||
state.offset = if let Some(selected) = selected {
|
||||
if selected >= remaining + state.offset - 1 {
|
||||
selected + 1 - remaining
|
||||
} else if selected < state.offset {
|
||||
selected
|
||||
} else {
|
||||
state.offset
|
||||
}
|
||||
} else {
|
||||
0
|
||||
if self.rows.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
|
||||
state.offset = start;
|
||||
for (i, table_row) in self
|
||||
.rows
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.skip(state.offset)
|
||||
.take(end - start)
|
||||
{
|
||||
let (row, col) = (table_area.top() + current_height, table_area.left());
|
||||
current_height += table_row.total_height();
|
||||
let table_row_area = Rect {
|
||||
x: col,
|
||||
y: row,
|
||||
width: table_area.width,
|
||||
height: table_row.height,
|
||||
};
|
||||
for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() {
|
||||
let (data, style, symbol) = match row {
|
||||
Row::Data(d) | Row::StyledData(d, _)
|
||||
if Some(i) == state.selected.map(|s| s - state.offset) =>
|
||||
{
|
||||
(d, highlight_style, highlight_symbol)
|
||||
}
|
||||
Row::Data(d) => (d, default_style, blank_symbol.as_ref()),
|
||||
Row::StyledData(d, s) => (d, s, blank_symbol.as_ref()),
|
||||
buf.set_style(table_row_area, table_row.style);
|
||||
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
|
||||
let table_row_start_col = if has_selection {
|
||||
let symbol = if is_selected {
|
||||
highlight_symbol
|
||||
} else {
|
||||
&blank_symbol
|
||||
};
|
||||
x = table_area.left();
|
||||
for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() {
|
||||
let s = if c == 0 {
|
||||
format!("{}{}", symbol, elt)
|
||||
} else {
|
||||
format!("{}", elt)
|
||||
};
|
||||
buf.set_stringn(x, y + i as u16, s, *w as usize, style);
|
||||
x += *w + self.column_spacing;
|
||||
}
|
||||
let (col, _) =
|
||||
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
|
||||
col
|
||||
} else {
|
||||
col
|
||||
};
|
||||
let mut col = table_row_start_col;
|
||||
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
|
||||
render_cell(
|
||||
buf,
|
||||
cell,
|
||||
Rect {
|
||||
x: col,
|
||||
y: row,
|
||||
width: *width,
|
||||
height: table_row.height,
|
||||
},
|
||||
);
|
||||
col += *width + self.column_spacing;
|
||||
}
|
||||
if is_selected {
|
||||
buf.set_style(table_row_area, self.highlight_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, H, D, R> Widget for Table<'a, H, R>
|
||||
where
|
||||
H: Iterator,
|
||||
H::Item: Display,
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
R: Iterator<Item = Row<D>>,
|
||||
{
|
||||
fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
|
||||
buf.set_style(area, cell.style);
|
||||
for (i, spans) in cell.content.lines.iter().enumerate() {
|
||||
if i as u16 >= area.height {
|
||||
break;
|
||||
}
|
||||
buf.set_spans(area.x, area.y + i as u16, spans, area.width);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Table<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default();
|
||||
StatefulWidget::render(self, area, buf, &mut state);
|
||||
@@ -362,7 +491,6 @@ mod tests {
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn table_invalid_percentages() {
|
||||
Table::new([""].iter(), vec![Row::Data([""].iter())].into_iter())
|
||||
.widths(&[Constraint::Percentage(110)]);
|
||||
Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
63
tests/backend_termion.rs
Normal 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('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('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('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(())
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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;
|
||||
|
||||
#[test]
|
||||
fn it_draws_a_block() {
|
||||
let backend = TestBackend::new(10, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let block = Block::default()
|
||||
.title("Title")
|
||||
.borders(Borders::ALL)
|
||||
.title_style(Style::default().fg(Color::LightBlue));
|
||||
f.render_widget(
|
||||
block,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 8,
|
||||
height: 8,
|
||||
},
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
for x in 1..=5 {
|
||||
expected.get_mut(x, 0).set_fg(Color::LightBlue);
|
||||
}
|
||||
assert_eq!(&expected, terminal.backend().buffer());
|
||||
}
|
||||
104
tests/chart.rs
104
tests/chart.rs
@@ -1,104 +0,0 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType::Line},
|
||||
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(&[
|
||||
(1_588_298_471.0, 1.0),
|
||||
(1_588_298_473.0, 0.0),
|
||||
(1_588_298_496.0, 1.0),
|
||||
])];
|
||||
let chart = Chart::default()
|
||||
.block(Block::default().title("Plot").borders(Borders::ALL))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.bounds([1_588_298_471.0, 1_588_992_600.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();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_dataset_line_test() {
|
||||
let backend = TestBackend::new(100, 100);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let datasets = [Dataset::default().data(&[]).graph_type(Line)];
|
||||
let chart = Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Empty Dataset With Line")
|
||||
.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();
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
use tui::backend::TestBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::widgets::{Block, Borders, Gauge};
|
||||
use tui::Terminal;
|
||||
|
||||
#[test]
|
||||
fn gauge_render() {
|
||||
let backend = TestBackend::new(40, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Percentage").borders(Borders::ALL))
|
||||
.percent(43);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Ratio").borders(Borders::ALL))
|
||||
.ratio(0.211_313_934_313_1);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ┌Percentage────────────────────────┐ ",
|
||||
" │ 43% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ┌Ratio─────────────────────────────┐ ",
|
||||
" │ 21% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(&expected, terminal.backend().buffer());
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
widgets::{Block, Borders, List, ListState, Text},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn it_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| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
Text::raw("Item 1"),
|
||||
Text::raw("Item 2"),
|
||||
Text::raw("Item 3"),
|
||||
];
|
||||
let list = List::new(items.into_iter())
|
||||
.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 {
|
||||
expected.get_mut(x, 1).set_bg(Color::Yellow);
|
||||
}
|
||||
assert_eq!(*terminal.backend().buffer(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_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>>,
|
||||
expected: Buffer,
|
||||
}
|
||||
|
||||
let cases = vec![
|
||||
TruncateTestCase {
|
||||
name: "an item is selected",
|
||||
selected: Some(0),
|
||||
items: vec![Text::raw("A very long line"), Text::raw("A very long line")],
|
||||
expected: Buffer::with_lines(vec![
|
||||
format!(">> A ve{} ", symbols::line::VERTICAL),
|
||||
format!(" A ve{} ", symbols::line::VERTICAL),
|
||||
]),
|
||||
},
|
||||
TruncateTestCase {
|
||||
name: "no item is selected",
|
||||
selected: None,
|
||||
items: vec![Text::raw("A very long line"), Text::raw("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 {
|
||||
let mut state = ListState::default();
|
||||
state.select(case.selected);
|
||||
let items = case.items.drain(..);
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let list = List::new(items)
|
||||
.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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
use tui::backend::TestBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Alignment;
|
||||
use tui::widgets::{Block, Borders, Paragraph, Text};
|
||||
use tui::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 \
|
||||
supposed to be part of the UI. While providing a great flexibility for rich and \
|
||||
interactive UI, this may introduce overhead for highly dynamic content.";
|
||||
|
||||
#[test]
|
||||
fn paragraph_render_wrap() {
|
||||
let render = |alignment| {
|
||||
let backend = TestBackend::new(20, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let text = [Text::raw(SAMPLE_STRING)];
|
||||
let paragraph = Paragraph::new(text.iter())
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.alignment(alignment)
|
||||
.wrap(true);
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().buffer().clone()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
render(Alignment::Left),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│The library is │",
|
||||
"│based on the │",
|
||||
"│principle of │",
|
||||
"│immediate │",
|
||||
"│rendering with │",
|
||||
"│intermediate │",
|
||||
"│buffers. This │",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
render(Alignment::Right),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ The library is│",
|
||||
"│ based on the│",
|
||||
"│ principle of│",
|
||||
"│ immediate│",
|
||||
"│ rendering with│",
|
||||
"│ intermediate│",
|
||||
"│ buffers. This│",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
render(Alignment::Center),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ The library is │",
|
||||
"│ based on the │",
|
||||
"│ principle of │",
|
||||
"│ immediate │",
|
||||
"│ rendering with │",
|
||||
"│ intermediate │",
|
||||
"│ buffers. This │",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_render_double_width() {
|
||||
let backend = TestBackend::new(10, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、";
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let text = [Text::raw(s)];
|
||||
let paragraph = Paragraph::new(text.iter())
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(true);
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"┌────────┐",
|
||||
"│コンピュ│",
|
||||
"│ータ上で│",
|
||||
"│文字を扱│",
|
||||
"│う場合、│",
|
||||
"│典型的に│",
|
||||
"│は文字に│",
|
||||
"│よる通信│",
|
||||
"│を行う場│",
|
||||
"└────────┘",
|
||||
]);
|
||||
assert_eq!(&expected, terminal.backend().buffer());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_render_mixed_width() {
|
||||
let backend = TestBackend::new(10, 7);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let s = "aコンピュータ上で文字を扱う場合、";
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let text = [Text::raw(s)];
|
||||
let paragraph = Paragraph::new(text.iter())
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(true);
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
// The internal width is 8 so only 4 slots for double-width characters.
|
||||
"┌────────┐",
|
||||
"│aコンピ │", // Here we have 1 latin character so only 3 double-width ones can fit.
|
||||
"│ュータ上│",
|
||||
"│で文字を│",
|
||||
"│扱う場合│",
|
||||
"│、 │",
|
||||
"└────────┘",
|
||||
]);
|
||||
assert_eq!(&expected, terminal.backend().buffer());
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
use tui::backend::{Backend, TestBackend};
|
||||
use tui::Terminal;
|
||||
|
||||
#[test]
|
||||
fn buffer_size_limited() {
|
||||
let backend = TestBackend::new(400, 400);
|
||||
let terminal = Terminal::new(backend).unwrap();
|
||||
let size = terminal.backend().size().unwrap();
|
||||
assert_eq!(size.width, 255);
|
||||
assert_eq!(size.height, 255);
|
||||
}
|
||||
418
tests/table.rs
418
tests/table.rs
@@ -1,418 +0,0 @@
|
||||
use tui::backend::TestBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Constraint;
|
||||
use tui::widgets::{Block, Borders, Row, Table};
|
||||
use tui::Terminal;
|
||||
|
||||
#[test]
|
||||
fn table_column_spacing() {
|
||||
let render = |column_spacing| {
|
||||
let backend = TestBackend::new(30, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(
|
||||
["Head1", "Head2", "Head3"].iter(),
|
||||
vec![
|
||||
Row::Data(["Row11", "Row12", "Row13"].iter()),
|
||||
Row::Data(["Row21", "Row22", "Row23"].iter()),
|
||||
Row::Data(["Row31", "Row32", "Row33"].iter()),
|
||||
Row::Data(["Row41", "Row42", "Row43"].iter()),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.column_spacing(column_spacing);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().buffer().clone()
|
||||
};
|
||||
|
||||
// no space between columns
|
||||
assert_eq!(
|
||||
render(0),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1Head2Head3 │",
|
||||
"│ │",
|
||||
"│Row11Row12Row13 │",
|
||||
"│Row21Row22Row23 │",
|
||||
"│Row31Row32Row33 │",
|
||||
"│Row41Row42Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
|
||||
// one space between columns
|
||||
assert_eq!(
|
||||
render(1),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
|
||||
// enough space to just not hide the third column
|
||||
assert_eq!(
|
||||
render(6),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
|
||||
// enough space to hide part of the third column
|
||||
assert_eq!(
|
||||
render(7),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head│",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row1│",
|
||||
"│Row21 Row22 Row2│",
|
||||
"│Row31 Row32 Row3│",
|
||||
"│Row41 Row42 Row4│",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_widths() {
|
||||
let render = |widths| {
|
||||
let backend = TestBackend::new(30, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(
|
||||
["Head1", "Head2", "Head3"].iter(),
|
||||
vec![
|
||||
Row::Data(["Row11", "Row12", "Row13"].iter()),
|
||||
Row::Data(["Row21", "Row22", "Row23"].iter()),
|
||||
Row::Data(["Row31", "Row32", "Row33"].iter()),
|
||||
Row::Data(["Row41", "Row42", "Row43"].iter()),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().buffer().clone()
|
||||
};
|
||||
|
||||
// columns of zero width show nothing
|
||||
assert_eq!(
|
||||
render(&[
|
||||
Constraint::Length(0),
|
||||
Constraint::Length(0),
|
||||
Constraint::Length(0)
|
||||
]),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
|
||||
// columns of 1 width trim
|
||||
assert_eq!(
|
||||
render(&[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1)
|
||||
]),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│H H H │",
|
||||
"│ │",
|
||||
"│R R R │",
|
||||
"│R R R │",
|
||||
"│R R R │",
|
||||
"│R R R │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
|
||||
// columns of large width just before pushing a column off
|
||||
assert_eq!(
|
||||
render(&[
|
||||
Constraint::Length(8),
|
||||
Constraint::Length(8),
|
||||
Constraint::Length(8)
|
||||
]),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_percentage_widths() {
|
||||
let render = |widths| {
|
||||
let backend = TestBackend::new(30, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(
|
||||
["Head1", "Head2", "Head3"].iter(),
|
||||
vec![
|
||||
Row::Data(["Row11", "Row12", "Row13"].iter()),
|
||||
Row::Data(["Row21", "Row22", "Row23"].iter()),
|
||||
Row::Data(["Row31", "Row32", "Row33"].iter()),
|
||||
Row::Data(["Row41", "Row42", "Row43"].iter()),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths)
|
||||
.column_spacing(0);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().buffer().clone()
|
||||
};
|
||||
|
||||
// columns of zero width show nothing
|
||||
assert_eq!(
|
||||
render(&[
|
||||
Constraint::Percentage(0),
|
||||
Constraint::Percentage(0),
|
||||
Constraint::Percentage(0)
|
||||
]),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
|
||||
// columns of not enough width trims the data
|
||||
assert_eq!(
|
||||
render(&[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(10)
|
||||
]),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│HeaHeaHea │",
|
||||
"│ │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
|
||||
// columns of large width just before pushing a column off
|
||||
assert_eq!(
|
||||
render(&[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30)
|
||||
]),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
|
||||
// percentages summing to 100 should give equal widths
|
||||
assert_eq!(
|
||||
render(&[Constraint::Percentage(50), Constraint::Percentage(50)]),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_mixed_widths() {
|
||||
let render = |widths| {
|
||||
let backend = TestBackend::new(30, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(
|
||||
["Head1", "Head2", "Head3"].iter(),
|
||||
vec![
|
||||
Row::Data(["Row11", "Row12", "Row13"].iter()),
|
||||
Row::Data(["Row21", "Row22", "Row23"].iter()),
|
||||
Row::Data(["Row31", "Row32", "Row33"].iter()),
|
||||
Row::Data(["Row41", "Row42", "Row43"].iter()),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().buffer().clone()
|
||||
};
|
||||
|
||||
// columns of zero width show nothing
|
||||
assert_eq!(
|
||||
render(&[
|
||||
Constraint::Percentage(0),
|
||||
Constraint::Length(0),
|
||||
Constraint::Percentage(0)
|
||||
]),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
|
||||
// columns of not enough width trims the data
|
||||
assert_eq!(
|
||||
render(&[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Length(20),
|
||||
Constraint::Percentage(10)
|
||||
]),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Hea Head2 Hea│",
|
||||
"│ │",
|
||||
"│Row Row12 Row│",
|
||||
"│Row Row22 Row│",
|
||||
"│Row Row32 Row│",
|
||||
"│Row Row42 Row│",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
|
||||
// columns of large width just before pushing a column off
|
||||
assert_eq!(
|
||||
render(&[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Length(10),
|
||||
Constraint::Percentage(30)
|
||||
]),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
|
||||
// columns of large size (>100% total) hide the last column
|
||||
assert_eq!(
|
||||
render(&[
|
||||
Constraint::Percentage(60),
|
||||
Constraint::Length(10),
|
||||
Constraint::Percentage(60)
|
||||
]),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
}
|
||||
36
tests/terminal.rs
Normal file
36
tests/terminal.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use std::error::Error;
|
||||
use tui::{
|
||||
backend::{Backend, TestBackend},
|
||||
layout::Rect,
|
||||
widgets::Paragraph,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
#[test]
|
||||
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();
|
||||
assert_eq!(size.width, 255);
|
||||
assert_eq!(size.height, 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_draw_returns_the_completed_frame() -> Result<(), Box<dyn Error>> {
|
||||
let backend = TestBackend::new(10, 10);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
let frame = terminal.draw(|f| {
|
||||
let paragrah = Paragraph::new("Test");
|
||||
f.render_widget(paragrah, f.size());
|
||||
})?;
|
||||
assert_eq!(frame.buffer.get(0, 0).symbol, "T");
|
||||
assert_eq!(frame.area, Rect::new(0, 0, 10, 10));
|
||||
terminal.backend_mut().resize(8, 8);
|
||||
let frame = terminal.draw(|f| {
|
||||
let paragrah = Paragraph::new("test");
|
||||
f.render_widget(paragrah, f.size());
|
||||
})?;
|
||||
assert_eq!(frame.buffer.get(0, 0).symbol, "t");
|
||||
assert_eq!(frame.area, Rect::new(0, 0, 8, 8));
|
||||
Ok(())
|
||||
}
|
||||
40
tests/widgets_barchart.rs
Normal file
40
tests/widgets_barchart.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use tui::backend::TestBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::widgets::{BarChart, Block, Borders};
|
||||
use tui::Terminal;
|
||||
|
||||
#[test]
|
||||
fn widgets_barchart_not_full_below_max_value() {
|
||||
let test_case = |expected| {
|
||||
let backend = TestBackend::new(30, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.data(&[("empty", 0), ("half", 50), ("almost", 99), ("full", 100)])
|
||||
.max(100)
|
||||
.bar_width(7)
|
||||
.bar_gap(0);
|
||||
f.render_widget(barchart, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
// check that bars fill up correctly up to max value
|
||||
test_case(Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ ▇▇▇▇▇▇▇███████│",
|
||||
"│ ██████████████│",
|
||||
"│ ██████████████│",
|
||||
"│ ▄▄▄▄▄▄▄██████████████│",
|
||||
"│ █████████████████████│",
|
||||
"│ █████████████████████│",
|
||||
"│ ██50█████99█████100██│",
|
||||
"│empty half almost full │",
|
||||
"└────────────────────────────┘",
|
||||
]));
|
||||
}
|
||||
346
tests/widgets_block.rs
Normal file
346
tests/widgets_block.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn widgets_block_renders() {
|
||||
let backend = TestBackend::new(10, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let block = Block::default()
|
||||
.title(Span::styled("Title", Style::default().fg(Color::LightBlue)))
|
||||
.borders(Borders::ALL);
|
||||
f.render_widget(
|
||||
block,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 8,
|
||||
height: 8,
|
||||
},
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
for x in 1..=5 {
|
||||
expected.get_mut(x, 0).set_fg(Color::LightBlue);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_block_renders_on_small_areas() {
|
||||
let test_case = |block, area: Rect, expected| {
|
||||
let backend = TestBackend::new(area.width, area.height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
f.render_widget(block, area);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
let one_cell_test_cases = [
|
||||
(Borders::NONE, "T"),
|
||||
(Borders::LEFT, "│"),
|
||||
(Borders::TOP, "T"),
|
||||
(Borders::RIGHT, "│"),
|
||||
(Borders::BOTTOM, "T"),
|
||||
(Borders::ALL, "┌"),
|
||||
];
|
||||
for (borders, symbol) in one_cell_test_cases.iter().cloned() {
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0,
|
||||
},
|
||||
Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0,
|
||||
}),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec![symbol]),
|
||||
);
|
||||
}
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::LEFT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["│Tes"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::RIGHT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["Tes│"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::RIGHT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["Tes│"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["│Te│"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["Test"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 5,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["Test─"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.borders(Borders::LEFT | Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 5,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["┌Test"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.borders(Borders::LEFT | Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 6,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["┌Test─"]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_block_title_alignment() {
|
||||
let test_case = |alignment, borders, expected| {
|
||||
let backend = TestBackend::new(15, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let block = Block::default()
|
||||
.title(Span::styled("Title", Style::default()))
|
||||
.title_alignment(alignment)
|
||||
.borders(borders);
|
||||
|
||||
let area = Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 13,
|
||||
height: 2,
|
||||
};
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
f.render_widget(block, area);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
// title top-left with all borders
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌Title──────┐ ", " └───────────┘ "]),
|
||||
);
|
||||
|
||||
// title top-left without top border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" │Title │ ", " └───────────┘ "]),
|
||||
);
|
||||
|
||||
// title top-left with no left border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" Title───────┐ ", " ────────────┘ "]),
|
||||
);
|
||||
|
||||
// title top-left without right border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌Title─────── ", " └──────────── "]),
|
||||
);
|
||||
|
||||
// title top-left without borders
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" Title ", " "]),
|
||||
);
|
||||
|
||||
// title center with all borders
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌───Title───┐ ", " └───────────┘ "]),
|
||||
);
|
||||
|
||||
// title center without top border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" │ Title │ ", " └───────────┘ "]),
|
||||
);
|
||||
|
||||
// title center with no left border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ────Title───┐ ", " ────────────┘ "]),
|
||||
);
|
||||
|
||||
// title center without right border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌───Title──── ", " └──────────── "]),
|
||||
);
|
||||
|
||||
// title center without borders
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" Title ", " "]),
|
||||
);
|
||||
|
||||
// title top-right with all borders
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌──────Title┐ ", " └───────────┘ "]),
|
||||
);
|
||||
|
||||
// title top-right without top border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" │ Title│ ", " └───────────┘ "]),
|
||||
);
|
||||
|
||||
// title top-right with no left border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ───────Title┐ ", " ────────────┘ "]),
|
||||
);
|
||||
|
||||
// title top-right without right border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌───────Title ", " └──────────── "]),
|
||||
);
|
||||
|
||||
// title top-right without borders
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" Title ", " "]),
|
||||
);
|
||||
}
|
||||
42
tests/widgets_canvas.rs
Normal file
42
tests/widgets_canvas.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::canvas::Canvas,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn widgets_canvas_draw_labels() {
|
||||
let backend = TestBackend::new(5, 5);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let label = String::from("test");
|
||||
let canvas = Canvas::default()
|
||||
.background_color(Color::Yellow)
|
||||
.x_bounds([0.0, 5.0])
|
||||
.y_bounds([0.0, 5.0])
|
||||
.paint(|ctx| {
|
||||
ctx.print(
|
||||
0.0,
|
||||
0.0,
|
||||
Span::styled(label.clone(), Style::default().fg(Color::Blue)),
|
||||
);
|
||||
});
|
||||
f.render_widget(canvas, f.size());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![" ", " ", " ", " ", "test "]);
|
||||
for row in 0..5 {
|
||||
for col in 0..5 {
|
||||
expected.get_mut(col, row).set_bg(Color::Yellow);
|
||||
}
|
||||
}
|
||||
for col in 0..4 {
|
||||
expected.get_mut(col, 4).set_fg(Color::Blue);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected)
|
||||
}
|
||||
621
tests/widgets_chart.rs
Normal file
621
tests/widgets_chart.rs
Normal file
@@ -0,0 +1,621 @@
|
||||
use tui::layout::Alignment;
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
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()
|
||||
}
|
||||
|
||||
fn axis_test_case<S>(width: u16, height: u16, x_axis: Axis, y_axis: Axis, lines: Vec<S>)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let backend = TestBackend::new(width, height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let chart = Chart::new(vec![]).x_axis(x_axis).y_axis(y_axis);
|
||||
f.render_widget(chart, f.size());
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(lines);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_can_render_on_small_areas() {
|
||||
let test_case = |width, height| {
|
||||
let backend = TestBackend::new(width, height);
|
||||
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, f.size());
|
||||
})
|
||||
.unwrap();
|
||||
};
|
||||
test_case(0, 0);
|
||||
test_case(0, 1);
|
||||
test_case(1, 0);
|
||||
test_case(1, 1);
|
||||
test_case(2, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_handles_long_labels() {
|
||||
let test_case = |x_labels, y_labels, x_alignment, lines| {
|
||||
let mut x_axis = Axis::default().bounds([0.0, 1.0]);
|
||||
if let Some((left_label, right_label)) = x_labels {
|
||||
x_axis = x_axis
|
||||
.labels(vec![Span::from(left_label), Span::from(right_label)])
|
||||
.labels_alignment(x_alignment);
|
||||
}
|
||||
|
||||
let mut y_axis = Axis::default().bounds([0.0, 1.0]);
|
||||
if let Some((left_label, right_label)) = y_labels {
|
||||
y_axis = y_axis.labels(vec![Span::from(left_label), Span::from(right_label)]);
|
||||
}
|
||||
|
||||
axis_test_case(10, 5, x_axis, y_axis, lines);
|
||||
};
|
||||
|
||||
test_case(
|
||||
Some(("AAAA", "B")),
|
||||
None,
|
||||
Alignment::Left,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ───────",
|
||||
"AAA B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Some(("A", "BBBB")),
|
||||
None,
|
||||
Alignment::Left,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ─────────",
|
||||
"A BBBB",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Some(("AAAAAAAAAAA", "B")),
|
||||
None,
|
||||
Alignment::Left,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ───────",
|
||||
"AAA B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Some(("A", "B")),
|
||||
Some(("CCCCCCC", "D")),
|
||||
Alignment::Left,
|
||||
vec![
|
||||
"D │ ",
|
||||
" │ ",
|
||||
"CCC│ ",
|
||||
" └──────",
|
||||
" A B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Some(("AAAAAAAAAA", "B")),
|
||||
Some(("C", "D")),
|
||||
Alignment::Center,
|
||||
vec![
|
||||
"D │ ",
|
||||
" │ ",
|
||||
"C │ ",
|
||||
" └──────",
|
||||
"AAAAAAA B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Some(("AAAAAAA", "B")),
|
||||
Some(("C", "D")),
|
||||
Alignment::Right,
|
||||
vec![
|
||||
"D│ ",
|
||||
" │ ",
|
||||
"C│ ",
|
||||
" └────────",
|
||||
" AAAAA B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Some(("AAAAAAA", "BBBBBBB")),
|
||||
Some(("C", "D")),
|
||||
Alignment::Right,
|
||||
vec![
|
||||
"D│ ",
|
||||
" │ ",
|
||||
"C│ ",
|
||||
" └────────",
|
||||
" AAAAABBBB",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_handles_x_axis_labels_alignments() {
|
||||
let test_case = |y_alignment, lines| {
|
||||
let x_axis = Axis::default()
|
||||
.labels(vec![Span::from("AAAA"), Span::from("B"), Span::from("C")])
|
||||
.labels_alignment(y_alignment);
|
||||
|
||||
let y_axis = Axis::default();
|
||||
|
||||
axis_test_case(10, 5, x_axis, y_axis, lines);
|
||||
};
|
||||
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ───────",
|
||||
"AAA B C",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ────────",
|
||||
"AAAA B C",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"──────────",
|
||||
"AAA B C",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_handles_y_axis_labels_alignments() {
|
||||
let test_case = |y_alignment, lines| {
|
||||
let x_axis = Axis::default().labels(create_labels(&["AAAAA", "B"]));
|
||||
|
||||
let y_axis = Axis::default()
|
||||
.labels(create_labels(&["C", "D"]))
|
||||
.labels_alignment(y_alignment);
|
||||
|
||||
axis_test_case(20, 5, x_axis, y_axis, lines);
|
||||
};
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
vec![
|
||||
"D │ ",
|
||||
" │ ",
|
||||
"C │ ",
|
||||
" └───────────────",
|
||||
"AAAAA B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
vec![
|
||||
" D │ ",
|
||||
" │ ",
|
||||
" C │ ",
|
||||
" └───────────────",
|
||||
"AAAAA B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
vec![
|
||||
" D│ ",
|
||||
" │ ",
|
||||
" C│ ",
|
||||
" └───────────────",
|
||||
"AAAAA B",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_can_have_a_legend() {
|
||||
let backend = TestBackend::new(60, 30);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("Dataset 1")
|
||||
.style(Style::default().fg(Color::Blue))
|
||||
.data(&[
|
||||
(0.0, 0.0),
|
||||
(10.0, 1.0),
|
||||
(20.0, 2.0),
|
||||
(30.0, 3.0),
|
||||
(40.0, 4.0),
|
||||
(50.0, 5.0),
|
||||
(60.0, 6.0),
|
||||
(70.0, 7.0),
|
||||
(80.0, 8.0),
|
||||
(90.0, 9.0),
|
||||
(100.0, 10.0),
|
||||
])
|
||||
.graph_type(Line),
|
||||
Dataset::default()
|
||||
.name("Dataset 2")
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.data(&[
|
||||
(0.0, 10.0),
|
||||
(10.0, 9.0),
|
||||
(20.0, 8.0),
|
||||
(30.0, 7.0),
|
||||
(40.0, 6.0),
|
||||
(50.0, 5.0),
|
||||
(60.0, 4.0),
|
||||
(70.0, 3.0),
|
||||
(80.0, 2.0),
|
||||
(90.0, 1.0),
|
||||
(100.0, 0.0),
|
||||
])
|
||||
.graph_type(Line),
|
||||
];
|
||||
let chart = Chart::new(datasets)
|
||||
.style(Style::default().bg(Color::White))
|
||||
.block(Block::default().title("Chart Test").borders(Borders::ALL))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.bounds([0.0, 100.0])
|
||||
.title(Span::styled("X Axis", Style::default().fg(Color::Yellow)))
|
||||
.labels(create_labels(&["0.0", "50.0", "100.0"])),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.bounds([0.0, 10.0])
|
||||
.title("Y Axis")
|
||||
.labels(create_labels(&["0.0", "5.0", "10.0"])),
|
||||
);
|
||||
f.render_widget(
|
||||
chart,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 60,
|
||||
height: 30,
|
||||
},
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"┌Chart Test────────────────────────────────────────────────┐",
|
||||
"│10.0│Y Axis ┌─────────┐│",
|
||||
"│ │ •• │Dataset 1││",
|
||||
"│ │ •• │Dataset 2││",
|
||||
"│ │ •• └─────────┘│",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ ••• •• │",
|
||||
"│ │ ••• │",
|
||||
"│5.0 │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ ••• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• ••• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│0.0 │• X Axis│",
|
||||
"│ └─────────────────────────────────────────────────────│",
|
||||
"│ 0.0 50.0 100.0│",
|
||||
"└──────────────────────────────────────────────────────────┘",
|
||||
]);
|
||||
|
||||
// Set expected backgound color
|
||||
for row in 0..30 {
|
||||
for col in 0..60 {
|
||||
expected.get_mut(col, row).set_bg(Color::White);
|
||||
}
|
||||
}
|
||||
|
||||
// Set expected colors of the first dataset
|
||||
let line1 = vec![
|
||||
(48, 5),
|
||||
(49, 5),
|
||||
(46, 6),
|
||||
(47, 6),
|
||||
(44, 7),
|
||||
(45, 7),
|
||||
(42, 8),
|
||||
(43, 8),
|
||||
(40, 9),
|
||||
(41, 9),
|
||||
(38, 10),
|
||||
(39, 10),
|
||||
(36, 11),
|
||||
(37, 11),
|
||||
(34, 12),
|
||||
(35, 12),
|
||||
(33, 13),
|
||||
(30, 14),
|
||||
(31, 14),
|
||||
(28, 15),
|
||||
(29, 15),
|
||||
(25, 16),
|
||||
(26, 16),
|
||||
(27, 16),
|
||||
(23, 17),
|
||||
(24, 17),
|
||||
(21, 18),
|
||||
(22, 18),
|
||||
(19, 19),
|
||||
(20, 19),
|
||||
(17, 20),
|
||||
(18, 20),
|
||||
(15, 21),
|
||||
(16, 21),
|
||||
(13, 22),
|
||||
(14, 22),
|
||||
(11, 23),
|
||||
(12, 23),
|
||||
(9, 24),
|
||||
(10, 24),
|
||||
(7, 25),
|
||||
(8, 25),
|
||||
(6, 26),
|
||||
];
|
||||
let legend1 = vec![
|
||||
(49, 2),
|
||||
(50, 2),
|
||||
(51, 2),
|
||||
(52, 2),
|
||||
(53, 2),
|
||||
(54, 2),
|
||||
(55, 2),
|
||||
(56, 2),
|
||||
(57, 2),
|
||||
];
|
||||
for (col, row) in line1 {
|
||||
expected.get_mut(col, row).set_fg(Color::Blue);
|
||||
}
|
||||
for (col, row) in legend1 {
|
||||
expected.get_mut(col, row).set_fg(Color::Blue);
|
||||
}
|
||||
|
||||
// Set expected colors of the second dataset
|
||||
let line2 = vec![
|
||||
(8, 2),
|
||||
(9, 2),
|
||||
(10, 3),
|
||||
(11, 3),
|
||||
(12, 4),
|
||||
(13, 4),
|
||||
(14, 5),
|
||||
(15, 5),
|
||||
(16, 6),
|
||||
(17, 6),
|
||||
(18, 7),
|
||||
(19, 7),
|
||||
(20, 8),
|
||||
(21, 8),
|
||||
(22, 9),
|
||||
(23, 9),
|
||||
(24, 10),
|
||||
(25, 10),
|
||||
(26, 11),
|
||||
(27, 11),
|
||||
(28, 12),
|
||||
(29, 12),
|
||||
(30, 12),
|
||||
(31, 13),
|
||||
(32, 13),
|
||||
(33, 14),
|
||||
(34, 14),
|
||||
(35, 15),
|
||||
(36, 15),
|
||||
(37, 16),
|
||||
(38, 16),
|
||||
(39, 17),
|
||||
(40, 17),
|
||||
(41, 18),
|
||||
(42, 18),
|
||||
(43, 19),
|
||||
(44, 19),
|
||||
(45, 20),
|
||||
(46, 20),
|
||||
(47, 21),
|
||||
(48, 21),
|
||||
(49, 22),
|
||||
(50, 22),
|
||||
(51, 23),
|
||||
(52, 23),
|
||||
(53, 23),
|
||||
(54, 24),
|
||||
(55, 24),
|
||||
(56, 25),
|
||||
(57, 25),
|
||||
];
|
||||
let legend2 = vec![
|
||||
(49, 3),
|
||||
(50, 3),
|
||||
(51, 3),
|
||||
(52, 3),
|
||||
(53, 3),
|
||||
(54, 3),
|
||||
(55, 3),
|
||||
(56, 3),
|
||||
(57, 3),
|
||||
];
|
||||
for (col, row) in line2 {
|
||||
expected.get_mut(col, row).set_fg(Color::Green);
|
||||
}
|
||||
for (col, row) in legend2 {
|
||||
expected.get_mut(col, row).set_fg(Color::Green);
|
||||
}
|
||||
|
||||
// Set expected colors of the x axis
|
||||
let x_axis_title = vec![(53, 26), (54, 26), (55, 26), (56, 26), (57, 26), (58, 26)];
|
||||
for (col, row) in x_axis_title {
|
||||
expected.get_mut(col, row).set_fg(Color::Yellow);
|
||||
}
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
253
tests/widgets_gauge.rs
Normal file
253
tests/widgets_gauge.rs
Normal file
@@ -0,0 +1,253 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Gauge, LineGauge},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn widgets_gauge_renders() {
|
||||
let backend = TestBackend::new(40, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Percentage").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().bg(Color::Blue).fg(Color::Red))
|
||||
.use_unicode(true)
|
||||
.percent(43);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Ratio").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().bg(Color::Blue).fg(Color::Red))
|
||||
.use_unicode(true)
|
||||
.ratio(0.511_313_934_313_1);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ┌Percentage────────────────────────┐ ",
|
||||
" │ ▋43% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ┌Ratio─────────────────────────────┐ ",
|
||||
" │ 51% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
|
||||
for i in 3..17 {
|
||||
expected
|
||||
.get_mut(i, 3)
|
||||
.set_bg(Color::Red)
|
||||
.set_fg(Color::Blue);
|
||||
}
|
||||
for i in 17..37 {
|
||||
expected
|
||||
.get_mut(i, 3)
|
||||
.set_bg(Color::Blue)
|
||||
.set_fg(Color::Red);
|
||||
}
|
||||
|
||||
for i in 3..20 {
|
||||
expected
|
||||
.get_mut(i, 6)
|
||||
.set_bg(Color::Red)
|
||||
.set_fg(Color::Blue);
|
||||
}
|
||||
for i in 20..37 {
|
||||
expected
|
||||
.get_mut(i, 6)
|
||||
.set_bg(Color::Blue)
|
||||
.set_fg(Color::Red);
|
||||
}
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_gauge_renders_no_unicode() {
|
||||
let backend = TestBackend::new(40, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Percentage").borders(Borders::ALL))
|
||||
.percent(43)
|
||||
.use_unicode(false);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Ratio").borders(Borders::ALL))
|
||||
.ratio(0.211_313_934_313_1)
|
||||
.use_unicode(false);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ┌Percentage────────────────────────┐ ",
|
||||
" │ 43% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ┌Ratio─────────────────────────────┐ ",
|
||||
" │ 21% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_gauge_applies_styles() {
|
||||
let backend = TestBackend::new(12, 5);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let gauge = Gauge::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled("Test", Style::default().fg(Color::Red)))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.gauge_style(Style::default().fg(Color::Blue).bg(Color::Red))
|
||||
.percent(43)
|
||||
.label(Span::styled(
|
||||
"43%",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
f.render_widget(gauge, f.size());
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"┌Test──────┐",
|
||||
"│ │",
|
||||
"│ 43% │",
|
||||
"│ │",
|
||||
"└──────────┘",
|
||||
]);
|
||||
// title
|
||||
expected.set_style(Rect::new(1, 0, 4, 1), Style::default().fg(Color::Red));
|
||||
// gauge area
|
||||
expected.set_style(
|
||||
Rect::new(1, 1, 10, 3),
|
||||
Style::default().fg(Color::Blue).bg(Color::Red),
|
||||
);
|
||||
// filled area
|
||||
for y in 1..4 {
|
||||
expected.set_style(
|
||||
Rect::new(1, y, 4, 1),
|
||||
// filled style is invert of gauge_style
|
||||
Style::default().fg(Color::Red).bg(Color::Blue),
|
||||
);
|
||||
}
|
||||
// label (foreground and modifier from label style)
|
||||
expected.set_style(
|
||||
Rect::new(4, 2, 1, 1),
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
// "4" is in the filled area so background is gauge_style foreground
|
||||
.bg(Color::Blue)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
expected.set_style(
|
||||
Rect::new(5, 2, 2, 1),
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
// "3%" is not in the filled area so background is gauge_style background
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_gauge_supports_large_labels() {
|
||||
let backend = TestBackend::new(10, 1);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let gauge = Gauge::default()
|
||||
.percent(43)
|
||||
.label("43333333333333333333333333333%");
|
||||
f.render_widget(gauge, f.size());
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec!["4333333333"]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_line_gauge_renders() {
|
||||
let backend = TestBackend::new(20, 4);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let gauge = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Green).bg(Color::White))
|
||||
.ratio(0.43);
|
||||
f.render_widget(
|
||||
gauge,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 20,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
let gauge = LineGauge::default()
|
||||
.block(Block::default().title("Gauge 2").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Green))
|
||||
.line_set(symbols::line::THICK)
|
||||
.ratio(0.211_313_934_313_1);
|
||||
f.render_widget(
|
||||
gauge,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 20,
|
||||
height: 3,
|
||||
},
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"43% ────────────────",
|
||||
"┌Gauge 2───────────┐",
|
||||
"│21% ━━━━━━━━━━━━━━│",
|
||||
"└──────────────────┘",
|
||||
]);
|
||||
for col in 4..10 {
|
||||
expected.get_mut(col, 0).set_fg(Color::Green);
|
||||
}
|
||||
for col in 10..20 {
|
||||
expected.get_mut(col, 0).set_fg(Color::White);
|
||||
}
|
||||
for col in 5..7 {
|
||||
expected.get_mut(col, 2).set_fg(Color::Green);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
200
tests/widgets_list.rs
Normal file
200
tests/widgets_list.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Spans,
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
#[test]
|
||||
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(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
];
|
||||
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..10 {
|
||||
expected.get_mut(x, 1).set_bg(Color::Yellow);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_list_should_truncate_items() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
struct TruncateTestCase<'a> {
|
||||
selected: Option<usize>,
|
||||
items: Vec<ListItem<'a>>,
|
||||
expected: Buffer,
|
||||
}
|
||||
|
||||
let cases = vec![
|
||||
// An item is selected
|
||||
TruncateTestCase {
|
||||
selected: Some(0),
|
||||
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 {
|
||||
selected: None,
|
||||
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 case in cases {
|
||||
let mut state = ListState::default();
|
||||
state.select(case.selected);
|
||||
terminal
|
||||
.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();
|
||||
terminal.backend().assert_buffer(&case.expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_list_should_clamp_offset_if_items_are_removed() {
|
||||
let backend = TestBackend::new(10, 4);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = ListState::default();
|
||||
|
||||
// render with 6 items => offset will be at 2
|
||||
state.select(Some(5));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new("Item 0"),
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
ListItem::new("Item 4"),
|
||||
ListItem::new("Item 5"),
|
||||
];
|
||||
let list = List::new(items).highlight_symbol(">> ");
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![" Item 2 ", " Item 3 ", " Item 4 ", ">> Item 5 "]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
|
||||
// render again with 1 items => check offset is clamped to 1
|
||||
state.select(Some(1));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![ListItem::new("Item 3")];
|
||||
let list = List::new(items).highlight_symbol(">> ");
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![" Item 3 ", " ", " ", " "]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_list_should_display_multiline_items() {
|
||||
let backend = TestBackend::new(10, 6);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(1));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new(vec![Spans::from("Item 1"), Spans::from("Item 1a")]),
|
||||
ListItem::new(vec![Spans::from("Item 2"), Spans::from("Item 2b")]),
|
||||
ListItem::new(vec![Spans::from("Item 3"), Spans::from("Item 3c")]),
|
||||
];
|
||||
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 1a",
|
||||
">> Item 2 ",
|
||||
" Item 2b",
|
||||
" Item 3 ",
|
||||
" Item 3c",
|
||||
]);
|
||||
for x in 0..10 {
|
||||
expected.get_mut(x, 2).set_bg(Color::Yellow);
|
||||
expected.get_mut(x, 3).set_bg(Color::Yellow);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_list_should_repeat_highlight_symbol() {
|
||||
let backend = TestBackend::new(10, 6);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(1));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new(vec![Spans::from("Item 1"), Spans::from("Item 1a")]),
|
||||
ListItem::new(vec![Spans::from("Item 2"), Spans::from("Item 2b")]),
|
||||
ListItem::new(vec![Spans::from("Item 3"), Spans::from("Item 3c")]),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_style(Style::default().bg(Color::Yellow))
|
||||
.highlight_symbol(">> ")
|
||||
.repeat_highlight_symbol(true);
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" Item 1 ",
|
||||
" Item 1a",
|
||||
">> Item 2 ",
|
||||
">> Item 2b",
|
||||
" Item 3 ",
|
||||
" Item 3c",
|
||||
]);
|
||||
for x in 0..10 {
|
||||
expected.get_mut(x, 2).set_bg(Color::Yellow);
|
||||
expected.get_mut(x, 3).set_bg(Color::Yellow);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
299
tests/widgets_paragraph.rs
Normal file
299
tests/widgets_paragraph.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Alignment,
|
||||
text::{Span, 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 \
|
||||
supposed to be part of the UI. While providing a great flexibility for rich and \
|
||||
interactive UI, this may introduce overhead for highly dynamic content.";
|
||||
|
||||
#[test]
|
||||
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(|f| {
|
||||
let size = f.size();
|
||||
let text = vec![Spans::from(SAMPLE_STRING)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.alignment(alignment)
|
||||
.wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: false,
|
||||
});
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│The library is │",
|
||||
"│based on the │",
|
||||
"│principle of │",
|
||||
"│immediate │",
|
||||
"│rendering with │",
|
||||
"│intermediate │",
|
||||
"│buffers. This │",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ The library is│",
|
||||
"│ based on the│",
|
||||
"│ principle of│",
|
||||
"│ immediate│",
|
||||
"│ rendering with│",
|
||||
"│ intermediate│",
|
||||
"│ buffers. This│",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ The library is │",
|
||||
"│ based on the │",
|
||||
"│ principle of │",
|
||||
"│ immediate │",
|
||||
"│ rendering with │",
|
||||
"│ intermediate │",
|
||||
"│ buffers. This │",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_can_wrap_its_content_with_word_break_enabled() {
|
||||
let test_case = |alignment, expected| {
|
||||
let backend = TestBackend::new(20, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let text = vec![Spans::from(SAMPLE_STRING)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.alignment(alignment)
|
||||
.wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
});
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│The library is bas│",
|
||||
"│ed on the principl│",
|
||||
"│e of immediate ren│",
|
||||
"│dering with interm│",
|
||||
"│ediate buffers. Th│",
|
||||
"│is means that at e│",
|
||||
"│ach new frame you │",
|
||||
"│should build all w│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│The library is bas│",
|
||||
"│ed on the principl│",
|
||||
"│e of immediate ren│",
|
||||
"│dering with interm│",
|
||||
"│ediate buffers. Th│",
|
||||
"│is means that at e│",
|
||||
"│ach new frame you │",
|
||||
"│should build all w│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│The library is bas│",
|
||||
"│ed on the principl│",
|
||||
"│e of immediate ren│",
|
||||
"│dering with interm│",
|
||||
"│ediate buffers. Th│",
|
||||
"│is means that at e│",
|
||||
"│ach new frame you │",
|
||||
"│should build all w│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_renders_double_width_graphemes() {
|
||||
let backend = TestBackend::new(10, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、";
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let text = vec![Spans::from(s)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: false,
|
||||
});
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"┌────────┐",
|
||||
"│コンピュ│",
|
||||
"│ータ上で│",
|
||||
"│文字を扱│",
|
||||
"│う場合、│",
|
||||
"│典型的に│",
|
||||
"│は文字に│",
|
||||
"│よる通信│",
|
||||
"│を行う場│",
|
||||
"└────────┘",
|
||||
]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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(|f| {
|
||||
let size = f.size();
|
||||
let text = vec![Spans::from(s)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: false,
|
||||
});
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
// The internal width is 8 so only 4 slots for double-width characters.
|
||||
"┌────────┐",
|
||||
"│aコンピ │", // Here we have 1 latin character so only 3 double-width ones can fit.
|
||||
"│ュータ上│",
|
||||
"│で文字を│",
|
||||
"│扱う場合│",
|
||||
"│、 │",
|
||||
"└────────┘",
|
||||
]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_can_wrap_with_a_trailing_nbsp() {
|
||||
let nbsp: &str = "\u{00a0}";
|
||||
let line = Spans::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]);
|
||||
let backend = TestBackend::new(20, 3);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│NBSP\u{00a0} │",
|
||||
"└──────────────────┘",
|
||||
]);
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
|
||||
let paragraph = Paragraph::new(line).block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
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│",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
823
tests/widgets_table.rs
Normal file
823
tests/widgets_table.rs
Normal file
@@ -0,0 +1,823 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Constraint,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
#[test]
|
||||
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(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.column_spacing(column_spacing);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
// no space between columns
|
||||
test_case(
|
||||
0,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1Head2Head3 │",
|
||||
"│ │",
|
||||
"│Row11Row12Row13 │",
|
||||
"│Row21Row22Row23 │",
|
||||
"│Row31Row32Row33 │",
|
||||
"│Row41Row42Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// one space between columns
|
||||
test_case(
|
||||
1,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// enough space to just not hide the third column
|
||||
test_case(
|
||||
6,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// enough space to hide part of the third column
|
||||
test_case(
|
||||
7,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head│",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row1│",
|
||||
"│Row21 Row22 Row2│",
|
||||
"│Row31 Row32 Row3│",
|
||||
"│Row41 Row42 Row4│",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
// columns of zero width show nothing
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Length(0),
|
||||
Constraint::Length(0),
|
||||
Constraint::Length(0),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// columns of 1 width trim
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│H H H │",
|
||||
"│ │",
|
||||
"│R R R │",
|
||||
"│R R R │",
|
||||
"│R R R │",
|
||||
"│R R R │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// columns of large width just before pushing a column off
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Length(8),
|
||||
Constraint::Length(8),
|
||||
Constraint::Length(8),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths)
|
||||
.column_spacing(0);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
// columns of zero width show nothing
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(0),
|
||||
Constraint::Percentage(0),
|
||||
Constraint::Percentage(0),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// columns of not enough width trims the data
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(11),
|
||||
Constraint::Percentage(11),
|
||||
Constraint::Percentage(11),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│HeaHeaHea │",
|
||||
"│ │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// columns of large width just before pushing a column off
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// percentages summing to 100 should give equal widths
|
||||
test_case(
|
||||
&[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
// columns of zero width show nothing
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(0),
|
||||
Constraint::Length(0),
|
||||
Constraint::Percentage(0),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// columns of not enough width trims the data
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(11),
|
||||
Constraint::Length(20),
|
||||
Constraint::Percentage(11),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Hea Head2 He │",
|
||||
"│ │",
|
||||
"│Row Row12 Ro │",
|
||||
"│Row Row22 Ro │",
|
||||
"│Row Row32 Ro │",
|
||||
"│Row Row42 Ro │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// columns of large width just before pushing a column off
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Length(10),
|
||||
Constraint::Percentage(33),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// columns of large size (>100% total) hide the last column
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(60),
|
||||
Constraint::Length(10),
|
||||
Constraint::Percentage(60),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_table_columns_widths_can_use_ratio_constraints() {
|
||||
let test_case = |widths, expected| {
|
||||
let backend = TestBackend::new(30, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths)
|
||||
.column_spacing(0);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
// columns of zero width show nothing
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Ratio(0, 1),
|
||||
Constraint::Ratio(0, 1),
|
||||
Constraint::Ratio(0, 1),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// columns of not enough width trims the data
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Ratio(1, 9),
|
||||
Constraint::Ratio(1, 9),
|
||||
Constraint::Ratio(1, 9),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│HeaHeaHea │",
|
||||
"│ │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// columns of large width just before pushing a column off
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// percentages summing to 100 should give equal widths
|
||||
test_case(
|
||||
&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_table_can_have_rows_with_multi_lines() {
|
||||
let test_case = |state: &mut TableState, expected: Buffer| {
|
||||
let backend = TestBackend::new(30, 8);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.highlight_symbol(">> ")
|
||||
.widths(&[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, size, state);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
let mut state = TableState::default();
|
||||
// no selection
|
||||
test_case(
|
||||
&mut state,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│ │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// select first
|
||||
state.select(Some(0));
|
||||
test_case(
|
||||
&mut state,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│>> Row11 Row12 Row13 │",
|
||||
"│ Row21 Row22 Row23 │",
|
||||
"│ │",
|
||||
"│ Row31 Row32 Row33 │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// select second (we don't show partially the 4th row)
|
||||
state.select(Some(1));
|
||||
test_case(
|
||||
&mut state,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│ Row11 Row12 Row13 │",
|
||||
"│>> Row21 Row22 Row23 │",
|
||||
"│ │",
|
||||
"│ Row31 Row32 Row33 │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// select 4th (we don't show partially the 1st row)
|
||||
state.select(Some(3));
|
||||
test_case(
|
||||
&mut state,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│ Row31 Row32 Row33 │",
|
||||
"│>> Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_table_can_have_elements_styled_individually() {
|
||||
let backend = TestBackend::new(30, 4);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = TableState::default();
|
||||
state.select(Some(0));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]).style(Style::default().fg(Color::Green)),
|
||||
Row::new(vec![
|
||||
Cell::from("Row21"),
|
||||
Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
|
||||
Cell::from(Spans::from(vec![
|
||||
Span::raw("Row"),
|
||||
Span::styled("23", Style::default().fg(Color::Blue)),
|
||||
]))
|
||||
.style(Style::default().fg(Color::Red)),
|
||||
])
|
||||
.style(Style::default().fg(Color::LightGreen)),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.highlight_symbol(">> ")
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.widths(&[
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"│ Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│>> Row11 Row12 Row13 │",
|
||||
"│ Row21 Row22 Row23 │",
|
||||
]);
|
||||
// First row = row color + highlight style
|
||||
for col in 1..=28 {
|
||||
expected.get_mut(col, 2).set_style(
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
}
|
||||
// Second row:
|
||||
// 1. row color
|
||||
for col in 1..=28 {
|
||||
expected
|
||||
.get_mut(col, 3)
|
||||
.set_style(Style::default().fg(Color::LightGreen));
|
||||
}
|
||||
// 2. cell color
|
||||
for col in 11..=16 {
|
||||
expected
|
||||
.get_mut(col, 3)
|
||||
.set_style(Style::default().fg(Color::Yellow));
|
||||
}
|
||||
for col in 18..=23 {
|
||||
expected
|
||||
.get_mut(col, 3)
|
||||
.set_style(Style::default().fg(Color::Red));
|
||||
}
|
||||
// 3. text color
|
||||
for col in 21..=22 {
|
||||
expected
|
||||
.get_mut(col, 3)
|
||||
.set_style(Style::default().fg(Color::Blue));
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_table_should_render_even_if_empty() {
|
||||
let backend = TestBackend::new(30, 4);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]))
|
||||
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.widths(&[
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
]);
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_table_columns_dont_panic() {
|
||||
let test_case = |state: &mut TableState, table: Table, width: u16| {
|
||||
let backend = TestBackend::new(width, 8);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
f.render_stateful_widget(table, size, state);
|
||||
})
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
// based on https://github.com/fdehau/tui-rs/issues/470#issuecomment-852562848
|
||||
let table1_width = 98;
|
||||
let table1 = Table::new(vec![Row::new(vec!["r1", "r2", "r3", "r4"])])
|
||||
.header(Row::new(vec!["h1", "h2", "h3", "h4"]))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.highlight_symbol(">> ")
|
||||
.column_spacing(1)
|
||||
.widths(&[
|
||||
Constraint::Percentage(15),
|
||||
Constraint::Percentage(15),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(45),
|
||||
]);
|
||||
|
||||
let mut state = TableState::default();
|
||||
|
||||
// select first, which would cause a panic before fix
|
||||
state.select(Some(0));
|
||||
test_case(&mut state, table1.clone(), table1_width);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_table_should_clamp_offset_if_rows_are_removed() {
|
||||
let backend = TestBackend::new(30, 8);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = TableState::default();
|
||||
|
||||
// render with 6 items => offset will be at 2
|
||||
state.select(Some(5));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row01", "Row02", "Row03"]),
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
Row::new(vec!["Row51", "Row52", "Row53"]),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│Row51 Row52 Row53 │",
|
||||
"└────────────────────────────┘",
|
||||
]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
|
||||
// render with 1 item => offset will be at 1
|
||||
state.select(Some(1));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![Row::new(vec!["Row31", "Row32", "Row33"])])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
48
tests/widgets_tabs.rs
Normal file
48
tests/widgets_tabs.rs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user