Compare commits
1 Commits
653/master
...
feat-wrap-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a9f07a53e |
60
.github/ISSUE_TEMPLATE/bug_report.md
vendored
60
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,60 +1,30 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create an issue about a bug you encountered
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
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
|
||||
<!--
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
**To Reproduce**
|
||||
If possible include a code sample exhibiting the problem.
|
||||
|
||||
## 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
|
||||
<!--
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If applicable, add screenshots, gifs or videos 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]
|
||||
|
||||
|
||||
## 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
|
||||
<!--
|
||||
**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
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,26 +7,14 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
<!--
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
-->
|
||||
|
||||
## Solution
|
||||
<!--
|
||||
**Describe the solution you'd like**
|
||||
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
|
||||
-->
|
||||
|
||||
## Alternatives
|
||||
<!--
|
||||
**Describe alternatives you've considered**
|
||||
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
17
.github/pull_request_template.md
vendored
@@ -1,17 +0,0 @@
|
||||
## 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.
|
||||
102
.github/workflows/ci.yml
vendored
102
.github/workflows/ci.yml
vendored
@@ -1,71 +1,81 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
on: [push, pull_request]
|
||||
|
||||
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"]
|
||||
rust: ["1.44.0", "stable"]
|
||||
steps:
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
with:
|
||||
rust-version: ${{ matrix.rust }}
|
||||
components: rustfmt,clippy
|
||||
- name: "Install dependencies"
|
||||
run: sudo apt-get install libncurses5-dev
|
||||
- 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
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
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
|
||||
profile: default
|
||||
toolchain: ${{ matrix.rust }}
|
||||
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
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
- name: "Clippy"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-targets --all-features -- -D warnings
|
||||
windows:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.56.1", "stable"]
|
||||
rust: ["1.44.0", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
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
|
||||
profile: default
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
- name: "Check (crossterm)"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
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
|
||||
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
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
158
CHANGELOG.md
158
CHANGELOG.md
@@ -2,164 +2,6 @@
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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.
|
||||
159
Cargo.toml
159
Cargo.toml
@@ -1,93 +1,126 @@
|
||||
[package]
|
||||
name = "tui"
|
||||
version = "0.18.0"
|
||||
version = "0.12.0"
|
||||
authors = ["Florian Dehau <work@fdehau.com>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
"""
|
||||
documentation = "https://docs.rs/tui/0.18.0/tui/"
|
||||
documentation = "https://docs.rs/tui/0.10.0/tui/"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/fdehau/tui-rs"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
exclude = ["assets/*", ".github"]
|
||||
autoexamples = true
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
|
||||
[badges]
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
default = ["termion"]
|
||||
curses = ["easycurses", "pancurses"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.3"
|
||||
bitflags = "1.0"
|
||||
cassowary = "0.3"
|
||||
unicode-segmentation = "1.2"
|
||||
unicode-width = "0.1"
|
||||
termion = { version = "1.5", optional = true }
|
||||
crossterm = { version = "0.23", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"]}
|
||||
rustbox = { version = "0.11", optional = true }
|
||||
crossterm = { version = "0.18", optional = true }
|
||||
easycurses = { version = "0.12.2", optional = true }
|
||||
pancurses = { version = "0.16.1", optional = true, features = ["win32a"] }
|
||||
serde = { version = "1", "optional" = true, features = ["derive"]}
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
rand = "0.7"
|
||||
argh = "0.1"
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
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"]
|
||||
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"]
|
||||
|
||||
116
Makefile
Normal file
116
Makefile
Normal file
@@ -0,0 +1,116 @@
|
||||
SHELL=/bin/bash
|
||||
|
||||
# ================================ Cargo ======================================
|
||||
|
||||
|
||||
RUST_CHANNEL ?= stable
|
||||
CARGO_FLAGS =
|
||||
RUSTUP_INSTALLED = $(shell command -v rustup 2> /dev/null)
|
||||
TEST_FILTER ?=
|
||||
|
||||
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 $(TEST_FILTER)
|
||||
|
||||
# =============================== Examples ====================================
|
||||
|
||||
.PHONY: build-examples
|
||||
build-examples: ## Build all examples
|
||||
@$(CARGO) build --release --examples --all-features
|
||||
|
||||
.PHONY: run-examples
|
||||
run-examples: build-examples ## Run all examples
|
||||
@for file in examples/*.rs; do \
|
||||
name=$$(basename $${file/.rs/}); \
|
||||
$(CARGO) run --all-features --release --example $$name; \
|
||||
done;
|
||||
|
||||
# ================================ Doc ========================================
|
||||
|
||||
|
||||
.PHONY: doc
|
||||
doc: RUST_CHANNEL = nightly
|
||||
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: RUST_CHANNEL = nightly
|
||||
watch-doc: ## Watch file changes and rebuild the documentation if any
|
||||
$(CARGO) watch -x doc -x 'test --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
160
Makefile.toml
@@ -1,160 +0,0 @@
|
||||
[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
|
||||
'''
|
||||
78
README.md
78
README.md
@@ -11,9 +11,15 @@ 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 supports multiple backends:
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||
The library itself supports four different backends to draw to the terminal. You
|
||||
can either choose from:
|
||||
|
||||
- [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
|
||||
@@ -28,53 +34,58 @@ 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**.
|
||||
Since version 0.10.0, `tui` requires **rustc version 1.44.0 or greater**.
|
||||
|
||||
### [Documentation](https://docs.rs/tui)
|
||||
|
||||
### Demo
|
||||
|
||||
The demo shown in the gif can be run with all available backends.
|
||||
The demo shown in the gif can be run with all available backends
|
||||
(`examples/*_demo.rs` files). For example to see the `termion` version one could
|
||||
run:
|
||||
|
||||
```
|
||||
# crossterm
|
||||
cargo run --example demo --release -- --tick-rate 200
|
||||
# termion
|
||||
cargo run --example demo --no-default-features --features=termion --release -- --tick-rate 200
|
||||
cargo run --example termion_demo --release -- --tick-rate 200
|
||||
```
|
||||
|
||||
where `tick-rate` is the UI refresh rate in ms.
|
||||
|
||||
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).
|
||||
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
|
||||
```
|
||||
|
||||
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 demo --release -- --tick-rate 200 --enhanced-graphics false
|
||||
cargo run --example crossterm_demo --no-default-features --features="crossterm" --release -- --tick-rate 200 --enhanced-graphics false
|
||||
```
|
||||
|
||||
### Widgets
|
||||
|
||||
The library comes with the following list of widgets:
|
||||
|
||||
* [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)
|
||||
* [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)
|
||||
|
||||
Click on each item to see the source of the example. Run the examples with with
|
||||
cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
|
||||
cargo (e.g. to run the demo `cargo run --example demo`), and quit by pressing `q`.
|
||||
|
||||
You can run all examples by running `cargo make run-examples` (require
|
||||
`cargo-make` that can be installed with `cargo install cargo-make`).
|
||||
You can run all examples by running `make run-examples`.
|
||||
|
||||
### Third-party widgets
|
||||
|
||||
@@ -85,7 +96,6 @@ You can run all examples by running `cargo make run-examples` (require
|
||||
* [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)
|
||||
@@ -97,24 +107,6 @@ You can run all examples by running `cargo make run-examples` (require
|
||||
* [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,19 +1,15 @@
|
||||
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},
|
||||
};
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{BarChart, Block, Borders},
|
||||
Frame, Terminal,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
@@ -52,110 +48,85 @@ impl<'a> App<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
fn update(&mut self) {
|
||||
let value = self.data.pop().unwrap();
|
||||
self.data.insert(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// 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);
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
loop {
|
||||
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 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]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,119 +1,86 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, BorderType, Borders},
|
||||
Frame, Terminal,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
|
||||
// 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(ui)?;
|
||||
terminal.draw(|f| {
|
||||
// Wrapping block for a group
|
||||
// Just draw the block and the group on the same area and build the group
|
||||
// with at least a margin of 1
|
||||
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());
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
let top_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
let block = Block::default()
|
||||
.title(vec![
|
||||
Span::styled("With", Style::default().fg(Color::Yellow)),
|
||||
Span::from(" background"),
|
||||
])
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(block, top_chunks[0]);
|
||||
|
||||
let block = Block::default().title(Span::styled(
|
||||
"Styled title",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
f.render_widget(block, top_chunks[1]);
|
||||
|
||||
let bottom_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
let block = Block::default().title("With borders").borders(Borders::ALL);
|
||||
f.render_widget(block, bottom_chunks[0]);
|
||||
let block = Block::default()
|
||||
.title("With styled borders and doubled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.border_type(BorderType::Double);
|
||||
f.render_widget(block, bottom_chunks[1]);
|
||||
})?;
|
||||
|
||||
if let Event::Input(key) = events.next()? {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
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},
|
||||
};
|
||||
#[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 tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
style::Color,
|
||||
widgets::{
|
||||
canvas::{Canvas, Map, MapResolution, Rectangle},
|
||||
Block, Borders,
|
||||
},
|
||||
Frame, Terminal,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
@@ -51,7 +46,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
fn update(&mut self) {
|
||||
if self.ball.x < self.playground.left() as f64
|
||||
|| self.ball.x + self.ball.width > self.playground.right() as f64
|
||||
{
|
||||
@@ -78,103 +73,76 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// 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);
|
||||
// Setup event handlers
|
||||
let config = Config {
|
||||
tick_rate: Duration::from_millis(250),
|
||||
..Default::default()
|
||||
};
|
||||
let events = Events::with_config(config);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
loop {
|
||||
terminal.draw(|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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,21 +1,20 @@
|
||||
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},
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
SinSignal,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
|
||||
Frame, Terminal,
|
||||
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)];
|
||||
@@ -29,34 +28,6 @@ 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)>,
|
||||
@@ -80,7 +51,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
fn update(&mut self) {
|
||||
for _ in 0..5 {
|
||||
self.data1.remove(0);
|
||||
}
|
||||
@@ -95,207 +66,181 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// 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 events = Events::new();
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
loop {
|
||||
terminal.draw(|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 = 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]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
108
examples/crossterm_demo.rs
Normal file
108
examples/crossterm_demo.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
#[allow(dead_code)]
|
||||
mod demo;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::demo::{ui, App};
|
||||
use argh::FromArgs;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, 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, EnableMouseCapture)?;
|
||||
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// 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.
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if event::poll(timeout).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(|f| ui::draw(f, &mut app))?;
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => match event.code {
|
||||
KeyCode::Char('q') => {
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
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(())
|
||||
}
|
||||
74
examples/curses_demo.rs
Normal file
74
examples/curses_demo.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
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(|f| ui::draw(f, &mut app))?;
|
||||
if let Some(input) = terminal.backend_mut().get_curses_mut().get_input() {
|
||||
match input {
|
||||
easycurses::Input::Character(c) => {
|
||||
app.on_key(c);
|
||||
}
|
||||
easycurses::Input::KeyUp => {
|
||||
app.on_up();
|
||||
}
|
||||
easycurses::Input::KeyDown => {
|
||||
app.on_down();
|
||||
}
|
||||
easycurses::Input::KeyLeft => {
|
||||
app.on_left();
|
||||
}
|
||||
easycurses::Input::KeyRight => {
|
||||
app.on_right();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
};
|
||||
terminal.backend_mut().get_curses_mut().flush_input();
|
||||
if last_tick.elapsed() > tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
widgets::Widget,
|
||||
Frame, Terminal,
|
||||
backend::TermionBackend, buffer::Buffer, layout::Rect, style::Style, widgets::Widget, 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,46 +32,28 @@ impl<'a> Label<'a> {
|
||||
}
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
let events = Events::new();
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let size = f.size();
|
||||
let label = Label::default().text("Test");
|
||||
f.render_widget(label, size);
|
||||
})?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
if let Event::Input(key) = events.next()? {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,8 +1,4 @@
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use tui::widgets::ListState;
|
||||
use crate::util::{RandomSignal, SinSignal, StatefulList, TabsState};
|
||||
|
||||
const TASKS: [&str; 24] = [
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
|
||||
@@ -66,120 +62,6 @@ 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>,
|
||||
@@ -247,7 +129,7 @@ impl<'a> App<'a> {
|
||||
App {
|
||||
title,
|
||||
should_quit: false,
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2"]),
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1"]),
|
||||
show_chart: true,
|
||||
progress: 0.0,
|
||||
sparkline: Signal {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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(())
|
||||
}
|
||||
3
examples/demo/mod.rs
Normal file
3
examples/demo/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod app;
|
||||
pub mod ui;
|
||||
pub use app::App;
|
||||
@@ -1,80 +0,0 @@
|
||||
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,4 +1,4 @@
|
||||
use crate::app::App;
|
||||
use crate::demo::App;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
@@ -7,7 +7,7 @@ use tui::{
|
||||
text::{Span, Spans},
|
||||
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
|
||||
widgets::{
|
||||
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
|
||||
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, LineGauge, List, ListItem,
|
||||
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
|
||||
},
|
||||
Frame,
|
||||
@@ -31,7 +31,6 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
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]),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
@@ -293,10 +292,7 @@ where
|
||||
.fg(Color::Magenta)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
let paragraph = Paragraph::new(text).block(block).wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: false,
|
||||
});
|
||||
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
@@ -312,21 +308,18 @@ where
|
||||
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::new(vec![s.name, s.location, s.status]).style(style)
|
||||
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
|
||||
});
|
||||
let table = Table::new(rows)
|
||||
.header(
|
||||
Row::new(vec!["Server", "Location", "Status"])
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
let table = Table::new(header.iter(), rows)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
.header_style(Style::default().fg(Color::Yellow))
|
||||
.widths(&[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
@@ -366,11 +359,7 @@ where
|
||||
} else {
|
||||
Color::Red
|
||||
};
|
||||
ctx.print(
|
||||
server.coords.1,
|
||||
server.coords.0,
|
||||
Span::styled("X", Style::default().fg(color)),
|
||||
);
|
||||
ctx.print(server.coords.1, server.coords.0, "X", color);
|
||||
}
|
||||
})
|
||||
.marker(if app.enhanced_graphics {
|
||||
@@ -382,51 +371,3 @@ 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,20 +1,15 @@
|
||||
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},
|
||||
};
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Gauge},
|
||||
Frame, Terminal,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
@@ -29,17 +24,17 @@ impl App {
|
||||
App {
|
||||
progress1: 0,
|
||||
progress2: 0,
|
||||
progress3: 0.45,
|
||||
progress3: 0.0,
|
||||
progress4: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.progress1 += 1;
|
||||
fn update(&mut self) {
|
||||
self.progress1 += 5;
|
||||
if self.progress1 > 100 {
|
||||
self.progress1 = 0;
|
||||
}
|
||||
self.progress2 += 2;
|
||||
self.progress2 += 10;
|
||||
if self.progress2 > 100 {
|
||||
self.progress2 = 0;
|
||||
}
|
||||
@@ -47,7 +42,7 @@ impl App {
|
||||
if self.progress3 > 1.0 {
|
||||
self.progress3 = 0.0;
|
||||
}
|
||||
self.progress4 += 1;
|
||||
self.progress4 += 3;
|
||||
if self.progress4 > 100 {
|
||||
self.progress4 = 0;
|
||||
}
|
||||
@@ -55,112 +50,77 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// 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 events = Events::new();
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
let mut app = App::new();
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
loop {
|
||||
terminal.draw(|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))
|
||||
.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 gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge3").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(app.progress3);
|
||||
f.render_widget(gauge, chunks[2]);
|
||||
|
||||
let label = format!("{}/100", app.progress2);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge4"))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,70 +1,52 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
widgets::{Block, Borders},
|
||||
Frame, Terminal,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
let events = Events::new();
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
309
examples/list.rs
309
examples/list.rs
@@ -1,74 +1,21 @@
|
||||
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},
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
StatefulList,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Frame, Terminal,
|
||||
widgets::{Block, Borders, List, ListItem},
|
||||
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, usize)>,
|
||||
events: Vec<(&'a str, &'a str)>,
|
||||
@@ -135,152 +82,112 @@ impl<'a> App<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 advance(&mut self) {
|
||||
let event = self.events.pop().unwrap();
|
||||
self.events.insert(0, event);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// 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 events = Events::new();
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let items: Vec<ListItem> = app
|
||||
.items
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let mut lines = vec![Spans::from(i.0)];
|
||||
for _ in 0..i.1 {
|
||||
lines.push(Spans::from(Span::styled(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
)));
|
||||
}
|
||||
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
let items = List::new(items)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
|
||||
|
||||
let events: Vec<ListItem> = app
|
||||
.events
|
||||
.iter()
|
||||
.map(|&(evt, level)| {
|
||||
let s = match level {
|
||||
"CRITICAL" => Style::default().fg(Color::Red),
|
||||
"ERROR" => Style::default().fg(Color::Magenta),
|
||||
"WARNING" => Style::default().fg(Color::Yellow),
|
||||
"INFO" => Style::default().fg(Color::Blue),
|
||||
_ => Style::default(),
|
||||
};
|
||||
let header = Spans::from(vec![
|
||||
Span::styled(format!("{:<9}", level), s),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
"2020-01-01 10:00:00",
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
),
|
||||
]);
|
||||
let log = Spans::from(vec![Span::raw(evt)]);
|
||||
ListItem::new(vec![
|
||||
Spans::from("-".repeat(chunks[1].width as usize)),
|
||||
header,
|
||||
Spans::from(""),
|
||||
log,
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
let events_list = List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.start_corner(Corner::BottomLeft);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
//! 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,180 +1,111 @@
|
||||
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},
|
||||
};
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
scroll: u16,
|
||||
}
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// 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 events = Events::new();
|
||||
|
||||
// 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();
|
||||
let mut scroll: u16 = 0;
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
terminal.draw(|f| {
|
||||
let size = f.size();
|
||||
|
||||
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(());
|
||||
}
|
||||
// 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 });
|
||||
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 })
|
||||
.scroll((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"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
})?;
|
||||
|
||||
scroll += 1;
|
||||
scroll %= 10;
|
||||
|
||||
if let Event::Input(key) = events.next()? {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
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]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,109 +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 tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
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`
|
||||
/// 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)
|
||||
@@ -129,3 +40,67 @@ 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)?;
|
||||
|
||||
let events = Events::new();
|
||||
|
||||
loop {
|
||||
terminal.draw(|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 = vec![
|
||||
Spans::from("This is a line "),
|
||||
Spans::from(Span::styled("This is a line ", Style::default().fg(Color::Red))),
|
||||
Spans::from(Span::styled("This is a line", Style::default().bg(Color::Blue))),
|
||||
Spans::from(Span::styled(
|
||||
"This is a longer line\n",
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT),
|
||||
)),
|
||||
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line\n",
|
||||
Style::default().fg(Color::Green).add_modifier(Modifier::ITALIC),
|
||||
)),
|
||||
];
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.block(Block::default().title("Left Block").borders(Borders::ALL))
|
||||
.alignment(Alignment::Left).wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().title("Right Block").borders(Borders::ALL))
|
||||
.alignment(Alignment::Left).wrap(Wrap { trim: 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(())
|
||||
}
|
||||
|
||||
68
examples/rustbox_demo.rs
Normal file
68
examples/rustbox_demo.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
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)?;
|
||||
|
||||
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(|f| ui::draw(f, &mut app))?;
|
||||
if let Ok(rustbox::Event::KeyEvent(key)) =
|
||||
terminal.backend().rustbox().peek_event(tick_rate, false)
|
||||
{
|
||||
match key {
|
||||
Key::Char(c) => {
|
||||
app.on_key(c);
|
||||
}
|
||||
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,47 +1,20 @@
|
||||
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},
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
RandomSignal,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
Frame, Terminal,
|
||||
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>,
|
||||
@@ -63,7 +36,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
fn update(&mut self) {
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data1.pop();
|
||||
self.data1.insert(0, value);
|
||||
@@ -77,100 +50,75 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// 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);
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
// Create default app state
|
||||
let mut app = App::new();
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
loop {
|
||||
terminal.draw(|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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Frame, Terminal,
|
||||
widgets::{Block, Borders, Row, Table, TableState},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
pub struct StatefulTable<'a> {
|
||||
state: TableState,
|
||||
items: Vec<Vec<&'a str>>,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
impl<'a> StatefulTable<'a> {
|
||||
fn new() -> StatefulTable<'a> {
|
||||
StatefulTable {
|
||||
state: TableState::default(),
|
||||
items: vec![
|
||||
vec!["Row11", "Row12", "Row13"],
|
||||
@@ -27,7 +27,7 @@ impl<'a> App<'a> {
|
||||
vec!["Row31", "Row32", "Row33"],
|
||||
vec!["Row41", "Row42", "Row43"],
|
||||
vec!["Row51", "Row52", "Row53"],
|
||||
vec!["Row61", "Row62\nTest", "Row63"],
|
||||
vec!["Row61", "Row62", "Row63"],
|
||||
vec!["Row71", "Row72", "Row73"],
|
||||
vec!["Row81", "Row82", "Row83"],
|
||||
vec!["Row91", "Row92", "Row93"],
|
||||
@@ -74,82 +74,61 @@ impl<'a> App<'a> {
|
||||
}
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
let events = Events::new();
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
let mut table = StatefulTable::new();
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
// Input
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let rects = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.margin(5)
|
||||
.split(f.size());
|
||||
|
||||
let selected_style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let normal_style = Style::default().fg(Color::White);
|
||||
let header = ["Header1", "Header2", "Header3"];
|
||||
let rows = table
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| Row::StyledData(i.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();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
170
examples/tabs.rs
170
examples/tabs.rs
@@ -1,124 +1,94 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
TabsState,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Tabs},
|
||||
Frame, Terminal,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
struct App<'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;
|
||||
}
|
||||
}
|
||||
tabs: TabsState<'a>,
|
||||
}
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
let events = Events::new();
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
// App
|
||||
let mut app = App {
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2", "Tab3"]),
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
// Main loop
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
terminal.draw(|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);
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => app.previous(),
|
||||
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
f.render_widget(block, size);
|
||||
let titles = app
|
||||
.tabs
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let (first, rest) = t.split_at(1);
|
||||
Spans::from(vec![
|
||||
Span::styled(first, Style::default().fg(Color::Yellow)),
|
||||
Span::styled(rest, Style::default().fg(Color::Green)),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.select(app.tabs.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.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(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
72
examples/termion_demo.rs
Normal file
72
examples/termion_demo.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
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)?;
|
||||
|
||||
let mut app = App::new("Termion demo", cli.enhanced_graphics);
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(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,19 +9,20 @@
|
||||
/// * Pressing Backspace erases a character
|
||||
/// * Pressing Enter pushes the current input in the history of previous
|
||||
/// messages
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
Frame, Terminal,
|
||||
Terminal,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -51,142 +52,130 @@ impl Default for App {
|
||||
}
|
||||
|
||||
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);
|
||||
// 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)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::default();
|
||||
let res = run_app(&mut terminal, app);
|
||||
// Setup event handlers
|
||||
let mut events = Events::new();
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
// Create default app state
|
||||
let mut app = App::default();
|
||||
|
||||
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))?;
|
||||
// Draw UI
|
||||
terminal.draw(|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());
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
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 => match key.code {
|
||||
KeyCode::Char('e') => {
|
||||
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]);
|
||||
})?;
|
||||
|
||||
// Handle input
|
||||
if let Event::Input(input) = events.next()? {
|
||||
match app.input_mode {
|
||||
InputMode::Normal => match input {
|
||||
Key::Char('e') => {
|
||||
app.input_mode = InputMode::Editing;
|
||||
events.disable_exit_key();
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Editing => match key.code {
|
||||
KeyCode::Enter => {
|
||||
InputMode::Editing => match input {
|
||||
Key::Char('\n') => {
|
||||
app.messages.push(app.input.drain(..).collect());
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
Key::Char(c) => {
|
||||
app.input.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
Key::Backspace => {
|
||||
app.input.pop();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
Key::Esc => {
|
||||
app.input_mode = InputMode::Normal;
|
||||
events.enable_exit_key();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
95
examples/util/event.rs
Normal file
95
examples/util/event.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
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 {
|
||||
if tx.send(Event::Tick).is_err() {
|
||||
break;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
131
examples/util/mod.rs
Normal file
131
examples/util/mod.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
280
src/backend/curses.rs
Normal file
280
src/backend/curses.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use std::io;
|
||||
|
||||
use crate::backend::Backend;
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::{Color, Modifier};
|
||||
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 fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut curses_style = CursesStyle {
|
||||
fg: easycurses::Color::White,
|
||||
bg: easycurses::Color::Black,
|
||||
};
|
||||
let mut update_color = false;
|
||||
for (col, row, cell) in content {
|
||||
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.modifier != modifier {
|
||||
apply_modifier_diff(&mut self.curses.win, modifier, cell.modifier);
|
||||
modifier = cell.modifier;
|
||||
};
|
||||
if cell.fg != fg {
|
||||
update_color = true;
|
||||
if let Some(ccolor) = cell.fg.into() {
|
||||
fg = cell.fg;
|
||||
curses_style.fg = ccolor;
|
||||
} else {
|
||||
fg = Color::White;
|
||||
curses_style.fg = easycurses::Color::White;
|
||||
}
|
||||
};
|
||||
if cell.bg != bg {
|
||||
update_color = true;
|
||||
if let Some(ccolor) = cell.bg.into() {
|
||||
bg = cell.bg;
|
||||
curses_style.bg = ccolor;
|
||||
} else {
|
||||
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,6 +3,11 @@ 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")]
|
||||
@@ -13,6 +18,11 @@ 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;
|
||||
|
||||
|
||||
123
src/backend/rustbox.rs
Normal file
123
src/backend/rustbox.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
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.modifier.into(),
|
||||
cell.fg.into(),
|
||||
cell.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
|
||||
}
|
||||
}
|
||||
@@ -60,12 +60,6 @@ impl TestBackend {
|
||||
&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);
|
||||
@@ -101,7 +95,7 @@ impl TestBackend {
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
debug_info.push_str(&nice_diff);
|
||||
panic!("{}", debug_info);
|
||||
panic!(debug_info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +131,6 @@ impl Backend for TestBackend {
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.buffer.reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ impl Default for Cell {
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol, "x");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
@@ -114,6 +114,15 @@ pub struct Buffer {
|
||||
pub content: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Default for Buffer {
|
||||
fn default() -> Buffer {
|
||||
Buffer {
|
||||
area: Default::default(),
|
||||
content: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
|
||||
@@ -63,9 +63,6 @@ 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! {
|
||||
@@ -81,7 +78,6 @@ impl Default for Layout {
|
||||
vertical: 0,
|
||||
},
|
||||
constraints: Vec::new(),
|
||||
expand_to_fill: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,11 +114,6 @@ 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.
|
||||
///
|
||||
@@ -231,13 +222,11 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
|
||||
});
|
||||
}
|
||||
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()),
|
||||
});
|
||||
}
|
||||
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 => {
|
||||
@@ -310,16 +299,14 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,7 +350,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, Default)]
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct Rect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
@@ -371,6 +358,17 @@ 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.
|
||||
@@ -403,7 +401,7 @@ impl Rect {
|
||||
}
|
||||
|
||||
pub fn right(self) -> u16 {
|
||||
self.x.saturating_add(self.width)
|
||||
self.x + self.width
|
||||
}
|
||||
|
||||
pub fn top(self) -> u16 {
|
||||
@@ -411,7 +409,7 @@ impl Rect {
|
||||
}
|
||||
|
||||
pub fn bottom(self) -> u16 {
|
||||
self.y.saturating_add(self.height)
|
||||
self.y + self.height
|
||||
}
|
||||
|
||||
pub fn inner(self, margin: &Margin) -> Rect {
|
||||
|
||||
152
src/lib.rs
152
src/lib.rs
@@ -9,19 +9,18 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! tui = "0.18"
|
||||
//! crossterm = "0.23"
|
||||
//! tui = "0.12"
|
||||
//! termion = "1.5"
|
||||
//! ```
|
||||
//!
|
||||
//! 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:
|
||||
//! 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:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! termion = "1.5"
|
||||
//! tui = { version = "0.18", default-features = false, features = ['termion'] }
|
||||
//!
|
||||
//! crossterm = "0.18"
|
||||
//! tui = { version = "0.12", default-features = false, features = ['crossterm'] }
|
||||
//! ```
|
||||
//!
|
||||
//! The same logic applies for all other available backends.
|
||||
@@ -34,22 +33,8 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use tui::{backend::CrosstermBackend, Terminal};
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout();
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! 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::{backend::TermionBackend, Terminal};
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::TermionBackend;
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
@@ -60,6 +45,22 @@
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If you had previously chosen `crossterm` as a backend, the terminal can be created in a similar
|
||||
//! way:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use std::io;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::CrosstermBackend;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout();
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! You may also refer to the examples to find out how to create a `Terminal` for each available
|
||||
//! backend.
|
||||
//!
|
||||
@@ -70,53 +71,30 @@
|
||||
//! 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 [`Frame::render_widget`] which takes
|
||||
//! your widget instance and an area to draw to.
|
||||
//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take
|
||||
//! your widget instance an area to draw to.
|
||||
//!
|
||||
//! The following example renders a block of the size of the terminal:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! 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},
|
||||
//! };
|
||||
//! 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> {
|
||||
//! // setup terminal
|
||||
//! enable_raw_mode()?;
|
||||
//! let mut stdout = io::stdout();
|
||||
//! execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//!
|
||||
//! 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(())
|
||||
//! })
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -127,32 +105,38 @@
|
||||
//! full customization. And `Layout` is no exception:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! 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]);
|
||||
//! 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(|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]);
|
||||
//! })
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
|
||||
66
src/style.rs
66
src/style.rs
@@ -54,6 +54,8 @@ bitflags! {
|
||||
|
||||
/// Style let you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// Style::default()
|
||||
@@ -61,60 +63,6 @@ bitflags! {
|
||||
/// .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 {
|
||||
@@ -136,16 +84,6 @@ impl Default for Style {
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Returns a `Style` resetting all properties.
|
||||
pub fn reset() -> Style {
|
||||
Style {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::all(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the foreground color.
|
||||
///
|
||||
/// ## Examples
|
||||
|
||||
@@ -82,12 +82,14 @@ where
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # use tui::backend::TermionBackend;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::Block;
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let stdout = io::stdout();
|
||||
/// # let backend = TermionBackend::new(stdout);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let block = Block::default();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
@@ -108,12 +110,14 @@ where
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # use tui::backend::TermionBackend;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::{List, ListItem, ListState};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let stdout = io::stdout();
|
||||
/// # let backend = TermionBackend::new(stdout);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select(Some(1));
|
||||
@@ -144,14 +148,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>
|
||||
where
|
||||
B: Backend,
|
||||
@@ -251,7 +247,7 @@ where
|
||||
|
||||
/// 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<CompletedFrame>
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Frame<B>),
|
||||
{
|
||||
@@ -283,10 +279,7 @@ where
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
Ok(CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.viewport.area,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
|
||||
206
src/text.rs
206
src/text.rs
@@ -51,6 +51,8 @@ use std::borrow::Cow;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
const NBSP: &str = "\u{00a0}";
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct StyledGrapheme<'a> {
|
||||
@@ -179,6 +181,29 @@ impl<'a> Span<'a> {
|
||||
})
|
||||
.filter(|s| s.symbol != "\n")
|
||||
}
|
||||
|
||||
fn split_at_in_place(&mut self, mid: usize) -> Span<'a> {
|
||||
let content = match self.content {
|
||||
Cow::Owned(ref mut s) => {
|
||||
let s2 = s[mid..].to_string();
|
||||
s.truncate(mid);
|
||||
Cow::Owned(s2)
|
||||
}
|
||||
Cow::Borrowed(s) => {
|
||||
let (s1, s2) = s.split_at(mid);
|
||||
self.content = Cow::Borrowed(s1);
|
||||
Cow::Borrowed(s2)
|
||||
}
|
||||
};
|
||||
Span {
|
||||
content,
|
||||
style: self.style,
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_start(&mut self) {
|
||||
self.content = Cow::Owned(String::from(self.content.trim_start()));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Span<'a> {
|
||||
@@ -194,9 +219,15 @@ impl<'a> From<&'a str> for Span<'a> {
|
||||
}
|
||||
|
||||
/// A string composed of clusters of graphemes, each with their own style.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Spans<'a>(pub Vec<Span<'a>>);
|
||||
|
||||
impl<'a> Default for Spans<'a> {
|
||||
fn default() -> Spans<'a> {
|
||||
Spans(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Spans<'a> {
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
@@ -249,6 +280,15 @@ impl<'a> From<Spans<'a>> for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Spans<'a> {
|
||||
type Item = Span<'a>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
///
|
||||
@@ -273,11 +313,17 @@ impl<'a> From<Spans<'a>> for String {
|
||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
/// assert_eq!(6, text.height());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Text<'a> {
|
||||
pub lines: Vec<Spans<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Default for Text<'a> {
|
||||
fn default() -> Text<'a> {
|
||||
Text { lines: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
/// Create some text (potentially multiple lines) with no style.
|
||||
///
|
||||
@@ -386,12 +432,6 @@ impl<'a> From<&'a str> for Text<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -426,3 +466,153 @@ impl<'a> Extend<Spans<'a>> for Text<'a> {
|
||||
self.lines.extend(iter);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct WrappedText<'a> {
|
||||
text: Text<'a>,
|
||||
trim: bool,
|
||||
width: u16,
|
||||
column: u16,
|
||||
last_word_end: u16,
|
||||
was_whitespace: bool,
|
||||
}
|
||||
|
||||
impl<'a> WrappedText<'a> {
|
||||
pub fn new(width: u16) -> Self {
|
||||
Self {
|
||||
text: Text::default(),
|
||||
width,
|
||||
trim: true,
|
||||
column: 0,
|
||||
last_word_end: 0,
|
||||
was_whitespace: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trim(mut self, trim: bool) -> Self {
|
||||
self.trim = trim;
|
||||
self
|
||||
}
|
||||
|
||||
fn push_span(&mut self, span: Span<'a>) {
|
||||
if self.text.lines.is_empty() {
|
||||
self.text.lines.push(Spans::default());
|
||||
}
|
||||
let last_line = self.text.lines.len() - 1;
|
||||
self.text.lines[last_line].0.push(span);
|
||||
}
|
||||
|
||||
fn push_spans<T>(&mut self, spans: T)
|
||||
where
|
||||
T: IntoIterator<Item = Span<'a>>,
|
||||
{
|
||||
let mut iter = spans.into_iter();
|
||||
let mut pending_span = iter.next();
|
||||
while let Some(mut span) = pending_span.take() {
|
||||
let span_position = self.column;
|
||||
let mut breakpoint = None;
|
||||
// Skip leading whitespaces when trim is enabled
|
||||
if self.column == 0 && self.trim {
|
||||
span.trim_start();
|
||||
}
|
||||
for grapheme in UnicodeSegmentation::graphemes(span.content.as_ref(), true) {
|
||||
let grapheme_width = grapheme.width() as u16;
|
||||
// Ignore grapheme that are larger than the allowed width
|
||||
if grapheme_width > self.width {
|
||||
continue;
|
||||
}
|
||||
let is_whitespace = grapheme.chars().all(&char::is_whitespace);
|
||||
if is_whitespace && !self.was_whitespace && grapheme != NBSP {
|
||||
self.last_word_end = self.column;
|
||||
}
|
||||
let next_column = self.column.saturating_add(grapheme_width);
|
||||
if next_column > self.width {
|
||||
let width = self.last_word_end.saturating_sub(span_position) as usize;
|
||||
breakpoint = Some(width);
|
||||
break;
|
||||
}
|
||||
self.column = next_column;
|
||||
self.was_whitespace = is_whitespace;
|
||||
}
|
||||
if let Some(b) = breakpoint {
|
||||
pending_span = if b > 0 {
|
||||
let new_span = span.split_at_in_place(b);
|
||||
self.push_span(span);
|
||||
Some(new_span)
|
||||
} else {
|
||||
Some(span)
|
||||
};
|
||||
self.start_new_line();
|
||||
} else {
|
||||
self.push_span(span);
|
||||
pending_span = iter.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_new_line(&mut self) {
|
||||
self.column = 0;
|
||||
self.last_word_end = 0;
|
||||
self.text.lines.push(Spans::default());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extend<Spans<'a>> for WrappedText<'a> {
|
||||
fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
|
||||
for spans in iter {
|
||||
self.start_new_line();
|
||||
self.push_spans(spans);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<WrappedText<'a>> for Text<'a> {
|
||||
fn from(text: WrappedText<'a>) -> Text<'a> {
|
||||
text.text
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Style};
|
||||
|
||||
#[test]
|
||||
fn text_can_be_wrapped() {
|
||||
let mut t = WrappedText::new(10);
|
||||
t.extend(Text::from(Spans::from(vec![
|
||||
Span::raw("This is "),
|
||||
Span::styled("a test.", Style::default().fg(Color::Red)),
|
||||
])));
|
||||
t.extend(Text::from("It should wrap."));
|
||||
let t = Text::from(t);
|
||||
let expected = Text::from(vec![
|
||||
Spans::from(vec![
|
||||
Span::raw("This is "),
|
||||
Span::styled("a", Style::default().fg(Color::Red)),
|
||||
]),
|
||||
Spans::from(Span::styled("test.", Style::default().fg(Color::Red))),
|
||||
Spans::from("It should"),
|
||||
Spans::from("wrap."),
|
||||
]);
|
||||
assert_eq!(expected, t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_with_trailing_nbsp_can_be_wrapped() {
|
||||
let mut t = WrappedText::new(10);
|
||||
t.extend(Text::from(Spans::from(vec![
|
||||
Span::raw("Line1"),
|
||||
Span::styled(NBSP, Style::default().add_modifier(Modifier::UNDERLINED)),
|
||||
Span::raw("Line2"),
|
||||
])));
|
||||
let expected = Text::from(vec![
|
||||
Spans::from(vec![
|
||||
Span::raw("Line1"),
|
||||
Span::styled(NBSP, Style::default().add_modifier(Modifier::UNDERLINED)),
|
||||
]),
|
||||
Spans::from(vec![Span::raw("Line2")]),
|
||||
]);
|
||||
assert_eq!(expected, Text::from(t));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols::line,
|
||||
text::{Span, Spans},
|
||||
widgets::{Borders, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum BorderType {
|
||||
Plain,
|
||||
Rounded,
|
||||
@@ -41,13 +41,10 @@ impl BorderType {
|
||||
/// .border_type(BorderType::Rounded)
|
||||
/// .style(Style::default().bg(Color::Black));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Block<'a> {
|
||||
/// Optional title place on the upper left of the block
|
||||
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
|
||||
@@ -63,7 +60,6 @@ impl<'a> Default for Block<'a> {
|
||||
fn default() -> Block<'a> {
|
||||
Block {
|
||||
title: None,
|
||||
title_alignment: Alignment::Left,
|
||||
borders: Borders::NONE,
|
||||
border_style: Default::default(),
|
||||
border_type: BorderType::Plain,
|
||||
@@ -93,11 +89,6 @@ impl<'a> Block<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
|
||||
self.title_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn border_style(mut self, style: Style) -> Block<'a> {
|
||||
self.border_style = style;
|
||||
self
|
||||
@@ -120,20 +111,23 @@ 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 = inner.x.saturating_add(1).min(inner.right());
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
inner.x += 1;
|
||||
inner.width -= 1;
|
||||
}
|
||||
if self.borders.intersects(Borders::TOP) || self.title.is_some() {
|
||||
inner.y = inner.y.saturating_add(1).min(inner.bottom());
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
inner.y += 1;
|
||||
inner.height -= 1;
|
||||
}
|
||||
if self.borders.intersects(Borders::RIGHT) {
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
inner.width -= 1;
|
||||
}
|
||||
if self.borders.intersects(Borders::BOTTOM) {
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
inner.height -= 1;
|
||||
}
|
||||
inner
|
||||
}
|
||||
@@ -141,12 +135,13 @@ impl<'a> Block<'a> {
|
||||
|
||||
impl<'a> Widget for Block<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.area() == 0 {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
if area.width < 2 || area.height < 2 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
let symbols = BorderType::line_symbols(self.border_type);
|
||||
|
||||
let symbols = BorderType::line_symbols(self.border_type);
|
||||
// Sides
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
for y in area.top()..area.bottom() {
|
||||
@@ -180,9 +175,9 @@ impl<'a> Widget for Block<'a> {
|
||||
}
|
||||
|
||||
// Corners
|
||||
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);
|
||||
}
|
||||
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
||||
@@ -195,379 +190,25 @@ impl<'a> Widget for Block<'a> {
|
||||
.set_symbol(symbols.bottom_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// Title
|
||||
if let Some(title) = self.title {
|
||||
let left_border_dx = if self.borders.intersects(Borders::LEFT) {
|
||||
let lx = if self.borders.intersects(Borders::LEFT) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let right_border_dx = if self.borders.intersects(Borders::RIGHT) {
|
||||
let rx = if self.borders.intersects(Borders::RIGHT) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
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);
|
||||
let width = area.width - lx - rx;
|
||||
buf.set_spans(area.left() + lx, area.top(), &title, 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,7 +14,6 @@ use crate::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Spans,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
@@ -27,9 +26,10 @@ pub trait Shape {
|
||||
/// Label to draw some text on the canvas
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Label<'a> {
|
||||
x: f64,
|
||||
y: f64,
|
||||
spans: Spans<'a>,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub text: &'a str,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -293,15 +293,8 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
|
||||
/// Print a string on the canvas at the given position
|
||||
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(),
|
||||
});
|
||||
pub fn print(&mut self, x: f64, y: f64, text: &'a str, color: Color) {
|
||||
self.labels.push(Label { x, y, text, color });
|
||||
}
|
||||
|
||||
/// Push the last layer if necessary
|
||||
@@ -440,8 +433,6 @@ where
|
||||
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 {
|
||||
@@ -473,12 +464,14 @@ 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_fg(color)
|
||||
.set_bg(self.background_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];
|
||||
@@ -497,7 +490,13 @@ 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_spans(x, y, &label.spans, canvas_area.right() - x);
|
||||
buf.set_stringn(
|
||||
x,
|
||||
y,
|
||||
label.text,
|
||||
(canvas_area.right() - x) as usize,
|
||||
style.fg(label.color),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6122,7 +6122,6 @@ 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,8 +1,3 @@
|
||||
use std::{borrow::Cow, cmp::max};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::layout::Alignment;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
@@ -14,6 +9,8 @@ use crate::{
|
||||
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)]
|
||||
@@ -26,8 +23,6 @@ pub struct Axis<'a> {
|
||||
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> Default for Axis<'a> {
|
||||
@@ -37,7 +32,6 @@ impl<'a> Default for Axis<'a> {
|
||||
bounds: [0.0, 0.0],
|
||||
labels: None,
|
||||
style: Default::default(),
|
||||
labels_alignment: Alignment::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,15 +71,6 @@ impl<'a> Axis<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to determine which style of graphing to use
|
||||
@@ -156,7 +141,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, Default)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct ChartLayout {
|
||||
/// Location of the title of the x axis
|
||||
title_x: Option<(u16, u16)>,
|
||||
@@ -176,6 +161,21 @@ 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
|
||||
@@ -296,8 +296,18 @@ impl<'a> Chart<'a> {
|
||||
y -= 1;
|
||||
}
|
||||
|
||||
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 let Some(ref y_labels) = self.y_axis.labels {
|
||||
let mut max_width = y_labels.iter().map(Span::width).max().unwrap_or_default() as u16;
|
||||
if let Some(ref x_labels) = self.x_axis.labels {
|
||||
if !x_labels.is_empty() {
|
||||
max_width = max(max_width, x_labels[0].content.width() as u16);
|
||||
}
|
||||
}
|
||||
if x + max_width < area.right() {
|
||||
layout.label_y = Some(x);
|
||||
x += max_width;
|
||||
}
|
||||
}
|
||||
|
||||
if self.x_axis.labels.is_some() && y > area.top() {
|
||||
layout.axis_x = Some(y);
|
||||
@@ -352,150 +362,10 @@ impl<'a> Chart<'a> {
|
||||
}
|
||||
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> Widget for Chart<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
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
|
||||
@@ -517,8 +387,33 @@ impl<'a> Widget for Chart<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
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.label_x {
|
||||
let labels = self.x_axis.labels.unwrap();
|
||||
let total_width = labels.iter().map(Span::width).sum::<usize>() 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_span(
|
||||
graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1)
|
||||
- label.content.width() as u16,
|
||||
y,
|
||||
label,
|
||||
label.width() as u16,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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_span(x, graph_area.bottom() - 1 - dy, label, label.width() as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(y) = layout.axis_x {
|
||||
for x in graph_area.left()..graph_area.right() {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::widgets::Widget;
|
||||
|
||||
/// 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.
|
||||
/// A widget to to clear/reset a certain area to allow overdrawing (e.g. for popups)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
||||
@@ -24,7 +24,6 @@ pub struct Gauge<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
ratio: f64,
|
||||
label: Option<Span<'a>>,
|
||||
use_unicode: bool,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
}
|
||||
@@ -35,7 +34,6 @@ impl<'a> Default for Gauge<'a> {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
use_unicode: false,
|
||||
style: Style::default(),
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
@@ -60,7 +58,7 @@ impl<'a> Gauge<'a> {
|
||||
/// Sets ratio ([0.0, 1.0]) directly.
|
||||
pub fn ratio(mut self, ratio: f64) -> Gauge<'a> {
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&ratio),
|
||||
ratio <= 1.0 && ratio >= 0.0,
|
||||
"Ratio should be between 0 and 1 inclusively."
|
||||
);
|
||||
self.ratio = ratio;
|
||||
@@ -84,11 +82,6 @@ impl<'a> 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> {
|
||||
@@ -107,54 +100,33 @@ impl<'a> Widget for Gauge<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
};
|
||||
let center = gauge_area.height / 2 + gauge_area.top();
|
||||
let width = (f64::from(gauge_area.width) * self.ratio).round() as u16;
|
||||
let end = gauge_area.left() + width;
|
||||
// Label
|
||||
let ratio = self.ratio;
|
||||
let label = self
|
||||
.label
|
||||
.unwrap_or_else(|| Span::from(format!("{}%", (ratio * 100.0).round())));
|
||||
for y in gauge_area.top()..gauge_area.bottom() {
|
||||
// render the filled area (left to end)
|
||||
// Gauge
|
||||
for x in gauge_area.left()..end {
|
||||
buf.get_mut(x, y).set_symbol(" ");
|
||||
}
|
||||
|
||||
if y == center {
|
||||
let label_width = label.width() as u16;
|
||||
let middle = (gauge_area.width - label_width) / 2 + gauge_area.left();
|
||||
buf.set_span(middle, y, &label, gauge_area.right() - middle);
|
||||
}
|
||||
|
||||
// Fix colors
|
||||
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,
|
||||
_ => " ",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +174,7 @@ impl<'a> LineGauge<'a> {
|
||||
|
||||
pub fn ratio(mut self, ratio: f64) -> Self {
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&ratio),
|
||||
ratio <= 1.0 && ratio >= 0.0,
|
||||
"Ratio should be between 0 and 1 inclusively."
|
||||
);
|
||||
self.ratio = ratio;
|
||||
|
||||
@@ -5,14 +5,24 @@ use crate::{
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
};
|
||||
use std::iter::{self, Iterator};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone)]
|
||||
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
|
||||
@@ -26,7 +36,7 @@ impl ListState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ListItem<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
@@ -78,8 +88,6 @@ pub struct List<'a> {
|
||||
highlight_style: Style,
|
||||
/// Symbol in front of the selected item (Shift all items to the right)
|
||||
highlight_symbol: Option<&'a str>,
|
||||
/// Whether to repeat the highlight symbol for each line of the selected item
|
||||
repeat_highlight_symbol: bool,
|
||||
}
|
||||
|
||||
impl<'a> List<'a> {
|
||||
@@ -94,7 +102,6 @@ impl<'a> List<'a> {
|
||||
start_corner: Corner::TopLeft,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
repeat_highlight_symbol: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,53 +125,10 @@ impl<'a> List<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
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<'a> StatefulWidget for List<'a> {
|
||||
@@ -190,11 +154,40 @@ impl<'a> StatefulWidget for List<'a> {
|
||||
}
|
||||
let list_height = list_area.height as usize;
|
||||
|
||||
let (start, end) = self.get_items_bounds(state.selected, state.offset, list_height);
|
||||
let mut start = state.offset;
|
||||
let mut end = state.offset;
|
||||
let mut height = 0;
|
||||
for item in self.items.iter().skip(state.offset) {
|
||||
if height + item.height() > list_height {
|
||||
break;
|
||||
}
|
||||
height += item.height();
|
||||
end += 1;
|
||||
}
|
||||
|
||||
let selected = state.selected.unwrap_or(0).min(self.items.len() - 1);
|
||||
while selected >= end {
|
||||
height = height.saturating_add(self.items[end].height());
|
||||
end += 1;
|
||||
while height > list_height {
|
||||
height = height.saturating_sub(self.items[start].height());
|
||||
start += 1;
|
||||
}
|
||||
}
|
||||
while selected < start {
|
||||
start -= 1;
|
||||
height = height.saturating_add(self.items[start].height());
|
||||
while height > list_height {
|
||||
end -= 1;
|
||||
height = height.saturating_sub(self.items[end].height());
|
||||
}
|
||||
}
|
||||
state.offset = start;
|
||||
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
let blank_symbol = " ".repeat(highlight_symbol.width());
|
||||
let blank_symbol = iter::repeat(" ")
|
||||
.take(highlight_symbol.width())
|
||||
.collect::<String>();
|
||||
|
||||
let mut current_height = 0;
|
||||
let has_selection = state.selected.is_some();
|
||||
@@ -226,27 +219,19 @@ impl<'a> StatefulWidget for List<'a> {
|
||||
buf.set_style(area, item_style);
|
||||
|
||||
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) {
|
||||
let elem_x = if has_selection {
|
||||
let symbol = if is_selected {
|
||||
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)
|
||||
};
|
||||
let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style);
|
||||
x
|
||||
} else {
|
||||
x
|
||||
};
|
||||
let max_element_width = (list_area.width - (elem_x - x)) as usize;
|
||||
for (j, line) in item.content.lines.iter().enumerate() {
|
||||
buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
|
||||
}
|
||||
if is_selected {
|
||||
|
||||
@@ -36,7 +36,7 @@ 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::{Cell, Row, Table, TableState};
|
||||
pub use self::table::{Row, Table, TableState};
|
||||
pub use self::tabs::Tabs;
|
||||
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
@@ -62,8 +62,8 @@ bitflags! {
|
||||
|
||||
/// Base requirements for a Widget
|
||||
pub trait Widget {
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom widget.
|
||||
/// Draws the current state of the widget in the given buffer. That the only method required to
|
||||
/// implement a custom widget.
|
||||
fn render(self, area: Rect, buf: &mut Buffer);
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ pub trait Widget {
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::{Backend, TestBackend};
|
||||
/// # use tui::backend::{Backend, TermionBackend};
|
||||
/// # use tui::widgets::{Widget, List, ListItem, ListState};
|
||||
///
|
||||
/// // Let's say we have some events to display.
|
||||
@@ -154,8 +154,9 @@ pub trait Widget {
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let stdout = io::stdout();
|
||||
/// let backend = TermionBackend::new(stdout);
|
||||
/// let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// let mut events = Events::new(vec![
|
||||
/// String::from("Item 1"),
|
||||
|
||||
@@ -40,7 +40,7 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
|
||||
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
|
||||
/// .style(Style::default().fg(Color::White).bg(Color::Black))
|
||||
/// .alignment(Alignment::Center)
|
||||
/// .wrap(Wrap { trim: true, break_words: false });
|
||||
/// .wrap(Wrap { trim: true });
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Paragraph<'a> {
|
||||
@@ -70,7 +70,7 @@ pub struct Paragraph<'a> {
|
||||
/// - 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 });
|
||||
/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
|
||||
/// // Some indented points:
|
||||
/// // - First thing goes here and is
|
||||
/// // long so that it wraps
|
||||
@@ -78,7 +78,7 @@ pub struct Paragraph<'a> {
|
||||
/// // is long enough to wrap
|
||||
///
|
||||
/// // But without trimming, indentation is preserved:
|
||||
/// Paragraph::new(bullet_points).wrap(Wrap { trim: false, break_words: false });
|
||||
/// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
|
||||
/// // Some indented points:
|
||||
/// // - First thing goes here
|
||||
/// // and is long so that it wraps
|
||||
@@ -89,8 +89,6 @@ pub struct Paragraph<'a> {
|
||||
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> {
|
||||
@@ -164,21 +162,15 @@ impl<'a> Widget for Paragraph<'a> {
|
||||
}))
|
||||
});
|
||||
|
||||
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 line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
|
||||
Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
|
||||
} 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.0 {
|
||||
|
||||
@@ -19,7 +19,6 @@ pub struct WordWrapper<'a, 'b> {
|
||||
next_line: Vec<StyledGrapheme<'a>>,
|
||||
/// Removes the leading whitespace from lines
|
||||
trim: bool,
|
||||
break_words: bool,
|
||||
}
|
||||
|
||||
impl<'a, 'b> WordWrapper<'a, 'b> {
|
||||
@@ -27,7 +26,6 @@ impl<'a, 'b> WordWrapper<'a, 'b> {
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
max_line_width: u16,
|
||||
trim: bool,
|
||||
break_words: bool,
|
||||
) -> WordWrapper<'a, 'b> {
|
||||
WordWrapper {
|
||||
symbols,
|
||||
@@ -35,7 +33,6 @@ impl<'a, 'b> WordWrapper<'a, 'b> {
|
||||
current_line: vec![],
|
||||
next_line: vec![],
|
||||
trim,
|
||||
break_words,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +57,7 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
||||
let mut symbols_exhausted = true;
|
||||
for StyledGrapheme { symbol, style } in &mut self.symbols {
|
||||
symbols_exhausted = false;
|
||||
let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
|
||||
let symbol_whitespace = symbol.chars().all(&char::is_whitespace);
|
||||
|
||||
// Ignore characters wider that the total max width.
|
||||
if symbol.width() as u16 > self.max_line_width
|
||||
@@ -80,7 +77,7 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
||||
}
|
||||
|
||||
// Mark the previous symbol as word end.
|
||||
if symbol_whitespace && !prev_whitespace {
|
||||
if symbol_whitespace && !prev_whitespace && symbol != NBSP {
|
||||
symbols_to_last_word_end = self.current_line.len();
|
||||
width_to_last_word_end = current_line_width;
|
||||
}
|
||||
@@ -90,12 +87,11 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
||||
|
||||
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 && !self.break_words {
|
||||
(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 {
|
||||
(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:
|
||||
{
|
||||
@@ -239,7 +235,7 @@ mod test {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
enum Composer {
|
||||
WordWrapper { trim: bool, break_words: bool },
|
||||
WordWrapper { trim: bool },
|
||||
LineTruncator,
|
||||
}
|
||||
|
||||
@@ -248,12 +244,9 @@ mod test {
|
||||
let mut styled =
|
||||
UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
|
||||
let mut composer: Box<dyn LineComposer> = match which {
|
||||
Composer::WordWrapper { trim, break_words } => Box::new(WordWrapper::new(
|
||||
&mut styled,
|
||||
text_area_width,
|
||||
trim,
|
||||
break_words,
|
||||
)),
|
||||
Composer::WordWrapper { trim } => {
|
||||
Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
|
||||
}
|
||||
Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
|
||||
};
|
||||
let mut lines = vec![];
|
||||
@@ -275,14 +268,8 @@ mod test {
|
||||
let width = 40;
|
||||
for i in 1..width {
|
||||
let text = "a".repeat(i);
|
||||
let (word_wrapper, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
&text,
|
||||
width as u16,
|
||||
);
|
||||
let (word_wrapper, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, &text, width as u16);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
|
||||
let expected = vec![text];
|
||||
assert_eq!(word_wrapper, expected);
|
||||
@@ -295,14 +282,7 @@ mod test {
|
||||
let width = 20;
|
||||
let text =
|
||||
"abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
|
||||
let (word_wrapper, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let wrapped: Vec<&str> = text.split('\n').collect();
|
||||
@@ -314,14 +294,8 @@ mod test {
|
||||
fn line_composer_long_word() {
|
||||
let width = 20;
|
||||
let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
|
||||
let (word_wrapper, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width as u16,
|
||||
);
|
||||
let (word_wrapper, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let wrapped = vec![
|
||||
@@ -346,19 +320,10 @@ 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 {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width as u16,
|
||||
);
|
||||
let (word_wrapper_single_space, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
||||
let (word_wrapper_multi_space, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
Composer::WordWrapper { trim: true },
|
||||
text_multi_space,
|
||||
width as u16,
|
||||
);
|
||||
@@ -381,14 +346,7 @@ mod test {
|
||||
fn line_composer_zero_width() {
|
||||
let width = 0;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = Vec::new();
|
||||
@@ -400,14 +358,7 @@ mod test {
|
||||
fn line_composer_max_line_width_of_1() {
|
||||
let width = 1;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
|
||||
@@ -422,14 +373,7 @@ mod test {
|
||||
let width = 1;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
|
||||
両端点では、";
|
||||
let (word_wrapper, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
|
||||
assert_eq!(line_truncator, vec!["", "a"]);
|
||||
@@ -440,14 +384,7 @@ mod test {
|
||||
fn line_composer_word_wrapper_mixed_length() {
|
||||
let width = 20;
|
||||
let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
|
||||
let (word_wrapper, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
@@ -465,15 +402,9 @@ mod test {
|
||||
let width = 20;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
|
||||
では、";
|
||||
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);
|
||||
let (word_wrapper, word_wrapper_width) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, &text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
|
||||
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
|
||||
let wrapped = vec![
|
||||
"コンピュータ上で文字",
|
||||
@@ -490,14 +421,7 @@ mod test {
|
||||
fn line_composer_leading_whitespace_removal() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
||||
let (word_wrapper, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
|
||||
assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
|
||||
@@ -508,14 +432,7 @@ mod test {
|
||||
fn line_composer_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = " ";
|
||||
let (word_wrapper, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec![""]);
|
||||
assert_eq!(line_truncator, vec![" "]);
|
||||
@@ -527,14 +444,7 @@ mod test {
|
||||
fn line_composer_char_plus_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = "a ";
|
||||
let (word_wrapper, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
// What's happening below is: the first line gets consumed, trailing spaces discarded,
|
||||
// after 20 of which a word break occurs (probably shouldn't). The second line break
|
||||
@@ -553,14 +463,8 @@ mod test {
|
||||
// hiragana and katakana...
|
||||
// This happens to also be a test case for mixed width because regular spaces are single width.
|
||||
let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
|
||||
let (word_wrapper, word_wrapper_width) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (word_wrapper, word_wrapper_width) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
@@ -581,26 +485,13 @@ 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 {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
text,
|
||||
width,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
|
||||
|
||||
// Ensure that if the character was a regular space, it would be wrapped differently.
|
||||
let text_space = text.replace('\u{00a0}', " ");
|
||||
let (word_wrapper_space, _) = run_composer(
|
||||
Composer::WordWrapper {
|
||||
trim: true,
|
||||
break_words: true,
|
||||
},
|
||||
&text_space,
|
||||
width,
|
||||
);
|
||||
let text_space = text.replace("\u{00a0}", " ");
|
||||
let (word_wrapper_space, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
|
||||
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
|
||||
}
|
||||
|
||||
@@ -608,14 +499,7 @@ mod 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,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
|
||||
}
|
||||
|
||||
@@ -623,14 +507,7 @@ mod 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,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"]
|
||||
@@ -641,14 +518,7 @@ mod 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,
|
||||
);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
|
||||
@@ -1,346 +1,36 @@
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
layout::{Constraint, 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;
|
||||
|
||||
/// 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)]
|
||||
#[derive(Debug, Clone)]
|
||||
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
|
||||
@@ -354,14 +44,185 @@ impl TableState {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StatefulWidget for Table<'a> {
|
||||
/// 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>>,
|
||||
{
|
||||
type State = TableState;
|
||||
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
// Render block if necessary and get the drawing area
|
||||
let table_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
@@ -371,113 +232,124 @@ impl<'a> StatefulWidget for Table<'a> {
|
||||
None => area,
|
||||
};
|
||||
|
||||
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;
|
||||
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();
|
||||
|
||||
// Draw header
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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
|
||||
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,
|
||||
};
|
||||
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
|
||||
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 {
|
||||
&blank_symbol
|
||||
};
|
||||
let (col, _) =
|
||||
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
|
||||
col
|
||||
state.offset
|
||||
}
|
||||
} else {
|
||||
col
|
||||
0
|
||||
};
|
||||
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);
|
||||
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()),
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
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(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default();
|
||||
StatefulWidget::render(self, area, buf, &mut state);
|
||||
@@ -491,6 +363,7 @@ mod tests {
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn table_invalid_percentages() {
|
||||
Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
|
||||
Table::new([""].iter(), vec![Row::Data([""].iter())].into_iter())
|
||||
.widths(&[Constraint::Percentage(110)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,21 +34,21 @@ fn backend_termion_should_only_write_diffs() -> Result<(), Box<dyn std::error::E
|
||||
let mut s = String::new();
|
||||
// First draw
|
||||
write!(s, "{}", cursor::Goto(1, 1))?;
|
||||
s.push('a');
|
||||
s.push_str("a");
|
||||
write!(s, "{}", color::Fg(color::Reset))?;
|
||||
write!(s, "{}", color::Bg(color::Reset))?;
|
||||
write!(s, "{}", style::Reset)?;
|
||||
write!(s, "{}", cursor::Hide)?;
|
||||
// Second draw
|
||||
write!(s, "{}", cursor::Goto(2, 1))?;
|
||||
s.push('b');
|
||||
s.push_str("b");
|
||||
write!(s, "{}", color::Fg(color::Reset))?;
|
||||
write!(s, "{}", color::Bg(color::Reset))?;
|
||||
write!(s, "{}", style::Reset)?;
|
||||
write!(s, "{}", cursor::Hide)?;
|
||||
// Third draw
|
||||
write!(s, "{}", cursor::Goto(3, 1))?;
|
||||
s.push('c');
|
||||
s.push_str("c");
|
||||
write!(s, "{}", color::Fg(color::Reset))?;
|
||||
write!(s, "{}", color::Bg(color::Reset))?;
|
||||
write!(s, "{}", style::Reset)?;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use std::error::Error;
|
||||
use tui::{
|
||||
backend::{Backend, TestBackend},
|
||||
layout::Rect,
|
||||
widgets::Paragraph,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
@@ -14,23 +11,3 @@ fn terminal_buffer_size_should_be_limited() {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders},
|
||||
@@ -45,302 +45,3 @@ fn widgets_block_renders() {
|
||||
}
|
||||
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 ", " "]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use tui::layout::Alignment;
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
@@ -14,247 +13,6 @@ 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);
|
||||
@@ -465,7 +223,7 @@ fn widgets_chart_can_have_a_legend() {
|
||||
"│ │ •• •• │",
|
||||
"│0.0 │• X Axis│",
|
||||
"│ └─────────────────────────────────────────────────────│",
|
||||
"│ 0.0 50.0 100.0│",
|
||||
"│ 0.0 50.0 100.0 │",
|
||||
"└──────────────────────────────────────────────────────────┘",
|
||||
]);
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Gauge, LineGauge},
|
||||
Terminal,
|
||||
};
|
||||
@@ -23,82 +22,11 @@ fn widgets_gauge_renders() {
|
||||
|
||||
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);
|
||||
.ratio(0.211_313_934_313_1);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -117,89 +45,6 @@ fn widgets_gauge_renders_no_unicode() {
|
||||
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);
|
||||
|
||||
@@ -4,7 +4,6 @@ use tui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Spans,
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Terminal,
|
||||
};
|
||||
@@ -87,114 +86,3 @@ fn widgets_list_should_truncate_items() {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Alignment,
|
||||
text::{Span, Spans, Text},
|
||||
text::{Spans, Text},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Terminal,
|
||||
};
|
||||
@@ -25,10 +25,7 @@ fn widgets_paragraph_can_wrap_its_content() {
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.alignment(alignment)
|
||||
.wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: false,
|
||||
});
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -82,76 +79,6 @@ fn widgets_paragraph_can_wrap_its_content() {
|
||||
);
|
||||
}
|
||||
|
||||
#[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);
|
||||
@@ -164,10 +91,7 @@ fn widgets_paragraph_renders_double_width_graphemes() {
|
||||
let text = vec![Spans::from(s)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: false,
|
||||
});
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -199,10 +123,7 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
|
||||
let text = vec![Spans::from(s)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(Wrap {
|
||||
trim: true,
|
||||
break_words: false,
|
||||
});
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -220,27 +141,6 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
|
||||
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| {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Constraint,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Terminal,
|
||||
};
|
||||
use tui::backend::TestBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Constraint;
|
||||
use tui::widgets::{Block, Borders, Row, Table};
|
||||
use tui::Terminal;
|
||||
|
||||
#[test]
|
||||
fn widgets_table_column_spacing_can_be_changed() {
|
||||
@@ -17,13 +13,16 @@ fn widgets_table_column_spacing_can_be_changed() {
|
||||
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))
|
||||
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),
|
||||
@@ -115,13 +114,16 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
|
||||
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))
|
||||
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);
|
||||
@@ -203,13 +205,16 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
|
||||
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))
|
||||
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);
|
||||
@@ -243,9 +248,9 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
|
||||
// columns of not enough width trims the data
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(11),
|
||||
Constraint::Percentage(11),
|
||||
Constraint::Percentage(11),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(10),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
@@ -264,9 +269,9 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
|
||||
// columns of large width just before pushing a column off
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
@@ -287,12 +292,12 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
|
||||
&[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 │",
|
||||
"│Head1 Head2 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
@@ -309,13 +314,16 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
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))
|
||||
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);
|
||||
@@ -348,18 +356,18 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
// columns of not enough width trims the data
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(11),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Length(20),
|
||||
Constraint::Percentage(11),
|
||||
Constraint::Percentage(10),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Hea Head2 He │",
|
||||
"│Hea Head2 Hea│",
|
||||
"│ │",
|
||||
"│Row Row12 Ro │",
|
||||
"│Row Row22 Ro │",
|
||||
"│Row Row32 Ro │",
|
||||
"│Row Row42 Ro │",
|
||||
"│Row Row12 Row│",
|
||||
"│Row Row22 Row│",
|
||||
"│Row Row32 Row│",
|
||||
"│Row Row42 Row│",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
@@ -369,9 +377,9 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
// columns of large width just before pushing a column off
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Length(10),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(30),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
@@ -408,416 +416,3 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user