Compare commits

..

122 Commits

Author SHA1 Message Date
Florian Dehau
eb47c778db Release v0.9.2 2020-05-10 15:59:40 +02:00
Darrien Glasser
359b7feb8c fix(widgets/canvas): Add bounds check when drawing line high/low (#283)
* Add bounds check when drawing line high/low
* Add test to ensure codepath doesn't break
2020-05-10 14:40:04 +02:00
chrunchyjesus
6ffdede95a chore: add documentation field in Cargo.toml (#277) 2020-05-10 12:27:52 +02:00
24seconds
4db0250b95 Add rust-sadari-cli in Apps using tui section (#278) 2020-05-10 12:25:23 +02:00
SoptikHa2
69780bbbec Add desed to list of apps using tui?
Desed is new application that is using tui-rs. It's debugger for sed with all the things like stepping, setting breakpoints and examining runtime state.
2020-05-09 12:23:09 +02:00
Florian Dehau
fda89d6859 Release v0.9.1 2020-04-16 18:43:13 +02:00
Florian Dehau
5d99b4af00 docs: improve widgets documentation 2020-04-16 18:43:13 +02:00
Florian Dehau
da4d4e1672 test: assert items are correctly truncated in the List widget 2020-04-16 16:24:43 +02:00
Björn Steinbrink
8f9aa276e8 fix(widgets/list): fix line length calculation for selectable lists
The code that outputs the list elements uses the full inner width of its
block, without taking the width of the highlight symbol into
consideration. This allows the elements to overflow the box and draw
over the block's border. To fix that, we need to reduce the target width
for the list elements.
2020-04-16 15:31:37 +02:00
Florian Dehau
8debb0d338 Release v0.9.0 2020-04-14 19:48:18 +02:00
Florian Dehau
bc2a512101 feat: add missing Clone and Copy on types 2020-04-14 19:25:49 +02:00
Florian Dehau
4f728d363f fix(widgets/list): stop highlighting blank placeholders 2020-04-14 18:40:59 +02:00
Florian Dehau
e81af75427 fix(examples): improve input handling in crossterm demo
* avoid stacking events
* ensure tick events are sent at the given tick rate (and not everytime a key is pressed).
2020-04-14 18:00:52 +02:00
Florian Dehau
8387b32bb8 chore: update changelog 2020-04-14 02:17:22 +02:00
Florian Dehau
2fccee740b chore: add command to README to run demos without all unicode symbols 2020-04-14 02:17:22 +02:00
Florian Dehau
c98002eb76 feat: add an option to run the examples without all unicode symbols 2020-04-14 02:17:22 +02:00
Florian Dehau
584e1b0500 refactor(widgets/canvas): allow canvas to render with a simple dot character instead of braille patterns
This change allows developers to gracefully degrade the output if the targeted
terminal does not support the full range of unicode symbols.
2020-04-14 02:17:22 +02:00
Florian Dehau
cee65ed283 feat: allow BarChart and Sparkline to use a more portable set of symbols
Add `BarChart::bar_set` and `Sparkline::bar_set` methods to customize
the set of symbols used to display the data. The new set should give
a better looking output on terminal that do not support a wide range
of unicode symbols.
2020-04-14 02:17:22 +02:00
Stephan Dilly
8104b17ee6 chore: bump crossterm to 0.17
this fixes #250 because crossterm `0.17.3` has a fix for the resize/size issue

Co-Authored-By: Florian Dehau <work@fdehau.com>

Co-authored-by: Florian Dehau <work@fdehau.com>
2020-04-12 19:48:43 +02:00
Stephan Dilly
7676d3c7df add clear widget and popup example utilizing it 2020-04-12 15:16:24 +02:00
orhun
3e6211e0a3 doc: Add kmon to 'apps using tui' in README 2020-04-12 15:05:27 +02:00
Stephan Dilly
05c472b741 add program using tui-rs 2020-04-12 15:02:07 +02:00
Florian Dehau
867ba1fd8c chore: update changelog 2020-03-21 22:38:05 +01:00
Stephan Dilly
fd48719040 fix some typos 2020-03-21 20:55:45 +01:00
Loïc Girault
d987225ac8 Add thick lines and line::Set struct
Add a new style of line and use a struct to avoid duplication of
matching
2020-03-21 13:35:37 +01:00
Florian Dehau
503bdeeadb chore: bump itertools to 0.9 2020-03-13 02:22:48 +01:00
Florian Dehau
d3f1669234 chore(Makefile): add lint to stable and beta rules 2020-03-13 02:22:15 +01:00
Florian Dehau
3f62ce9c19 chore: remove unecessary dependencies
* Remove log, stderrlog, structopt
* Add argh
2020-03-13 02:07:13 +01:00
Florian Dehau
278c153d31 style: remove clippy warnings 2020-03-13 01:12:14 +01:00
Florian Dehau
ae677099d6 feat(widgets/table): allow one row to be selected 2020-03-13 00:36:19 +01:00
Florian Dehau
140db9b2e2 refactor(canvas): update shape drawing strategy
* Update the `Shape` trait. Instead of returning an iterator of point, all
shapes are now aware of the surface they will be drawn to through a `Painter`.
In order to draw themselves, they paint points of the "braille grid".
* Rewrite how lines are drawn using a common line drawing algorithm (Bresenham).
2020-03-12 23:14:46 +01:00
Florian Dehau
a6b35031ae chore: use master branch instead of latest release for crossterm
This will prevent [this](https://github.com/crossterm-rs/crossterm/pull/383)
descriptor leak bug when people use the crossterm backend with the current
master.
2020-03-12 22:44:43 +01:00
hatoo
004cf2687a Add oha to the "apps using tui" list 2020-03-12 22:28:11 +01:00
Clement Tsang
cf8db5ea23 Add bottom to the "apps using tui" list 2020-03-05 23:00:41 +01:00
Vadim Chekan
1683e8d609 Clean redundant generics params in Table (#234)
* Clean redundant generics params in Table

It is possible to use associated types the same way as generics parameters. In fact associated types are nothing more than better organized generics params. For example, there is no need to introduce another param to constraint iterator item to be Display, you can just say `where I::Iterator, I::Item: Display`. This allows to drop type params for Table from 5 to 2.
2020-03-03 09:21:45 +01:00
Benjamin Vaisvil
f372e034e8 added to Apps using tui 2020-03-03 09:15:56 +01:00
Florian Dehau
8c3db49fba chore: bump crossterm to 0.16 2020-02-23 20:14:46 +01:00
Florian Dehau
02b1aac0b0 chore: remove outdated badges 2020-02-23 20:14:14 +01:00
Florian Dehau
6cb57f5d2a feat: add stateful widgets
Most widgets can be drawn directly based on the input parameters. However, some
features may require some kind of associated state to be implemented.

For example, the `List` widget can highlight the item currently selected. This
can be translated in an offset, which is the number of elements to skip in
order to have the selected item within the viewport currently allocated to this
widget. The widget can therefore only provide the following behavior: whenever
the selected item is out of the viewport scroll to a predefined position (make
the selected item the last viewable item or the one in the middle).
Nonetheless, if the widget has access to the last computed offset then it can
implement a natural scrolling experience where the last offset is reused until
the selected item is out of the viewport.

To allow such behavior within the widgets, this commit introduces the following
changes:
- Add a `StatefulWidget` trait with an associated `State` type. Widgets that
can take advantage of having a "memory" between two draw calls needs to
implement this trait.
- Add a `render_stateful_widget` method on `Frame` where the associated
state is given as a parameter.

The chosen approach is thus to let the developers manage their widgets' states
themselves as they are already responsible for the lifecycle of the wigets
(given that the crate exposes an immediate mode api).

The following changes were also introduced:

- `Widget::render` has been deleted. Developers should use `Frame::render_widget`
instead.
- `Widget::background` has been deleted. Developers should use `Buffer::set_background`
instead.
- `SelectableList` has been deleted. Developers can directly use `List` where
`SelectableList` features have been back-ported.
2020-02-23 19:23:37 +01:00
Florian Dehau
67dd1ac608 fix: remove array_into_iter warnings 2020-02-23 16:20:54 +01:00
Malte Tammena
808a5c9ffd Mark Style::* functions const 2020-02-23 15:58:37 +01:00
Florian Dehau
d16db5ed90 style: fix clippy warnings 2020-02-23 15:46:46 +01:00
Florian Dehau
6e24f9d47b style: run cargo fmt 2020-02-23 15:46:30 +01:00
tarkah
92ab09496a add ytop to apps using 2020-02-23 15:40:55 +01:00
Florian Dehau
28017f97ea feat(widgets/chart): add more control on the visibility of the legend 2020-02-23 15:37:50 +01:00
Florian Dehau
ea43413507 fix: remove clippy warnings 2020-01-19 23:11:12 +01:00
Caleb Bassi
829b7b6b70 Change linechart to draw the points also 2020-01-19 21:25:17 +01:00
Caleb Bassi
262bf441ce Add linechart support
Closes #73

This commit only adds support for linecharts for the braille marker.
2020-01-19 21:25:17 +01:00
Caleb Bassi
7aae9b380e Add header_gap field to Table 2020-01-19 20:30:28 +01:00
Florian Dehau
d50327548b style: run rustfmt 2020-01-19 18:44:00 +01:00
Florian Dehau
e6ce0ab9a7 refactor(examples): add input modes to user input examples 2020-01-19 18:41:00 +01:00
Florian Dehau
9085c81e76 refactor: clean up border type for blocks
* Merge line symbols in a single module.
* Replace set_border_type with border_type to match other builder methods.
* Remove unecessary branching.
2020-01-19 15:44:03 +01:00
Matthew Stevenson
682349c03e update block example; add BorderType to exposed widgets API 2020-01-19 15:17:59 +01:00
Matthew Stevenson
a72389b28c revert to single Block struct; add set_border_type method and BorderType enum 2020-01-19 15:17:59 +01:00
Matthew Stevenson
f1bc00b67f add rounded corners and double borders to block example 2020-01-19 15:17:59 +01:00
Matthew Stevenson
06d159fb7b add RoundedBlock and DoubleBlock structs that impl From Block; add Block::rounded() and Block::double_border() 2020-01-19 15:17:59 +01:00
Matthew Stevenson
578560766d add round corners and double lines to symbols 2020-01-19 15:17:59 +01:00
Caleb Bassi
9e5c924ef1 Fix crossterm link in readme 2020-01-19 15:09:12 +01:00
Aram Drevekenin
cf39de882a docs(readme): add bandwhich to "apps using tui" 2020-01-19 15:08:04 +01:00
Florian Dehau
8293cef703 Release v0.8.0 2019-12-15 23:12:55 +01:00
Timon
60b99cfc66 feat: bump crossterm to 0.14 2019-12-15 23:03:02 +01:00
Florian Dehau
7cc4189eb0 chore: update issue templates 2019-12-13 21:32:20 +01:00
Florian Dehau
86d4a32314 chore: update issue templates 2019-12-13 21:30:07 +01:00
Florian Dehau
67c9c64eab chore: add spotify-tui to the list of apps using tui 2019-12-13 21:19:34 +01:00
Kyle Ruzic
b8d0f947e8 Added a verticle 'cross' to the symbols as it was missing for no real reason 2019-12-13 20:36:08 +01:00
Sebastian Woetzel
bbd4363fa9 Bugfix: title_style was not used to style the axis title 2019-12-13 20:26:20 +01:00
Florian Dehau
e0083fb8de chore: make the onboarding easier for Windows users. 2019-12-13 20:19:59 +01:00
Florian Dehau
3abafc307c Release v0.7.0 2019-11-29 10:05:06 +01:00
Florian Dehau
055af0f78a chore: bump dev dependencies
* bump rand to 0.7
* bump structopt to 0.3
2019-11-29 09:20:31 +01:00
Timon
e4873e4da9 feat(backend): bump crossterm to 0.13
* removed flush calls because execute already calls flush under the hood.
* moved some static functions into From traits
* removed useless clone in demo
* upgrade to crossterm 0.13
* map all errors
2019-11-29 09:06:59 +01:00
Florian Dehau
2233cdc9cc chore: add CI based on github actions 2019-11-05 09:10:57 +01:00
Florian Dehau
816bc9b5c8 style: fix formatting and clippy issues 2019-11-05 08:08:14 +01:00
Florian Dehau
a82c82fcd7 fix(widgets): remove compilation warning in table widget 2019-10-31 09:27:08 +01:00
TheZoq2
bb28d02277 Update docs to point encourage installing 0.6 2019-10-31 09:18:51 +01:00
Jeffas
94877f4e7e Use constraints for table column widths
This allows table column widths to be adapted more and scale with the
UI.

The constraints are solved using the Cassowary solver. An added
constraint for fitting them all in the width is added.
2019-10-31 09:18:24 +01:00
Florian Dehau
3747ddbefb feat(backend): Refactor crossterm backend
* Remove compilation warnings
* Fix rendering artifacts in the crossterm demo. In particular, the bold modifier
was leaking on most of the terminal screen because the old logic was not
properly unsetting the bold modifier after use (took inspiration of the termion
backend implementation)
2019-10-31 09:17:47 +01:00
Florian Dehau
42731da546 Enable build failure on compilation warnings 2019-10-31 09:17:47 +01:00
Joseph Knight
e183d63a5e typo in barcharg.rs 2019-08-11 14:03:58 +02:00
Joseph Knight
e5fdd442c3 typo in sparkline.rs 2019-08-11 14:03:58 +02:00
Joseph Knight
97357c0e08 typo in curses.rs 2019-08-11 14:03:58 +02:00
Joseph Knight
8649ce4c78 fixed typo in symbols.rs 2019-08-11 14:03:58 +02:00
Timon_Post
a0f6605f59 Implemented command api crossterm, for better perfomance. 2019-08-04 13:18:03 +02:00
Jeffas
db9b1dd689 Make margins be vertical or horizontal
This adds support for margins to be either vertical or horizontal, or
both.
2019-07-31 08:02:50 +02:00
Florian Dehau
9c8d62151b Replace build status badge in README 2019-07-16 08:31:23 +02:00
Florian Dehau
c44d521279 Remove appveyor and travis config 2019-07-16 08:31:23 +02:00
Florian Dehau
ba9da05cef Update azure pipelines config 2019-07-16 08:31:23 +02:00
Florian Dehau
abd552fde6 Run cargo fmt 2019-07-16 08:31:23 +02:00
Florian Dehau
3726761549 List required features for all examples 2019-07-16 08:31:23 +02:00
Florian Dehau
06c7145ac5 Add azure pipelines config 2019-07-16 06:40:38 +02:00
Joe Ardent
85f74dd802 Fix typo in table example. 2019-07-16 06:16:14 +02:00
Joe Ardent
86f681a007 Silence check warnings about [lack of] use of 'dyn' for boxed trait objects. 2019-07-16 06:16:14 +02:00
Florian Dehau
bd5862437d Release v0.6.2 2019-07-16 05:53:33 +02:00
Florian Dehau
a3827aaeae Remove try call in termion backend 2019-07-16 05:46:53 +02:00
Jeremy Day
47c68e40a2 fix for canvas rendering edge cases causing overflow errors 2019-06-23 11:12:51 +02:00
Florian Dehau
2a7eec816a Add PartialEq to Text 2019-06-16 23:14:17 +02:00
Florian Dehau
fe0ddf6c83 Release v0.6.1 2019-06-16 23:05:41 +02:00
Florian Dehau
9a73ead88d Improve crossterm demo
* Use AlternateScreen
* Handle input events
2019-06-16 23:03:36 +02:00
Florian Dehau
8fbb764c9e Move Borders documentation 2019-06-16 23:02:52 +02:00
Florian Dehau
4756801fd9 Format 2019-06-16 22:21:55 +02:00
Russ
f0e0b515ad avoid divide by zero 2019-06-16 20:29:14 +02:00
defiori
25a0825ae4 fix: curses backend cursor positions 2019-06-16 20:22:24 +02:00
scauligi
b1ac297d71 fix crossterm terminal size and dark gray color 2019-06-16 20:17:31 +02:00
Sebastian Thiel
2dfe9c1663 [example: user_input] Assure the cursor responds immediatel when hitting backspace
This was discovered with the termion backend in alacritty on OSX.
2019-06-03 20:12:10 +02:00
nytopop
8a9c76b003 Don't highlight Tabs separator behind selection 2019-06-03 20:11:03 +02:00
DarrienG
41cdd3e261 Provide clone and debug for Text type 2019-05-28 07:52:24 +02:00
Florian Dehau
fe17165c39 Release v0.6.0 2019-05-18 18:52:58 +02:00
svartalf
e18671c1e4 Relaxing crossterm dependency version 2019-05-18 18:49:12 +02:00
Timon_Post
b5f6219d39 updated to 0.9.4 2019-05-17 14:25:55 +02:00
Timon_Post
5ed82aac5f removed project files 2019-05-17 14:25:55 +02:00
Timon_Post
f6a0a91a23 fmt 2019-05-17 14:25:55 +02:00
timonpost@hotmail.nl
5645d0de03 gitignore 2019-05-17 14:25:55 +02:00
timonpost@hotmail.nl
ffaaf5e39c review update 2019-05-17 14:25:55 +02:00
timonpost@hotmail.nl
567cf7b8e5 update 0.9.2 2019-05-17 14:25:55 +02:00
Florian Dehau
5f8dd38135 Release v0.5.1 2019-04-14 12:18:45 +02:00
Florian Dehau
a74d335cb4 Fix clippy warnings 2019-04-14 11:48:35 +02:00
Florian Dehau
6d594143ed Format 2019-04-14 11:43:12 +02:00
Florian Dehau
7a5ad3fbdb Fix sparkline panic when max is zero 2019-04-14 11:35:41 +02:00
lcolaholicl
584f7688f4 Fix a wrongly linked link 2019-04-02 12:35:37 +02:00
Florian Dehau
4436110c44 Improve onboarding in documentation 2019-03-24 21:37:55 +01:00
lws
8a7c9d49b2 fix typo of CHANGELOG 2019-03-17 17:43:24 +01:00
lws
b5d41caace fix typo of CHANFELOG 2019-03-17 17:43:24 +01:00
Curtis Malainey
206813d560 fix typo 2019-03-11 11:58:30 +01:00
65 changed files with 4095 additions and 1859 deletions

30
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,30 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
If possible include a code sample exhibiting the problem.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Linux,Windows]
- Terminal Emulator [e.g xterm, Konsole, Terminal, iTerm2, ConEmu]
- Font [e.g Inconsolata, Monospace]
- Crate version [e.g. 0.7]
- Backend [e.g termion, crossterm]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**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 [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

70
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
on: [push, pull_request]
name: CI
jobs:
linux:
name: Linux
runs-on: ubuntu-latest
steps:
- name: "Install dependencies"
run: sudo apt-get install libncurses5-dev
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: default
toolchain: stable
override: true
- name: "Format"
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: "Check"
uses: actions-rs/cargo@v1
with:
command: check
args: --examples
- name: "Check (crossterm)"
uses: actions-rs/cargo@v1
with:
command: check
args: --no-default-features --features=crossterm --example crossterm_demo
- name: "Check (rustbox)"
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features=rustbox --example rustbox_demo
- name: "Check (curses)"
uses: actions-rs/cargo@v1
with:
command: check
args: --no-default-features --features=curses --example curses_demo
- name: "Test"
uses: actions-rs/cargo@v1
with:
command: test
- name: "Clippy"
uses: actions-rs/cargo@v1
with:
command: clippy
windows:
name: Windows
runs-on: windows-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: default
toolchain: stable
override: true
- name: "Check (crossterm)"
uses: actions-rs/cargo@v1
with:
command: check
args: --no-default-features --features=crossterm --example crossterm_demo
- name: "Test (crossterm)"
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features=crossterm --tests --examples

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ Cargo.lock
*.log
*.rs.rustfmt
.gdb_history
.idea/

View File

@@ -1,21 +0,0 @@
language: rust
rust:
- stable
- beta
- nightly
cache: cargo
matrix:
fast_finish: true
allow_failures:
- rust: nightly
before_script:
- rustup component add rustfmt-preview
script:
- if [ "$TRAVIS_RUST_VERSION" == "stable" ]; then make fmt; fi
- make build
- make test

View File

@@ -2,9 +2,213 @@
## To be released
## v0.5.0 - 2019-03-10
## v0.9.2 - 2020-05-10
### Added
### Bug Fixes
* Fix usize overflows in `widgets::canvas::Line` drawing algorithm.
## v0.9.1 - 2020-04-16
### Bug Fixes
* The `List` widget now takes into account the width of the `highlight_symbol`
when calculating the total width of its items. It prevents items to overflow
outside of the widget area.
## v0.9.0 - 2020-04-14
### Features
* Introduce stateful widgets, i.e widgets that can take advantage of keeping
some state around between two draw calls (#210 goes a bit more into the
details).
* Allow a `Table` row to be selected.
```rust
// State initialization
let mut state = TableState::default();
// In the terminal.draw closure
let header = ["Col1", "Col2", "Col"];
let rows = [
Row::Data(["Row11", "Row12", "Row13"].into_iter())
];
let table = Table::new(header.into_iter(), rows.into_iter());
f.render_stateful_widget(table, area, &mut state);
// In response to some event:
state.select(Some(1));
```
* Add a way to choose the type of border used to draw a block. You can now
choose from plain, rounded, double and thick lines.
* Add a `graph_type` property on the `Dataset` of a `Chart` widget. By
default it will be `Scatter` where the points are drawn as is. An other
option is `Line` where a line will be draw between each consecutive points
of the dataset.
* Style methods are now const, allowing you to initialize const `Style`
objects.
* Improve control over whether the legend in the `Chart` widget is shown or
not. You can now set custom constraints using
`Chart::hidden_legend_constraints`.
* Add `Table::header_gap` to add some space between the header and the first
row.
* Remove `log` from the dependencies
* Add a way to use a restricted set of unicode symbols in several widgets to
improve portability in exchange of a degraded output. (see `BarChart::bar_set`,
`Sparkline::bar_set` and `Canvas::marker`). You can check how the
`--enhanced-graphics` flag is used in the demos.
### Breaking Changes
* `Widget::render` has been deleted. You should now use `Frame::render_widget`
to render a widget on the corresponding `Frame`. This makes the `Widget`
implementation totally decoupled from the `Frame`.
```rust
// Before
Block::default().render(&mut f, size);
// After
let block = Block::default();
f.render_widget(block, size);
```
* `Widget::draw` has been renamed to `Widget::render` and the signature has
been updated to reflect that widgets are consumable objects. Thus the method
takes `self` instead of `&mut self`.
```rust
// Before
impl Widget for MyWidget {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
}
}
/// After
impl Widget for MyWidget {
fn render(self, arera: Rect, buf: &mut Buffer) {
}
}
```
* `Widget::background` has been replaced by `Buffer::set_background`
```rust
// Before
impl Widget for MyWidget {
fn render(self, arera: Rect, buf: &mut Buffer) {
self.background(area, buf, self.style.bg);
}
}
// After
impl Widget for MyWidget {
fn render(self, arera: Rect, buf: &mut Buffer) {
buf.set_background(area, self.style.bg);
}
}
```
* Update the `Shape` trait for objects that can be draw on a `Canvas` widgets.
Instead of returning an iterator over its points, a `Shape` is given a
`Painter` object that provides a `paint` as well as a `get_point` method. This
gives the `Shape` more information about the surface it will be drawn to. In
particular, this change allows the `Line` shape to use a more precise and
efficient drawing algorithm (Bresenham's line algorithm).
* `SelectableList` has been deleted. You can now take advantage of the
associated `ListState` of the `List` widget to select an item.
```rust
// Before
List::new(&["Item1", "Item2", "Item3"])
.select(Some(1))
.render(&mut f, area);
// After
// State initialization
let mut state = ListState::default();
// In the terminal.draw closure
let list = List::new(&["Item1", "Item2", "Item3"]);
f.render_stateful_widget(list, area, &mut state);
// In response to some events
state.select(Some(1));
```
* `widgets::Marker` has been moved to `symbols::Marker`
## v0.8.0 - 2019-12-15
### Breaking Changes
* Bump crossterm to 0.14.
* Add cross symbol to the symbols list.
### Bug Fixes
* Use the value of `title_style` to style the title of `Axis`.
## v0.7.0 - 2019-11-29
### Breaking Changes
* Use `Constraint` instead of integers to specify the widths of the `Table`
widget's columns. This will allow more responsive tables.
```rust
Table::new(header, row)
.widths(&[15, 15, 10])
.render(f, chunk);
```
becomes:
```rust
Table::new(header, row)
.widths(&[
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(10),
])
.render(f, chunk);
```
* Bump crossterm to 0.13.
* Use Github Actions for CI (Travis and Azure Pipelines integrations have been deleted).
### Features
* Add support for horizontal and vertical margins in `Layout`.
## v0.6.2 - 2019-07-16
### Features
* `Text` implements PartialEq
### Bug Fixes
* Avoid overflow errors in canvas
## v0.6.1 - 2019-06-16
### Bug Fixes
* Avoid a division by zero when all values in a barchart are equal to 0.
* Fix the inverted cursor position in the curses backend.
* Ensure that the correct terminal size is returned when using the crossterm
backend.
* Avoid highlighting the separator after the selected item in the Tabs widget.
## v0.6.0 - 2019-05-18
### Breaking Changes
* Update crossterm backend
## v0.5.1 - 2019-04-14
### Bug Fixes
* Fix a panic in the Sparkline widget
## v0.5.0 - 2019-03-10
### Features
* Add a new curses backend (with Windows support thanks to `pancurses`).
* Add `Backend::get_cursor` and `Backend::set_cursor` methods to query and
@@ -14,7 +218,7 @@ set the position of the cursor.
* Add `Ratio` as a new variant of layout `Constraint`. It can be used to define
exact ratios constraints.
### Changed
### Breaking Changes
* Add support for multiple modifiers on the same `Style` by changing `Modifier`
from an enum to a bitflags struct.
@@ -33,14 +237,14 @@ let style = Style::default().modifier(Modifier::ITALIC);
let style = Style::default().modifier(Modifier::ITALIC | Modifier::BOLD);
```
### Fixed
### Bug Fixes
* Ensure correct behavoir of the alternate screens with the `Crossterm` backend.
* Fix out of bounds panic when two `Buffer` are merged.
## v0.4.0 - 2019-02-03
### Added
### Features
* Add a new canvas shape: `Rectangle`.
* Official support of `Crossterm` backend.
@@ -49,11 +253,11 @@ let style = Style::default().modifier(Modifier::ITALIC | Modifier::BOLD);
* The gauge widget accepts a ratio (f64 between 0 and 1) in addition of a
percentage.
### Changed
### Breaking Changes
* Upgrade to Rust 2018 edition.
### Fixed
### Bug Fixes
* Fix rendering of double-width characters.
* Fix race condition on the size of the terminal and expose a size that is
@@ -62,19 +266,19 @@ safe to use when drawing through `Frame::size`.
## v0.3.0 - 2018-11-04
### Added
### Features
* Add experimental test backend
## v0.3.0-beta.3 - 2018-09-24
### Changed
### Features
* `show_cursor` is called when `Terminal` is dropped if the cursor is hidden.
## v0.3.0-beta.2 - 2018-09-23
### Changed
### Breaking Changes
* Remove custom `termion` backends. This is motivated by the fact that
`termion` structs are meant to be combined/wrapped to provide additional
@@ -100,7 +304,7 @@ additional `termion` features.
## v0.3.0-beta.1 - 2018-09-08
### Changed
### Breaking Changes
* Replace `Item` by a generic and flexible `Text` that can be used in both
`Paragraph` and `List` widgets.
@@ -108,11 +312,11 @@ additional `termion` features.
## v0.3.0-beta.0 - 2018-09-04
### Added
### Features
* Add a basic `Crossterm` backend
### Changed
### Breaking Changes
* Remove `Group` and introduce `Layout` in its place
- `Terminal` is no longer required to compute a layout
@@ -132,24 +336,24 @@ of `Text` items
## v0.2.3 - 2018-06-09
### Added
### Features
* Add `start_corner` option for `List`
* Add more text aligment options for `Paragraph`
## v0.2.2 - 2018-05-06
### Added
### Features
* `Terminal` implements `Debug`
### Changed
### Breaking Changes
* Use `FnOnce` instead of `FnMut` in Group::render
## v0.2.1 - 2018-04-01
### Added
### Features
* Add `AlternateScreenBackend` in `termion` backend
* Add `TermionBackend::with_stdout` in order to let an user of the library
@@ -157,7 +361,7 @@ provides its own termion struct
* Add tests and documentation for `Buffer::pos_of`
* Remove leading whitespaces when wrapping text
### Fixed
### Bug Fixes
* Fix `debug_assert` in `Buffer::pos_of`
* Pass the style of `SelectableList` to the underlying `List`
@@ -166,21 +370,16 @@ provides its own termion struct
## v0.2.0 - 2017-12-26
### Added
### Features
* Add `MouseBackend` in `termion` backend to handle scroll and mouse events
* Add generic `Item` for items in a `List`
* Drop `log4rs` as a dev-dependencies in favor of `stderrlog`
### Changed
### Breaking Changes
* Rename `TermionBackend` to `RawBackend` (to distinguish it from the `MouseBackend`)
* Generic parameters for `List` to allow passing iterators as items
* Generic parameters for `Table` to allow using iterators as rows and header
* Generic parameters for `Tabs`
* Rename `border` bitflags to `Borders`
* Run latest `rustfmt` on all sources
### Removed
* Drop `log4rs` as a dev-dependencies in favor of `stderrlog`

View File

@@ -1,20 +1,19 @@
[package]
name = "tui"
version = "0.5.0"
version = "0.9.2"
authors = ["Florian Dehau <work@fdehau.com>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
documentation = "https://docs.rs/tui/0.9.2/tui/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/fdehau/tui-rs"
license = "MIT"
exclude = ["assets/*", ".travis.yml"]
exclude = ["assets/*", ".github"]
autoexamples = true
edition = "2018"
[badges]
travis-ci = { repository = "fdehau/tui-rs" }
appveyor = { repository = "fdehau/tui-rs" }
[features]
default = ["termion"]
@@ -23,22 +22,89 @@ curses = ["easycurses", "pancurses"]
[dependencies]
bitflags = "1.0"
cassowary = "0.3"
itertools = "0.8"
log = "0.4"
itertools = "0.9"
either = "1.5"
unicode-segmentation = "1.2"
unicode-width = "0.1"
termion = { version = "1.5", optional = true }
rustbox = { version = "0.11", optional = true }
crossterm = { version = "0.6", optional = true }
crossterm = { version = "0.17", optional = true }
easycurses = { version = "0.12.2", optional = true }
pancurses = { version = "0.16.1", optional = true, features = ["win32a"] }
[dev-dependencies]
stderrlog = "0.4"
rand = "0.6"
failure = "0.1"
structopt = "0.2"
rand = "0.7"
argh = "0.1"
[[example]]
name = "canvas"
path = "examples/canvas.rs"
required-features = ["termion"]
[[example]]
name = "user_input"
path = "examples/user_input.rs"
required-features = ["termion"]
[[example]]
name = "gauge"
path = "examples/gauge.rs"
required-features = ["termion"]
[[example]]
name = "barchart"
path = "examples/barchart.rs"
required-features = ["termion"]
[[example]]
name = "chart"
path = "examples/chart.rs"
required-features = ["termion"]
[[example]]
name = "paragraph"
path = "examples/paragraph.rs"
required-features = ["termion"]
[[example]]
name = "list"
path = "examples/list.rs"
required-features = ["termion"]
[[example]]
name = "table"
path = "examples/table.rs"
required-features = ["termion"]
[[example]]
name = "tabs"
path = "examples/tabs.rs"
required-features = ["termion"]
[[example]]
name = "custom_widget"
path = "examples/custom_widget.rs"
required-features = ["termion"]
[[example]]
name = "layout"
path = "examples/layout.rs"
required-features = ["termion"]
[[example]]
name = "popup"
path = "examples/popup.rs"
required-features = ["termion"]
[[example]]
name = "block"
path = "examples/block.rs"
required-features = ["termion"]
[[example]]
name = "sparkline"
path = "examples/sparkline.rs"
required-features = ["termion"]
[[example]]
name = "termion_demo"

View File

@@ -102,11 +102,11 @@ watch-doc: ## Watch file changes and rebuild the documentation if any
.PHONY: stable
stable: RUST_CHANNEL = stable
stable: build test ## Run build and tests for stable
stable: build lint test ## Run build and tests for stable
.PHONY: beta
beta: RUST_CHANNEL = beta
beta: build test ## Run build and tests for beta
beta: build lint test ## Run build and tests for beta
.PHONY: nightly
nightly: RUST_CHANNEL = nightly

View File

@@ -1,7 +1,6 @@
# tui-rs
[![Build Status](https://travis-ci.org/fdehau/tui-rs.svg?branch=master)](https://travis-ci.org/fdehau/tui-rs)
[![Build status](https://ci.appveyor.com/api/projects/status/t724mb1q31xpyxy5/branch/master?svg=true)](https://ci.appveyor.com/project/fdehau/tui-rs/branch/master)
[![Build Status](https://github.com/fdehau/tui-rs/workflows/CI/badge.svg)](https://github.com/fdehau/tui-rs/actions?query=workflow%3ACI+)
[![Crate Status](https://img.shields.io/crates/v/tui.svg)](https://crates.io/crates/tui)
[![Docs Status](https://docs.rs/tui/badge.svg)](https://docs.rs/crate/tui/)
@@ -17,10 +16,10 @@ can either choose from:
- [termion](https://github.com/ticki/termion)
- [rustbox](https://github.com/gchp/rustbox)
- [crossterm](https://github.com/TimonPost/crossterm)
- [crossterm](https://github.com/crossterm-rs/crossterm)
- [pancurses](https://github.com/ihalila/pancurses)
However, some features may only be available in one of the three.
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
@@ -45,9 +44,25 @@ run:
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](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 crossterm_demo --no-default-features --features="crossterm" --release -- --tick-rate 200 --enhanced-graphics false
```
### Widgets
The library comes with the following list of widgets:
@@ -56,7 +71,7 @@ The library comes with the following list of widgets:
* [Gauge](examples/gauge.rs)
* [Sparkline](examples/sparkline.rs)
* [Chart](examples/chart.rs)
* [BarChart](examples/bar_chart.rs)
* [BarChart](examples/barchart.rs)
* [List](examples/list.rs)
* [Table](examples/table.rs)
* [Paragraph](examples/paragraph.rs)
@@ -72,6 +87,19 @@ You can run all examples by running `make run-examples`.
* [tui-logger](https://github.com/gin66/tui-logger)
### Apps using tui
* [spotify-tui](https://github.com/Rigellute/spotify-tui)
* [bandwhich](https://github.com/imsnif/bandwhich)
* [kmon](https://github.com/orhun/kmon)
* [ytop](https://github.com/cjbassi/ytop)
* [zenith](https://github.com/bvaisvil/zenith)
* [bottom](https://github.com/ClementTsang/bottom)
* [oha](https://github.com/hatoo/oha)
* [gitui](https://github.com/extrawurst/gitui)
* [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli)
* [desed](https://github.com/SoptikHa2/desed)
### Alternatives
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an

View File

@@ -1,19 +0,0 @@
environment:
matrix:
# Stable channel
- TARGET: x86_64-pc-windows-gnu
CHANNEL: stable
- TARGET: x86_64-pc-windows-msvc
CHANNEL: stable
install:
- curl -sSf -o rustup-init.exe https://win.rustup.rs
- rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustc -Vv
- cargo -V
build: false
test_script:
- cargo build --no-default-features --features crossterm

View File

@@ -1,19 +1,16 @@
#[allow(dead_code)]
mod util;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{BarChart, Block, Borders, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{BarChart, Block, Borders},
Terminal,
};
struct App<'a> {
data: Vec<(&'a str, u64)>,
@@ -57,7 +54,7 @@ impl<'a> App<'a> {
}
}
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -79,36 +76,37 @@ fn main() -> Result<(), failure::Error> {
.margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
BarChart::default()
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
.bar_width(9)
.style(Style::default().fg(Color::Yellow))
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow))
.render(&mut f, chunks[0]);
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
BarChart::default()
.block(Block::default().title("Data2").borders(Borders::ALL))
.data(&app.data)
.bar_width(5)
.bar_gap(3)
.style(Style::default().fg(Color::Green))
.value_style(Style::default().bg(Color::Green).modifier(Modifier::BOLD))
.render(&mut f, chunks[0]);
BarChart::default()
.block(Block::default().title("Data3").borders(Borders::ALL))
.data(&app.data)
.style(Style::default().fg(Color::Red))
.bar_width(7)
.bar_gap(0)
.value_style(Style::default().bg(Color::Red))
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC))
.render(&mut f, chunks[1]);
}
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
f.render_widget(barchart, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let barchart = BarChart::default()
.block(Block::default().title("Data2").borders(Borders::ALL))
.data(&app.data)
.bar_width(5)
.bar_gap(3)
.style(Style::default().fg(Color::Green))
.value_style(Style::default().bg(Color::Green).modifier(Modifier::BOLD));
f.render_widget(barchart, chunks[0]);
let barchart = BarChart::default()
.block(Block::default().title("Data3").borders(Borders::ALL))
.data(&app.data)
.style(Style::default().fg(Color::Red))
.bar_width(7)
.bar_gap(0)
.value_style(Style::default().bg(Color::Red))
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC));
f.render_widget(barchart, chunks[1]);
})?;
match events.next()? {

View File

@@ -1,20 +1,18 @@
#[allow(dead_code)]
mod util;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, BorderType, Borders},
Terminal,
};
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -32,7 +30,11 @@ fn main() -> Result<(), failure::Error> {
// 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();
Block::default().borders(Borders::ALL).render(&mut 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)
@@ -43,35 +45,33 @@ fn main() -> Result<(), failure::Error> {
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
Block::default()
let block = Block::default()
.title("With background")
.title_style(Style::default().fg(Color::Yellow))
.style(Style::default().bg(Color::Green))
.render(&mut f, chunks[0]);
Block::default()
.style(Style::default().bg(Color::Green));
f.render_widget(block, chunks[0]);
let title_style = Style::default()
.fg(Color::White)
.bg(Color::Red)
.modifier(Modifier::BOLD);
let block = Block::default()
.title("Styled title")
.title_style(
Style::default()
.fg(Color::White)
.bg(Color::Red)
.modifier(Modifier::BOLD),
)
.render(&mut f, chunks[1]);
.title_style(title_style);
f.render_widget(block, chunks[1]);
}
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
Block::default()
.title("With borders")
.borders(Borders::ALL)
.render(&mut f, chunks[0]);
Block::default()
.title("With styled borders")
let block = Block::default().title("With borders").borders(Borders::ALL);
f.render_widget(block, chunks[0]);
let block = Block::default()
.title("With styled borders and doubled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.render(&mut f, chunks[1]);
.border_type(BorderType::Double);
f.render_widget(block, chunks[1]);
}
})?;

View File

@@ -1,29 +1,27 @@
#[allow(dead_code)]
mod util;
use std::io;
use std::time::Duration;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout, Rect};
use tui::style::Color;
use tui::widgets::canvas::{Canvas, Map, MapResolution, Rectangle};
use tui::widgets::{Block, Borders, Widget};
use tui::Terminal;
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::TermionBackend,
layout::{Constraint, Direction, Layout, Rect},
style::Color,
widgets::{
canvas::{Canvas, Map, MapResolution, Rectangle},
Block, Borders,
},
Terminal,
};
struct App {
x: f64,
y: f64,
ball: Rect,
ball: Rectangle,
playground: Rect,
vx: u16,
vy: u16,
vx: f64,
vy: f64,
dir_x: bool,
dir_y: bool,
}
@@ -33,21 +31,29 @@ impl App {
App {
x: 0.0,
y: 0.0,
ball: Rect::new(10, 30, 10, 10),
ball: Rectangle {
x: 10.0,
y: 30.0,
width: 10.0,
height: 10.0,
color: Color::Yellow,
},
playground: Rect::new(10, 10, 100, 100),
vx: 1,
vy: 1,
vx: 1.0,
vy: 1.0,
dir_x: true,
dir_y: true,
}
}
fn update(&mut self) {
if self.ball.left() < self.playground.left() || self.ball.right() > self.playground.right()
if self.ball.x < self.playground.left() as f64
|| self.ball.x + self.ball.width > self.playground.right() as f64
{
self.dir_x = !self.dir_x;
}
if self.ball.top() < self.playground.top() || self.ball.bottom() > self.playground.bottom()
if self.ball.y < self.playground.top() as f64
|| self.ball.y + self.ball.height > self.playground.bottom() as f64
{
self.dir_y = !self.dir_y;
}
@@ -66,7 +72,7 @@ impl App {
}
}
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -77,7 +83,7 @@ fn main() -> Result<(), failure::Error> {
// Setup event handlers
let config = Config {
tick_rate: Duration::from_millis(100),
tick_rate: Duration::from_millis(250),
..Default::default()
};
let events = Events::with_config(config);
@@ -91,7 +97,7 @@ fn main() -> Result<(), failure::Error> {
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
Canvas::default()
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.paint(|ctx| {
ctx.draw(&Map {
@@ -101,19 +107,16 @@ fn main() -> Result<(), failure::Error> {
ctx.print(app.x, -app.y, "You are here", Color::Yellow);
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
.render(&mut f, chunks[0]);
Canvas::default()
.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(&Rectangle {
rect: app.ball,
color: Color::Yellow,
});
ctx.draw(&app.ball);
})
.x_bounds([10.0, 110.0])
.y_bounds([10.0, 110.0])
.render(&mut f, chunks[1]);
.y_bounds([10.0, 110.0]);
f.render_widget(canvas, chunks[1]);
})?;
match events.next()? {

View File

@@ -1,19 +1,31 @@
#[allow(dead_code)]
mod util;
use std::io;
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::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
symbols,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
Terminal,
};
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use crate::util::SinSignal;
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)];
const DATA2: [(f64, f64); 7] = [
(0.0, 0.0),
(10.0, 1.0),
(20.0, 0.5),
(30.0, 1.5),
(40.0, 1.0),
(50.0, 2.5),
(60.0, 3.0),
];
struct App {
signal1: SinSignal,
@@ -52,7 +64,7 @@ impl App {
}
}
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -69,10 +81,38 @@ fn main() -> Result<(), failure::Error> {
loop {
terminal.draw(|mut f| {
let size = f.size();
Chart::default()
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]
.as_ref(),
)
.split(size);
let x_labels = [
format!("{}", app.window[0]),
format!("{}", (app.window[0] + app.window[1]) / 2.0),
format!("{}", app.window[1]),
];
let datasets = [
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data1),
Dataset::default()
.name("data3")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
];
let chart = Chart::default()
.block(
Block::default()
.title("Chart")
.title("Chart 1")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.borders(Borders::ALL),
)
@@ -82,11 +122,7 @@ fn main() -> Result<(), failure::Error> {
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds(app.window)
.labels(&[
&format!("{}", app.window[0]),
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
&format!("{}", app.window[1]),
]),
.labels(&x_labels),
)
.y_axis(
Axis::default()
@@ -96,19 +132,72 @@ fn main() -> Result<(), failure::Error> {
.bounds([-20.0, 20.0])
.labels(&["-20", "0", "20"]),
)
.datasets(&[
Dataset::default()
.name("data2")
.marker(Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data1),
Dataset::default()
.name("data3")
.marker(Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
])
.render(&mut f, size);
.datasets(&datasets);
f.render_widget(chart, chunks[0]);
let datasets = [Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA)];
let chart = Chart::default()
.block(
Block::default()
.title("Chart 2")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5.0"]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5.0"]),
)
.datasets(&datasets);
f.render_widget(chart, chunks[1]);
let datasets = [Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA2)];
let chart = Chart::default()
.block(
Block::default()
.title("Chart 3")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 50.0])
.labels(&["0", "25", "50"]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5"]),
)
.datasets(&datasets);
f.render_widget(chart, chunks[2]);
})?;
match events.next()? {

View File

@@ -3,83 +3,92 @@ mod demo;
#[allow(dead_code)]
mod util;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use crossterm;
use structopt::StructOpt;
use tui::backend::CrosstermBackend;
use tui::Terminal;
use crate::demo::{ui, App};
use argh::FromArgs;
use crossterm::{
event::{self, Event as CEvent, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io::{stdout, Write},
sync::mpsc,
thread,
time::{Duration, Instant},
};
use tui::{backend::CrosstermBackend, Terminal};
enum Event<I> {
Input(I),
Tick,
}
#[derive(Debug, StructOpt)]
/// Crossterm demo
#[derive(Debug, FromArgs)]
struct Cli {
#[structopt(long = "tick-rate", default_value = "250")]
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
#[structopt(long = "log")]
log: bool,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), failure::Error> {
let cli = Cli::from_args();
stderrlog::new().quiet(!cli.log).verbosity(4).init()?;
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let screen = crossterm::Screen::default();
let alternate_screen = screen.enable_alternate_modes(true)?;
let backend = CrosstermBackend::with_alternate_screen(alternate_screen)?;
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup input handling
let (tx, rx) = mpsc::channel();
{
let tx = tx.clone();
thread::spawn(move || {
let input = crossterm::input();
loop {
match input.read_char() {
Ok(key) => {
if let Err(_) = tx.send(Event::Input(key)) {
return;
}
if key == 'q' {
return;
}
}
Err(_) => {}
let tick_rate = Duration::from_millis(cli.tick_rate);
thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
// poll for tick rate duration, if no events, sent tick event.
if event::poll(tick_rate - last_tick.elapsed()).unwrap() {
if let CEvent::Key(key) = event::read().unwrap() {
tx.send(Event::Input(key)).unwrap();
}
}
});
}
{
let tx = tx.clone();
thread::spawn(move || {
let tx = tx.clone();
loop {
if last_tick.elapsed() >= tick_rate {
tx.send(Event::Tick).unwrap();
thread::sleep(Duration::from_millis(cli.tick_rate));
last_tick = Instant::now();
}
});
}
}
});
let mut app = App::new("Crossterm Demo");
let mut app = App::new("Crossterm Demo", cli.enhanced_graphics);
terminal.clear()?;
loop {
ui::draw(&mut terminal, &app)?;
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
match rx.recv()? {
Event::Input(key) => {
// TODO: handle key events once they are supported by crossterm
app.on_key(key);
}
Event::Input(event) => match event.code {
KeyCode::Char('q') => {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
break;
}
KeyCode::Char(c) => app.on_key(c),
KeyCode::Left => app.on_left(),
KeyCode::Up => app.on_up(),
KeyCode::Right => app.on_right(),
KeyCode::Down => app.on_down(),
_ => {}
},
Event::Tick => {
app.on_tick();
}

View File

@@ -2,27 +2,29 @@ mod demo;
#[allow(dead_code)]
mod util;
use std::io;
use std::time::{Duration, Instant};
use easycurses;
use structopt::StructOpt;
use tui::backend::CursesBackend;
use tui::Terminal;
use crate::demo::{ui, App};
use argh::FromArgs;
use easycurses;
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{backend::CursesBackend, Terminal};
#[derive(Debug, StructOpt)]
/// Curses demo
#[derive(Debug, FromArgs)]
struct Cli {
#[structopt(long = "tick-rate", default_value = "250")]
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
#[structopt(long = "log")]
log: bool,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), failure::Error> {
let cli = Cli::from_args();
stderrlog::new().quiet(!cli.log).verbosity(4).init()?;
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let mut backend = CursesBackend::new().ok_or(io::Error::new(io::ErrorKind::Other, ""))?;
let curses = backend.get_curses_mut();
@@ -33,12 +35,12 @@ fn main() -> Result<(), failure::Error> {
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Curses demo");
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 {
ui::draw(&mut terminal, &app)?;
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
match terminal.backend_mut().get_curses_mut().get_input() {
Some(input) => {
match input {

View File

@@ -1,20 +1,12 @@
#[allow(dead_code)]
mod util;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::style::Style;
use tui::widgets::Widget;
use tui::Terminal;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend, buffer::Buffer, layout::Rect, style::Style, widgets::Widget, Terminal,
};
struct Label<'a> {
text: &'a str,
@@ -27,19 +19,19 @@ impl<'a> Default for Label<'a> {
}
impl<'a> Widget for Label<'a> {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.left(), area.top(), self.text, Style::default());
}
}
impl<'a> Label<'a> {
fn text(&mut self, text: &'a str) -> &mut Label<'a> {
fn text(mut self, text: &'a str) -> Label<'a> {
self.text = text;
self
}
}
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -52,7 +44,8 @@ fn main() -> Result<(), failure::Error> {
loop {
terminal.draw(|mut f| {
let size = f.size();
Label::default().text("Test").render(&mut f, size);
let label = Label::default().text("Test");
f.render_widget(label, size);
})?;
match events.next()? {

View File

@@ -1,4 +1,4 @@
use crate::util::{RandomSignal, SinSignal, TabsState};
use crate::util::{RandomSignal, SinSignal, StatefulList, TabsState};
const TASKS: [&'static str; 24] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
@@ -96,27 +96,6 @@ impl Signals {
}
}
pub struct ListState<I> {
pub items: Vec<I>,
pub selected: usize,
}
impl<I> ListState<I> {
fn new(items: Vec<I>) -> ListState<I> {
ListState { items, selected: 0 }
}
fn select_previous(&mut self) {
if self.selected > 0 {
self.selected -= 1;
}
}
fn select_next(&mut self) {
if self.selected < self.items.len() - 1 {
self.selected += 1
}
}
}
pub struct Server<'a> {
pub name: &'a str,
pub location: &'a str,
@@ -129,17 +108,18 @@ pub struct App<'a> {
pub should_quit: bool,
pub tabs: TabsState<'a>,
pub show_chart: bool,
pub progress: u16,
pub progress: f64,
pub sparkline: Signal<RandomSignal>,
pub tasks: ListState<(&'a str)>,
pub logs: ListState<(&'a str, &'a str)>,
pub tasks: StatefulList<&'a str>,
pub logs: StatefulList<(&'a str, &'a str)>,
pub signals: Signals,
pub barchart: Vec<(&'a str, u64)>,
pub servers: Vec<Server<'a>>,
pub enhanced_graphics: bool,
}
impl<'a> App<'a> {
pub fn new(title: &'a str) -> App<'a> {
pub fn new(title: &'a str, enhanced_graphics: bool) -> App<'a> {
let mut rand_signal = RandomSignal::new(0, 100);
let sparkline_points = rand_signal.by_ref().take(300).collect();
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
@@ -151,14 +131,14 @@ impl<'a> App<'a> {
should_quit: false,
tabs: TabsState::new(vec!["Tab0", "Tab1"]),
show_chart: true,
progress: 0,
progress: 0.0,
sparkline: Signal {
source: rand_signal,
points: sparkline_points,
tick_rate: 1,
},
tasks: ListState::new(TASKS.to_vec()),
logs: ListState::new(LOGS.to_vec()),
tasks: StatefulList::with_items(TASKS.to_vec()),
logs: StatefulList::with_items(LOGS.to_vec()),
signals: Signals {
sin1: Signal {
source: sin_signal,
@@ -199,15 +179,16 @@ impl<'a> App<'a> {
status: "Up",
},
],
enhanced_graphics,
}
}
pub fn on_up(&mut self) {
self.tasks.select_previous();
self.tasks.previous();
}
pub fn on_down(&mut self) {
self.tasks.select_next();
self.tasks.next();
}
pub fn on_right(&mut self) {
@@ -232,9 +213,9 @@ impl<'a> App<'a> {
pub fn on_tick(&mut self) {
// Update progress
self.progress += 5;
if self.progress > 100 {
self.progress = 0;
self.progress += 0.001;
if self.progress > 1.0 {
self.progress = 0.0;
}
self.sparkline.on_tick();

View File

@@ -1,38 +1,37 @@
use std::io;
use tui::backend::Backend;
use tui::layout::{Constraint, Direction, Layout, Rect};
use tui::style::{Color, Modifier, Style};
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle};
use tui::widgets::{
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Marker, Paragraph, Row,
SelectableList, Sparkline, Table, Tabs, Text, Widget,
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
widgets::{
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Paragraph, Row, Sparkline,
Table, Tabs, Text,
},
Frame,
};
use tui::{Frame, Terminal};
use crate::demo::App;
pub fn draw<B: Backend>(terminal: &mut Terminal<B>, app: &App) -> Result<(), io::Error> {
terminal.draw(|mut f| {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size());
Tabs::default()
.block(Block::default().borders(Borders::ALL).title(app.title))
.titles(&app.tabs.titles)
.style(Style::default().fg(Color::Green))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index)
.render(&mut f, chunks[0]);
match app.tabs.index {
0 => draw_first_tab(&mut f, &app, chunks[1]),
1 => draw_second_tab(&mut f, &app, chunks[1]),
_ => {}
};
})
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size());
let tabs = Tabs::default()
.block(Block::default().borders(Borders::ALL).title(app.title))
.titles(&app.tabs.titles)
.style(Style::default().fg(Color::Green))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index);
f.render_widget(tabs, chunks[0]);
match app.tabs.index {
0 => draw_first_tab(f, app, chunks[1]),
1 => draw_second_tab(f, app, chunks[1]),
_ => {}
};
}
fn draw_first_tab<B>(f: &mut Frame<B>, app: &App, area: Rect)
fn draw_first_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
@@ -51,7 +50,7 @@ where
draw_text(f, chunks[2]);
}
fn draw_gauges<B>(f: &mut Frame<B>, app: &App, area: Rect)
fn draw_gauges<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
@@ -59,11 +58,11 @@ where
.constraints([Constraint::Length(2), Constraint::Length(3)].as_ref())
.margin(1)
.split(area);
Block::default()
.borders(Borders::ALL)
.title("Graphs")
.render(f, area);
Gauge::default()
let block = Block::default().borders(Borders::ALL).title("Graphs");
f.render_widget(block, area);
let label = format!("{:.2}%", app.progress * 100.0);
let gauge = Gauge::default()
.block(Block::default().title("Gauge:"))
.style(
Style::default()
@@ -71,17 +70,23 @@ where
.bg(Color::Black)
.modifier(Modifier::ITALIC | Modifier::BOLD),
)
.label(&format!("{} / 100", app.progress))
.percent(app.progress)
.render(f, chunks[0]);
Sparkline::default()
.label(&label)
.ratio(app.progress);
f.render_widget(gauge, chunks[0]);
let sparkline = Sparkline::default()
.block(Block::default().title("Sparkline:"))
.style(Style::default().fg(Color::Green))
.data(&app.sparkline.points)
.render(f, chunks[1]);
.bar_set(if app.enhanced_graphics {
symbols::bar::NINE_LEVELS
} else {
symbols::bar::THREE_LEVELS
});
f.render_widget(sparkline, chunks[1]);
}
fn draw_charts<B>(f: &mut Frame<B>, app: &App, area: Rect)
fn draw_charts<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
@@ -103,18 +108,21 @@ where
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.direction(Direction::Horizontal)
.split(chunks[0]);
SelectableList::default()
// Draw tasks
let tasks = app.tasks.items.iter().map(|i| Text::raw(*i));
let tasks = List::new(tasks)
.block(Block::default().borders(Borders::ALL).title("List"))
.items(&app.tasks.items)
.select(Some(app.tasks.selected))
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::BOLD))
.highlight_symbol(">")
.render(f, chunks[0]);
.highlight_symbol("> ");
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
// Draw logs
let info_style = Style::default().fg(Color::White);
let warning_style = Style::default().fg(Color::Yellow);
let error_style = Style::default().fg(Color::Magenta);
let critical_style = Style::default().fg(Color::Red);
let events = app.logs.items.iter().map(|&(evt, level)| {
let logs = app.logs.items.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
@@ -125,15 +133,20 @@ where
},
)
});
List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.render(f, chunks[1]);
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
}
BarChart::default()
let barchart = BarChart::default()
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
.data(&app.barchart)
.bar_width(3)
.bar_gap(2)
.bar_set(if app.enhanced_graphics {
symbols::bar::NINE_LEVELS
} else {
symbols::bar::THREE_LEVELS
})
.value_style(
Style::default()
.fg(Color::Black)
@@ -141,11 +154,32 @@ where
.modifier(Modifier::ITALIC),
)
.label_style(Style::default().fg(Color::Yellow))
.style(Style::default().fg(Color::Green))
.render(f, chunks[1]);
.style(Style::default().fg(Color::Green));
f.render_widget(barchart, chunks[1]);
}
if app.show_chart {
Chart::default()
let x_labels = [
format!("{}", app.signals.window[0]),
format!("{}", (app.signals.window[0] + app.signals.window[1]) / 2.0),
format!("{}", app.signals.window[1]),
];
let datasets = [
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.signals.sin1.points),
Dataset::default()
.name("data3")
.marker(if app.enhanced_graphics {
symbols::Marker::Braille
} else {
symbols::Marker::Dot
})
.style(Style::default().fg(Color::Yellow))
.data(&app.signals.sin2.points),
];
let chart = Chart::default()
.block(
Block::default()
.title("Chart")
@@ -158,11 +192,7 @@ where
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds(app.signals.window)
.labels(&[
&format!("{}", app.signals.window[0]),
&format!("{}", (app.signals.window[0] + app.signals.window[1]) / 2.0),
&format!("{}", app.signals.window[1]),
]),
.labels(&x_labels),
)
.y_axis(
Axis::default()
@@ -172,19 +202,8 @@ where
.bounds([-20.0, 20.0])
.labels(&["-20", "0", "20"]),
)
.datasets(&[
Dataset::default()
.name("data2")
.marker(Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.signals.sin1.points),
Dataset::default()
.name("data3")
.marker(Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.signals.sin2.points),
])
.render(f, chunks[1]);
.datasets(&datasets);
f.render_widget(chart, chunks[1]);
}
}
@@ -209,18 +228,15 @@ where
Text::styled("text", Style::default().modifier(Modifier::UNDERLINED)),
Text::raw(".\nOne more thing is that it should display unicode characters: 10€")
];
Paragraph::new(text.iter())
.block(
Block::default()
.borders(Borders::ALL)
.title("Footer")
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::BOLD)),
)
.wrap(true)
.render(f, area);
let block = Block::default()
.borders(Borders::ALL)
.title("Footer")
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::BOLD));
let paragraph = Paragraph::new(text.iter()).block(block).wrap(true);
f.render_widget(paragraph, area);
}
fn draw_second_tab<B>(f: &mut Frame<B>, app: &App, area: Rect)
fn draw_second_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
@@ -241,13 +257,17 @@ where
};
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
});
Table::new(header.into_iter(), rows)
let table = Table::new(header.iter(), rows)
.block(Block::default().title("Servers").borders(Borders::ALL))
.header_style(Style::default().fg(Color::Yellow))
.widths(&[15, 15, 10])
.render(f, chunks[0]);
.widths(&[
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(10),
]);
f.render_widget(table, chunks[0]);
Canvas::default()
let map = Canvas::default()
.block(Block::default().title("World").borders(Borders::ALL))
.paint(|ctx| {
ctx.draw(&Map {
@@ -256,12 +276,10 @@ where
});
ctx.layer();
ctx.draw(&Rectangle {
rect: Rect {
x: 0,
y: 30,
width: 10,
height: 10,
},
x: 0.0,
y: 30.0,
width: 10.0,
height: 10.0,
color: Color::Yellow,
});
for (i, s1) in app.servers.iter().enumerate() {
@@ -284,7 +302,12 @@ where
ctx.print(server.coords.1, server.coords.0, "X", color);
}
})
.marker(if app.enhanced_graphics {
symbols::Marker::Braille
} else {
symbols::Marker::Dot
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
.render(f, chunks[1]);
.y_bounds([-90.0, 90.0]);
f.render_widget(map, chunks[1]);
}

View File

@@ -1,19 +1,16 @@
#[allow(dead_code)]
mod util;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Gauge, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Gauge},
Terminal,
};
struct App {
progress1: u16,
@@ -52,7 +49,7 @@ impl App {
}
}
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -81,28 +78,33 @@ fn main() -> Result<(), failure::Error> {
)
.split(f.size());
Gauge::default()
let gauge = Gauge::default()
.block(Block::default().title("Gauge1").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow))
.percent(app.progress1)
.render(&mut f, chunks[0]);
Gauge::default()
.percent(app.progress1);
f.render_widget(gauge, chunks[0]);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge2").borders(Borders::ALL))
.style(Style::default().fg(Color::Magenta).bg(Color::Green))
.percent(app.progress2)
.label(&format!("{}/100", app.progress2))
.render(&mut f, chunks[1]);
Gauge::default()
.label(&label);
f.render_widget(gauge, chunks[1]);
let gauge = Gauge::default()
.block(Block::default().title("Gauge3").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow))
.ratio(app.progress3)
.render(&mut f, chunks[2]);
Gauge::default()
.ratio(app.progress3);
f.render_widget(gauge, chunks[2]);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge4").borders(Borders::ALL))
.style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC))
.percent(app.progress4)
.label(&format!("{}/100", app.progress2))
.render(&mut f, chunks[3]);
.label(&label);
f.render_widget(gauge, chunks[3]);
})?;
match events.next()? {

View File

@@ -1,20 +1,17 @@
#[allow(dead_code)]
mod util;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::widgets::{Block, Borders, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders},
Terminal,
};
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -39,14 +36,10 @@ fn main() -> Result<(), failure::Error> {
)
.split(f.size());
Block::default()
.title("Block")
.borders(Borders::ALL)
.render(&mut f, chunks[0]);
Block::default()
.title("Block 2")
.borders(Borders::ALL)
.render(&mut f, chunks[2]);
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]);
})?;
match events.next()? {

View File

@@ -1,23 +1,22 @@
#[allow(dead_code)]
mod util;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Corner, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, List, SelectableList, Text, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
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::TermionBackend,
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, List, Text},
Terminal,
};
struct App<'a> {
items: Vec<&'a str>,
selected: Option<usize>,
items: StatefulList<&'a str>,
events: Vec<(&'a str, &'a str)>,
info_style: Style,
warning_style: Style,
@@ -28,12 +27,11 @@ struct App<'a> {
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
items: vec![
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9",
"Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17",
"Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
],
selected: None,
items: StatefulList::with_items(vec![
"Item0", "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8",
"Item9", "Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16",
"Item17", "Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
]),
events: vec![
("Event1", "INFO"),
("Event2", "INFO"),
@@ -75,7 +73,7 @@ impl<'a> App<'a> {
}
}
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -97,31 +95,30 @@ fn main() -> Result<(), failure::Error> {
.split(f.size());
let style = Style::default().fg(Color::Black).bg(Color::White);
SelectableList::default()
let items = app.items.items.iter().map(|i| Text::raw(*i));
let items = List::new(items)
.block(Block::default().borders(Borders::ALL).title("List"))
.items(&app.items)
.select(app.selected)
.style(style)
.highlight_style(style.fg(Color::LightGreen).modifier(Modifier::BOLD))
.highlight_symbol(">")
.render(&mut f, chunks[0]);
{
let events = app.events.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
"ERROR" => app.error_style,
"CRITICAL" => app.critical_style,
"WARNING" => app.warning_style,
_ => app.info_style,
},
)
});
List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.start_corner(Corner::BottomLeft)
.render(&mut f, chunks[1]);
}
.highlight_symbol(">");
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
let events = app.events.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
"ERROR" => app.error_style,
"CRITICAL" => app.critical_style,
"WARNING" => app.warning_style,
_ => app.info_style,
},
)
});
let events_list = List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.start_corner(Corner::BottomLeft);
f.render_widget(events_list, chunks[1]);
})?;
match events.next()? {
@@ -130,29 +127,13 @@ fn main() -> Result<(), failure::Error> {
break;
}
Key::Left => {
app.selected = None;
app.items.unselect();
}
Key::Down => {
app.selected = if let Some(selected) = app.selected {
if selected >= app.items.len() - 1 {
Some(0)
} else {
Some(selected + 1)
}
} else {
Some(0)
}
app.items.next();
}
Key::Up => {
app.selected = if let Some(selected) = app.selected {
if selected > 0 {
Some(selected - 1)
} else {
Some(app.items.len() - 1)
}
} else {
Some(0)
}
app.items.previous();
}
_ => {}
},

View File

@@ -1,21 +1,18 @@
#[allow(dead_code)]
mod util;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Alignment, Constraint, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Paragraph, Text},
Terminal,
};
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -36,9 +33,9 @@ fn main() -> Result<(), failure::Error> {
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
Block::default()
.style(Style::default().bg(Color::White))
.render(&mut f, size);
let block = Block::default()
.style(Style::default().bg(Color::White));
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -72,26 +69,26 @@ fn main() -> Result<(), failure::Error> {
let block = Block::default()
.borders(Borders::ALL)
.title_style(Style::default().modifier(Modifier::BOLD));
Paragraph::new(text.iter())
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Left, no wrap"))
.alignment(Alignment::Left)
.render(&mut f, chunks[0]);
Paragraph::new(text.iter())
.alignment(Alignment::Left);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Left, wrap"))
.alignment(Alignment::Left)
.wrap(true)
.render(&mut f, chunks[1]);
Paragraph::new(text.iter())
.wrap(true);
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Center, wrap"))
.alignment(Alignment::Center)
.wrap(true)
.scroll(scroll)
.render(&mut f, chunks[2]);
Paragraph::new(text.iter())
.scroll(scroll);
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Right, wrap"))
.alignment(Alignment::Right)
.wrap(true)
.render(&mut f, chunks[3]);
.wrap(true);
f.render_widget(paragraph, chunks[3]);
})?;
scroll += 1;

111
examples/popup.rs Normal file
View File

@@ -0,0 +1,111 @@
#[allow(dead_code)]
mod util;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::layout::Rect;
use tui::widgets::Clear;
use tui::{
backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Paragraph, Text},
Terminal,
};
/// 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)
.constraints(
[
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
]
.as_ref(),
)
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
]
.as_ref(),
)
.split(popup_layout[1])[1]
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
loop {
terminal.draw(|mut f| {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(size);
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width)*usize::from(size.height)/300);
long_line.push('\n');
let text = [
Text::raw("This is a line \n"),
Text::styled("This is a line \n", Style::default().fg(Color::Red)),
Text::styled("This is a line\n", Style::default().bg(Color::Blue)),
Text::styled(
"This is a longer line\n",
Style::default().modifier(Modifier::CROSSED_OUT),
),
Text::styled(&long_line, Style::default().bg(Color::Green)),
Text::styled(
"This is a line\n",
Style::default().fg(Color::Green).modifier(Modifier::ITALIC),
),
];
let paragraph = Paragraph::new(text.iter())
.block(Block::default().title("Left Block").borders(Borders::ALL))
.alignment(Alignment::Left).wrap(true);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.iter())
.block(Block::default().title("Right Block").borders(Borders::ALL))
.alignment(Alignment::Left).wrap(true);
f.render_widget(paragraph, chunks[1]);
let block = Block::default().title("Popup").borders(Borders::ALL);
let area = centered_rect(60, 20, size);
f.render_widget(Clear, area); //this clears out the background
f.render_widget(block, area);
})?;
match events.next()? {
Event::Input(input) => {
if let Key::Char('q') = input {
break;
}
}
_ => {}
}
}
Ok(())
}

View File

@@ -2,37 +2,39 @@ mod demo;
#[allow(dead_code)]
mod util;
use std::time::{Duration, Instant};
use rustbox::keyboard::Key;
use structopt::StructOpt;
use tui::backend::RustboxBackend;
use tui::Terminal;
use crate::demo::{ui, App};
use argh::FromArgs;
use rustbox::keyboard::Key;
use std::{
error::Error,
time::{Duration, Instant},
};
use tui::{backend::RustboxBackend, Terminal};
#[derive(Debug, StructOpt)]
/// Rustbox demo
#[derive(Debug, FromArgs)]
struct Cli {
#[structopt(long = "tick-rate", default_value = "250")]
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
#[structopt(long = "log")]
log: bool,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), failure::Error> {
let cli = Cli::from_args();
stderrlog::new().quiet(!cli.log).verbosity(4).init()?;
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let backend = RustboxBackend::new()?;
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Rustbox demo");
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 {
ui::draw(&mut terminal, &app)?;
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
match terminal.backend().rustbox().peek_event(tick_rate, false) {
Ok(rustbox::Event::KeyEvent(key)) => match key {
Key::Char(c) => {

View File

@@ -1,20 +1,19 @@
#[allow(dead_code)]
mod util;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, Sparkline, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use crate::util::RandomSignal;
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::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, Sparkline},
Terminal,
};
struct App {
signal: RandomSignal,
@@ -50,7 +49,7 @@ impl App {
}
}
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -80,34 +79,34 @@ fn main() -> Result<(), failure::Error> {
.as_ref(),
)
.split(f.size());
Sparkline::default()
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data1")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data1)
.style(Style::default().fg(Color::Yellow))
.render(&mut f, chunks[0]);
Sparkline::default()
.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))
.render(&mut f, chunks[1]);
.style(Style::default().bg(Color::Green));
f.render_widget(sparkline, chunks[1]);
// Multiline
Sparkline::default()
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data3")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data3)
.style(Style::default().fg(Color::Red))
.render(&mut f, chunks[2]);
.style(Style::default().fg(Color::Red));
f.render_widget(sparkline, chunks[2]);
})?;
match events.next()? {

View File

@@ -1,42 +1,79 @@
#[allow(dead_code)]
mod util;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Row, Table, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Row, Table, TableState},
Terminal,
};
struct App<'a> {
pub struct StatefulTable<'a> {
state: TableState,
items: Vec<Vec<&'a str>>,
selected: usize,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
impl<'a> StatefulTable<'a> {
fn new() -> StatefulTable<'a> {
StatefulTable {
state: TableState::default(),
items: vec![
vec!["Row12", "Row12", "Row13"],
vec!["Row11", "Row12", "Row13"],
vec!["Row21", "Row22", "Row23"],
vec!["Row31", "Row32", "Row33"],
vec!["Row41", "Row42", "Row43"],
vec!["Row51", "Row52", "Row53"],
vec!["Row61", "Row62", "Row63"],
vec!["Row71", "Row72", "Row73"],
vec!["Row81", "Row82", "Row83"],
vec!["Row91", "Row92", "Row93"],
vec!["Row101", "Row102", "Row103"],
vec!["Row111", "Row112", "Row113"],
vec!["Row121", "Row122", "Row123"],
vec!["Row131", "Row132", "Row133"],
vec!["Row141", "Row142", "Row143"],
vec!["Row151", "Row152", "Row153"],
vec!["Row161", "Row162", "Row163"],
vec!["Row171", "Row172", "Row173"],
vec!["Row181", "Row182", "Row183"],
vec!["Row191", "Row192", "Row193"],
],
selected: 0,
}
}
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));
}
}
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -47,31 +84,33 @@ fn main() -> Result<(), failure::Error> {
let events = Events::new();
// App
let mut app = App::new();
let mut table = StatefulTable::new();
// Input
loop {
terminal.draw(|mut f| {
let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::BOLD);
let normal_style = Style::default().fg(Color::White);
let header = ["Header1", "Header2", "Header3"];
let rows = app.items.iter().enumerate().map(|(i, item)| {
if i == app.selected {
Row::StyledData(item.into_iter(), selected_style)
} else {
Row::StyledData(item.into_iter(), normal_style)
}
});
let rects = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.margin(5)
.split(f.size());
Table::new(header.into_iter(), rows)
let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::BOLD);
let normal_style = Style::default().fg(Color::White);
let header = ["Header1", "Header2", "Header3"];
let rows = table
.items
.iter()
.map(|i| Row::StyledData(i.into_iter(), normal_style));
let t = Table::new(header.iter(), rows)
.block(Block::default().borders(Borders::ALL).title("Table"))
.widths(&[10, 10, 10])
.render(&mut f, rects[0]);
.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);
})?;
match events.next()? {
@@ -80,17 +119,10 @@ fn main() -> Result<(), failure::Error> {
break;
}
Key::Down => {
app.selected += 1;
if app.selected > app.items.len() - 1 {
app.selected = 0;
}
table.next();
}
Key::Up => {
if app.selected > 0 {
app.selected -= 1;
} else {
app.selected = app.items.len() - 1;
}
table.previous();
}
_ => {}
},

View File

@@ -1,26 +1,25 @@
#[allow(dead_code)]
mod util;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, Tabs, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use crate::util::TabsState;
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::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, Tabs},
Terminal,
};
struct App<'a> {
tabs: TabsState<'a>,
}
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -46,35 +45,23 @@ fn main() -> Result<(), failure::Error> {
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(size);
Block::default()
.style(Style::default().bg(Color::White))
.render(&mut f, size);
Tabs::default()
let block = Block::default().style(Style::default().bg(Color::White));
f.render_widget(block, size);
let tabs = Tabs::default()
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.titles(&app.tabs.titles)
.select(app.tabs.index)
.style(Style::default().fg(Color::Cyan))
.highlight_style(Style::default().fg(Color::Yellow))
.render(&mut f, chunks[0]);
match app.tabs.index {
0 => Block::default()
.title("Inner 0")
.borders(Borders::ALL)
.render(&mut f, chunks[1]),
1 => Block::default()
.title("Inner 1")
.borders(Borders::ALL)
.render(&mut f, chunks[1]),
2 => Block::default()
.title("Inner 2")
.borders(Borders::ALL)
.render(&mut f, chunks[1]),
3 => Block::default()
.title("Inner 3")
.borders(Borders::ALL)
.render(&mut f, chunks[1]),
_ => {}
}
.highlight_style(Style::default().fg(Color::Yellow));
f.render_widget(tabs, chunks[0]);
let inner = match app.tabs.index {
0 => Block::default().title("Inner 0").borders(Borders::ALL),
1 => Block::default().title("Inner 1").borders(Borders::ALL),
2 => Block::default().title("Inner 2").borders(Borders::ALL),
3 => Block::default().title("Inner 3").borders(Borders::ALL),
_ => unreachable!(),
};
f.render_widget(inner, chunks[1]);
})?;
match events.next()? {

View File

@@ -2,31 +2,28 @@ mod demo;
#[allow(dead_code)]
mod util;
use std::io;
use std::time::Duration;
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};
use structopt::StructOpt;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::Terminal;
use crate::demo::{ui, App};
use crate::util::event::{Config, Event, Events};
#[derive(Debug, StructOpt)]
/// Termion demo
#[derive(Debug, FromArgs)]
struct Cli {
#[structopt(long = "tick-rate", default_value = "250")]
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
#[structopt(long = "log")]
log: bool,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), failure::Error> {
let cli = Cli::from_args();
stderrlog::new().quiet(!cli.log).verbosity(4).init()?;
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),
@@ -40,9 +37,10 @@ fn main() -> Result<(), failure::Error> {
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Termion demo");
let mut app = App::new("Termion demo", cli.enhanced_graphics);
loop {
ui::draw(&mut terminal, &app)?;
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
match events.next()? {
Event::Input(key) => match key {
Key::Char(c) => {

View File

@@ -13,26 +13,34 @@
#[allow(dead_code)]
mod util;
use std::io::{self, Write};
use termion::cursor::Goto;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use std::{
error::Error,
io::{self, Write},
};
use termion::{
cursor::Goto, event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen,
};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, List, Paragraph, Text},
Terminal,
};
use unicode_width::UnicodeWidthStr;
use crate::util::event::{Event, Events};
enum InputMode {
Normal,
Editing,
}
/// App holds the state of the application
struct App {
/// Current value of the input box
input: String,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
messages: Vec<String>,
}
@@ -41,12 +49,13 @@ impl Default for App {
fn default() -> App {
App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
}
}
}
fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
@@ -55,7 +64,7 @@ fn main() -> Result<(), failure::Error> {
let mut terminal = Terminal::new(backend)?;
// Setup event handlers
let events = Events::new();
let mut events = Events::new();
// Create default app state
let mut app = App::default();
@@ -66,45 +75,77 @@ fn main() -> Result<(), failure::Error> {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
.constraints(
[
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
]
.as_ref(),
)
.split(f.size());
Paragraph::new([Text::raw(&app.input)].iter())
let msg = match app.input_mode {
InputMode::Normal => "Press q to exit, e to start editing.",
InputMode::Editing => "Press Esc to stop editing, Enter to record the message",
};
let text = [Text::raw(msg)];
let help_message = Paragraph::new(text.iter());
f.render_widget(help_message, chunks[0]);
let text = [Text::raw(&app.input)];
let input = Paragraph::new(text.iter())
.style(Style::default().fg(Color::Yellow))
.block(Block::default().borders(Borders::ALL).title("Input"))
.render(&mut f, chunks[0]);
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, chunks[1]);
let messages = app
.messages
.iter()
.enumerate()
.map(|(i, m)| Text::raw(format!("{}: {}", i, m)));
List::new(messages)
.block(Block::default().borders(Borders::ALL).title("Messages"))
.render(&mut f, chunks[1]);
let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, chunks[2]);
})?;
// Put the cursor back inside the input box
write!(
terminal.backend_mut(),
"{}",
Goto(4 + app.input.width() as u16, 4)
Goto(4 + app.input.width() as u16, 5)
)?;
// stdout is buffered, flush it to see the effect immediately when hitting backspace
io::stdout().flush().ok();
// Handle input
match events.next()? {
Event::Input(input) => match input {
Key::Char('q') => {
break;
}
Key::Char('\n') => {
app.messages.push(app.input.drain(..).collect());
}
Key::Char(c) => {
app.input.push(c);
}
Key::Backspace => {
app.input.pop();
}
_ => {}
Event::Input(input) => match app.input_mode {
InputMode::Normal => match input {
Key::Char('e') => {
app.input_mode = InputMode::Editing;
events.disable_exit_key();
}
Key::Char('q') => {
break;
}
_ => {}
},
InputMode::Editing => match input {
Key::Char('\n') => {
app.messages.push(app.input.drain(..).collect());
}
Key::Char(c) => {
app.input.push(c);
}
Key::Backspace => {
app.input.pop();
}
Key::Esc => {
app.input_mode = InputMode::Normal;
events.enable_exit_key();
}
_ => {}
},
},
_ => {}
}

View File

@@ -1,5 +1,9 @@
use std::io;
use std::sync::mpsc;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::thread;
use std::time::Duration;
@@ -16,6 +20,7 @@ pub enum Event<I> {
pub struct Events {
rx: mpsc::Receiver<Event<Key>>,
input_handle: thread::JoinHandle<()>,
ignore_exit_key: Arc<AtomicBool>,
tick_handle: thread::JoinHandle<()>,
}
@@ -41,8 +46,10 @@ impl Events {
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() {
@@ -51,7 +58,7 @@ impl Events {
if let Err(_) = tx.send(Event::Input(key)) {
return;
}
if key == config.exit_key {
if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key {
return;
}
}
@@ -72,6 +79,7 @@ impl Events {
};
Events {
rx,
ignore_exit_key,
input_handle,
tick_handle,
}
@@ -80,4 +88,12 @@ impl Events {
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);
}
}

View File

@@ -3,6 +3,7 @@ pub mod event;
use rand::distributions::{Distribution, Uniform};
use rand::rngs::ThreadRng;
use tui::widgets::ListState;
#[derive(Clone)]
pub struct RandomSignal {
@@ -75,3 +76,56 @@ impl<'a> TabsState<'a> {
}
}
}
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: 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);
}
}

View File

@@ -1,235 +1,258 @@
use std::io;
use std::{
fmt,
io::{self, Write},
};
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
style::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
SetForegroundColor,
},
terminal::{self, Clear, ClearType},
};
use crate::backend::Backend;
use crate::buffer::Cell;
use crate::layout::Rect;
use crate::style::{Color, Modifier};
use crossterm::error::ErrorKind;
use crate::{buffer::Cell, layout::Rect, style};
pub struct CrosstermBackend {
screen: Option<crossterm::Screen>,
crossterm: crossterm::Crossterm,
// Need to keep the AlternateScreen around even when not using it directly,
// see https://github.com/TimonPost/crossterm/issues/88
alternate_screen: Option<crossterm::AlternateScreen>,
pub struct CrosstermBackend<W: Write> {
buffer: W,
}
impl Default for CrosstermBackend {
fn default() -> CrosstermBackend {
let screen = crossterm::Screen::default();
let crossterm = crossterm::Crossterm::from_screen(&screen);
CrosstermBackend {
screen: Some(screen),
crossterm,
alternate_screen: None,
}
impl<W> CrosstermBackend<W>
where
W: Write,
{
pub fn new(buffer: W) -> CrosstermBackend<W> {
CrosstermBackend { buffer }
}
}
impl CrosstermBackend {
pub fn new() -> CrosstermBackend {
CrosstermBackend::default()
}
pub fn with_screen(screen: crossterm::Screen) -> CrosstermBackend {
let crossterm = crossterm::Crossterm::from_screen(&screen);
CrosstermBackend {
screen: Some(screen),
crossterm,
alternate_screen: None,
}
}
pub fn with_alternate_screen(
alternate_screen: crossterm::AlternateScreen,
) -> Result<CrosstermBackend, io::Error> {
let crossterm = crossterm::Crossterm::from_screen(&alternate_screen.screen);
Ok(CrosstermBackend {
screen: None,
crossterm,
alternate_screen: Some(alternate_screen),
})
}
pub fn screen(&self) -> Option<&crossterm::Screen> {
match &self.screen {
Some(screen) => Some(&screen),
None => None,
}
}
pub fn alternate_screen(&self) -> Option<&crossterm::AlternateScreen> {
match &self.alternate_screen {
Some(alt_screen) => Some(&alt_screen),
None => None,
}
}
pub fn crossterm(&self) -> &crossterm::Crossterm {
&self.crossterm
}
}
// TODO: consider associated Error type on Backend to allow custom error types
// per backend
fn convert_error(error: ErrorKind) -> io::Error {
match error {
ErrorKind::IoError(err) => err,
ErrorKind::FmtError(err) => {
io::Error::new(io::ErrorKind::Other, format!("Invalid formatting: {}", err))
}
ErrorKind::ResizingTerminalFailure(err) => io::Error::new(
io::ErrorKind::Other,
format!("Failed to resize terminal: {}", err),
),
_ => io::Error::new(io::ErrorKind::Other, "Unknown crossterm error"),
}
}
impl Backend for CrosstermBackend {
fn clear(&mut self) -> io::Result<()> {
let terminal = self.crossterm.terminal();
terminal
.clear(crossterm::ClearType::All)
.map_err(convert_error)?;
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
let cursor = self.crossterm.cursor();
cursor.hide().map_err(convert_error)?;
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
let cursor = self.crossterm.cursor();
cursor.show().map_err(convert_error)?;
Ok(())
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
let cursor = crossterm::cursor();
Ok(cursor.pos())
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
let cursor = crossterm::cursor();
cursor.goto(x, y).map_err(convert_error)
}
fn size(&self) -> io::Result<Rect> {
let terminal = self.crossterm.terminal();
let (width, height) = terminal.terminal_size();
Ok(Rect::new(0, 0, width, height))
impl<W> Write for CrosstermBackend<W>
where
W: Write,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buffer.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
self.buffer.flush()
}
}
impl<W> Backend for CrosstermBackend<W>
where
W: Write,
{
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
let cursor = self.crossterm.cursor();
use fmt::Write;
let mut string = String::with_capacity(content.size_hint().0 * 3);
let mut style = style::Style::default();
let mut last_y = 0;
let mut last_x = 0;
let mut first = true;
let mut inst = 0;
for (x, y, cell) in content {
if y != last_y || x != last_x + 1 || first {
cursor.goto(x, y).map_err(convert_error)?;
first = false;
if y != last_y || x != last_x + 1 || inst == 0 {
map_error(queue!(string, MoveTo(x, y)))?;
}
last_x = x;
last_y = y;
let mut s = self.crossterm.style(&cell.symbol);
if let Some(color) = cell.style.fg.into() {
s = s.with(color)
if cell.style.modifier != style.modifier {
let diff = ModifierDiff {
from: style.modifier,
to: cell.style.modifier,
};
diff.queue(&mut string)?;
inst += 1;
style.modifier = cell.style.modifier;
}
if let Some(color) = cell.style.bg.into() {
s = s.on(color)
if cell.style.fg != style.fg {
let color = CColor::from(cell.style.fg);
map_error(queue!(string, SetForegroundColor(color)))?;
style.fg = cell.style.fg;
inst += 1;
}
if cell.style.bg != style.bg {
let color = CColor::from(cell.style.bg);
map_error(queue!(string, SetBackgroundColor(color)))?;
style.bg = cell.style.bg;
inst += 1;
}
s.object_style.attrs = cell.style.modifier.into();
self.crossterm.paint(s).map_err(convert_error)?;
string.push_str(&cell.symbol);
inst += 1;
}
map_error(queue!(
self.buffer,
Print(string),
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
))
}
fn hide_cursor(&mut self) -> io::Result<()> {
map_error(execute!(self.buffer, Hide))
}
fn show_cursor(&mut self) -> io::Result<()> {
map_error(execute!(self.buffer, Show))
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
crossterm::cursor::position()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
map_error(execute!(self.buffer, MoveTo(x, y)))
}
fn clear(&mut self) -> io::Result<()> {
map_error(execute!(self.buffer, Clear(ClearType::All)))
}
fn size(&self) -> io::Result<Rect> {
let (width, height) =
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
Ok(Rect::new(0, 0, width, height))
}
fn flush(&mut self) -> io::Result<()> {
self.buffer.flush()
}
}
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
impl From<Color> for CColor {
fn from(color: Color) -> Self {
match color {
Color::Reset => CColor::Reset,
Color::Black => CColor::Black,
Color::Red => CColor::DarkRed,
Color::Green => CColor::DarkGreen,
Color::Yellow => CColor::DarkYellow,
Color::Blue => CColor::DarkBlue,
Color::Magenta => CColor::DarkMagenta,
Color::Cyan => CColor::DarkCyan,
Color::Gray => CColor::Grey,
Color::DarkGray => CColor::DarkGrey,
Color::LightRed => CColor::Red,
Color::LightGreen => CColor::Green,
Color::LightBlue => CColor::Blue,
Color::LightYellow => CColor::Yellow,
Color::LightMagenta => CColor::Magenta,
Color::LightCyan => CColor::Cyan,
Color::White => CColor::White,
Color::Indexed(i) => CColor::AnsiValue(i),
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
}
}
}
#[derive(Debug)]
struct ModifierDiff {
pub from: Modifier,
pub to: Modifier,
}
#[cfg(unix)]
impl ModifierDiff {
fn queue<W>(&self, mut w: W) -> io::Result<()>
where
W: fmt::Write,
{
//use crossterm::Attribute;
let removed = self.from - self.to;
if removed.contains(Modifier::REVERSED) {
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
}
if removed.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
if self.to.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
}
}
if removed.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
}
if removed.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
}
if removed.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
}
if removed.contains(Modifier::CROSSED_OUT) {
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
}
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
}
let added = self.to - self.from;
if added.contains(Modifier::REVERSED) {
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
}
if added.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
}
if added.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
}
if added.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
}
if added.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
}
if added.contains(Modifier::CROSSED_OUT) {
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
}
if added.contains(Modifier::SLOW_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
}
if added.contains(Modifier::RAPID_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
}
Ok(())
}
}
impl From<Color> for Option<crossterm::Color> {
fn from(color: Color) -> Option<crossterm::Color> {
match color {
Color::Reset => None,
Color::Black => Some(crossterm::Color::Black),
Color::Red => Some(crossterm::Color::DarkRed),
Color::Green => Some(crossterm::Color::DarkGreen),
Color::Yellow => Some(crossterm::Color::DarkYellow),
Color::Blue => Some(crossterm::Color::DarkBlue),
Color::Magenta => Some(crossterm::Color::DarkMagenta),
Color::Cyan => Some(crossterm::Color::DarkCyan),
Color::Gray => Some(crossterm::Color::Grey),
Color::DarkGray => Some(crossterm::Color::Grey),
Color::LightRed => Some(crossterm::Color::Red),
Color::LightGreen => Some(crossterm::Color::Green),
Color::LightBlue => Some(crossterm::Color::Blue),
Color::LightYellow => Some(crossterm::Color::Yellow),
Color::LightMagenta => Some(crossterm::Color::Magenta),
Color::LightCyan => Some(crossterm::Color::Cyan),
Color::White => Some(crossterm::Color::White),
Color::Indexed(i) => Some(crossterm::Color::AnsiValue(i)),
Color::Rgb(r, g, b) => Some(crossterm::Color::Rgb { r, g, b }),
#[cfg(windows)]
impl ModifierDiff {
fn queue<W>(&self, mut w: W) -> io::Result<()>
where
W: fmt::Write,
{
let removed = self.from - self.to;
if removed.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
}
}
}
impl From<Modifier> for Vec<crossterm::Attribute> {
#[cfg(unix)]
fn from(modifier: Modifier) -> Vec<crossterm::Attribute> {
let mut result = Vec::new();
if modifier.contains(Modifier::BOLD) {
result.push(crossterm::Attribute::Bold)
}
if modifier.contains(Modifier::DIM) {
result.push(crossterm::Attribute::Dim)
}
if modifier.contains(Modifier::ITALIC) {
result.push(crossterm::Attribute::Italic)
}
if modifier.contains(Modifier::UNDERLINED) {
result.push(crossterm::Attribute::Underlined)
}
if modifier.contains(Modifier::SLOW_BLINK) {
result.push(crossterm::Attribute::SlowBlink)
}
if modifier.contains(Modifier::RAPID_BLINK) {
result.push(crossterm::Attribute::RapidBlink)
}
if modifier.contains(Modifier::REVERSED) {
result.push(crossterm::Attribute::Reverse)
}
if modifier.contains(Modifier::HIDDEN) {
result.push(crossterm::Attribute::Hidden)
}
if modifier.contains(Modifier::CROSSED_OUT) {
result.push(crossterm::Attribute::CrossedOut)
}
result
}
#[cfg(windows)]
fn from(modifier: Modifier) -> Vec<crossterm::Attribute> {
let mut result = Vec::new();
if modifier.contains(Modifier::BOLD) {
result.push(crossterm::Attribute::Bold)
}
if modifier.contains(Modifier::UNDERLINED) {
result.push(crossterm::Attribute::Underlined)
}
result
if removed.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
}
let added = self.to - self.from;
if added.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
}
if added.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
}
Ok(())
}
}

View File

@@ -54,7 +54,7 @@ impl Backend for CursesBackend {
for (col, row, cell) in content {
// eprintln!("{:?}", cell);
if row != last_row || col != last_col + 1 {
self.curses.move_rc(row as i32, col as i32);
self.curses.move_rc(i32::from(row), i32::from(col));
}
last_col = col;
last_row = row;
@@ -107,11 +107,11 @@ impl Backend for CursesBackend {
Ok(())
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
let (x, y) = self.curses.get_cursor_rc();
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(x as i32, y as i32);
self.curses.move_rc(i32::from(y), i32::from(x));
Ok(())
}
fn clear(&mut self) -> io::Result<()> {
@@ -151,18 +151,18 @@ fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) {
line::HORIZONTAL_UP => pancurses::ACS_BTEE(),
block::FULL => pancurses::ACS_BLOCK(),
block::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(),
block::THREE_QUATERS => 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_QUATER => ' ' as chtype,
block::ONE_QUARTER => ' ' as chtype,
block::ONE_EIGHTH => ' ' as chtype,
bar::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(),
bar::THREE_QUATERS => 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_QUATER => pancurses::ACS_S9(),
bar::ONE_QUARTER => pancurses::ACS_S9(),
bar::ONE_EIGHTH => pancurses::ACS_S9(),
DOT => pancurses::ACS_BULLET(),
unicode_char => {
@@ -187,16 +187,16 @@ 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_QUATERS => block::FULL,
block::THREE_QUARTERS => block::FULL,
block::FIVE_EIGHTHS => block::HALF,
block::THREE_EIGHTHS => block::HALF,
block::ONE_QUATER => block::HALF,
block::ONE_QUARTER => block::HALF,
block::ONE_EIGHTH => " ",
bar::SEVEN_EIGHTHS => bar::FULL,
bar::THREE_QUATERS => bar::FULL,
bar::THREE_QUARTERS => bar::FULL,
bar::FIVE_EIGHTHS => bar::HALF,
bar::THREE_EIGHTHS => bar::HALF,
bar::ONE_QUATER => bar::HALF,
bar::ONE_QUARTER => bar::HALF,
bar::ONE_EIGHTH => " ",
ch => ch,
};

View File

@@ -1,18 +1,18 @@
use log::debug;
use crate::{
backend::Backend,
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
};
use std::io;
use super::Backend;
use crate::buffer::Cell;
use crate::layout::Rect;
use crate::style::{Color, Modifier};
pub struct RustboxBackend {
rustbox: rustbox::RustBox,
}
impl RustboxBackend {
pub fn new() -> Result<RustboxBackend, rustbox::InitError> {
let rustbox = r#try!(rustbox::RustBox::init(Default::default()));
let rustbox = rustbox::RustBox::init(Default::default())?;
Ok(RustboxBackend { rustbox })
}
@@ -30,9 +30,7 @@ impl Backend for RustboxBackend {
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
let mut inst = 0;
for (x, y, cell) in content {
inst += 1;
self.rustbox.print(
x as usize,
y as usize,
@@ -42,7 +40,6 @@ impl Backend for RustboxBackend {
&cell.symbol,
);
}
debug!("{} instructions outputed", inst);
Ok(())
}
fn hide_cursor(&mut self) -> Result<(), io::Error> {
@@ -103,7 +100,7 @@ impl Into<rustbox::Color> for Color {
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(i as u16),
Color::Indexed(i) => rustbox::Color::Byte(u16::from(i)),
Color::Rgb(r, g, b) => rustbox::Color::Byte(rgb_to_byte(r, g, b)),
}
}

View File

@@ -1,4 +1,3 @@
use log::debug;
use std::fmt;
use std::io;
use std::io::Write;
@@ -115,7 +114,6 @@ where
string.push_str(&cell.symbol);
inst += 1;
}
debug!("{} instructions outputed.", inst);
write!(
self.stdout,
"{}{}{}{}",
@@ -128,7 +126,7 @@ where
/// Return the size of the terminal
fn size(&self) -> io::Result<Rect> {
let terminal = r#try!(termion::terminal_size());
let terminal = termion::terminal_size()?;
Ok(Rect::new(0, 0, terminal.0, terminal.1))
}

View File

@@ -78,7 +78,6 @@ impl Default for Cell {
/// use tui::layout::Rect;
/// use tui::style::{Color, Style, Modifier};
///
/// # fn main() {
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf.get_mut(0, 2).set_symbol("x");
/// assert_eq!(buf.get(0, 2).symbol, "x");
@@ -92,7 +91,6 @@ impl Default for Cell {
/// }});
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol, "x");
/// # }
/// ```
#[derive(Clone, PartialEq)]
pub struct Buffer {
@@ -307,7 +305,14 @@ impl Buffer {
/// Print at most the first n characters of a string if enough space is available
/// until the end of the line
pub fn set_stringn<S>(&mut self, x: u16, y: u16, string: S, width: usize, style: Style)
pub fn set_stringn<S>(
&mut self,
x: u16,
y: u16,
string: S,
width: usize,
style: Style,
) -> (u16, u16)
where
S: AsRef<str>,
{
@@ -332,6 +337,15 @@ impl Buffer {
index += width;
x_offset += width;
}
(x_offset as u16, y)
}
pub fn set_background(&mut self, area: Rect, color: Color) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
self.get_mut(x, y).set_bg(color);
}
}
}
/// Resize the buffer so that the mapped area matches the given area and that the buffer
@@ -520,12 +534,8 @@ mod tests {
#[test]
fn buffer_with_lines() {
let buffer = Buffer::with_lines(vec![
"┌────────┐",
"│コンピュ│",
"│ーa 上で│",
"└────────┘",
]);
let buffer =
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
assert_eq!(buffer.area.x, 0);
assert_eq!(buffer.area.y, 0);
assert_eq!(buffer.area.width, 10);
@@ -619,11 +629,7 @@ mod tests {
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![
(1, 0, &cell("")),
(2, 0, &cell("")),
(4, 0, &cell("")),
]
vec![(1, 0, &cell("")), (2, 0, &cell("")), (4, 0, &cell("")),]
);
}

View File

@@ -30,6 +30,27 @@ pub enum Constraint {
Min(u16),
}
impl Constraint {
pub fn apply(&self, length: u16) -> u16 {
match *self {
Constraint::Percentage(p) => length * p / 100,
Constraint::Ratio(num, den) => {
let r = num * u32::from(length) / den;
r as u16
}
Constraint::Length(l) => length.min(l),
Constraint::Max(m) => length.min(m),
Constraint::Min(m) => length.max(m),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Margin {
pub vertical: u16,
pub horizontal: u16,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alignment {
Left,
@@ -37,11 +58,10 @@ pub enum Alignment {
Right,
}
// TODO: enforce constraints size once const generics has landed
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Layout {
direction: Direction,
margin: u16,
margin: Margin,
constraints: Vec<Constraint>,
}
@@ -53,7 +73,10 @@ impl Default for Layout {
fn default() -> Layout {
Layout {
direction: Direction::Vertical,
margin: 0,
margin: Margin {
horizontal: 0,
vertical: 0,
},
constraints: Vec::new(),
}
}
@@ -69,7 +92,20 @@ impl Layout {
}
pub fn margin(mut self, margin: u16) -> Layout {
self.margin = margin;
self.margin = Margin {
horizontal: margin,
vertical: margin,
};
self
}
pub fn horizontal_margin(mut self, horizontal: u16) -> Layout {
self.margin.horizontal = horizontal;
self
}
pub fn vertical_margin(mut self, vertical: u16) -> Layout {
self.margin.vertical = vertical;
self
}
@@ -84,63 +120,59 @@ impl Layout {
/// # Examples
/// ```
/// # use tui::layout::{Rect, Constraint, Direction, Layout};
/// let chunks = Layout::default()
/// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
/// .split(Rect {
/// x: 2,
/// y: 2,
/// width: 10,
/// height: 10,
/// });
/// assert_eq!(
/// chunks,
/// vec![
/// Rect {
/// x: 2,
/// y: 2,
/// width: 10,
/// height: 5
/// },
/// Rect {
/// x: 2,
/// y: 7,
/// width: 10,
/// height: 5
/// }
/// ]
/// );
///
/// # fn main() {
/// let chunks = Layout::default()
/// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
/// .split(Rect {
/// x: 2,
/// y: 2,
/// width: 10,
/// height: 10,
/// });
/// assert_eq!(
/// chunks,
/// vec![
/// Rect {
/// x: 2,
/// y: 2,
/// width: 10,
/// height: 5
/// },
/// Rect {
/// x: 2,
/// y: 7,
/// width: 10,
/// height: 5
/// }
/// ]
/// );
///
/// let chunks = Layout::default()
/// .direction(Direction::Horizontal)
/// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
/// .split(Rect {
/// x: 0,
/// y: 0,
/// width: 9,
/// height: 2,
/// });
/// assert_eq!(
/// chunks,
/// vec![
/// Rect {
/// x: 0,
/// y: 0,
/// width: 3,
/// height: 2
/// },
/// Rect {
/// x: 3,
/// y: 0,
/// width: 6,
/// height: 2
/// }
/// ]
/// );
/// # }
///
/// let chunks = Layout::default()
/// .direction(Direction::Horizontal)
/// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
/// .split(Rect {
/// x: 0,
/// y: 0,
/// width: 9,
/// height: 2,
/// });
/// assert_eq!(
/// chunks,
/// vec![
/// Rect {
/// x: 0,
/// y: 0,
/// width: 3,
/// height: 2
/// },
/// Rect {
/// x: 3,
/// y: 0,
/// width: 6,
/// height: 2
/// }
/// ]
/// );
/// ```
pub fn split(self, area: Rect) -> Vec<Rect> {
// TODO: Maybe use a fixed size cache ?
@@ -167,7 +199,7 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
.map(|_| Rect::default())
.collect::<Vec<Rect>>();
let dest_area = area.inner(layout.margin);
let dest_area = area.inner(&layout.margin);
for (i, e) in elements.iter().enumerate() {
vars.insert(e.x, (i, 0));
vars.insert(e.y, (i, 1));
@@ -378,15 +410,15 @@ impl Rect {
self.y + self.height
}
pub fn inner(self, margin: u16) -> Rect {
if self.width < 2 * margin || self.height < 2 * margin {
pub fn inner(self, margin: &Margin) -> Rect {
if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
Rect::default()
} else {
Rect {
x: self.x + margin,
y: self.y + margin,
width: self.width - 2 * margin,
height: self.height - 2 * margin,
x: self.x + margin.horizontal,
y: self.y + margin.vertical,
width: self.width - 2 * margin.horizontal,
height: self.height - 2 * margin.vertical,
}
}
}
@@ -425,46 +457,51 @@ impl Rect {
}
}
#[test]
fn test_rect_size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rect_size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
)
}
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
}
#[test]
fn test_rect_size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
#[test]
fn test_rect_size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
}
}
}
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
}
}

View File

@@ -5,12 +5,31 @@
//!
//! # Get started
//!
//! ## Adding `tui` as a dependency
//!
//! ```toml
//! [dependencies]
//! tui = "0.9"
//! termion = "1.5"
//! ```
//!
//! 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]
//! crossterm = "0.17"
//! tui = { version = "0.9", default-features = false, features = ['crossterm'] }
//! ```
//!
//! The same logic applies for all other available backends.
//!
//! ## Creating a `Terminal`
//!
//! Every application using `tui` should start by instantiating a `Terminal`. It is
//! a light abstraction over available backends that provides basic functionalities
//! such as clearing the screen, hiding the cursor, etc. By default only the `termion`
//! backend is available.
//! Every application using `tui` should start by instantiating a `Terminal`. It is a light
//! abstraction over available backends that provides basic functionalities such as clearing the
//! screen, hiding the cursor, etc.
//!
//! ```rust,no_run
//! use std::io;
@@ -26,45 +45,38 @@
//! }
//! ```
//!
//! If for some reason, you might want to use the `rustbox` backend instead, you
//! need the to replace your `tui` dependency specification by:
//!
//! ```toml
//! [dependencies.tui]
//! version = "0.3.0"
//! default-features = false
//! features = ['rustbox']
//! ```
//!
//! and then create the terminal in a similar way:
//! 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::RustboxBackend;
//! use tui::backend::CrosstermBackend;
//!
//! fn main() -> Result<(), io::Error> {
//! let backend = RustboxBackend::new()?;
//! let mut terminal = Terminal::new(backend);
//! 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.
//!
//! ## Building a User Interface (UI)
//!
//! Every component of your interface will be implementing the `Widget` trait.
//! The library comes with a predefined set of widgets that should met most of
//! your use cases. You are also free to implement your owns.
//! Every component of your interface will be implementing the `Widget` trait. The library comes
//! with a predefined set of widgets that should meet most of your use cases. You are also free to
//! implement your own.
//!
//! Each widget follows a builder pattern API providing a default configuration
//! along with methods to customize them. The widget is then registered using
//! its `render` method that take a `Frame` instance and an area to draw
//! to.
//! Each widget follows a builder pattern API providing a default configuration along with methods
//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take
//! your widget instance an area to draw to.
//!
//! The following example renders a block of the size of the terminal:
//!
//! ```rust,no_run
//!
//! use std::io;
//! use termion::raw::IntoRawMode;
//! use tui::Terminal;
@@ -78,10 +90,10 @@
//! let mut terminal = Terminal::new(backend)?;
//! terminal.draw(|mut f| {
//! let size = f.size();
//! Block::default()
//! let block = Block::default()
//! .title("Block")
//! .borders(Borders::ALL)
//! .render(&mut f, size);
//! .borders(Borders::ALL);
//! f.render_widget(block, size);
//! })
//! }
//! ```
@@ -93,7 +105,6 @@
//! full customization. And `Layout` is no exception:
//!
//! ```rust,no_run
//!
//! use std::io;
//! use termion::raw::IntoRawMode;
//! use tui::Terminal;
@@ -117,22 +128,24 @@
//! ].as_ref()
//! )
//! .split(f.size());
//! Block::default()
//! let block = Block::default()
//! .title("Block")
//! .borders(Borders::ALL)
//! .render(&mut f, chunks[0]);
//! Block::default()
//! .borders(Borders::ALL);
//! f.render_widget(block, chunks[0]);
//! let block = Block::default()
//! .title("Block 2")
//! .borders(Borders::ALL)
//! .render(&mut f, chunks[2]);
//! .borders(Borders::ALL);
//! f.render_widget(block, chunks[1]);
//! })
//! }
//! ```
//!
//! This let you describe responsive terminal UI by nesting layouts. You should note
//! that by default the computed layout tries to fill the available space
//! completely. So if for any reason you might need a blank space somewhere, try to
//! pass an additional constraint and don't use the corresponding area.
//! This let you describe responsive terminal UI by nesting layouts. You should note that by
//! default the computed layout tries to fill the available space completely. So if for any reason
//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
//! corresponding area.
#![deny(warnings)]
pub mod backend;
pub mod buffer;

View File

@@ -26,7 +26,7 @@ pub enum Color {
impl Color {
/// Returns a short code associated with the color, used for debug purpose
/// only
pub(crate) fn code(&self) -> &str {
pub(crate) fn code(self) -> &'static str {
match self {
Color::Reset => "X",
Color::Black => "b",
@@ -68,7 +68,7 @@ bitflags! {
impl Modifier {
/// Returns a short code associated with the color, used for debug purpose
/// only
pub(crate) fn code(&self) -> String {
pub(crate) fn code(self) -> String {
use std::fmt::Write;
let mut result = String::new();
@@ -114,30 +114,33 @@ pub struct Style {
impl Default for Style {
fn default() -> Style {
Style::new()
}
}
impl Style {
pub const fn new() -> Self {
Style {
fg: Color::Reset,
bg: Color::Reset,
modifier: Modifier::empty(),
}
}
}
impl Style {
pub fn reset(&mut self) {
self.fg = Color::Reset;
self.bg = Color::Reset;
self.modifier = Modifier::empty();
}
pub fn fg(mut self, color: Color) -> Style {
pub const fn fg(mut self, color: Color) -> Style {
self.fg = color;
self
}
pub fn bg(mut self, color: Color) -> Style {
pub const fn bg(mut self, color: Color) -> Style {
self.bg = color;
self
}
pub fn modifier(mut self, modifier: Modifier) -> Style {
pub const fn modifier(mut self, modifier: Modifier) -> Style {
self.modifier = modifier;
self
}

View File

@@ -1,36 +1,231 @@
pub mod block {
pub const FULL: &str = "";
pub const SEVEN_EIGHTHS: &str = "";
pub const THREE_QUATERS: &str = "";
pub const THREE_QUARTERS: &str = "";
pub const FIVE_EIGHTHS: &str = "";
pub const HALF: &str = "";
pub const THREE_EIGHTHS: &str = "";
pub const ONE_QUATER: &str = "";
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
pub three_quarters: &'static str,
pub five_eighths: &'static str,
pub half: &'static str,
pub three_eighths: &'static str,
pub one_quarter: &'static str,
pub one_eighth: &'static str,
pub empty: &'static str,
}
pub const THREE_LEVELS: Set = Set {
full: FULL,
seven_eighths: FULL,
three_quarters: HALF,
five_eighths: HALF,
half: HALF,
three_eighths: HALF,
one_quarter: HALF,
one_eighth: " ",
empty: " ",
};
pub const NINE_LEVELS: Set = Set {
full: FULL,
seven_eighths: SEVEN_EIGHTHS,
three_quarters: THREE_QUARTERS,
five_eighths: FIVE_EIGHTHS,
half: HALF,
three_eighths: THREE_EIGHTHS,
one_quarter: ONE_QUARTER,
one_eighth: ONE_EIGHTH,
empty: " ",
};
}
pub mod bar {
pub const FULL: &str = "";
pub const SEVEN_EIGHTHS: &str = "";
pub const THREE_QUATERS: &str = "";
pub const THREE_QUARTERS: &str = "";
pub const FIVE_EIGHTHS: &str = "";
pub const HALF: &str = "";
pub const THREE_EIGHTHS: &str = "";
pub const ONE_QUATER: &str = "";
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
pub three_quarters: &'static str,
pub five_eighths: &'static str,
pub half: &'static str,
pub three_eighths: &'static str,
pub one_quarter: &'static str,
pub one_eighth: &'static str,
pub empty: &'static str,
}
pub const THREE_LEVELS: Set = Set {
full: FULL,
seven_eighths: FULL,
three_quarters: HALF,
five_eighths: HALF,
half: HALF,
three_eighths: HALF,
one_quarter: HALF,
one_eighth: " ",
empty: " ",
};
pub const NINE_LEVELS: Set = Set {
full: FULL,
seven_eighths: SEVEN_EIGHTHS,
three_quarters: THREE_QUARTERS,
five_eighths: FIVE_EIGHTHS,
half: HALF,
three_eighths: THREE_EIGHTHS,
one_quarter: ONE_QUARTER,
one_eighth: ONE_EIGHTH,
empty: " ",
};
}
pub mod line {
pub const TOP_RIGHT: &str = "";
pub const VERTICAL: &str = "";
pub const DOUBLE_VERTICAL: &str = "";
pub const THICK_VERTICAL: &str = "";
pub const HORIZONTAL: &str = "";
pub const DOUBLE_HORIZONTAL: &str = "";
pub const THICK_HORIZONTAL: &str = "";
pub const TOP_RIGHT: &str = "";
pub const ROUNDED_TOP_RIGHT: &str = "";
pub const DOUBLE_TOP_RIGHT: &str = "";
pub const THICK_TOP_RIGHT: &str = "";
pub const TOP_LEFT: &str = "";
pub const ROUNDED_TOP_LEFT: &str = "";
pub const DOUBLE_TOP_LEFT: &str = "";
pub const THICK_TOP_LEFT: &str = "";
pub const BOTTOM_RIGHT: &str = "";
pub const ROUNDED_BOTTOM_RIGHT: &str = "";
pub const DOUBLE_BOTTOM_RIGHT: &str = "";
pub const THICK_BOTTOM_RIGHT: &str = "";
pub const BOTTOM_LEFT: &str = "";
pub const ROUNDED_BOTTOM_LEFT: &str = "";
pub const DOUBLE_BOTTOM_LEFT: &str = "";
pub const THICK_BOTTOM_LEFT: &str = "";
pub const VERTICAL_LEFT: &str = "";
pub const DOUBLE_VERTICAL_LEFT: &str = "";
pub const THICK_VERTICAL_LEFT: &str = "";
pub const VERTICAL_RIGHT: &str = "";
pub const DOUBLE_VERTICAL_RIGHT: &str = "";
pub const THICK_VERTICAL_RIGHT: &str = "";
pub const HORIZONTAL_DOWN: &str = "";
pub const DOUBLE_HORIZONTAL_DOWN: &str = "";
pub const THICK_HORIZONTAL_DOWN: &str = "";
pub const HORIZONTAL_UP: &str = "";
pub const DOUBLE_HORIZONTAL_UP: &str = "";
pub const THICK_HORIZONTAL_UP: &str = "";
pub const CROSS: &str = "";
pub const DOUBLE_CROSS: &str = "";
pub const THICK_CROSS: &str = "";
#[derive(Debug, Clone)]
pub struct Set {
pub vertical: &'static str,
pub horizontal: &'static str,
pub top_right: &'static str,
pub top_left: &'static str,
pub bottom_right: &'static str,
pub bottom_left: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_down: &'static str,
pub horizontal_up: &'static str,
pub cross: &'static str,
}
pub const NORMAL: Set = Set {
vertical: VERTICAL,
horizontal: HORIZONTAL,
top_right: TOP_RIGHT,
top_left: TOP_LEFT,
bottom_right: BOTTOM_RIGHT,
bottom_left: BOTTOM_LEFT,
vertical_left: VERTICAL_LEFT,
vertical_right: VERTICAL_RIGHT,
horizontal_down: HORIZONTAL_DOWN,
horizontal_up: HORIZONTAL_UP,
cross: CROSS,
};
pub const ROUNDED: Set = Set {
top_right: ROUNDED_TOP_RIGHT,
top_left: ROUNDED_TOP_LEFT,
bottom_right: ROUNDED_BOTTOM_RIGHT,
bottom_left: ROUNDED_BOTTOM_LEFT,
..NORMAL
};
pub const DOUBLE: Set = Set {
vertical: DOUBLE_VERTICAL,
horizontal: DOUBLE_HORIZONTAL,
top_right: DOUBLE_TOP_RIGHT,
top_left: DOUBLE_TOP_LEFT,
bottom_right: DOUBLE_BOTTOM_RIGHT,
bottom_left: DOUBLE_BOTTOM_LEFT,
vertical_left: DOUBLE_VERTICAL_LEFT,
vertical_right: DOUBLE_VERTICAL_RIGHT,
horizontal_down: DOUBLE_HORIZONTAL_DOWN,
horizontal_up: DOUBLE_HORIZONTAL_UP,
cross: DOUBLE_CROSS,
};
pub const THICK: Set = Set {
vertical: THICK_VERTICAL,
horizontal: THICK_HORIZONTAL,
top_right: THICK_TOP_RIGHT,
top_left: THICK_TOP_LEFT,
bottom_right: THICK_BOTTOM_RIGHT,
bottom_left: THICK_BOTTOM_LEFT,
vertical_left: THICK_VERTICAL_LEFT,
vertical_right: THICK_VERTICAL_RIGHT,
horizontal_down: THICK_HORIZONTAL_DOWN,
horizontal_up: THICK_HORIZONTAL_UP,
cross: THICK_CROSS,
};
}
pub const DOT: &str = "";
pub mod braille {
pub const BLANK: u16 = 0x2800;
pub const DOTS: [[u16; 2]; 4] = [
[0x0001, 0x0008],
[0x0002, 0x0010],
[0x0004, 0x0020],
[0x0040, 0x0080],
];
}
/// Marker to use when plotting data points
#[derive(Debug, Clone, Copy)]
pub enum Marker {
/// One point per cell
Dot,
/// Up to 8 points per cell
Braille,
}

View File

@@ -1,10 +1,9 @@
use log::error;
use std::io;
use crate::backend::Backend;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::widgets::Widget;
use crate::widgets::{StatefulWidget, Widget};
/// Interface to the terminal backed by Termion
#[derive(Debug)]
@@ -41,12 +40,60 @@ where
self.terminal.known_size
}
/// Calls the draw method of a given widget on the current buffer
pub fn render<W>(&mut self, widget: &mut W, area: Rect)
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
///
/// # Examples
///
/// ```rust,no_run
/// # use std::io;
/// # use tui::Terminal;
/// # use tui::backend::TermionBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::Block;
/// # 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);
/// let mut frame = terminal.get_frame();
/// frame.render_widget(block, area);
/// ```
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
where
W: Widget,
{
widget.draw(area, self.terminal.current_buffer_mut());
widget.render(area, self.terminal.current_buffer_mut());
}
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
///
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
/// given [`StatefulWidget`].
///
/// # Examples
///
/// ```rust,no_run
/// # use std::io;
/// # use tui::Terminal;
/// # use tui::backend::TermionBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::{List, ListState, Text};
/// # let stdout = io::stdout();
/// # let backend = TermionBackend::new(stdout);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let mut state = ListState::default();
/// state.select(Some(1));
/// let items = vec![Text::raw("Item 1"), Text::raw("Item 2")];
/// let list = List::new(items.into_iter());
/// let area = Rect::new(0, 0, 5, 5);
/// let mut frame = terminal.get_frame();
/// frame.render_stateful_widget(list, area, &mut state);
/// ```
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidget,
{
widget.render(area, self.terminal.current_buffer_mut(), state);
}
}
@@ -58,7 +105,7 @@ where
// Attempt to restore the cursor state
if self.hidden_cursor {
if let Err(err) = self.show_cursor() {
error!("Failed to show the cursor: {}", err);
eprintln!("Failed to show the cursor: {}", err);
}
}
}

View File

@@ -1,13 +1,13 @@
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols,
widgets::{Block, Widget},
};
use std::cmp::{max, min};
use unicode_width::UnicodeWidthStr;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::symbols::bar;
use crate::widgets::{Block, Widget};
/// Display multiple bars in a single widgets
///
/// # Examples
@@ -15,7 +15,6 @@ use crate::widgets::{Block, Widget};
/// ```
/// # use tui::widgets::{Block, Borders, BarChart};
/// # use tui::style::{Style, Color, Modifier};
/// # fn main() {
/// BarChart::default()
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .bar_width(3)
@@ -25,8 +24,8 @@ use crate::widgets::{Block, Widget};
/// .label_style(Style::default().fg(Color::White))
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .max(4);
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct BarChart<'a> {
/// Block to wrap the widget in
block: Option<Block<'a>>,
@@ -34,6 +33,8 @@ pub struct BarChart<'a> {
bar_width: u16,
/// The gap between each bar
bar_gap: u16,
/// Set of symbols used to display the data
bar_set: symbols::bar::Set,
/// Style of the values printed at the bottom of each bar
value_style: Style,
/// Style of the labels printed under each bar
@@ -58,6 +59,7 @@ impl<'a> Default for BarChart<'a> {
values: Vec::new(),
bar_width: 1,
bar_gap: 1,
bar_set: symbols::bar::NINE_LEVELS,
value_style: Default::default(),
label_style: Default::default(),
style: Default::default(),
@@ -79,6 +81,7 @@ impl<'a> BarChart<'a> {
self.block = Some(block);
self
}
pub fn max(mut self, max: u64) -> BarChart<'a> {
self.max = Some(max);
self
@@ -88,18 +91,27 @@ impl<'a> BarChart<'a> {
self.bar_width = width;
self
}
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
self.bar_gap = gap;
self
}
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
self.bar_set = bar_set;
self
}
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
self.value_style = style;
self
}
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
self.label_style = style;
self
}
pub fn style(mut self, style: Style) -> BarChart<'a> {
self.style = style;
self
@@ -107,10 +119,10 @@ impl<'a> BarChart<'a> {
}
impl<'a> Widget for BarChart<'a> {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let chart_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.render(area, buf);
b.inner(area)
}
None => area,
@@ -120,7 +132,7 @@ impl<'a> Widget for BarChart<'a> {
return;
}
self.background(chart_area, buf, self.style.bg);
buf.set_background(chart_area, self.style.bg);
let max = self
.max
@@ -133,20 +145,25 @@ impl<'a> Widget for BarChart<'a> {
.data
.iter()
.take(max_index)
.map(|&(l, v)| (l, v * u64::from(chart_area.height) * 8 / max))
.map(|&(l, v)| {
(
l,
v * u64::from(chart_area.height) * 8 / std::cmp::max(max, 1),
)
})
.collect::<Vec<(&str, u64)>>();
for j in (0..chart_area.height - 1).rev() {
for (i, d) in data.iter_mut().enumerate() {
let symbol = match d.1 {
0 => " ",
1 => bar::ONE_EIGHTH,
2 => bar::ONE_QUATER,
3 => bar::THREE_EIGHTHS,
4 => bar::HALF,
5 => bar::FIVE_EIGHTHS,
6 => bar::THREE_QUATERS,
7 => bar::SEVEN_EIGHTHS,
_ => bar::FULL,
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
3 => self.bar_set.three_eighths,
4 => self.bar_set.half,
5 => self.bar_set.five_eighths,
6 => self.bar_set.three_quarters,
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
};
for x in 0..self.bar_width {

View File

@@ -4,24 +4,42 @@ use crate::style::Style;
use crate::symbols::line;
use crate::widgets::{Borders, Widget};
#[derive(Debug, Clone, Copy)]
pub enum BorderType {
Plain,
Rounded,
Double,
Thick,
}
impl BorderType {
pub fn line_symbols(border_type: BorderType) -> line::Set {
match border_type {
BorderType::Plain => line::NORMAL,
BorderType::Rounded => line::ROUNDED,
BorderType::Double => line::DOUBLE,
BorderType::Thick => line::THICK,
}
}
}
/// Base widget to be used with all upper level ones. It may be used to display a box border around
/// the widget and/or add a title.
///
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders};
/// # use tui::widgets::{Block, BorderType, Borders};
/// # use tui::style::{Style, Color};
/// # fn main() {
/// Block::default()
/// .title("Block")
/// .title_style(Style::default().fg(Color::Red))
/// .borders(Borders::LEFT | Borders::RIGHT)
/// .border_style(Style::default().fg(Color::White))
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// # }
/// ```
#[derive(Clone, Copy)]
#[derive(Debug, Clone, Copy)]
pub struct Block<'a> {
/// Optional title place on the upper left of the block
title: Option<&'a str>,
@@ -31,6 +49,9 @@ pub struct Block<'a> {
borders: Borders,
/// Border style
border_style: Style,
/// Type of the border. The default is plain lines but one can choose to have rounded corners
/// or doubled lines instead.
border_type: BorderType,
/// Widget style
style: Style,
}
@@ -42,6 +63,7 @@ impl<'a> Default for Block<'a> {
title_style: Default::default(),
borders: Borders::NONE,
border_style: Default::default(),
border_type: BorderType::Plain,
style: Default::default(),
}
}
@@ -73,6 +95,11 @@ impl<'a> Block<'a> {
self
}
pub fn border_type(mut self, border_type: BorderType) -> Block<'a> {
self.border_type = border_type;
self
}
/// 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 {
@@ -98,25 +125,26 @@ impl<'a> Block<'a> {
}
impl<'a> Widget for Block<'a> {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width < 2 || area.height < 2 {
return;
}
self.background(area, buf, self.style.bg);
buf.set_background(area, self.style.bg);
let symbols = BorderType::line_symbols(self.border_type);
// Sides
if self.borders.intersects(Borders::LEFT) {
for y in area.top()..area.bottom() {
buf.get_mut(area.left(), y)
.set_symbol(line::VERTICAL)
.set_symbol(symbols.vertical)
.set_style(self.border_style);
}
}
if self.borders.intersects(Borders::TOP) {
for x in area.left()..area.right() {
buf.get_mut(x, area.top())
.set_symbol(line::HORIZONTAL)
.set_symbol(symbols.horizontal)
.set_style(self.border_style);
}
}
@@ -124,7 +152,7 @@ impl<'a> Widget for Block<'a> {
let x = area.right() - 1;
for y in area.top()..area.bottom() {
buf.get_mut(x, y)
.set_symbol(line::VERTICAL)
.set_symbol(symbols.vertical)
.set_style(self.border_style);
}
}
@@ -132,7 +160,7 @@ impl<'a> Widget for Block<'a> {
let y = area.bottom() - 1;
for x in area.left()..area.right() {
buf.get_mut(x, y)
.set_symbol(line::HORIZONTAL)
.set_symbol(symbols.horizontal)
.set_style(self.border_style);
}
}
@@ -140,46 +168,44 @@ impl<'a> Widget for Block<'a> {
// Corners
if self.borders.contains(Borders::LEFT | Borders::TOP) {
buf.get_mut(area.left(), area.top())
.set_symbol(line::TOP_LEFT)
.set_symbol(symbols.top_left)
.set_style(self.border_style);
}
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
buf.get_mut(area.right() - 1, area.top())
.set_symbol(line::TOP_RIGHT)
.set_symbol(symbols.top_right)
.set_style(self.border_style);
}
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
buf.get_mut(area.left(), area.bottom() - 1)
.set_symbol(line::BOTTOM_LEFT)
.set_symbol(symbols.bottom_left)
.set_style(self.border_style);
}
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
buf.get_mut(area.right() - 1, area.bottom() - 1)
.set_symbol(line::BOTTOM_RIGHT)
.set_symbol(symbols.bottom_right)
.set_style(self.border_style);
}
if area.width > 2 {
if let Some(title) = self.title {
let lx = if self.borders.intersects(Borders::LEFT) {
1
} else {
0
};
let rx = if self.borders.intersects(Borders::RIGHT) {
1
} else {
0
};
let width = area.width - lx - rx;
buf.set_stringn(
area.left() + lx,
area.top(),
title,
width as usize,
self.title_style,
);
}
if let Some(title) = self.title {
let lx = if self.borders.intersects(Borders::LEFT) {
1
} else {
0
};
let rx = if self.borders.intersects(Borders::RIGHT) {
1
} else {
0
};
let width = area.width - lx - rx;
buf.set_stringn(
area.left() + lx,
area.top(),
title,
width as usize,
self.title_style,
);
}
}
}

View File

@@ -1,7 +1,10 @@
use super::Shape;
use crate::style::Color;
use crate::{
style::Color,
widgets::canvas::{Painter, Shape},
};
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
#[derive(Debug, Clone)]
pub struct Line {
pub x1: f64,
pub y1: f64,
@@ -10,63 +13,83 @@ pub struct Line {
pub color: Color,
}
pub struct LineIterator {
x: f64,
y: f64,
dx: f64,
dy: f64,
dir_x: f64,
dir_y: f64,
current: f64,
end: f64,
}
impl Iterator for LineIterator {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.end {
let pos = (
self.x + (self.current * self.dx) / self.end * self.dir_x,
self.y + (self.current * self.dy) / self.end * self.dir_y,
);
self.current += 1.0;
Some(pos)
impl Shape for Line {
fn draw(&self, painter: &mut Painter) {
let (x1, y1) = match painter.get_point(self.x1, self.y1) {
Some(c) => c,
None => return,
};
let (x2, y2) = match painter.get_point(self.x2, self.y2) {
Some(c) => c,
None => return,
};
let (dx, x_range) = if x2 >= x1 {
(x2 - x1, x1..=x2)
} else {
None
(x1 - x2, x2..=x1)
};
let (dy, y_range) = if y2 >= y1 {
(y2 - y1, y1..=y2)
} else {
(y1 - y2, y2..=y1)
};
if dx == 0 {
for y in y_range {
painter.paint(x1, y, self.color);
}
} else if dy == 0 {
for x in x_range {
painter.paint(x, y1, self.color);
}
} else if dy < dx {
if x1 > x2 {
draw_line_low(painter, x2, y2, x1, y1, self.color);
} else {
draw_line_low(painter, x1, y1, x2, y2, self.color);
}
} else if y1 > y2 {
draw_line_high(painter, x2, y2, x1, y1, self.color);
} else {
draw_line_high(painter, x1, y1, x2, y2, self.color);
}
}
}
impl<'a> IntoIterator for &'a Line {
type Item = (f64, f64);
type IntoIter = LineIterator;
fn into_iter(self) -> Self::IntoIter {
let dx = self.x1.max(self.x2) - self.x1.min(self.x2);
let dy = self.y1.max(self.y2) - self.y1.min(self.y2);
let dir_x = if self.x1 <= self.x2 { 1.0 } else { -1.0 };
let dir_y = if self.y1 <= self.y2 { 1.0 } else { -1.0 };
let end = dx.max(dy);
LineIterator {
x: self.x1,
y: self.y1,
dx,
dy,
dir_x,
dir_y,
current: 0.0,
end,
fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
let dx = (x2 - x1) as isize;
let dy = (y2 as isize - y1 as isize).abs();
let mut d = 2 * dy - dx;
let mut y = y1;
for x in x1..=x2 {
painter.paint(x, y, color);
if d > 0 {
y = if y1 > y2 {
y.saturating_sub(1)
} else {
y.saturating_add(1)
};
d -= 2 * dx;
}
d += 2 * dy;
}
}
impl<'a> Shape<'a> for Line {
fn color(&self) -> Color {
self.color
}
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
Box::new(self.into_iter())
fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
let dx = (x2 as isize - x1 as isize).abs();
let dy = (y2 - y1) as isize;
let mut d = 2 * dx - dy;
let mut x = x1;
for y in y1..=y2 {
painter.paint(x, y, color);
if d > 0 {
x = if x1 > x2 {
x.saturating_sub(1)
} else {
x.saturating_add(1)
};
d -= 2 * dy;
}
d += 2 * dx;
}
}

View File

@@ -1,9 +1,12 @@
use crate::style::Color;
use crate::widgets::canvas::points::PointsIterator;
use crate::widgets::canvas::world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION};
use crate::widgets::canvas::Shape;
use crate::{
style::Color,
widgets::canvas::{
world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION},
Painter, Shape,
},
};
#[derive(Clone, Copy)]
#[derive(Debug, Clone, Copy)]
pub enum MapResolution {
Low,
High,
@@ -19,6 +22,7 @@ impl MapResolution {
}
/// Shape to draw a world map with the given resolution and color
#[derive(Debug, Clone)]
pub struct Map {
pub resolution: MapResolution,
pub color: Color,
@@ -33,19 +37,12 @@ impl Default for Map {
}
}
impl<'a> Shape<'a> for Map {
fn color(&self) -> Color {
self.color
}
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
Box::new(self.into_iter())
}
}
impl<'a> IntoIterator for &'a Map {
type Item = (f64, f64);
type IntoIter = PointsIterator<'a>;
fn into_iter(self) -> Self::IntoIter {
PointsIterator::from(self.resolution.data())
impl Shape for Map {
fn draw(&self, painter: &mut Painter) {
for (x, y) in self.resolution.data() {
if let Some((x, y)) = painter.get_point(*x, *y) {
painter.paint(x, y, self.color);
}
}
}
}

View File

@@ -9,29 +9,22 @@ pub use self::map::{Map, MapResolution};
pub use self::points::Points;
pub use self::rectangle::Rectangle;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::{Color, Style};
use crate::widgets::{Block, Widget};
pub const DOTS: [[u16; 2]; 4] = [
[0x0001, 0x0008],
[0x0002, 0x0010],
[0x0004, 0x0020],
[0x0040, 0x0080],
];
pub const BRAILLE_OFFSET: u16 = 0x2800;
pub const BRAILLE_BLANK: char = '';
use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols,
widgets::{Block, Widget},
};
use std::fmt::Debug;
/// Interface for all shapes that may be drawn on a Canvas widget.
pub trait Shape<'a> {
/// Returns the color of the shape
fn color(&self) -> Color;
/// Returns an iterator over all points of the shape
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a>;
pub trait Shape {
fn draw(&self, painter: &mut Painter);
}
/// Label to draw some text on the canvas
#[derive(Debug, Clone)]
pub struct Label<'a> {
pub x: f64,
pub y: f64,
@@ -39,23 +32,56 @@ pub struct Label<'a> {
pub color: Color,
}
#[derive(Debug, Clone)]
struct Layer {
string: String,
colors: Vec<Color>,
}
struct Grid {
trait Grid: Debug {
fn width(&self) -> u16;
fn height(&self) -> u16;
fn resolution(&self) -> (f64, f64);
fn paint(&mut self, x: usize, y: usize, color: Color);
fn save(&self) -> Layer;
fn reset(&mut self);
}
#[derive(Debug, Clone)]
struct BrailleGrid {
width: u16,
height: u16,
cells: Vec<u16>,
colors: Vec<Color>,
}
impl Grid {
fn new(width: usize, height: usize) -> Grid {
Grid {
cells: vec![BRAILLE_OFFSET; width * height],
colors: vec![Color::Reset; width * height],
impl BrailleGrid {
fn new(width: u16, height: u16) -> BrailleGrid {
let length = usize::from(width * height);
BrailleGrid {
width,
height,
cells: vec![symbols::braille::BLANK; length],
colors: vec![Color::Reset; length],
}
}
}
impl Grid for BrailleGrid {
fn width(&self) -> u16 {
self.width
}
fn height(&self) -> u16 {
self.height
}
fn resolution(&self) -> (f64, f64) {
(
f64::from(self.width) * 2.0 - 1.0,
f64::from(self.height) * 4.0 - 1.0,
)
}
fn save(&self) -> Layer {
Layer {
@@ -66,47 +92,191 @@ impl Grid {
fn reset(&mut self) {
for c in &mut self.cells {
*c = BRAILLE_OFFSET;
*c = symbols::braille::BLANK;
}
for c in &mut self.colors {
*c = Color::Reset;
}
}
fn paint(&mut self, x: usize, y: usize, color: Color) {
let index = y / 4 * self.width as usize + x / 2;
if let Some(c) = self.cells.get_mut(index) {
*c |= symbols::braille::DOTS[y % 4][x % 2];
}
if let Some(c) = self.colors.get_mut(index) {
*c = color;
}
}
}
#[derive(Debug, Clone)]
struct DotGrid {
width: u16,
height: u16,
cells: Vec<char>,
colors: Vec<Color>,
}
impl DotGrid {
fn new(width: u16, height: u16) -> DotGrid {
let length = usize::from(width * height);
DotGrid {
width,
height,
cells: vec![' '; length],
colors: vec![Color::Reset; length],
}
}
}
impl Grid for DotGrid {
fn width(&self) -> u16 {
self.width
}
fn height(&self) -> u16 {
self.height
}
fn resolution(&self) -> (f64, f64) {
(f64::from(self.width) - 1.0, f64::from(self.height) - 1.0)
}
fn save(&self) -> Layer {
Layer {
string: self.cells.iter().collect(),
colors: self.colors.clone(),
}
}
fn reset(&mut self) {
for c in &mut self.cells {
*c = ' ';
}
for c in &mut self.colors {
*c = Color::Reset;
}
}
fn paint(&mut self, x: usize, y: usize, color: Color) {
let index = y * self.width as usize + x;
if let Some(c) = self.cells.get_mut(index) {
*c = '•';
}
if let Some(c) = self.colors.get_mut(index) {
*c = color;
}
}
}
#[derive(Debug)]
pub struct Painter<'a, 'b> {
context: &'a mut Context<'b>,
resolution: (f64, f64),
}
impl<'a, 'b> Painter<'a, 'b> {
/// Convert the (x, y) coordinates to location of a point on the grid
///
/// # Examples:
/// ```
/// use tui::{symbols, widgets::canvas::{Painter, Context}};
///
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
/// let point = painter.get_point(1.0, 0.0);
/// assert_eq!(point, Some((0, 7)));
/// let point = painter.get_point(1.5, 1.0);
/// assert_eq!(point, Some((1, 3)));
/// let point = painter.get_point(0.0, 0.0);
/// assert_eq!(point, None);
/// let point = painter.get_point(2.0, 2.0);
/// assert_eq!(point, Some((3, 0)));
/// let point = painter.get_point(1.0, 2.0);
/// assert_eq!(point, Some((0, 0)));
/// ```
pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {
let left = self.context.x_bounds[0];
let right = self.context.x_bounds[1];
let top = self.context.y_bounds[1];
let bottom = self.context.y_bounds[0];
if x < left || x > right || y < bottom || y > top {
return None;
}
let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs();
let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs();
let x = ((x - left) * self.resolution.0 / width) as usize;
let y = ((top - y) * self.resolution.1 / height) as usize;
Some((x, y))
}
/// Paint a point of the grid
///
/// # Examples:
/// ```
/// use tui::{style::Color, symbols, widgets::canvas::{Painter, Context}};
///
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
/// let cell = painter.paint(1, 3, Color::Red);
/// ```
pub fn paint(&mut self, x: usize, y: usize, color: Color) {
self.context.grid.paint(x, y, color);
}
}
impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
fn from(context: &'a mut Context<'b>) -> Painter<'a, 'b> {
let resolution = context.grid.resolution();
Painter {
context,
resolution,
}
}
}
/// Holds the state of the Canvas when painting to it.
#[derive(Debug)]
pub struct Context<'a> {
width: u16,
height: u16,
x_bounds: [f64; 2],
y_bounds: [f64; 2],
grid: Grid,
grid: Box<dyn Grid>,
dirty: bool,
layers: Vec<Layer>,
labels: Vec<Label<'a>>,
}
impl<'a> Context<'a> {
pub fn new(
width: u16,
height: u16,
x_bounds: [f64; 2],
y_bounds: [f64; 2],
marker: symbols::Marker,
) -> Context<'a> {
let grid: Box<dyn Grid> = match marker {
symbols::Marker::Dot => Box::new(DotGrid::new(width, height)),
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
};
Context {
x_bounds,
y_bounds,
grid,
dirty: false,
layers: Vec::new(),
labels: Vec::new(),
}
}
/// Draw any object that may implement the Shape trait
pub fn draw<'b, S>(&mut self, shape: &'b S)
pub fn draw<S>(&mut self, shape: &S)
where
S: Shape<'b>,
S: Shape,
{
self.dirty = true;
let left = self.x_bounds[0];
let right = self.x_bounds[1];
let bottom = self.y_bounds[0];
let top = self.y_bounds[1];
for (x, y) in shape
.points()
.filter(|&(x, y)| !(x < left || x > right || y < bottom || y > top))
{
let dy = ((top - y) * f64::from(self.height - 1) * 4.0 / (top - bottom)) as usize;
let dx = ((x - left) * f64::from(self.width - 1) * 2.0 / (right - left)) as usize;
let index = dy / 4 * self.width as usize + dx / 2;
self.grid.cells[index] |= DOTS[dy % 4][dx % 2];
self.grid.colors[index] = shape.color();
}
let mut painter = Painter::from(self);
shape.draw(&mut painter);
}
/// Go one layer above in the canvas.
@@ -138,7 +308,6 @@ impl<'a> Context<'a> {
/// # use tui::layout::Rect;
/// # use tui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
/// # use tui::style::Color;
/// # fn main() {
/// Canvas::default()
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
/// .x_bounds([-180.0, 180.0])
@@ -157,16 +326,13 @@ impl<'a> Context<'a> {
/// color: Color::White,
/// });
/// ctx.draw(&Rectangle {
/// rect: Rect {
/// x: 10,
/// y: 20,
/// width: 10,
/// height: 10,
/// },
/// x: 10.0,
/// y: 20.0,
/// width: 10.0,
/// height: 10.0,
/// color: Color::Red
/// });
/// });
/// # }
/// ```
pub struct Canvas<'a, F>
where
@@ -177,6 +343,7 @@ where
y_bounds: [f64; 2],
painter: Option<F>,
background_color: Color,
marker: symbols::Marker,
}
impl<'a, F> Default for Canvas<'a, F>
@@ -190,6 +357,7 @@ where
y_bounds: [0.0, 0.0],
painter: None,
background_color: Color::Reset,
marker: symbols::Marker::Braille,
}
}
}
@@ -202,10 +370,12 @@ where
self.block = Some(block);
self
}
pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
self.x_bounds = bounds;
self
}
pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
self.y_bounds = bounds;
self
@@ -221,77 +391,103 @@ where
self.background_color = color;
self
}
/// Change the type of points used to draw the shapes. By default the braille patterns are used
/// as they provide a more fine grained result but you might want to use the simple dot instead
/// if the targeted terminal does not support those symbols.
///
/// # Examples
///
/// ```
/// # use tui::widgets::canvas::Canvas;
/// # use tui::symbols;
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
///
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
/// ```
pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> {
self.marker = marker;
self
}
}
impl<'a, F> Widget for Canvas<'a, F>
where
F: Fn(&mut Context),
{
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let canvas_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.render(area, buf);
b.inner(area)
}
None => area,
};
let width = canvas_area.width as usize;
let height = canvas_area.height as usize;
if let Some(ref painter) = self.painter {
// Create a blank context that match the size of the terminal
let mut ctx = Context {
x_bounds: self.x_bounds,
y_bounds: self.y_bounds,
width: canvas_area.width,
height: canvas_area.height,
grid: Grid::new(width, height),
dirty: false,
layers: Vec::new(),
labels: Vec::new(),
};
// Paint to this context
painter(&mut ctx);
ctx.finish();
let painter = match self.painter {
Some(ref p) => p,
None => return,
};
// Retreive painted points for each layer
for layer in ctx.layers {
for (i, (ch, color)) in layer
.string
.chars()
.zip(layer.colors.into_iter())
.enumerate()
{
if ch != BRAILLE_BLANK {
let (x, y) = (i % width, i / width);
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
.set_char(ch)
.set_fg(color)
.set_bg(self.background_color);
}
// Create a blank context that match the size of the canvas
let mut ctx = Context::new(
canvas_area.width,
canvas_area.height,
self.x_bounds,
self.y_bounds,
self.marker,
);
// Paint to this context
painter(&mut ctx);
ctx.finish();
// Retreive painted points for each layer
for layer in ctx.layers {
for (i, (ch, color)) in layer
.string
.chars()
.zip(layer.colors.into_iter())
.enumerate()
{
if ch != ' ' && ch != '\u{2800}' {
let (x, y) = (i % width, i / width);
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
.set_char(ch)
.set_fg(color)
.set_bg(self.background_color);
}
}
}
// Finally draw the labels
let style = Style::default().bg(self.background_color);
for label in ctx.labels.iter().filter(|l| {
!(l.x < self.x_bounds[0]
|| l.x > self.x_bounds[1]
|| l.y < self.y_bounds[0]
|| l.y > self.y_bounds[1])
}) {
let dy = ((self.y_bounds[1] - label.y) * f64::from(canvas_area.height - 1)
/ (self.y_bounds[1] - self.y_bounds[0])) as u16;
let dx = ((label.x - self.x_bounds[0]) * f64::from(canvas_area.width - 1)
/ (self.x_bounds[1] - self.x_bounds[0])) as u16;
buf.set_string(
dx + canvas_area.left(),
dy + canvas_area.top(),
label.text,
style.fg(label.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];
let bottom = self.y_bounds[0];
let width = (self.x_bounds[1] - self.x_bounds[0]).abs();
let height = (self.y_bounds[1] - self.y_bounds[0]).abs();
let resolution = {
let width = f64::from(canvas_area.width - 1);
let height = f64::from(canvas_area.height - 1);
(width, height)
};
for label in ctx
.labels
.iter()
.filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom)
{
let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
buf.set_stringn(
x,
y,
label.text,
(canvas_area.right() - x) as usize,
style.fg(label.color),
);
}
}
}

View File

@@ -1,20 +1,22 @@
use std::slice;
use super::Shape;
use crate::style::Color;
use crate::{
style::Color,
widgets::canvas::{Painter, Shape},
};
/// A shape to draw a group of points with the given color
#[derive(Debug, Clone)]
pub struct Points<'a> {
pub coords: &'a [(f64, f64)],
pub color: Color,
}
impl<'a> Shape<'a> for Points<'a> {
fn color(&self) -> Color {
self.color
}
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
Box::new(self.into_iter())
impl<'a> Shape for Points<'a> {
fn draw(&self, painter: &mut Painter) {
for (x, y) in self.coords {
if let Some((x, y)) = painter.get_point(*x, *y) {
painter.paint(x, y, self.color);
}
}
}
}
@@ -26,33 +28,3 @@ impl<'a> Default for Points<'a> {
}
}
}
impl<'a> IntoIterator for &'a Points<'a> {
type Item = (f64, f64);
type IntoIter = PointsIterator<'a>;
fn into_iter(self) -> Self::IntoIter {
PointsIterator {
iter: self.coords.iter(),
}
}
}
pub struct PointsIterator<'a> {
iter: slice::Iter<'a, (f64, f64)>,
}
impl<'a> From<&'a [(f64, f64)]> for PointsIterator<'a> {
fn from(data: &'a [(f64, f64)]) -> PointsIterator<'a> {
PointsIterator { iter: data.iter() }
}
}
impl<'a> Iterator for PointsIterator<'a> {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
match self.iter.next() {
Some(p) => Some(*p),
None => None,
}
}
}

View File

@@ -1,54 +1,52 @@
use crate::layout::Rect;
use crate::style::Color;
use crate::widgets::canvas::{Line, Shape};
use itertools::Itertools;
use crate::{
style::Color,
widgets::canvas::{Line, Painter, Shape},
};
/// Shape to draw a rectangle from a `Rect` with the given color
#[derive(Debug, Clone)]
pub struct Rectangle {
pub rect: Rect,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub color: Color,
}
impl<'a> Shape<'a> for Rectangle {
fn color(&self) -> Color {
self.color
}
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
let left_line = Line {
x1: f64::from(self.rect.x),
y1: f64::from(self.rect.y),
x2: f64::from(self.rect.x),
y2: f64::from(self.rect.y + self.rect.height),
color: self.color,
};
let top_line = Line {
x1: f64::from(self.rect.x),
y1: f64::from(self.rect.y + self.rect.height),
x2: f64::from(self.rect.x + self.rect.width),
y2: f64::from(self.rect.y + self.rect.height),
color: self.color,
};
let right_line = Line {
x1: f64::from(self.rect.x + self.rect.width),
y1: f64::from(self.rect.y),
x2: f64::from(self.rect.x + self.rect.width),
y2: f64::from(self.rect.y + self.rect.height),
color: self.color,
};
let bottom_line = Line {
x1: f64::from(self.rect.x),
y1: f64::from(self.rect.y),
x2: f64::from(self.rect.x + self.rect.width),
y2: f64::from(self.rect.y),
color: self.color,
};
Box::new(
left_line.into_iter().merge(
top_line
.into_iter()
.merge(right_line.into_iter().merge(bottom_line.into_iter())),
),
)
impl Shape for Rectangle {
fn draw(&self, painter: &mut Painter) {
let lines: [Line; 4] = [
Line {
x1: self.x,
y1: self.y,
x2: self.x,
y2: self.y + self.height,
color: self.color,
},
Line {
x1: self.x,
y1: self.y + self.height,
x2: self.x + self.width,
y2: self.y + self.height,
color: self.color,
},
Line {
x1: self.x + self.width,
y1: self.y,
x2: self.x + self.width,
y2: self.y + self.height,
color: self.color,
},
Line {
x1: self.x,
y1: self.y,
x2: self.x + self.width,
y2: self.y,
color: self.color,
},
];
for line in &lines {
line.draw(painter);
}
}
}

View File

@@ -1,15 +1,18 @@
use std::cmp::max;
use crate::{
buffer::Buffer,
layout::{Constraint, Rect},
style::Style,
symbols,
widgets::{
canvas::{Canvas, Line, Points},
Block, Borders, Widget,
},
};
use std::{borrow::Cow, cmp::max};
use unicode_width::UnicodeWidthStr;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::symbols;
use crate::widgets::canvas::{Canvas, Points};
use crate::widgets::{Block, Borders, Widget};
/// An X or Y axis for the chart widget
#[derive(Debug, Clone)]
pub struct Axis<'a, L>
where
L: AsRef<str> + 'a,
@@ -79,22 +82,26 @@ where
}
}
/// Marker to use when plotting data points
pub enum Marker {
/// One point per cell
Dot,
/// Up to 8 points per cell
Braille,
/// Used to determine which style of graphing to use
#[derive(Debug, Clone, Copy)]
pub enum GraphType {
/// Draw each point
Scatter,
/// Draw each point and lines between each point using the same marker
Line,
}
/// A group of data points
#[derive(Debug, Clone)]
pub struct Dataset<'a> {
/// Name of the dataset (used in the legend if shown)
name: &'a str,
name: Cow<'a, str>,
/// A reference to the actual data
data: &'a [(f64, f64)],
/// Symbol used for each points of this dataset
marker: Marker,
marker: symbols::Marker,
/// Determines graph type used for drawing points
graph_type: GraphType,
/// Style used to plot this dataset
style: Style,
}
@@ -102,17 +109,21 @@ pub struct Dataset<'a> {
impl<'a> Default for Dataset<'a> {
fn default() -> Dataset<'a> {
Dataset {
name: "",
name: Cow::from(""),
data: &[],
marker: Marker::Dot,
marker: symbols::Marker::Dot,
graph_type: GraphType::Scatter,
style: Style::default(),
}
}
}
impl<'a> Dataset<'a> {
pub fn name(mut self, name: &'a str) -> Dataset<'a> {
self.name = name;
pub fn name<S>(mut self, name: S) -> Dataset<'a>
where
S: Into<Cow<'a, str>>,
{
self.name = name.into();
self
}
@@ -121,11 +132,16 @@ impl<'a> Dataset<'a> {
self
}
pub fn marker(mut self, marker: Marker) -> Dataset<'a> {
pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
self.marker = marker;
self
}
pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
self.graph_type = graph_type;
self
}
pub fn style(mut self, style: Style) -> Dataset<'a> {
self.style = style;
self
@@ -134,15 +150,23 @@ 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)]
#[derive(Debug, Clone, PartialEq)]
struct ChartLayout {
/// Location of the title of the x axis
title_x: Option<(u16, u16)>,
/// Location of the title of the y axis
title_y: Option<(u16, u16)>,
/// Location of the first label of the x axis
label_x: Option<u16>,
/// Location of the first label of the y axis
label_y: Option<u16>,
/// Y coordinate of the horizontal axis
axis_x: Option<u16>,
/// X coordinate of the vertical axis
axis_y: Option<u16>,
/// Area of the legend
legend_area: Option<Rect>,
/// Area of the graph
graph_area: Rect,
}
@@ -166,9 +190,9 @@ impl Default for ChartLayout {
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, Marker};
/// # use tui::symbols;
/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
/// # use tui::style::{Style, Color};
/// # fn main() {
/// Chart::default()
/// .block(Block::default().title("Chart"))
/// .x_axis(Axis::default()
@@ -185,16 +209,18 @@ impl Default for ChartLayout {
/// .labels(&["0.0", "5.0", "10.0"]))
/// .datasets(&[Dataset::default()
/// .name("data1")
/// .marker(Marker::Dot)
/// .marker(symbols::Marker::Dot)
/// .graph_type(GraphType::Scatter)
/// .style(Style::default().fg(Color::Cyan))
/// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
/// Dataset::default()
/// .name("data2")
/// .marker(Marker::Braille)
/// .marker(symbols::Marker::Braille)
/// .graph_type(GraphType::Line)
/// .style(Style::default().fg(Color::Magenta))
/// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)])]);
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct Chart<'a, LX, LY>
where
LX: AsRef<str> + 'a,
@@ -210,6 +236,9 @@ where
datasets: &'a [Dataset<'a>],
/// The widget base style
style: Style,
/// Constraints used to determine whether the legend should be shown or
/// not
hidden_legend_constraints: (Constraint, Constraint),
}
impl<'a, LX, LY> Default for Chart<'a, LX, LY>
@@ -224,6 +253,7 @@ where
y_axis: Axis::default(),
style: Default::default(),
datasets: &[],
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
}
}
}
@@ -258,6 +288,30 @@ where
self
}
/// Set the constraints used to determine whether the legend should be shown or
/// not.
///
/// # Examples
///
/// ```
/// # use tui::widgets::Chart;
/// # use tui::layout::Constraint;
/// let constraints = (
/// Constraint::Ratio(1, 3),
/// Constraint::Ratio(1, 4)
/// );
/// // Hide the legend when either its width is greater than 33% of the total widget width
/// // or if its height is greater than 25% of the total widget height.
/// let _chart: Chart<String, String> = Chart::default()
/// .hidden_legend_constraints(constraints);
pub fn hidden_legend_constraints(
mut self,
constraints: (Constraint, Constraint),
) -> Chart<'a, LX, LY> {
self.hidden_legend_constraints = constraints;
self
}
/// Compute the internal layout of the chart given the area. If the area is too small some
/// elements may be automatically hidden
fn layout(&self, area: Rect) -> ChartLayout {
@@ -320,9 +374,17 @@ where
if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
let legend_width = inner_width + 2;
let legend_height = self.datasets.len() as u16 + 2;
if legend_width < layout.graph_area.width / 3
&& legend_height < layout.graph_area.height / 3
&& inner_width > 0
let max_legend_width = self
.hidden_legend_constraints
.0
.apply(layout.graph_area.width);
let max_legend_height = self
.hidden_legend_constraints
.1
.apply(layout.graph_area.height);
if inner_width > 0
&& legend_width < max_legend_width
&& legend_height < max_legend_height
{
layout.legend_area = Some(Rect::new(
layout.graph_area.right() - legend_width,
@@ -341,10 +403,10 @@ where
LX: AsRef<str>,
LY: AsRef<str>,
{
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let chart_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.render(area, buf);
b.inner(area)
}
None => area,
@@ -356,16 +418,16 @@ where
return;
}
self.background(chart_area, buf, self.style.bg);
buf.set_background(chart_area, self.style.bg);
if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap();
buf.set_string(x, y, title, self.x_axis.style);
buf.set_string(x, y, title, self.x_axis.title_style);
}
if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap();
buf.set_string(x, y, title, self.y_axis.style);
buf.set_string(x, y, title, self.y_axis.title_style);
}
if let Some(y) = layout.label_x {
@@ -426,55 +488,86 @@ where
}
for dataset in self.datasets {
match dataset.marker {
Marker::Dot => {
for &(x, y) in dataset.data.iter().filter(|&&(x, y)| {
!(x < self.x_axis.bounds[0]
|| x > self.x_axis.bounds[1]
|| y < self.y_axis.bounds[0]
|| y > self.y_axis.bounds[1])
}) {
let dy = ((self.y_axis.bounds[1] - y) * f64::from(graph_area.height - 1)
/ (self.y_axis.bounds[1] - self.y_axis.bounds[0]))
as u16;
let dx = ((x - self.x_axis.bounds[0]) * f64::from(graph_area.width - 1)
/ (self.x_axis.bounds[1] - self.x_axis.bounds[0]))
as u16;
buf.get_mut(graph_area.left() + dx, graph_area.top() + dy)
.set_symbol(symbols::DOT)
.set_fg(dataset.style.fg)
.set_bg(dataset.style.bg);
}
}
Marker::Braille => {
Canvas::default()
.background_color(self.style.bg)
.x_bounds(self.x_axis.bounds)
.y_bounds(self.y_axis.bounds)
.paint(|ctx| {
ctx.draw(&Points {
coords: dataset.data,
Canvas::default()
.background_color(self.style.bg)
.x_bounds(self.x_axis.bounds)
.y_bounds(self.y_axis.bounds)
.marker(dataset.marker)
.paint(|ctx| {
ctx.draw(&Points {
coords: dataset.data,
color: dataset.style.fg,
});
if let GraphType::Line = dataset.graph_type {
for i in 0..dataset.data.len() - 1 {
ctx.draw(&Line {
x1: dataset.data[i].0,
y1: dataset.data[i].1,
x2: dataset.data[i + 1].0,
y2: dataset.data[i + 1].1,
color: dataset.style.fg,
});
})
.draw(graph_area, buf);
}
}
})
}
}
})
.render(graph_area, buf);
}
if let Some(legend_area) = layout.legend_area {
Block::default()
.borders(Borders::ALL)
.draw(legend_area, buf);
.render(legend_area, buf);
for (i, dataset) in self.datasets.iter().enumerate() {
buf.set_string(
legend_area.x + 1,
legend_area.y + 1 + i as u16,
dataset.name,
&dataset.name,
dataset.style,
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct LegendTestCase {
chart_area: Rect,
hidden_legend_constraints: (Constraint, Constraint),
legend_area: Option<Rect>,
}
#[test]
fn it_should_hide_the_legend() {
let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)];
let datasets = (0..10)
.map(|i| {
let name = format!("Dataset #{}", i);
Dataset::default().name(name).data(&data)
})
.collect::<Vec<_>>();
let cases = [
LegendTestCase {
chart_area: Rect::new(0, 0, 100, 100),
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
legend_area: Some(Rect::new(88, 0, 12, 12)),
},
LegendTestCase {
chart_area: Rect::new(0, 0, 100, 100),
hidden_legend_constraints: (Constraint::Ratio(1, 10), Constraint::Ratio(1, 4)),
legend_area: None,
},
];
for case in &cases {
let chart: Chart<String, String> = Chart::default()
.x_axis(Axis::default().title("X axis"))
.y_axis(Axis::default().title("Y axis"))
.hidden_legend_constraints(case.hidden_legend_constraints)
.datasets(datasets.as_slice());
let layout = chart.layout(case.chart_area);
assert_eq!(layout.legend_area, case.legend_area);
}
}
}

36
src/widgets/clear.rs Normal file
View File

@@ -0,0 +1,36 @@
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::widgets::Widget;
/// A widget to to clear/reset a certain area to allow overdrawing (e.g. for popups)
///
/// # Examples
///
/// ```
/// # use tui::widgets::{Clear, Block, Borders};
/// # use tui::layout::Rect;
/// # use tui::Frame;
/// # use tui::backend::Backend;
/// fn draw_on_clear<B: Backend>(f: &mut Frame<B>, area: Rect) {
/// let block = Block::default().title("Block").borders(Borders::ALL);
/// f.render_widget(Clear, area); // <- this will clear/reset the area first
/// f.render_widget(block, area); // now render the block widget
/// }
/// ```
///
/// # Popup Example
///
/// For a more complete example how to utilize `Clear` to realize popups see
/// the example `examples/popup.rs`
#[derive(Debug, Clone)]
pub struct Clear;
impl Widget for Clear {
fn render(self, area: Rect, buf: &mut Buffer) {
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
buf.get_mut(x, y).reset();
}
}
}
}

View File

@@ -12,13 +12,12 @@ use crate::widgets::{Block, Widget};
/// ```
/// # use tui::widgets::{Widget, Gauge, Block, Borders};
/// # use tui::style::{Style, Color, Modifier};
/// # fn main() {
/// Gauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .style(Style::default().fg(Color::White).bg(Color::Black).modifier(Modifier::ITALIC))
/// .percent(20);
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct Gauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
@@ -74,10 +73,10 @@ impl<'a> Gauge<'a> {
}
impl<'a> Widget for Gauge<'a> {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let gauge_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.render(area, buf);
b.inner(area)
}
None => area,
@@ -87,7 +86,7 @@ impl<'a> Widget for Gauge<'a> {
}
if self.style.bg != Color::Reset {
self.background(gauge_area, buf, self.style.bg);
buf.set_background(gauge_area, self.style.bg);
}
let center = gauge_area.height / 2 + gauge_area.top();

View File

@@ -1,21 +1,68 @@
use std::iter;
use std::iter::Iterator;
use std::iter::{self, Iterator};
use unicode_width::UnicodeWidthStr;
use crate::buffer::Buffer;
use crate::layout::{Corner, Rect};
use crate::style::Style;
use crate::widgets::{Block, Text, Widget};
use crate::widgets::{Block, StatefulWidget, Text, Widget};
#[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
}
pub fn select(&mut self, index: Option<usize>) {
self.selected = index;
if index.is_none() {
self.offset = 0;
}
}
}
/// A widget to display several items among which one can be selected (optional)
///
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, List, Text};
/// # use tui::style::{Style, Color, Modifier};
/// let items = ["Item 1", "Item 2", "Item 3"].iter().map(|i| Text::raw(*i));
/// List::new(items)
/// .block(Block::default().title("List").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().modifier(Modifier::ITALIC))
/// .highlight_symbol(">>");
/// ```
#[derive(Debug, Clone)]
pub struct List<'b, L>
where
L: Iterator<Item = Text<'b>>,
{
block: Option<Block<'b>>,
items: L,
style: Style,
start_corner: Corner,
/// Base style of the widget
style: Style,
/// Style used to render selected item
highlight_style: Style,
/// Symbol in front of the selected item (Shift all items to the right)
highlight_symbol: Option<&'b str>,
}
impl<'b, L> Default for List<'b, L>
@@ -28,6 +75,8 @@ where
items: L::default(),
style: Default::default(),
start_corner: Corner::TopLeft,
highlight_style: Style::default(),
highlight_symbol: None,
}
}
}
@@ -42,6 +91,8 @@ where
items,
style: Default::default(),
start_corner: Corner::TopLeft,
highlight_style: Style::default(),
highlight_symbol: None,
}
}
@@ -63,20 +114,32 @@ where
self
}
pub fn highlight_symbol(mut self, highlight_symbol: &'b str) -> List<'b, L> {
self.highlight_symbol = Some(highlight_symbol);
self
}
pub fn highlight_style(mut self, highlight_style: Style) -> List<'b, L> {
self.highlight_style = highlight_style;
self
}
pub fn start_corner(mut self, corner: Corner) -> List<'b, L> {
self.start_corner = corner;
self
}
}
impl<'b, L> Widget for List<'b, L>
impl<'b, L> StatefulWidget for List<'b, L>
where
L: Iterator<Item = Text<'b>>,
{
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
type State = ListState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let list_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.render(area, buf);
b.inner(area)
}
None => area,
@@ -86,11 +149,36 @@ where
return;
}
self.background(list_area, buf, self.style.bg);
let list_height = list_area.height as usize;
buf.set_background(list_area, self.style.bg);
// Use highlight_style only if something is selected
let (selected, highlight_style) = match state.selected {
Some(i) => (Some(i), self.highlight_style),
None => (None, self.style),
};
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
// Make sure the list show the selected item
state.offset = if let Some(selected) = selected {
if selected >= list_height + state.offset - 1 {
selected + 1 - list_height
} else if selected < state.offset {
selected
} else {
state.offset
}
} else {
0
};
for (i, item) in self
.items
.by_ref()
.skip(state.offset)
.enumerate()
.take(list_area.height as usize)
{
@@ -100,146 +188,44 @@ where
// Not supported
_ => (list_area.left(), list_area.top() + i as u16),
};
let (elem_x, style) = if let Some(s) = selected {
if s == i + state.offset {
let (x, _) = buf.set_stringn(
x,
y,
highlight_symbol,
list_area.width as usize,
highlight_style,
);
(x, Some(highlight_style))
} else {
let (x, _) =
buf.set_stringn(x, y, &blank_symbol, list_area.width as usize, self.style);
(x, None)
}
} else {
(x, None)
};
let max_element_width = (list_area.width - (elem_x - x)) as usize;
match item {
Text::Raw(ref v) => {
buf.set_stringn(x, y, v, list_area.width as usize, Style::default());
buf.set_stringn(elem_x, y, v, max_element_width, style.unwrap_or(self.style));
}
Text::Styled(ref v, s) => {
buf.set_stringn(x, y, v, list_area.width as usize, s);
buf.set_stringn(elem_x, y, v, max_element_width, style.unwrap_or(s));
}
};
}
}
}
/// A widget to display several items among which one can be selected (optional)
///
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, SelectableList};
/// # use tui::style::{Style, Color, Modifier};
/// # fn main() {
/// SelectableList::default()
/// .block(Block::default().title("SelectableList").borders(Borders::ALL))
/// .items(&["Item 1", "Item 2", "Item 3"])
/// .select(Some(1))
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().modifier(Modifier::ITALIC))
/// .highlight_symbol(">>");
/// # }
/// ```
pub struct SelectableList<'b> {
block: Option<Block<'b>>,
/// Items to be displayed
items: Vec<&'b str>,
/// Index of the one selected
selected: Option<usize>,
/// Base style of the widget
style: Style,
/// Style used to render selected item
highlight_style: Style,
/// Symbol in front of the selected item (Shift all items to the right)
highlight_symbol: Option<&'b str>,
}
impl<'b> Default for SelectableList<'b> {
fn default() -> SelectableList<'b> {
SelectableList {
block: None,
items: Vec::new(),
selected: None,
style: Default::default(),
highlight_style: Default::default(),
highlight_symbol: None,
}
}
}
impl<'b> SelectableList<'b> {
pub fn block(mut self, block: Block<'b>) -> SelectableList<'b> {
self.block = Some(block);
self
}
pub fn items<I>(mut self, items: &'b [I]) -> SelectableList<'b>
where
I: AsRef<str> + 'b,
{
self.items = items.iter().map(|i| i.as_ref()).collect::<Vec<&str>>();
self
}
pub fn style(mut self, style: Style) -> SelectableList<'b> {
self.style = style;
self
}
pub fn highlight_symbol(mut self, highlight_symbol: &'b str) -> SelectableList<'b> {
self.highlight_symbol = Some(highlight_symbol);
self
}
pub fn highlight_style(mut self, highlight_style: Style) -> SelectableList<'b> {
self.highlight_style = highlight_style;
self
}
pub fn select(mut self, index: Option<usize>) -> SelectableList<'b> {
self.selected = index;
self
}
}
impl<'b> Widget for SelectableList<'b> {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
let list_area = match self.block {
Some(ref mut b) => b.inner(area),
None => area,
};
let list_height = list_area.height as usize;
// Use highlight_style only if something is selected
let (selected, highlight_style) = match self.selected {
Some(i) => (Some(i), self.highlight_style),
None => (None, self.style),
};
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
// Make sure the list show the selected item
let offset = if let Some(selected) = selected {
if selected >= list_height {
selected - list_height + 1
} else {
0
}
} else {
0
};
// Render items
let items = self
.items
.iter()
.enumerate()
.map(|(i, &item)| {
if let Some(s) = selected {
if i == s {
Text::styled(format!("{} {}", highlight_symbol, item), highlight_style)
} else {
Text::styled(format!("{} {}", blank_symbol, item), self.style)
}
} else {
Text::styled(item, self.style)
}
})
.skip(offset as usize);
List::new(items)
.block(self.block.unwrap_or_default())
.style(self.style)
.draw(area, buf);
impl<'b, L> Widget for List<'b, L>
where
L: Iterator<Item = Text<'b>>,
{
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = ListState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
}

View File

@@ -1,3 +1,20 @@
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
//!
//! All widgets are implemented using the builder pattern and are consumable objects. They are not
//! meant to be stored but used as *commands* to draw common figures in the UI.
//!
//! The available widgets are:
//! - [`Block`]
//! - [`Tabs`]
//! - [`List`]
//! - [`Table`]
//! - [`Paragraph`]
//! - [`Chart`]
//! - [`BarChart`]
//! - [`Gauge`]
//! - [`Sparkline`]
//! - [`Clear`]
use bitflags::bitflags;
use std::borrow::Cow;
@@ -5,6 +22,7 @@ mod barchart;
mod block;
pub mod canvas;
mod chart;
mod clear;
mod gauge;
mod list;
mod paragraph;
@@ -14,23 +32,22 @@ mod table;
mod tabs;
pub use self::barchart::BarChart;
pub use self::block::Block;
pub use self::chart::{Axis, Chart, Dataset, Marker};
pub use self::block::{Block, BorderType};
pub use self::chart::{Axis, Chart, Dataset, GraphType};
pub use self::clear::Clear;
pub use self::gauge::Gauge;
pub use self::list::{List, SelectableList};
pub use self::list::{List, ListState};
pub use self::paragraph::Paragraph;
pub use self::sparkline::Sparkline;
pub use self::table::{Row, Table};
pub use self::table::{Row, Table, TableState};
pub use self::tabs::Tabs;
use crate::backend::Backend;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::{Color, Style};
use crate::terminal::Frame;
use crate::style::Style;
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
pub struct Borders: u32 {
/// Show no border (default)
const NONE = 0b0000_0001;
@@ -47,6 +64,7 @@ bitflags! {
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Text<'b> {
Raw(Cow<'b, str>),
Styled(Cow<'b, str>, Style),
@@ -66,21 +84,122 @@ impl<'b> Text<'b> {
pub trait Widget {
/// Draws the current state of the widget in the given buffer. That the only method required to
/// implement a custom widget.
fn draw(&mut self, area: Rect, buf: &mut Buffer);
/// Helper method to quickly set the background of all cells inside the specified area.
fn background(&self, area: Rect, buf: &mut Buffer, color: Color) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
buf.get_mut(x, y).set_bg(color);
}
}
}
/// Helper method that can be chained with a widget's builder methods to render it.
fn render<B>(&mut self, f: &mut Frame<B>, area: Rect)
where
Self: Sized,
B: Backend,
{
f.render(self, area);
}
fn render(self, area: Rect, buf: &mut Buffer);
}
/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things
/// between two draw calls.
///
/// Most widgets can be drawn directly based on the input parameters. However, some features may
/// require some kind of associated state to be implemented.
///
/// For example, the [`List`] widget can highlight the item currently selected. This can be
/// translated in an offset, which is the number of elements to skip in order to have the selected
/// item within the viewport currently allocated to this widget. The widget can therefore only
/// provide the following behavior: whenever the selected item is out of the viewport scroll to a
/// predefined position (making the selected item the last viewable item or the one in the middle
/// for example). Nonetheless, if the widget has access to the last computed offset then it can
/// implement a natural scrolling experience where the last offset is reused until the selected
/// item is out of the viewport.
///
/// ## Examples
///
/// ```rust,no_run
/// # use std::io;
/// # use tui::Terminal;
/// # use tui::backend::{Backend, TermionBackend};
/// # use tui::widgets::{Widget, List, ListState, Text};
///
/// // Let's say we have some events to display.
/// struct Events {
/// // `items` is the state managed by your application.
/// items: Vec<String>,
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
/// // item as well as the offset computed during the previous draw call (used to implement
/// // natural scrolling).
/// state: ListState
/// }
///
/// impl Events {
/// fn new(items: Vec<String>) -> Events {
/// Events {
/// items,
/// state: ListState::default(),
/// }
/// }
///
/// pub fn set_items(&mut self, items: Vec<String>) {
/// self.items = items;
/// // We reset the state as the associated items have changed. This effectively reset
/// // the selection as well as the stored offset.
/// self.state = ListState::default();
/// }
///
/// // Select the next item. This will not be reflected until the widget is drawn in the
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
/// 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));
/// }
///
/// // Select the previous item. This will not be reflected until the widget is drawn in the
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
/// 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));
/// }
///
/// // Unselect the currently selected item if any. The implementation of `ListState` makes
/// // sure that the stored offset is also reset.
/// pub fn unselect(&mut self) {
/// self.state.select(None);
/// }
/// }
///
/// 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"),
/// String::from("Item 2")
/// ]);
///
/// loop {
/// terminal.draw(|mut f| {
/// // The items managed by the application are transformed to something
/// // that is understood by tui.
/// let items = events.items.iter().map(Text::raw);
/// // The `List` widget is then built with those items.
/// let list = List::new(items);
/// // Finally the widget is rendered using the associated state. `events.state` is
/// // effectively the only thing that we will "remember" from this draw call.
/// f.render_stateful_widget(list, f.size(), &mut events.state);
/// });
///
/// // In response to some input events or an external http request or whatever:
/// events.next();
/// }
/// ```
pub trait StatefulWidget {
type State;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
}

View File

@@ -24,7 +24,6 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// # use tui::widgets::{Block, Borders, Paragraph, Text};
/// # use tui::style::{Style, Color};
/// # use tui::layout::{Alignment};
/// # fn main() {
/// let text = [
/// Text::raw("First line\n"),
/// Text::styled("Second line\n", Style::default().fg(Color::Red))
@@ -34,8 +33,8 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// .style(Style::default().fg(Color::White).bg(Color::Black))
/// .alignment(Alignment::Center)
/// .wrap(true);
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
@@ -107,10 +106,10 @@ impl<'a, 't, 'b, T> Widget for Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let text_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.render(area, buf);
b.inner(area)
}
None => area,
@@ -120,7 +119,7 @@ where
return;
}
self.background(text_area, buf, self.style.bg);
buf.set_background(text_area, self.style.bg);
let style = self.style;
let mut styled = self.text.by_ref().flat_map(|t| match *t {

View File

@@ -15,7 +15,7 @@ pub trait LineComposer<'a> {
/// A state machine that wraps lines on word boundaries.
pub struct WordWrapper<'a, 'b> {
symbols: &'b mut Iterator<Item = Styled<'a>>,
symbols: &'b mut dyn Iterator<Item = Styled<'a>>,
max_line_width: u16,
current_line: Vec<Styled<'a>>,
next_line: Vec<Styled<'a>>,
@@ -23,7 +23,7 @@ pub struct WordWrapper<'a, 'b> {
impl<'a, 'b> WordWrapper<'a, 'b> {
pub fn new(
symbols: &'b mut Iterator<Item = Styled<'a>>,
symbols: &'b mut dyn Iterator<Item = Styled<'a>>,
max_line_width: u16,
) -> WordWrapper<'a, 'b> {
WordWrapper {
@@ -121,14 +121,14 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
/// A state machine that truncates overhanging lines.
pub struct LineTruncator<'a, 'b> {
symbols: &'b mut Iterator<Item = Styled<'a>>,
symbols: &'b mut dyn Iterator<Item = Styled<'a>>,
max_line_width: u16,
current_line: Vec<Styled<'a>>,
}
impl<'a, 'b> LineTruncator<'a, 'b> {
pub fn new(
symbols: &'b mut Iterator<Item = Styled<'a>>,
symbols: &'b mut dyn Iterator<Item = Styled<'a>>,
max_line_width: u16,
) -> LineTruncator<'a, 'b> {
LineTruncator {

View File

@@ -1,11 +1,12 @@
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols,
widgets::{Block, Widget},
};
use std::cmp::min;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::symbols::bar;
use crate::widgets::{Block, Widget};
/// Widget to render a sparkline over one or more lines.
///
/// # Examples
@@ -13,14 +14,13 @@ use crate::widgets::{Block, Widget};
/// ```
/// # use tui::widgets::{Block, Borders, Sparkline};
/// # use tui::style::{Style, Color};
/// # fn main() {
/// Sparkline::default()
/// .block(Block::default().title("Sparkline").borders(Borders::ALL))
/// .data(&[0, 2, 3, 4, 1, 4, 10])
/// .max(5)
/// .style(Style::default().fg(Color::Red).bg(Color::White));
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct Sparkline<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
@@ -31,6 +31,8 @@ pub struct Sparkline<'a> {
/// The maximum value to take to compute the maximum bar height (if nothing is specified, the
/// widget uses the max of the dataset)
max: Option<u64>,
/// A set of bar symbols used to represent the give data
bar_set: symbols::bar::Set,
}
impl<'a> Default for Sparkline<'a> {
@@ -40,6 +42,7 @@ impl<'a> Default for Sparkline<'a> {
style: Default::default(),
data: &[],
max: None,
bar_set: symbols::bar::NINE_LEVELS,
}
}
}
@@ -64,13 +67,18 @@ impl<'a> Sparkline<'a> {
self.max = Some(max);
self
}
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> Sparkline<'a> {
self.bar_set = bar_set;
self
}
}
impl<'a> Widget for Sparkline<'a> {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let spark_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.render(area, buf);
b.inner(area)
}
None => area,
@@ -89,20 +97,26 @@ impl<'a> Widget for Sparkline<'a> {
.data
.iter()
.take(max_index)
.map(|e| e * u64::from(spark_area.height) * 8 / max)
.map(|e| {
if max != 0 {
e * u64::from(spark_area.height) * 8 / max
} else {
0
}
})
.collect::<Vec<u64>>();
for j in (0..spark_area.height).rev() {
for (i, d) in data.iter_mut().enumerate() {
let symbol = match *d {
0 => " ",
1 => bar::ONE_EIGHTH,
2 => bar::ONE_QUATER,
3 => bar::THREE_EIGHTHS,
4 => bar::HALF,
5 => bar::FIVE_EIGHTHS,
6 => bar::THREE_QUATERS,
7 => bar::SEVEN_EIGHTHS,
_ => bar::FULL,
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
3 => self.bar_set.three_eighths,
4 => self.bar_set.half,
5 => self.bar_set.five_eighths,
6 => self.bar_set.three_quarters,
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
};
buf.get_mut(spark_area.left() + i as u16, spark_area.top() + j)
.set_symbol(symbol)
@@ -118,3 +132,24 @@ impl<'a> Widget for Sparkline<'a> {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_does_not_panic_if_max_is_zero() {
let widget = Sparkline::default().data(&[0, 0, 0]);
let area = Rect::new(0, 0, 3, 1);
let mut buffer = Buffer::empty(area);
widget.render(area, &mut buffer);
}
#[test]
fn it_does_not_panic_if_max_is_set_to_zero() {
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
let area = Rect::new(0, 0, 3, 1);
let mut buffer = Buffer::empty(area);
widget.render(area, &mut buffer);
}
}

View File

@@ -1,16 +1,55 @@
use std::fmt::Display;
use std::iter::Iterator;
use crate::{
buffer::Buffer,
layout::{Constraint, Rect},
style::Style,
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;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::widgets::{Block, Widget};
#[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
}
pub fn select(&mut self, index: Option<usize>) {
self.selected = index;
if index.is_none() {
self.offset = 0;
}
}
}
/// Holds data to be displayed in a Table widget
pub enum Row<D, I>
#[derive(Debug, Clone)]
pub enum Row<D>
where
D: Iterator<Item = I>,
I: Display,
D: Iterator,
D::Item: Display,
{
Data(D),
StyledData(D, Style),
@@ -22,8 +61,8 @@ where
///
/// ```
/// # use tui::widgets::{Block, Borders, Table, Row};
/// # use tui::layout::Constraint;
/// # use tui::style::{Style, Color};
/// # fn main() {
/// let row_style = Style::default().fg(Color::White);
/// Table::new(
/// ["Col1", "Col2", "Col3"].into_iter(),
@@ -36,19 +75,12 @@ where
/// )
/// .block(Block::default().title("Table"))
/// .header_style(Style::default().fg(Color::Yellow))
/// .widths(&[5, 5, 10])
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
/// .style(Style::default().fg(Color::White))
/// .column_spacing(1);
/// # }
/// ```
pub struct Table<'a, T, H, I, D, R>
where
T: Display,
H: Iterator<Item = T>,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<D, I>>,
{
#[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
@@ -57,155 +89,280 @@ where
header: H,
/// Style for the header
header_style: Style,
/// Width of each column (if the total width is greater than the widget width some columns may
/// not be displayed)
widths: &'a [u16],
/// 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, T, H, I, D, R> Default for Table<'a, T, H, I, D, R>
impl<'a, H, R> Default for Table<'a, H, R>
where
T: Display,
H: Iterator<Item = T> + Default,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<D, I>> + Default,
H: Iterator + Default,
R: Iterator + Default,
{
fn default() -> Table<'a, T, H, I, D, R> {
fn default() -> Table<'a, H, R> {
Table {
block: None,
style: Style::default(),
header: H::default(),
header_style: Style::default(),
widths: &[],
rows: R::default(),
column_spacing: 1,
header_gap: 1,
highlight_style: Style::default(),
highlight_symbol: None,
rows: R::default(),
}
}
}
impl<'a, T, H, I, D, R> Table<'a, T, H, I, D, R>
impl<'a, H, D, R> Table<'a, H, R>
where
T: Display,
H: Iterator<Item = T>,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<D, I>>,
H: Iterator,
D: Iterator,
D::Item: Display,
R: Iterator<Item = Row<D>>,
{
pub fn new(header: H, rows: R) -> Table<'a, T, H, I, D, R> {
pub fn new(header: H, rows: R) -> Table<'a, H, R> {
Table {
block: None,
style: Style::default(),
header,
header_style: Style::default(),
widths: &[],
rows,
column_spacing: 1,
header_gap: 1,
highlight_style: Style::default(),
highlight_symbol: None,
rows,
}
}
pub fn block(mut self, block: Block<'a>) -> Table<'a, T, H, I, D, R> {
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, T, H, I, D, R>
pub fn header<II>(mut self, header: II) -> Table<'a, H, R>
where
II: IntoIterator<Item = T, IntoIter = H>,
II: IntoIterator<Item = H::Item, IntoIter = H>,
{
self.header = header.into_iter();
self
}
pub fn header_style(mut self, style: Style) -> Table<'a, T, H, I, D, R> {
pub fn header_style(mut self, style: Style) -> Table<'a, H, R> {
self.header_style = style;
self
}
pub fn widths(mut self, widths: &'a [u16]) -> Table<'a, T, H, I, D, R> {
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, T, H, I, D, R>
pub fn rows<II>(mut self, rows: II) -> Table<'a, H, R>
where
II: IntoIterator<Item = Row<D, I>, IntoIter = R>,
II: IntoIterator<Item = Row<D>, IntoIter = R>,
{
self.rows = rows.into_iter();
self
}
pub fn style(mut self, style: Style) -> Table<'a, T, H, I, D, R> {
pub fn style(mut self, style: Style) -> Table<'a, H, R> {
self.style = style;
self
}
pub fn column_spacing(mut self, spacing: u16) -> Table<'a, T, H, I, D, R> {
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, T, H, I, D, R> Widget for Table<'a, T, H, I, D, R>
impl<'a, H, D, R> StatefulWidget for Table<'a, H, R>
where
T: Display,
H: Iterator<Item = T>,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<D, I>>,
H: Iterator,
H::Item: Display,
D: Iterator,
D::Item: Display,
R: Iterator<Item = Row<D>>,
{
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// Render block if necessary and get the drawing area
let table_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.render(area, buf);
b.inner(area)
}
None => area,
};
// Set the background
self.background(table_area, buf, self.style.bg);
buf.set_background(table_area, self.style.bg);
// Save widths of the columns that will fit in the given area
let mut x = 0;
let mut widths = Vec::with_capacity(self.widths.len());
for width in self.widths.iter() {
if x + width < table_area.width {
widths.push(*width);
}
x += *width;
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 y < table_area.bottom() {
x = table_area.left();
for (w, t) in widths.iter().zip(self.header.by_ref()) {
buf.set_string(x, y, format!("{}", t), self.header_style);
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;
}
}
y += 2;
y += 1 + self.header_gap;
// Use highlight_style only if something is selected
let (selected, highlight_style) = match state.selected {
Some(i) => (Some(i), self.highlight_style),
None => (None, self.style),
};
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
// Draw rows
let default_style = Style::default();
if y < table_area.bottom() {
let remaining = (table_area.bottom() - y) as usize;
for (i, row) in self.rows.by_ref().take(remaining).enumerate() {
let (data, style) = match row {
Row::Data(d) => (d, default_style),
Row::StyledData(d, s) => (d, s),
// Make sure the table shows the selected item
state.offset = if let Some(selected) = selected {
if selected >= remaining + state.offset - 1 {
selected + 1 - remaining
} else if selected < state.offset {
selected
} else {
state.offset
}
} else {
0
};
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 (w, elt) in widths.iter().zip(data) {
buf.set_stringn(x, y + i as u16, format!("{}", elt), *w as usize, style);
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;
}
}
}
}
}
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);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn table_invalid_percentages() {
Table::new([""].iter(), vec![Row::Data([""].iter())].into_iter())
.widths(&[Constraint::Percentage(110)]);
}
}

View File

@@ -14,15 +14,14 @@ use crate::widgets::{Block, Widget};
/// # use tui::widgets::{Block, Borders, Tabs};
/// # use tui::style::{Style, Color};
/// # use tui::symbols::{DOT};
/// # fn main() {
/// Tabs::default()
/// .block(Block::default().title("Tabs").borders(Borders::ALL))
/// .titles(&["Tab1", "Tab2", "Tab3", "Tab4"])
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().fg(Color::Yellow))
/// .divider(DOT);
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct Tabs<'a, T>
where
T: AsRef<str> + 'a,
@@ -96,10 +95,10 @@ impl<'a, T> Widget for Tabs<'a, T>
where
T: AsRef<str>,
{
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let tabs_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.render(area, buf);
b.inner(area)
}
None => area,
@@ -109,7 +108,7 @@ where
return;
}
self.background(tabs_area, buf, self.style.bg);
buf.set_background(tabs_area, self.style.bg);
let mut x = tabs_area.left();
let titles_length = self.titles.len();
@@ -131,7 +130,7 @@ where
if x >= tabs_area.right() || last_title {
break;
} else {
buf.set_string(x, tabs_area.top(), self.divider, style);
buf.set_string(x, tabs_area.top(), self.divider, self.style);
x += divider_width;
}
}

View File

@@ -2,7 +2,7 @@ use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, Widget};
use tui::widgets::{Block, Borders};
use tui::Terminal;
#[test]
@@ -11,19 +11,19 @@ fn it_draws_a_block() {
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
Block::default()
let block = Block::default()
.title("Title")
.borders(Borders::ALL)
.title_style(Style::default().fg(Color::LightBlue))
.render(
&mut f,
Rect {
x: 0,
y: 0,
width: 8,
height: 8,
},
);
.title_style(Style::default().fg(Color::LightBlue));
f.render_widget(
block,
Rect {
x: 0,
y: 0,
width: 8,
height: 8,
},
);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![

74
tests/chart.rs Normal file
View File

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

View File

@@ -1,7 +1,7 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::{Constraint, Direction, Layout};
use tui::widgets::{Block, Borders, Gauge, Widget};
use tui::widgets::{Block, Borders, Gauge};
use tui::Terminal;
#[test]
@@ -16,14 +16,14 @@ fn gauge_render() {
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
Gauge::default()
let gauge = Gauge::default()
.block(Block::default().title("Percentage").borders(Borders::ALL))
.percent(43)
.render(&mut f, chunks[0]);
Gauge::default()
.percent(43);
f.render_widget(gauge, chunks[0]);
let gauge = Gauge::default()
.block(Block::default().title("Ratio").borders(Borders::ALL))
.ratio(0.2113139343131)
.render(&mut f, chunks[1]);
.ratio(0.211_313_934_313_1);
f.render_widget(gauge, chunks[1]);
})
.unwrap();
let expected = Buffer::with_lines(vec![

89
tests/list.rs Normal file
View File

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

View File

@@ -1,11 +1,10 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::Alignment;
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
use tui::widgets::{Block, Borders, Paragraph, Text};
use tui::Terminal;
const SAMPLE_STRING: &str =
"The library is based on the principle of immediate rendering with \
const SAMPLE_STRING: &str = "The library is based on the principle of immediate rendering with \
intermediate buffers. This means that at each new frame you should build all widgets that are \
supposed to be part of the UI. While providing a great flexibility for rich and \
interactive UI, this may introduce overhead for highly dynamic content.";
@@ -20,11 +19,11 @@ fn paragraph_render_wrap() {
.draw(|mut f| {
let size = f.size();
let text = [Text::raw(SAMPLE_STRING)];
Paragraph::new(text.iter())
let paragraph = Paragraph::new(text.iter())
.block(Block::default().borders(Borders::ALL))
.alignment(alignment)
.wrap(true)
.render(&mut f, size);
.wrap(true);
f.render_widget(paragraph, size);
})
.unwrap();
terminal.backend().buffer().clone()
@@ -87,10 +86,10 @@ fn paragraph_render_double_width() {
.draw(|mut f| {
let size = f.size();
let text = [Text::raw(s)];
Paragraph::new(text.iter())
let paragraph = Paragraph::new(text.iter())
.block(Block::default().borders(Borders::ALL))
.wrap(true)
.render(&mut f, size);
.wrap(true);
f.render_widget(paragraph, size);
})
.unwrap();
@@ -119,10 +118,10 @@ fn paragraph_render_mixed_width() {
.draw(|mut f| {
let size = f.size();
let text = [Text::raw(s)];
Paragraph::new(text.iter())
let paragraph = Paragraph::new(text.iter())
.block(Block::default().borders(Borders::ALL))
.wrap(true)
.render(&mut f, size);
.wrap(true);
f.render_widget(paragraph, size);
})
.unwrap();

418
tests/table.rs Normal file
View File

@@ -0,0 +1,418 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::Constraint;
use tui::widgets::{Block, Borders, Row, Table};
use tui::Terminal;
#[test]
fn table_column_spacing() {
let render = |column_spacing| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
vec![
Row::Data(["Row11", "Row12", "Row13"].iter()),
Row::Data(["Row21", "Row22", "Row23"].iter()),
Row::Data(["Row31", "Row32", "Row33"].iter()),
Row::Data(["Row41", "Row42", "Row43"].iter()),
]
.into_iter(),
)
.block(Block::default().borders(Borders::ALL))
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(column_spacing);
f.render_widget(table, size);
})
.unwrap();
terminal.backend().buffer().clone()
};
// no space between columns
assert_eq!(
render(0),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1Head2Head3 │",
"│ │",
"│Row11Row12Row13 │",
"│Row21Row22Row23 │",
"│Row31Row32Row33 │",
"│Row41Row42Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
// one space between columns
assert_eq!(
render(1),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
// enough space to just not hide the third column
assert_eq!(
render(6),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
// enough space to hide part of the third column
assert_eq!(
render(7),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head│",
"│ │",
"│Row11 Row12 Row1│",
"│Row21 Row22 Row2│",
"│Row31 Row32 Row3│",
"│Row41 Row42 Row4│",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
}
#[test]
fn table_widths() {
let render = |widths| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
vec![
Row::Data(["Row11", "Row12", "Row13"].iter()),
Row::Data(["Row21", "Row22", "Row23"].iter()),
Row::Data(["Row31", "Row32", "Row33"].iter()),
Row::Data(["Row41", "Row42", "Row43"].iter()),
]
.into_iter(),
)
.block(Block::default().borders(Borders::ALL))
.widths(widths);
f.render_widget(table, size);
})
.unwrap();
terminal.backend().buffer().clone()
};
// columns of zero width show nothing
assert_eq!(
render(&[
Constraint::Length(0),
Constraint::Length(0),
Constraint::Length(0)
]),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
// columns of 1 width trim
assert_eq!(
render(&[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1)
]),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│H H H │",
"│ │",
"│R R R │",
"│R R R │",
"│R R R │",
"│R R R │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
// columns of large width just before pushing a column off
assert_eq!(
render(&[
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(8)
]),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
}
#[test]
fn table_percentage_widths() {
let render = |widths| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
vec![
Row::Data(["Row11", "Row12", "Row13"].iter()),
Row::Data(["Row21", "Row22", "Row23"].iter()),
Row::Data(["Row31", "Row32", "Row33"].iter()),
Row::Data(["Row41", "Row42", "Row43"].iter()),
]
.into_iter(),
)
.block(Block::default().borders(Borders::ALL))
.widths(widths)
.column_spacing(0);
f.render_widget(table, size);
})
.unwrap();
terminal.backend().buffer().clone()
};
// columns of zero width show nothing
assert_eq!(
render(&[
Constraint::Percentage(0),
Constraint::Percentage(0),
Constraint::Percentage(0)
]),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
// columns of not enough width trims the data
assert_eq!(
render(&[
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10)
]),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│HeaHeaHea │",
"│ │",
"│RowRowRow │",
"│RowRowRow │",
"│RowRowRow │",
"│RowRowRow │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
// columns of large width just before pushing a column off
assert_eq!(
render(&[
Constraint::Percentage(30),
Constraint::Percentage(30),
Constraint::Percentage(30)
]),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
// percentages summing to 100 should give equal widths
assert_eq!(
render(&[Constraint::Percentage(50), Constraint::Percentage(50)]),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 │",
"│ │",
"│Row11 Row12 │",
"│Row21 Row22 │",
"│Row31 Row32 │",
"│Row41 Row42 │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
}
#[test]
fn table_mixed_widths() {
let render = |widths| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
vec![
Row::Data(["Row11", "Row12", "Row13"].iter()),
Row::Data(["Row21", "Row22", "Row23"].iter()),
Row::Data(["Row31", "Row32", "Row33"].iter()),
Row::Data(["Row41", "Row42", "Row43"].iter()),
]
.into_iter(),
)
.block(Block::default().borders(Borders::ALL))
.widths(widths);
f.render_widget(table, size);
})
.unwrap();
terminal.backend().buffer().clone()
};
// columns of zero width show nothing
assert_eq!(
render(&[
Constraint::Percentage(0),
Constraint::Length(0),
Constraint::Percentage(0)
]),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
// columns of not enough width trims the data
assert_eq!(
render(&[
Constraint::Percentage(10),
Constraint::Length(20),
Constraint::Percentage(10)
]),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Hea Head2 Hea│",
"│ │",
"│Row Row12 Row│",
"│Row Row22 Row│",
"│Row Row32 Row│",
"│Row Row42 Row│",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
// columns of large width just before pushing a column off
assert_eq!(
render(&[
Constraint::Percentage(30),
Constraint::Length(10),
Constraint::Percentage(30)
]),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
// columns of large size (>100% total) hide the last column
assert_eq!(
render(&[
Constraint::Percentage(60),
Constraint::Length(10),
Constraint::Percentage(60)
]),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 │",
"│ │",
"│Row11 Row12 │",
"│Row21 Row22 │",
"│Row31 Row32 │",
"│Row41 Row42 │",
"│ │",
"│ │",
"└────────────────────────────┘",
])
);
}