Compare commits

...

132 Commits

Author SHA1 Message Date
Leon Sautour
f32fb022e0 docs(example): add break word behaviour to paragraph example 2023-04-26 21:14:41 +02:00
charliedu2000
12e4f0d921 Feature: allow word-break in wrapped paragraphs, add it to document. 2022-08-13 00:07:29 +08:00
charliedu2000
727e13d876 Feature: allow word-break in wrapped paragraphs. 2022-08-12 23:49:24 +08:00
charliedu2000
71835a0a52 Feature: allow word-break in wrapped paragraphs. 2022-08-12 23:40:08 +08:00
♫ Christian Krause ♫
a6b25a4877 chore: add panic hook example (#593)
Without a terminal-resetting panic hook there are two main problems when
an application panics:

1.  The report of the panic is distorted because the terminal has not
    properly left the alternate screen and is still in raw mode.

2.  The terminal needs to be manually reset with the `reset` command.

To avoid this, the standard panic hook can be extended to first reset
the terminal.
2022-04-24 16:49:57 +02:00
Florian Dehau
90d8cb6526 chore: add more apps using tui to the README 2022-04-24 15:49:03 +02:00
Florian Dehau
e71faa988e Release v0.18.0 2022-04-24 15:03:09 +02:00
Atk
ed0ae81aae chore: update crossterm to v0.23 (#598) 2022-04-24 14:47:54 +02:00
David
a61b078dea chore: fix clippy warning (#601) 2022-04-24 14:25:50 +02:00
Florian Dehau
85939306e3 Release v0.17.0 2022-01-22 13:30:35 +01:00
Florian Dehau
cf2d9c2c1d feat!: bump MSRV to 1.56.1 and migrate to edition 2021 2022-01-22 13:18:56 +01:00
theogilbert
853d9047b0 feat(widgets/chart): add option to control alignment of axis labels (#568)
* feat(chart): allow custom alignment of first X-Axis label

* refactor(chart): rename ambiguous function parameter

* feat(chart): allow custom alignment of Y-Axis labels

* refactor(chart): refactor axis test cases

* refactor(chart): rename minor variable

* fix(chart): force centered x-axis label near Y-Axis

* fix(chart): fix subtract overflow on small rendering area

* refactor(chart): rename alignment property

* refactor(chart): merge two nested conditions

* refactor(chart): decompose x labels rendering loop
2021-12-23 19:02:10 +01:00
Florian Dehau
6069d89dee chore: fix all clippy warnings 2021-12-23 18:55:43 +01:00
Florian Dehau
d25e263b8e chore: enable clippy on all targets 2021-12-23 18:55:43 +01:00
ljedrz
d05e696d45 chore: fix optional attribute for serde feature (#571)
Signed-off-by: ljedrz <ljedrz@gmail.com>
2021-12-23 18:51:13 +01:00
Petr Portnov
ef583cead9 chore(examples): remove unused demo/util.rs
This module is unused and is not imported by any other module.
2021-11-30 22:35:45 +01:00
Denis
90c4da4e68 Update README.md 2021-11-30 22:25:00 +01:00
Florian Dehau
8032191366 chore: fix table example
Third column in table example was using the `Max` constraint.

But since version 0.16, the layout system does not add a hidden constraint on the last column which would ensure that it fills the remaining available space (a change that was already mentioned in #525). In addition, `tui` does not support sizing based on content because of its immediate mode nature. Therefore, `Max` is now resolved to `0`. Replacing with `Min` fixes the issue.

A new way of specifying constraints is being worked on at #519 which should for more deterministic and advanced layout.
2021-11-21 21:00:34 +01:00
Florian Dehau
c8c03294e1 chore: self contained examples 2021-11-11 16:18:49 +01:00
wcampbell
e00df22588 chore: add adsb_deku/radar to apps using tui (#555)
My `adsb_deku/radar` application uses tui, using the Table and Canvas to show
information and plot airplanes on a latitude/longitude coordinates map.

Signed-off-by: wcampbell <wcampbell1995@gmail.com>
2021-11-11 16:13:42 +01:00
Florian Dehau
9806217a6a feat!: use crossterm as default backend 2021-11-01 23:21:55 +01:00
Florian Dehau
1be5cf2d90 chore: add joshuto to the apps using tui 2021-10-17 19:39:15 +02:00
Florian Dehau
ca68bae4ed feat!(widgets/canvas): use spans for text of labels 2021-10-17 18:55:59 +02:00
Florian Dehau
8c1f58079f chore: fix build 2021-10-17 17:27:32 +02:00
Antoine Büsch
4845c03eec feat(widgets/list): repeat highlight symbol on multi-line items (#533)
When this option is true, the hightlight symbol is repeated for each
line of the selected item, instead of just the first line.
2021-10-17 17:05:51 +02:00
Florian Dehau
532a595c41 chore: pin bitflags version to 1.3 2021-10-17 16:20:40 +02:00
Antoine Büsch
25ce5bc90b chore: bump the minimum supported Rust version to 1.52.1 (#534)
- `const_fn` usage in the `bitflags` crate.
- `unsafe_op_in_unsafe_fn` lint usage in `rust_info` despite pinned `cargo-make` version.
2021-10-17 15:49:31 +02:00
JerzySpendel
80a929ccc6 chore: fix typo (#513) 2021-08-08 11:10:21 +02:00
Christian Visintin
3797863e14 chore: add termscp to list of apps using tui (#510) 2021-08-01 23:08:19 +02:00
Florian Dehau
7870793b4b Release v0.16.0 2021-08-01 20:12:20 +02:00
Florian Dehau
a7c21a9729 fix(widgets): avoid offset panic in Table and List when input changes 2021-08-01 18:01:24 +02:00
Florian Dehau
914d54e672 chore: bump crossterm to 0.20 2021-08-01 17:14:11 +02:00
Florian Dehau
a68e38e59e fix(table): use Layout in table column widths computation 2021-08-01 16:46:54 +02:00
Florian Dehau
e870e5d8a5 feat(layout): add private option to control last chunk expansion 2021-08-01 16:46:54 +02:00
Deepu K Sasidharan
29387e785c Add battleship.rs 2021-08-01 16:44:36 +02:00
Florian Dehau
8eb6336f5e refactor(widgets): remove iter::repeat for blank symbols 2021-08-01 15:08:53 +02:00
Florian Dehau
34a2be6458 fix(widgets/chart): remove panics with long axis labels 2021-08-01 15:08:53 +02:00
Florian Dehau
fbd834469f doc(widgets/clear): clarify usage of clear 2021-06-16 21:58:04 +02:00
Florian Dehau
8da5f740af refactor(examples): show more use case in gauge example 2021-06-16 20:24:14 +02:00
Florian Dehau
38dcddb126 fix(widgets/gauge): apply label style and avoid overflow on large labels 2021-06-16 20:24:14 +02:00
Phillip Cloud
92948d2394 chore: add minesweep to list of apps using tui-rs 2021-06-16 17:25:11 +02:00
orhun
a3a0a80a02 docs: add gpg-tui to the "apps using tui" list 2021-06-16 17:19:34 +02:00
jmrgibson
a5f7019b2a doc: fix minor grammatical errors (#489)
A missing "and" after "an" (which I do all the time) and some tense clarification.
2021-06-16 17:15:13 +02:00
Moritz
e05b80cec1 doc: fix typos in comments. (#486) 2021-06-16 17:14:16 +02:00
Florian Dehau
23d5fbde56 refactor(examples): remove exit key from Events handler
The thread spawned by `Events` to listen for keyboard inputs had knowlegde of
the exit key to exit on its own when it was pressed. It is however a source of
confusion (#491) because the exit behavior is wired in both the event handler
and the input handling performed by the app. In addition, this is not needed as
the thread will exit anyway when the main thread finishes as it is already the
case for the "tick" thread. Therefore, this commit removes both the option to
configure the exit key in the `Events` handler and the option to temporarily
ignore it.
2021-06-16 17:07:59 +02:00
Oleks (オレクス)
a346704cdc feat(block): add option to center and right align the title (#462)
* Added ability to set title alignment, added tests, modified blocks example to show the feature

* Added test for inner with title in block

* Updated block example to show center alignment

* Formatting fixed

* Updated tests to use lamdas and be more concise. Updated title alignmnet code to be more straightforward and have correct behavior when placing title in the center without left border
2021-05-22 16:55:24 +02:00
Andrew Chin
24396d97ed doc: Add doctests that shows how Text can be created from Cow<str> 2021-05-22 16:47:06 +02:00
Andrew Chin
703e41cd49 feat(Text): Add a From<Cow<str>> impl for Text 2021-05-22 16:47:06 +02:00
Florian Dehau
975c4165d0 chore: fix clippy warnings 2021-05-22 15:54:38 +02:00
Arijit Basu
dbf38d847a Add xplr to the "apps using tui" list
`xplr` is a hackable TUI file explorer.
2021-05-22 15:08:06 +02:00
Florian Dehau
91a2519cc3 chore: update links to examples in README
Links now include the fully qualified domain as well as the version.
This will make them work in docs.rs and make sure readers are looking at code which is consistent with the latest version available.
2021-05-03 00:30:59 +02:00
Alexandru Scvortov
a1c3ba2088 fix: actually clear buffer in TestBackend::clear (#461) 2021-05-02 22:35:27 +02:00
Alexandru Scvortov
d47565be5c fix: actually clear buffer in TestBackend::clear (#461) 2021-05-02 22:35:08 +02:00
Florian Dehau
1028d39db0 chore: improve contributing guidelines
* Improve issue templates and make them mandatory.
* Improve CONTRIBUTING.md.
* Add template for pull requests.
2021-05-02 21:42:31 +02:00
Deepu K Sasidharan
b250825c38 Add kdash to apps using this section (#469)
chore: add `kdash` to apps using `tui`
2021-05-02 20:13:56 +02:00
Florian Dehau
90a6a8f2d6 Release v0.15.0 2021-05-02 19:03:01 +02:00
Florian Dehau
414386e797 chore: update rand to 0.8 (#472) 2021-05-02 18:51:59 +02:00
Joey Ezechiëls
3a843d5074 fix(test): remove compile warning in TestBackend::assert_buffer (#466) 2021-05-02 18:27:47 +02:00
Luc Perkins
4e76bfa2ca chore: add Vector to list of apps using tui (#452)
Signed-off-by: Luc Perkins <luc@timber.io>
2021-02-21 14:58:57 +01:00
Simas Toleikis
8832281dcf Update crossterm to 0.19. 2021-01-04 23:26:04 +01:00
Florian Dehau
853f3d9200 feat(terminal): add a read-only view of the terminal state after the draw call 2021-01-04 22:18:28 +01:00
Florian Dehau
67e996c5f4 feat(examples): add third tab to demo to show colors 2021-01-01 15:41:49 +01:00
Florian Dehau
f09863faa0 Release v0.14.0 2021-01-01 14:51:08 +01:00
Florian Dehau
eb1e3be722 fix(widgets/block): make Block::inner return more accurate results on small areas 2020-12-13 17:21:10 +01:00
Sagie Gur-Ari
4ec902b96f chore: make run-examples available on all platforms (#429)
* Make examples available for all platforms
* limit windows to crossterm_demo only and make q exit demos work
2020-12-13 15:29:31 +01:00
Vadim Chekan
74243394d9 fix(widgets/table): draw table header and border even if rows are empty (#426) 2020-12-08 21:49:16 +01:00
Florian Dehau
e7f263efa7 chore(ci): fix cargo-make cache on windows runner 2020-12-07 23:58:27 +01:00
Florian Dehau
0991145c58 chore(ci): simplify ci workflow (#428)
* chore(ci): simplify ci workflow

* use more up to date action
* restrict actions allowed to run
* cache cargo-make
2020-12-06 18:31:54 +01:00
Florian Dehau
01d2a8588a chore(ci): reduce the number of triggered jobs 2020-12-06 16:58:07 +01:00
Florian Dehau
45431a2649 chore: add first contributing guidelines 2020-12-06 16:58:07 +01:00
Florian Dehau
0b78fb9201 chore: use cargo-make in the CI as well 2020-12-06 16:58:07 +01:00
Florian Dehau
9cdff275cb chore: replace make with cargo-make
`cargo-make` make it easier to provide developers of all platforms an unified build workflow.
2020-12-06 16:58:07 +01:00
Arne Beer
77c6e106e4 doc(examples): Add comments to "list" example and fix list direction (#425)
* Add docs to list example and fix list direction

* List example: review adjustments and typo fixes
2020-12-06 16:33:31 +01:00
Florian Dehau
efdd6bfb19 feat(tests): add tests covering new table features 2020-11-29 23:47:58 +01:00
Florian Dehau
117098d2d2 refactor(examples): add missing margin at the bottom of the header of table in the demo 2020-11-29 23:47:58 +01:00
Florian Dehau
f933d892aa chore: update CHANGELOG 2020-11-29 23:47:58 +01:00
Florian Dehau
5ea54792c0 refactor(widgets/table): more flexible table
- control over the style of each cell and its content using the styling capabilities of Text.
- rows with multiple lines.
- fix panics on small areas.
- less generic type parameters.
2020-11-29 23:47:58 +01:00
Tom Forbes
23a9280db7 chore: add gping to the lists of apps using tui (#422)
* Add gping to the lists of apps using tui
2020-11-29 19:28:53 +01:00
Florian Dehau
79e27b1778 refactor(widgets/gauge): stop using unicode blocks by default 2020-11-29 19:27:34 +01:00
DashEightMate
0a05579a1c feat(widgets/gauge): allow gauge to use unicode block for more descriptive progress (#377)
* gauge now uses unicode blocks for more descriptive progress

* removed unnecessary if

* changed function name to better reflect unicode

* standardized block symbols, added no unicode option, added tests

* formatting

* improved readability

* gauge tests now check color

* formatted
2020-11-29 19:20:08 +01:00
Tom Forbes
0030eb4a13 fix(tests): remove clippy warnings about single char push (#424) 2020-11-29 18:35:52 +01:00
pm100
5bf40343eb fix(widgets/paragraph): handle trailing nbsp in wrapped text (#405) 2020-11-15 20:03:33 +01:00
Florian Dehau
1e35f983c4 doc(style): improve documentation of Style 2020-11-14 21:29:56 +01:00
Florian Dehau
a15ac8870b feat(style): add a method to create a style that reset all properties until that point 2020-11-14 21:29:56 +01:00
Florian Dehau
8a27036a54 fix(widgets/block): allow Block to render on small areas 2020-11-14 20:32:10 +01:00
Florian Dehau
8543523f18 Release v0.13.0 2020-11-14 17:37:21 +01:00
acheronfail
5a9b59866b feat(widgets/listitem): derive PartialEq 2020-11-14 16:51:19 +01:00
Dheepak Krishnamurthy
dc76956215 chore: add taskwarrior-tui to the list of apps using tui-rs (#403) 2020-11-14 16:48:43 +01:00
Kemyt
98fb5e4bbd fix(widgets/table): take borders into account when percentage and ration constraints are used (#385)
* Fix percentage and ratio constraints for table to take borders into account

Percentage and ratio constraints don't take borders into account, which causes
the table to not be correctly aligned. This is easily seen when using 50/50
percentage split with bordered table. However fixing this causes the last column
of table to not get printed, so there is probably another problem with columns
width determination.

* Fix rounding of cassowary solved widths to eliminate imprecisions

* Fix formatting to fit convention

Co-authored-by: Kemyt <kemyt4@gmail.com>
2020-11-14 16:45:36 +01:00
Sebastian Thiel
25ff2e5e61 upgrade crossterm to v0.18
It reduces the amount of dependencies, among other improvements.
2020-09-29 23:28:45 +02:00
Florian Dehau
5050f1ce1c feat(widgets/gauge): add LineGauge variant of Gauge 2020-09-28 00:40:19 +02:00
Florian Dehau
51b691e7ac Release v0.12.0 2020-09-27 19:55:45 +02:00
Florian Dehau
c4cd0a5f31 fix(widgets/chart): use the correct style to draw legend and axis titles
Before this change, the style of the points drawn in the graph area could reused to draw the
title of the axis and the legend. Now the style of these components put on top of the graph area
is solely based on the widget style.
2020-09-27 19:12:35 +02:00
Florian Dehau
41142732ec feat(buffer): add a method to build a Style out of an existing Cell 2020-09-27 19:12:35 +02:00
Kemyt
62495c3bd1 fix(widgets/barchart): fix chart filled more than actual (#383)
* Fix barchart incorrectly showing chart filled more than actual

Determination of how filled the bar should be was incorrectly taking the
entire chart height into account, when in fact it should take height-1, because
of extra line with label. Because of that the chart appeared fuller than it
should and was full before reaching maximum value.

* Add a test case for checking if barchart fills correctly up to max value

Co-authored-by: Kemyt <kemyt4@gmail.com>
2020-09-27 17:15:44 +02:00
Brooks Rady
d00184a7c3 feat(text): extend Text to be stylable and extendable (#361)
* Extend `Text` to be extendable
* Add some documentation
2020-09-27 14:20:14 +02:00
Brooks Rady
ce32d5537d chore: clippy fixes (#386) 2020-09-27 13:41:28 +02:00
Luis Enrique Muñoz Martín
25921fa91a chore: added termchat to "apps using tui" (#371) 2020-09-20 18:23:10 +02:00
luak
932a496c3c chore: add rkm to the list of apps using tui (#376) 2020-09-20 16:43:02 +02:00
Florian Dehau
57862eeda6 Release v0.11.0 2020-09-20 16:34:44 +02:00
Florian Dehau
11df94d601 fix(examples): avoid panic when computing event poll timeout in crossterm demo (#380) 2020-09-20 15:51:44 +02:00
Brooks Rady
0abaa20de9 refactor: clean up some folds (#362) 2020-09-20 15:02:06 +02:00
Amjad Alsharafi
c35a1dd79f feat(widgets/canvas): added type Block in canvas markers (#350)
This allows for clearer colors than using Dot, especially when
decreasing the size of the terminal font in order to increase the
resolution of the canvas
2020-09-20 14:57:35 +02:00
alvinhochun
e0b2572eba refactor(backend/crossterm): support more style modifiers on Windows and fix build with Crossterm 0.17.8 (#368)
* Support more style modifiers on Windows
* Change Crossterm backend to write directly to buffer instead of String

Crossterm might actually do WinAPI calls instead of writing ANSI excape
codes so writing to an intermediate String may cause issues on older
versions of Windows. It also fails to compile with Crossterm 0.17.8 due
to Crossterm now expecting the writer to support `flush`, which String
doesn't.

Fixes #373
2020-09-20 14:54:16 +02:00
Cory Forsstrom
aada695b3f chore: add tickrs to apps using tui (#351) 2020-08-23 21:20:30 +02:00
Florian Dehau
90f3858eff feat(backend): keep the internal buffers in sync when the terminal is cleared. 2020-08-23 21:13:12 +02:00
Florian Dehau
ecb482f297 fix(backend): move the cursor when first diff is on second cell
Both termion and crossterm backends were not moving the cursor if the first diff to draw was on the
second cell. The condition triggering the cursor move has been updated to fix this. In addition, two
tests have been added to avoid future regressions.
2020-08-02 21:10:44 +02:00
Florian Dehau
641f391137 feat(terminal): add unstable api to use a fixed viewport
There was now way to avoid the autoresize behavior of `Terminal`. While it was fine for most users,
it made the testing experience painful as it was impossible to avoid the calls to `Backend::size()`.
Indeed they trigger the following error: "Inappropriate ioctl for device" since we are not running
the tests in a real terminal (at least in the CI).

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

View File

@@ -1,30 +1,60 @@
---
name: Bug report
about: Create a report to help us improve
about: Create an issue about a bug you encountered
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
<!--
Hi there, sorry `tui` is not working as expected.
Please fill this bug report conscientiously.
A detailed and complete issue is more likely to be processed quickly.
-->
## Description
<!--
A clear and concise description of what the bug is.
-->
**To Reproduce**
If possible include a code sample exhibiting the problem.
**Expected behavior**
## To Reproduce
<!--
Try to reduce the issue to a simple code sample exhibiting the problem.
Ideally, fork the project and add a test or an example.
-->
## Expected behavior
<!--
A clear and concise description of what you expected to happen.
-->
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Linux,Windows]
- Terminal Emulator [e.g xterm, Konsole, Terminal, iTerm2, ConEmu]
- Font [e.g Inconsolata, Monospace]
- Crate version [e.g. 0.7]
- Backend [e.g termion, crossterm]
## Screenshots
<!--
If applicable, add screenshots, gifs or videos to help explain your problem.
-->
**Additional context**
## Environment
<!--
Add a description of the systems where you are observing the issue. For example:
- OS: Linux
- Terminal Emulator: xterm
- Font: Inconsolata (Patched)
- Crate version: 0.7
- Backend: termion
-->
- OS:
- Terminal Emulator:
- Font:
- Crate version:
- Backend:
## Additional context
<!--
Add any other context about the problem here.
If you already looked into the issue, include all the leads you have explored.
-->

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -7,14 +7,26 @@ assignees: ''
---
**Is your feature request related to a problem? Please describe.**
## Problem
<!--
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-->
**Describe the solution you'd like**
## Solution
<!--
A clear and concise description of what you want to happen.
Things to consider:
- backward compatibility
- ease of use of the API (https://rust-lang.github.io/api-guidelines/)
- consistency with the rest of the crate
-->
**Describe alternatives you've considered**
## Alternatives
<!--
A clear and concise description of any alternative solutions or features you've considered.
-->
**Additional context**
## Additional context
<!--
Add any other context or screenshots about the feature request here.
-->

17
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,17 @@
## Description
<!--
A clear and concise description of what this PR changes.
-->
## Testing guidelines
<!--
A clear and concise description of how the changes can be tested.
For example, you can include a command to run the relevant tests or examples.
You can also include screenshots of the expected behavior.
-->
## Checklist
* [ ] I have read the [contributing guidelines](../CONTRIBUTING.md).
* [ ] I have added relevant tests.
* [ ] I have documented all new additions.

View File

@@ -1,71 +1,71 @@
on: [push, pull_request]
on:
push:
branches:
- master
pull_request:
branches:
- master
name: CI
env:
CI_CARGO_MAKE_VERSION: 0.35.8
jobs:
linux:
name: Linux
runs-on: ubuntu-latest
strategy:
matrix:
rust: ["1.56.1", "stable"]
steps:
- name: "Install dependencies"
run: sudo apt-get install libncurses5-dev
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
with:
rust-version: ${{ matrix.rust }}
components: rustfmt,clippy
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- name: "Get cargo bin directory"
id: cargo-bin-dir
run: echo "::set-output name=dir::$HOME/.cargo/bin"
- name: "Cache cargo make"
id: cache-cargo-make
uses: actions/cache@v2
with:
profile: default
toolchain: stable
override: true
- name: "Format"
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: "Check"
uses: actions-rs/cargo@v1
with:
command: check
args: --examples
- name: "Check (crossterm)"
uses: actions-rs/cargo@v1
with:
command: check
args: --no-default-features --features=crossterm --example crossterm_demo
- name: "Check (rustbox)"
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features=rustbox --example rustbox_demo
- name: "Check (curses)"
uses: actions-rs/cargo@v1
with:
command: check
args: --no-default-features --features=curses --example curses_demo
- name: "Test"
uses: actions-rs/cargo@v1
with:
command: test
- name: "Clippy"
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --all-features -- -D warnings
path: ${{ steps.cargo-bin-dir.outputs.dir }}/cargo-make
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Install cargo-make"
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Format / Build / Test"
run: cargo make ci
env:
RUST_BACKTRACE: full
windows:
name: Windows
runs-on: windows-latest
strategy:
matrix:
rust: ["1.56.1", "stable"]
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
with:
profile: default
toolchain: stable
override: true
- name: "Check (crossterm)"
uses: actions-rs/cargo@v1
rust-version: ${{ matrix.rust }}
components: rustfmt,clippy
- uses: actions/checkout@v1
- name: "Get cargo bin directory"
id: cargo-bin-dir
run: echo "::set-output name=dir::$HOME\.cargo\bin"
- name: "Cache cargo make"
id: cache-cargo-make
uses: actions/cache@v2
with:
command: check
args: --no-default-features --features=crossterm --example crossterm_demo
- name: "Test (crossterm)"
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features=crossterm --tests --examples
path: ${{ steps.cargo-bin-dir.outputs.dir }}\cargo-make.exe
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Install cargo-make"
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Format / Build / Test"
run: cargo make ci
env:
RUST_BACKTRACE: full

View File

@@ -2,6 +2,346 @@
## To be released
## v0.18.0 - 2022-04-24
### Features
* Update `crossterm` to `0.23`
## v0.17.0 - 2022-01-22
### Features
* Add option to `widgets::List` to repeat the hightlight symbol for each line of multi-line items (#533).
* Add option to control the alignment of `Axis` labels in the `Chart` widget (#568).
### Breaking changes
* The minimum supported rust version is now `1.56.1`.
#### New default backend and consolidated backend options (#553)
* `crossterm` is now the default backend.
If you are already using the `crossterm` backend, you can simplify your dependency specification in `Cargo.toml`:
```diff
- tui = { version = "0.16", default-features = false, features = ["crossterm"] }
+ tui = "0.17"
```
If you are using the `termion` backend, your `Cargo` is now a bit more verbose:
```diff
- tui = "0.16"
+ tui = { version = "0.17", default-features = false, features = ["termion"] }
```
`crossterm` has also been bumped to version `0.22`.
Because of their apparent low usage, `curses` and `rustbox` backends have been removed.
If you are using one of them, you can import their last implementation in your own project:
* [curses](https://github.com/fdehau/tui-rs/blob/v0.16.0/src/backend/curses.rs)
* [rustbox](https://github.com/fdehau/tui-rs/blob/v0.16.0/src/backend/rustbox.rs)
#### Canvas labels (#543)
* Labels of the `Canvas` widget are now `text::Spans`.
The signature of `widgets::canvas::Context::print` has thus been updated:
```diff
- ctx.print(x, y, "Some text", Color::Yellow);
+ ctx.print(x, y, Span::styled("Some text", Style::default().fg(Color::Yellow)))
```
## v0.16.0 - 2021-08-01
### Features
* Update `crossterm` to `0.20`.
* Add `From<Cow<str>>` implementation for `text::Text` (#471).
* Add option to right or center align the title of a `widgets::Block` (#462).
### Fixes
* Apply label style in `widgets::Gauge` and avoid panics because of overflows with long labels (#494).
* Avoid panics because of overflows with long axis labels in `widgets::Chart` (#512).
* Fix computation of column widths in `widgets::Table` (#514).
* Fix panics because of invalid offset when input changes between two frames in `widgets::List` and
`widgets::Chart` (#516).
## v0.15.0 - 2021-05-02
### Features
* Update `crossterm` to `0.19`.
* Update `rand` to `0.8`.
* Add a read-only view of the terminal state after the draw call (#440).
### Fixes
* Remove compile warning in `TestBackend::assert_buffer` (#466).
## v0.14.0 - 2021-01-01
### Breaking changes
#### New API for the Table widget
The `Table` widget got a lot of improvements that should make it easier to work with:
* It should not longer panic when rendered on small areas.
* `Row`s are now a collection of `Cell`s, themselves wrapping a `Text`. This means you can style
the entire `Table`, an entire `Row`, an entire `Cell` and rely on the styling capabilities of
`Text` to get full control over the look of your `Table`.
* `Row`s can have multiple lines.
* The header is now optional and is just another `Row` always visible at the top.
* `Row`s can have a bottom margin.
* The header alignment is no longer off when an item is selected.
Taking the example of the code in `examples/demo/ui.rs`, this is what you may have to change:
```diff
let failure_style = Style::default()
.fg(Color::Red)
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
- let header = ["Server", "Location", "Status"];
let rows = app.servers.iter().map(|s| {
let style = if s.status == "Up" {
up_style
} else {
failure_style
};
- Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
+ Row::new(vec![s.name, s.location, s.status]).style(style)
});
- let table = Table::new(header.iter(), rows)
+ let table = Table::new(rows)
+ .header(
+ Row::new(vec!["Server", "Location", "Status"])
+ .style(Style::default().fg(Color::Yellow))
+ .bottom_margin(1),
+ )
.block(Block::default().title("Servers").borders(Borders::ALL))
- .header_style(Style::default().fg(Color::Yellow))
.widths(&[
Constraint::Length(15),
Constraint::Length(15),
```
Here, we had to:
- Change the way we construct [`Row`](https://docs.rs/tui/*/tui/widgets/struct.Row.html) which is no
longer an `enum` but a `struct`. It accepts anything that can be converted to an iterator of things
that can be converted to a [`Cell`](https://docs.rs/tui/*/tui/widgets/struct.Cell.html)
- The header is no longer a required parameter so we use
[`Table::header`](https://docs.rs/tui/*/tui/widgets/struct.Table.html#method.header) to set it.
`Table::header_style` has been removed since the style can be directly set using
[`Row::style`](https://docs.rs/tui/*/tui/widgets/struct.Row.html#method.style). In addition, we want
to preserve the old margin between the header and the rest of the rows so we add a bottom margin to
the header using
[`Row::bottom_margin`](https://docs.rs/tui/*/tui/widgets/struct.Row.html#method.bottom_margin).
You may want to look at the documentation of the different types to get a better understanding:
- [`Table`](https://docs.rs/tui/*/tui/widgets/struct.Table.html)
- [`Row`](https://docs.rs/tui/*/tui/widgets/struct.Row.html)
- [`Cell`](https://docs.rs/tui/*/tui/widgets/struct.Cell.html)
### Fixes
- Fix handling of Non Breaking Space (NBSP) in wrapped text in `Paragraph` widget.
### Features
- Add `Style::reset` to create a `Style` resetting all styling properties when applied.
- Add an option to render the `Gauge` widget with unicode blocks.
- Manage common project tasks with `cargo-make` rather than `make` for easier on-boarding.
## v0.13.0 - 2020-11-14
### Features
* Add `LineGauge` widget which is a more compact variant of the existing `Gauge`.
* Bump `crossterm` to 0.18
### Fixes
* Take into account the borders of the `Table` widget when the widths of columns is controlled by
`Percentage` and `Ratio` constraints.
## v0.12.0 - 2020-09-27
### Features
* Make it easier to work with string with multiple lines in `Text` (#361).
### Fixes
* Fix a style leak in `Graph` so components drawn on top of the plotted data (i.e legend and axis
titles) are not affected by the style of the `Dataset`s (#388).
* Make sure `BarChart` shows bars with the max height only when the plotted data is actually equal
to the max (#383).
## v0.11.0 - 2020-09-20
### Features
* Add the dot character as a new type of canvas marker (#350).
* Support more style modifiers on Windows (#368).
### Fixes
* Clearing the terminal through `Terminal::clear` will cause the whole UI to be redrawn (#380).
* Fix incorrect output when the first diff to draw is on the second cell of the terminal (#347).
## v0.10.0 - 2020-07-17
### Breaking changes
#### Easier cursor management
A new method has been added to `Frame` called `set_cursor`. It lets you specify where the cursor
should be placed after the draw call. Furthermore like any other widgets, if you do not set a cursor
position during a draw call, the cursor is automatically hidden.
For example:
```rust
fn draw_input(f: &mut Frame, app: &App) {
if app.editing {
let input_width = app.input.width() as u16;
// The cursor will be placed just after the last character of the input
f.set_cursor((input_width + 1, 0));
} else {
// We are no longer editing, the cursor does not have to be shown, set_cursor is not called and
// thus automatically hidden.
}
}
```
In order to make this possible, the draw closure takes in input `&mut Frame` instead of `mut Frame`.
#### Advanced text styling
It has been reported several times that the text styling capabilities were somewhat limited in many
places of the crate. To solve the issue, this release includes a new set of text primitives that are
now used by a majority of widgets to provide flexible text styling.
`Text` is replaced by the following types:
- `Span`: a string with a unique style.
- `Spans`: a string with multiple styles.
- `Text`: a multi-lines string with multiple styles.
However, you do not always need this complexity so the crate provides `From` implementations to
let you use simple strings as a default and switch to the previous primitives when you need
additional styling capabilities.
For example, the title of a `Block` can be set in the following ways:
```rust
// A title with no styling
Block::default().title("My title");
// A yellow title
Block::default().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
// A title where "My" is bold and "title" is a simple string
Block::default().title(vec![
Span::styled("My", Style::default().add_modifier(Modifier::BOLD)),
Span::from("title")
]);
```
- `Buffer::set_spans` and `Buffer::set_span` were added.
- `Paragraph::new` expects an input that can be converted to a `Text`.
- `Block::title_style` is deprecated.
- `Block::title` expects a `Spans`.
- `Tabs` expects a list of `Spans`.
- `Gauge` custom label is now a `Span`.
- `Axis` title and labels are `Spans` (as a consequence `Chart` no longer has generic bounds).
#### Incremental styling
Previously `Style` was used to represent an exhaustive set of style rules to be applied to an UI
element. It implied that whenever you wanted to change even only one property you had to provide the
complete style. For example, if you had a `Block` where you wanted to have a green background and
a title in bold, you had to do the following:
```rust
let style = Style::default().bg(Color::Green);
Block::default()
.style(style)
.title("My title")
// Here we reused the style otherwise the background color would have been reset
.title_style(style.modifier(Modifier::BOLD));
```
In this new release, you may now write this as:
```rust
Block::default()
.style(Style::default().bg(Color::Green))
// The style is not overidden anymore, we simply add new style rule for the title.
.title(Span::styled("My title", Style::default().add_modifier(Modifier::BOLD)))
```
In addition, the crate now provides a method `patch` to combine two styles into a new set of style
rules:
```rust
let style = Style::default().modifer(Modifier::BOLD);
let style = style.patch(Style::default().add_modifier(Modifier::ITALIC));
// style.modifer == Modifier::BOLD | Modifier::ITALIC, the modifier has been enriched not overidden
```
- `Style::modifier` has been removed in favor of `Style::add_modifier` and `Style::remove_modifier`.
- `Buffer::set_style` has been added. `Buffer::set_background` is deprecated.
- `BarChart::style` no longer set the style of the bars. Use `BarChart::bar_style` in replacement.
- `Gauge::style` no longer set the style of the gauge. Use `Gauge::gauge_style` in replacement.
#### List with item on multiple lines
The `List` widget has been refactored once again to support items with variable heights and complex
styling.
- `List::new` expects an input that can be converted to a `Vec<ListItem>` where `ListItem` is a
wrapper around the item content to provide additional styling capabilities. `ListItem` contains a
`Text`.
- `List::items` has been removed.
```rust
// Before
let items = vec![
"Item1",
"Item2",
"Item3"
];
List::default().items(items.iters());
// After
let items = vec![
ListItem::new("Item1"),
ListItem::new("Item2"),
ListItem::new("Item3"),
];
List::new(items);
```
See the examples for more advanced usages.
#### More wrapping options
`Paragraph::wrap` expects `Wrap` instead of `bool` to let users decided whether they want to trim
whitespaces when the text is wrapped.
```rust
// before
Paragraph::new(text).wrap(true)
// after
Paragraph::new(text).wrap(Wrap { trim: true }) // to have the same behavior
Paragraph::new(text).wrap(Wrap { trim: false }) // to use the new behavior
```
#### Horizontal scrolling in paragraph
You can now scroll horizontally in `Paragraph`. The argument of `Paragraph::scroll` has thus be
changed from `u16` to `(u16, u16)`.
### Features
#### Serialization of style
You can now serialize and de-serialize `Style` using the optional `serde` feature.
## v0.9.5 - 2020-05-21
### Bug Fixes
@@ -245,15 +585,15 @@ from an enum to a bitflags struct.
So instead of writing:
```rust
let style = Style::default().modifier(Modifier::Italic);
let style = Style::default().add_modifier(Modifier::Italic);
```
one should use:
```rust
let style = Style::default().modifier(Modifier::ITALIC);
let style = Style::default().add_modifier(Modifier::ITALIC);
// or
let style = Style::default().modifier(Modifier::ITALIC | Modifier::BOLD);
let style = Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD);
```
### Bug Fixes

33
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,33 @@
# Contributing
## Building
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
`tui` is an ordinary Rust project where common tasks are managed with [cargo-make].
It wraps common `cargo` commands with sane defaults depending on your platform of choice.
Building the project should be as easy as running `cargo make build`.
## :hammer_and_wrench: Pull requests
All contributions are obviously welcome.
Please include as many details as possible in your PR description to help the reviewer (follow the provided template).
Make sure to highlight changes which may need additional attention or you are uncertain about.
Any idea with a large scale impact on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
## Continuous Integration
We use Github Actions for the CI where we perform the following checks:
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
- The tests (docs, lib, tests and examples) should pass.
- The code should conform to the default format enforced by `rustfmt`.
- The code should not contain common style issues `clippy`.
You can also check most of those things yourself locally using `cargo make ci` which will offer you a shorter feedback loop.
## Tests
The test coverage of the crate is far from being ideal but we already have a fair amount of tests in place.
Beside the usual doc and unit tests, one of the most valuable test you can write for `tui` is a test again the `TestBackend`.
It allows you to assert the content of the output buffer that would have been flushed to the terminal after a given draw call.
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.

View File

@@ -1,127 +1,93 @@
[package]
name = "tui"
version = "0.9.5"
version = "0.18.0"
authors = ["Florian Dehau <work@fdehau.com>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
documentation = "https://docs.rs/tui/0.9.5/tui/"
documentation = "https://docs.rs/tui/0.18.0/tui/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/fdehau/tui-rs"
readme = "README.md"
license = "MIT"
exclude = ["assets/*", ".github"]
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
autoexamples = true
edition = "2018"
edition = "2021"
[badges]
[features]
default = ["termion"]
curses = ["easycurses", "pancurses"]
default = ["crossterm"]
[dependencies]
bitflags = "1.0"
bitflags = "1.3"
cassowary = "0.3"
itertools = "0.9"
either = "1.5"
unicode-segmentation = "1.2"
unicode-width = "0.1"
termion = { version = "1.5", optional = true }
rustbox = { version = "0.11", optional = true }
crossterm = { version = "0.17", optional = true }
easycurses = { version = "0.12.2", optional = true }
pancurses = { version = "0.16.1", optional = true, features = ["win32a"] }
crossterm = { version = "0.23", optional = true }
serde = { version = "1", optional = true, features = ["derive"]}
[dev-dependencies]
rand = "0.7"
rand = "0.8"
argh = "0.1"
[[example]]
name = "canvas"
path = "examples/canvas.rs"
required-features = ["termion"]
[[example]]
name = "user_input"
path = "examples/user_input.rs"
required-features = ["termion"]
[[example]]
name = "gauge"
path = "examples/gauge.rs"
required-features = ["termion"]
[[example]]
name = "barchart"
path = "examples/barchart.rs"
required-features = ["termion"]
[[example]]
name = "chart"
path = "examples/chart.rs"
required-features = ["termion"]
[[example]]
name = "paragraph"
path = "examples/paragraph.rs"
required-features = ["termion"]
[[example]]
name = "list"
path = "examples/list.rs"
required-features = ["termion"]
[[example]]
name = "table"
path = "examples/table.rs"
required-features = ["termion"]
[[example]]
name = "tabs"
path = "examples/tabs.rs"
required-features = ["termion"]
[[example]]
name = "custom_widget"
path = "examples/custom_widget.rs"
required-features = ["termion"]
[[example]]
name = "layout"
path = "examples/layout.rs"
required-features = ["termion"]
[[example]]
name = "popup"
path = "examples/popup.rs"
required-features = ["termion"]
[[example]]
name = "block"
path = "examples/block.rs"
required-features = ["termion"]
[[example]]
name = "sparkline"
path = "examples/sparkline.rs"
required-features = ["termion"]
[[example]]
name = "termion_demo"
path = "examples/termion_demo.rs"
required-features = ["termion"]
[[example]]
name = "rustbox_demo"
path = "examples/rustbox_demo.rs"
required-features = ["rustbox"]
[[example]]
name = "crossterm_demo"
path = "examples/crossterm_demo.rs"
required-features = ["crossterm"]
[[example]]
name = "curses_demo"
path = "examples/curses_demo.rs"
required-features = ["curses"]
name = "block"
required-features = ["crossterm"]
[[example]]
name = "canvas"
required-features = ["crossterm"]
[[example]]
name = "chart"
required-features = ["crossterm"]
[[example]]
name = "custom_widget"
required-features = ["crossterm"]
[[example]]
name = "gauge"
required-features = ["crossterm"]
[[example]]
name = "layout"
required-features = ["crossterm"]
[[example]]
name = "list"
required-features = ["crossterm"]
[[example]]
name = "panic"
required-features = ["crossterm"]
[[example]]
name = "paragraph"
required-features = ["crossterm"]
[[example]]
name = "popup"
required-features = ["crossterm"]
[[example]]
name = "sparkline"
required-features = ["crossterm"]
[[example]]
name = "table"
required-features = ["crossterm"]
[[example]]
name = "tabs"
required-features = ["crossterm"]
[[example]]
name = "user_input"
required-features = ["crossterm"]

113
Makefile
View File

@@ -1,113 +0,0 @@
SHELL=/bin/bash
# ================================ Cargo ======================================
RUST_CHANNEL ?= stable
CARGO_FLAGS =
RUSTUP_INSTALLED = $(shell command -v rustup 2> /dev/null)
ifndef RUSTUP_INSTALLED
CARGO = cargo
else
ifdef CI
CARGO = cargo
else
CARGO = rustup run $(RUST_CHANNEL) cargo
endif
endif
# ================================ Help =======================================
.PHONY: help
help: ## Print all the available commands
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
# =============================== Build =======================================
.PHONY: check
check: ## Validate the project code
$(CARGO) check
.PHONY: build
build: ## Build the project in debug mode
$(CARGO) build $(CARGO_FLAGS)
.PHONY: release
release: CARGO_FLAGS += --release
release: build ## Build the project in release mode
# ================================ Lint =======================================
.PHONY: lint
lint: fmt clippy ## Lint project files
.PHONY: fmt
fmt: ## Check the format of the source code
cargo fmt --all -- --check
.PHONY: clippy
clippy: ## Check the style of the source code and catch common errors
$(CARGO) clippy --all-targets --all-features -- -D warnings
# ================================ Test =======================================
.PHONY: test
test: ## Run the tests
$(CARGO) test --all-features
# =============================== Examples ====================================
.PHONY: build-examples
build-examples: ## Build all examples
@$(CARGO) build --examples --all-features
.PHONY: run-examples
run-examples: ## Run all examples
@for file in examples/*.rs; do \
name=$$(basename $${file/.rs/}); \
$(CARGO) run --all-features --release --example $$name; \
done;
# ================================ Doc ========================================
.PHONY: doc
doc: ## Build the documentation (available at ./target/doc)
$(CARGO) doc
# ================================= Watch =====================================
# Requires watchman and watchman-make (https://facebook.github.io/watchman/docs/install.html)
.PHONY: watch
watch: ## Watch file changes and build the project if any
watchman-make -p 'src/**/*.rs' -t check build
.PHONY: watch-test
watch-test: ## Watch files changes and run the tests if any
watchman-make -p 'src/**/*.rs' 'tests/**/*.rs' 'examples/**/*.rs' -t test
.PHONY: watch-doc
watch-doc: ## Watch file changes and rebuild the documentation if any
watchman-make -p 'src/**/*.rs' -t doc
# ================================= Pipelines =================================
.PHONY: stable
stable: RUST_CHANNEL = stable
stable: build lint test ## Run build and tests for stable
.PHONY: beta
beta: RUST_CHANNEL = beta
beta: build lint test ## Run build and tests for beta
.PHONY: nightly
nightly: RUST_CHANNEL = nightly
nightly: build lint test ## Run build, lint and tests for nightly

160
Makefile.toml Normal file
View File

@@ -0,0 +1,160 @@
[config]
skip_core_tasks = true
[tasks.ci]
run_task = [
{ name = "ci-unix", condition = { platforms = ["linux", "mac"] } },
{ name = "ci-windows", condition = { platforms = ["windows"] } },
]
[tasks.ci-unix]
private = true
dependencies = [
"fmt",
"check-crossterm",
"check-termion",
"test-crossterm",
"test-termion",
"clippy-crossterm",
"clippy-termion",
"test-doc",
]
[tasks.ci-windows]
private = true
dependencies = [
"fmt",
"check-crossterm",
"test-crossterm",
"clippy-crossterm",
"test-doc",
]
[tasks.fmt]
command = "cargo"
args = [
"fmt",
"--all",
"--",
"--check",
]
[tasks.check-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "check"
[tasks.check-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "check"
[tasks.check]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"check",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--all-targets",
]
[tasks.build-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "build"
[tasks.build-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "build"
[tasks.build]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"build",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--all-targets",
]
[tasks.clippy-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "clippy"
[tasks.clippy-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "clippy"
[tasks.clippy]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"clippy",
"--all-targets",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--",
"-D",
"warnings",
]
[tasks.test-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "test"
[tasks.test-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "test"
[tasks.test]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"test",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--lib",
"--tests",
"--examples",
]
[tasks.test-doc]
command = "cargo"
args = [
"test",
"--doc",
]
[tasks.run-example]
private = true
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
command = "cargo"
args = [
"run",
"--release",
"--example",
"${TUI_EXAMPLE_NAME}"
]
[tasks.build-examples]
command = "cargo"
args = [
"build",
"--examples",
"--release"
]
[tasks.run-examples]
dependencies = ["build-examples"]
script = '''
#!@duckscript
files = glob_array ./examples/*.rs
for file in ${files}
name = basename ${file}
name = substring ${name} -3
set_env TUI_EXAMPLE_NAME ${name}
cm_run_task run-example
end
'''

View File

@@ -11,15 +11,9 @@ user interfaces and dashboards. It is heavily inspired by the `Javascript`
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
`Go` library [termui](https://github.com/gizak/termui).
The library itself supports four different backends to draw to the terminal. You
can either choose from:
The library supports multiple backends:
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
- [termion](https://github.com/ticki/termion)
- [rustbox](https://github.com/gchp/rustbox)
- [crossterm](https://github.com/crossterm-rs/crossterm)
- [pancurses](https://github.com/ihalila/pancurses)
However, some features may only be available in one of the four.
The library is based on the principle of immediate rendering with intermediate
buffers. This means that at each new frame you should build all widgets that are
@@ -32,56 +26,55 @@ comes from the terminal emulator than the library itself.
Moreover, the library does not provide any input handling nor any event system and
you may rely on the previously cited libraries to achieve such features.
### Rust version requirements
Since version 0.17.0, `tui` requires **rustc version 1.56.1 or greater**.
### [Documentation](https://docs.rs/tui)
### Demo
The demo shown in the gif can be run with all available backends
(`exmples/*_demo.rs` files). For example to see the `termion` version one could
run:
The demo shown in the gif can be run with all available backends.
```
cargo run --example termion_demo --release -- --tick-rate 200
# crossterm
cargo run --example demo --release -- --tick-rate 200
# termion
cargo run --example demo --no-default-features --features=termion --release -- --tick-rate 200
```
where `tick-rate` is the UI refresh rate in ms.
The UI code is in [examples/demo/ui.rs](examples/demo/ui.rs) while the
application state is in [examples/demo/app.rs](examples/demo/app.rs).
Beware that the `termion_demo` only works on Unix platforms. If you are a Windows user,
you can see the same demo using the `crossterm` backend with the following command:
```
cargo run --example crossterm_demo --no-default-features --features="crossterm" --release -- --tick-rate 200
```
The UI code is in [examples/demo/ui.rs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/demo/ui.rs) while the
application state is in [examples/demo/app.rs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/demo/app.rs).
If the user interface contains glyphs that are not displayed correctly by your terminal, you may want to run
the demo without those symbols:
```
cargo run --example crossterm_demo --no-default-features --features="crossterm" --release -- --tick-rate 200 --enhanced-graphics false
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
```
### Widgets
The library comes with the following list of widgets:
* [Block](examples/block.rs)
* [Gauge](examples/gauge.rs)
* [Sparkline](examples/sparkline.rs)
* [Chart](examples/chart.rs)
* [BarChart](examples/barchart.rs)
* [List](examples/list.rs)
* [Table](examples/table.rs)
* [Paragraph](examples/paragraph.rs)
* [Canvas (with line, point cloud, map)](examples/canvas.rs)
* [Tabs](examples/tabs.rs)
* [Block](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/block.rs)
* [Gauge](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/gauge.rs)
* [Sparkline](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/sparkline.rs)
* [Chart](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/chart.rs)
* [BarChart](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/barchart.rs)
* [List](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/list.rs)
* [Table](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/table.rs)
* [Paragraph](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/paragraph.rs)
* [Canvas (with line, point cloud, map)](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/canvas.rs)
* [Tabs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/tabs.rs)
Click on each item to see the source of the example. Run the examples with with
cargo (e.g. to run the demo `cargo run --example demo`), and quit by pressing `q`.
cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
You can run all examples by running `make run-examples`.
You can run all examples by running `cargo make run-examples` (require
`cargo-make` that can be installed with `cargo install cargo-make`).
### Third-party widgets
@@ -92,6 +85,7 @@ You can run all examples by running `make run-examples`.
* [spotify-tui](https://github.com/Rigellute/spotify-tui)
* [bandwhich](https://github.com/imsnif/bandwhich)
* [kmon](https://github.com/orhun/kmon)
* [gpg-tui](https://github.com/orhun/gpg-tui)
* [ytop](https://github.com/cjbassi/ytop)
* [zenith](https://github.com/bvaisvil/zenith)
* [bottom](https://github.com/ClementTsang/bottom)
@@ -99,6 +93,28 @@ You can run all examples by running `make run-examples`.
* [gitui](https://github.com/extrawurst/gitui)
* [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli)
* [desed](https://github.com/SoptikHa2/desed)
* [diskonaut](https://github.com/imsnif/diskonaut)
* [tickrs](https://github.com/tarkah/tickrs)
* [rusty-krab-manager](https://github.com/aryakaul/rusty-krab-manager)
* [termchat](https://github.com/lemunozm/termchat)
* [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui)
* [gping](https://github.com/orf/gping/)
* [Vector](https://vector.dev)
* [KDash](https://github.com/kdash-rs/kdash)
* [xplr](https://github.com/sayanarijit/xplr)
* [minesweep](https://github.com/cpcloud/minesweep-rs)
* [Battleship.rs](https://github.com/deepu105/battleship-rs)
* [termscp](https://github.com/veeso/termscp)
* [joshuto](https://github.com/kamiyaa/joshuto)
* [adsb_deku/radar](https://github.com/wcampbell0x2a/adsb_deku#radar-tui)
* [hoard](https://github.com/Hyde46/hoard)
* [tokio-console](https://github.com/tokio-rs/console): a diagnostics and debugging tool for asynchronous Rust programs.
* [hwatch](https://github.com/blacknon/hwatch): a alternative watch command that records the result of command execution and can display its history and diffs.
* [ytui-music](https://github.com/sudipghimire533/ytui-music): listen to music from youtube inside your terminal.
* [mqttui](https://github.com/EdJoPaTo/mqttui): subscribe or publish to a MQTT Topic quickly from the terminal.
* [meteo-tui](https://github.com/16arpi/meteo-tui): french weather via the command line.
* [picterm](https://github.com/ksk001100/picterm): preview images in your terminal.
* [gobang](https://github.com/TaKO8Ki/gobang): a cross-platform TUI database management tool.
### Alternatives

View File

@@ -1,15 +1,19 @@
#[allow(dead_code)]
mod util;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::TermionBackend,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{BarChart, Block, Borders},
Terminal,
Frame, Terminal,
};
struct App<'a> {
@@ -48,78 +52,110 @@ impl<'a> App<'a> {
}
}
fn update(&mut self) {
fn on_tick(&mut self) {
let value = self.data.pop().unwrap();
self.data.insert(0, value);
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup event handlers
let events = Events::new();
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// App
let mut app = App::new();
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
.bar_width(9)
.style(Style::default().fg(Color::Yellow))
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
f.render_widget(barchart, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let barchart = BarChart::default()
.block(Block::default().title("Data2").borders(Borders::ALL))
.data(&app.data)
.bar_width(5)
.bar_gap(3)
.style(Style::default().fg(Color::Green))
.value_style(Style::default().bg(Color::Green).modifier(Modifier::BOLD));
f.render_widget(barchart, chunks[0]);
let barchart = BarChart::default()
.block(Block::default().title("Data3").borders(Borders::ALL))
.data(&app.data)
.style(Style::default().fg(Color::Red))
.bar_width(7)
.bar_gap(0)
.value_style(Style::default().bg(Color::Red))
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC));
f.render_widget(barchart, chunks[1]);
})?;
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
}
}
Event::Tick => {
app.update();
}
}
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
.bar_width(9)
.bar_style(Style::default().fg(Color::Yellow))
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
f.render_widget(barchart, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let barchart = BarChart::default()
.block(Block::default().title("Data2").borders(Borders::ALL))
.data(&app.data)
.bar_width(5)
.bar_gap(3)
.bar_style(Style::default().fg(Color::Green))
.value_style(
Style::default()
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
);
f.render_widget(barchart, chunks[0]);
let barchart = BarChart::default()
.block(Block::default().title("Data3").borders(Borders::ALL))
.data(&app.data)
.bar_style(Style::default().fg(Color::Red))
.bar_width(7)
.bar_gap(0)
.value_style(Style::default().bg(Color::Red))
.label_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::ITALIC),
);
f.render_widget(barchart, chunks[1]);
}

View File

@@ -1,85 +1,119 @@
#[allow(dead_code)]
mod util;
use crate::util::event::{Event, Events};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, BorderType, Borders},
Terminal,
Frame, Terminal,
};
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup event handlers
let events = Events::new();
// create app and run it
let res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(|mut f| {
// Wrapping block for a group
// Just draw the block and the group on the same area and build the group
// with at least a margin of 1
let size = f.size();
let block = Block::default()
.borders(Borders::ALL)
.title("Main block with round corners")
.border_type(BorderType::Rounded);
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(4)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
let block = Block::default()
.title("With background")
.title_style(Style::default().fg(Color::Yellow))
.style(Style::default().bg(Color::Green));
f.render_widget(block, chunks[0]);
let title_style = Style::default()
.fg(Color::White)
.bg(Color::Red)
.modifier(Modifier::BOLD);
let block = Block::default()
.title("Styled title")
.title_style(title_style);
f.render_widget(block, chunks[1]);
}
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let block = Block::default().title("With borders").borders(Borders::ALL);
f.render_widget(block, chunks[0]);
let block = Block::default()
.title("With styled borders and doubled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Double);
f.render_widget(block, chunks[1]);
}
})?;
terminal.draw(ui)?;
if let Event::Input(key) = events.next()? {
if key == Key::Char('q') {
break;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
Ok(())
}
fn ui<B: Backend>(f: &mut Frame<B>) {
// Wrapping block for a group
// Just draw the block and the group on the same area and build the group
// with at least a margin of 1
let size = f.size();
// Surrounding block
let block = Block::default()
.borders(Borders::ALL)
.title("Main block with round corners")
.title_alignment(Alignment::Center)
.border_type(BorderType::Rounded);
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(4)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
// Top two inner blocks
let top_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
// Top left inner block with green background
let block = Block::default()
.title(vec![
Span::styled("With", Style::default().fg(Color::Yellow)),
Span::from(" background"),
])
.style(Style::default().bg(Color::Green));
f.render_widget(block, top_chunks[0]);
// Top right inner block with styled title aligned to the right
let block = Block::default()
.title(Span::styled(
"Styled title",
Style::default()
.fg(Color::White)
.bg(Color::Red)
.add_modifier(Modifier::BOLD),
))
.title_alignment(Alignment::Right);
f.render_widget(block, top_chunks[1]);
// Bottom two inner blocks
let bottom_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
// Bottom left block with all default borders
let block = Block::default().title("With borders").borders(Borders::ALL);
f.render_widget(block, bottom_chunks[0]);
// Bottom right block with styled left and right border
let block = Block::default()
.title("With styled borders and doubled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Double);
f.render_widget(block, bottom_chunks[1]);
}

View File

@@ -1,18 +1,23 @@
#[allow(dead_code)]
mod util;
use crate::util::event::{Config, Event, Events};
use std::{error::Error, io, time::Duration};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::TermionBackend,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::Color,
style::{Color, Style},
text::Span,
widgets::{
canvas::{Canvas, Map, MapResolution, Rectangle},
Block, Borders,
},
Terminal,
Frame, Terminal,
};
struct App {
@@ -46,7 +51,7 @@ impl App {
}
}
fn update(&mut self) {
fn on_tick(&mut self) {
if self.ball.x < self.playground.left() as f64
|| self.ball.x + self.ball.width > self.playground.right() as f64
{
@@ -73,77 +78,103 @@ impl App {
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup event handlers
let config = Config {
tick_rate: Duration::from_millis(250),
..Default::default()
};
let events = Events::with_config(config);
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// App
let mut app = App::new();
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
resolution: MapResolution::High,
});
ctx.print(app.x, -app.y, "You are here", Color::Yellow);
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]);
f.render_widget(canvas, chunks[0]);
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.paint(|ctx| {
ctx.draw(&app.ball);
})
.x_bounds([10.0, 110.0])
.y_bounds([10.0, 110.0]);
f.render_widget(canvas, chunks[1]);
})?;
match events.next()? {
Event::Input(input) => match input {
Key::Char('q') => {
break;
}
Key::Down => {
app.y += 1.0;
}
Key::Up => {
app.y -= 1.0;
}
Key::Right => {
app.x += 1.0;
}
Key::Left => {
app.x -= 1.0;
}
_ => {}
},
Event::Tick => {
app.update();
}
}
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => {
return Ok(());
}
KeyCode::Down => {
app.y += 1.0;
}
KeyCode::Up => {
app.y -= 1.0;
}
KeyCode::Right => {
app.x += 1.0;
}
KeyCode::Left => {
app.x -= 1.0;
}
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
resolution: MapResolution::High,
});
ctx.print(
app.x,
-app.y,
Span::styled("You are here", Style::default().fg(Color::Yellow)),
);
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]);
f.render_widget(canvas, chunks[0]);
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.paint(|ctx| {
ctx.draw(&app.ball);
})
.x_bounds([10.0, 110.0])
.y_bounds([10.0, 110.0]);
f.render_widget(canvas, chunks[1]);
}

View File

@@ -1,19 +1,21 @@
#[allow(dead_code)]
mod util;
use crate::util::{
event::{Event, Events},
SinSignal,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
symbols,
text::Span,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
Terminal,
Frame, Terminal,
};
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
@@ -27,6 +29,34 @@ const DATA2: [(f64, f64); 7] = [
(60.0, 3.0),
];
#[derive(Clone)]
pub struct SinSignal {
x: f64,
interval: f64,
period: f64,
scale: f64,
}
impl SinSignal {
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
SinSignal {
x: 0.0,
interval,
period,
scale,
}
}
}
impl Iterator for SinSignal {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
self.x += self.interval;
Some(point)
}
}
struct App {
signal1: SinSignal,
data1: Vec<(f64, f64)>,
@@ -50,7 +80,7 @@ impl App {
}
}
fn update(&mut self) {
fn on_tick(&mut self) {
for _ in 0..5 {
self.data1.remove(0);
}
@@ -65,152 +95,207 @@ impl App {
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// App
let mut app = App::new();
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
loop {
terminal.draw(|mut f| {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]
.as_ref(),
)
.split(size);
let x_labels = [
format!("{}", app.window[0]),
format!("{}", (app.window[0] + app.window[1]) / 2.0),
format!("{}", app.window[1]),
];
let datasets = [
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data1),
Dataset::default()
.name("data3")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
];
let chart = Chart::default()
.block(
Block::default()
.title("Chart 1")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds(app.window)
.labels(&x_labels),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([-20.0, 20.0])
.labels(&["-20", "0", "20"]),
)
.datasets(&datasets);
f.render_widget(chart, chunks[0]);
let datasets = [Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA)];
let chart = Chart::default()
.block(
Block::default()
.title("Chart 2")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5.0"]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5.0"]),
)
.datasets(&datasets);
f.render_widget(chart, chunks[1]);
let datasets = [Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA2)];
let chart = Chart::default()
.block(
Block::default()
.title("Chart 3")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 50.0])
.labels(&["0", "25", "50"]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5"]),
)
.datasets(&datasets);
f.render_widget(chart, chunks[2]);
})?;
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
}
}
Event::Tick => {
app.update();
}
}
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]
.as_ref(),
)
.split(size);
let x_labels = vec![
Span::styled(
format!("{}", app.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
Span::styled(
format!("{}", app.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data1),
Dataset::default()
.name("data3")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
];
let chart = Chart::new(datasets)
.block(
Block::default()
.title(Span::styled(
"Chart 1",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels(x_labels)
.bounds(app.window),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels(vec![
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("0"),
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
])
.bounds([-20.0, 20.0]),
);
f.render_widget(chart, chunks[0]);
let datasets = vec![Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA)];
let chart = Chart::new(datasets)
.block(
Block::default()
.title(Span::styled(
"Chart 2",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[1]);
let datasets = vec![Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA2)];
let chart = Chart::new(datasets)
.block(
Block::default()
.title(Span::styled(
"Chart 3",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 50.0])
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("25"),
Span::styled("50", Style::default().add_modifier(Modifier::BOLD)),
]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5", Style::default().add_modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[2]);
}

View File

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

View File

@@ -1,74 +0,0 @@
mod demo;
#[allow(dead_code)]
mod util;
use crate::demo::{ui, App};
use argh::FromArgs;
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{backend::CursesBackend, Terminal};
/// Curses demo
#[derive(Debug, FromArgs)]
struct Cli {
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let mut backend =
CursesBackend::new().ok_or_else(|| io::Error::new(io::ErrorKind::Other, ""))?;
let curses = backend.get_curses_mut();
curses.set_echo(false);
curses.set_input_timeout(easycurses::TimeoutMode::WaitUpTo(50));
curses.set_input_mode(easycurses::InputMode::RawCharacter);
curses.set_keypad_enabled(true);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Curses demo", cli.enhanced_graphics);
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(cli.tick_rate);
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
if let Some(input) = terminal.backend_mut().get_curses_mut().get_input() {
match input {
easycurses::Input::Character(c) => {
app.on_key(c);
}
easycurses::Input::KeyUp => {
app.on_up();
}
easycurses::Input::KeyDown => {
app.on_down();
}
easycurses::Input::KeyLeft => {
app.on_left();
}
easycurses::Input::KeyRight => {
app.on_right();
}
_ => {}
};
};
terminal.backend_mut().get_curses_mut().flush_input();
if last_tick.elapsed() > tick_rate {
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
break;
}
}
Ok(())
}

View File

@@ -1,23 +1,23 @@
#[allow(dead_code)]
mod util;
use crate::util::event::{Event, Events};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend, buffer::Buffer, layout::Rect, style::Style, widgets::Widget, Terminal,
backend::{Backend, CrosstermBackend},
buffer::Buffer,
layout::Rect,
style::Style,
widgets::Widget,
Frame, Terminal,
};
#[derive(Default)]
struct Label<'a> {
text: &'a str,
}
impl<'a> Default for Label<'a> {
fn default() -> Label<'a> {
Label { text: "" }
}
}
impl<'a> Widget for Label<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.left(), area.top(), self.text, Style::default());
@@ -32,28 +32,46 @@ impl<'a> Label<'a> {
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let events = Events::new();
// create app and run it
let res = run_app(&mut terminal);
loop {
terminal.draw(|mut f| {
let size = f.size();
let label = Label::default().text("Test");
f.render_widget(label, size);
})?;
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Event::Input(key) = events.next()? {
if key == Key::Char('q') {
break;
}
}
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>) {
let size = f.size();
let label = Label::default().text("Test");
f.render_widget(label, size);
}

View File

@@ -1,4 +1,8 @@
use crate::util::{RandomSignal, SinSignal, StatefulList, TabsState};
use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use tui::widgets::ListState;
const TASKS: [&str; 24] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
@@ -62,6 +66,120 @@ const EVENTS: [(&str, u64); 24] = [
("B24", 5),
];
#[derive(Clone)]
pub struct RandomSignal {
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> RandomSignal {
RandomSignal {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
}
}
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.distribution.sample(&mut self.rng))
}
}
#[derive(Clone)]
pub struct SinSignal {
x: f64,
interval: f64,
period: f64,
scale: f64,
}
impl SinSignal {
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
SinSignal {
x: 0.0,
interval,
period,
scale,
}
}
}
impl Iterator for SinSignal {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
self.x += self.interval;
Some(point)
}
}
pub struct TabsState<'a> {
pub titles: Vec<&'a str>,
pub index: usize,
}
impl<'a> TabsState<'a> {
pub fn new(titles: Vec<&'a str>) -> TabsState {
TabsState { titles, index: 0 }
}
pub fn next(&mut self) {
self.index = (self.index + 1) % self.titles.len();
}
pub fn previous(&mut self) {
if self.index > 0 {
self.index -= 1;
} else {
self.index = self.titles.len() - 1;
}
}
}
pub struct StatefulList<T> {
pub state: ListState,
pub items: Vec<T>,
}
impl<T> StatefulList<T> {
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items,
}
}
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
}
pub struct Signal<S: Iterator> {
source: S,
pub points: Vec<S::Item>,
@@ -129,7 +247,7 @@ impl<'a> App<'a> {
App {
title,
should_quit: false,
tabs: TabsState::new(vec!["Tab0", "Tab1"]),
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2"]),
show_chart: true,
progress: 0.0,
sparkline: Signal {

View File

@@ -0,0 +1,77 @@
use crate::{app::App, ui};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
Terminal,
};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new("Crossterm Demo", enhanced_graphics);
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char(c) => app.on_key(c),
KeyCode::Left => app.on_left(),
KeyCode::Up => app.on_up(),
KeyCode::Right => app.on_right(),
KeyCode::Down => app.on_down(),
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
return Ok(());
}
}
}

31
examples/demo/main.rs Normal file
View File

@@ -0,0 +1,31 @@
mod app;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "termion")]
mod termion;
mod ui;
#[cfg(feature = "crossterm")]
use crate::crossterm::run;
#[cfg(feature = "termion")]
use crate::termion::run;
use argh::FromArgs;
use std::{error::Error, time::Duration};
/// Demo
#[derive(Debug, FromArgs)]
struct Cli {
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let tick_rate = Duration::from_millis(cli.tick_rate);
run(tick_rate, cli.enhanced_graphics)?;
Ok(())
}

View File

@@ -1,3 +0,0 @@
mod app;
pub mod ui;
pub use app::App;

80
examples/demo/termion.rs Normal file
View File

@@ -0,0 +1,80 @@
use crate::{app::App, ui};
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::AlternateScreen,
};
use tui::{
backend::{Backend, TermionBackend},
Terminal,
};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new("Termion demo", enhanced_graphics);
run_app(&mut terminal, app, tick_rate)?;
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> Result<(), Box<dyn Error>> {
let events = events(tick_rate);
loop {
terminal.draw(|f| ui::draw(f, &mut app))?;
match events.recv()? {
Event::Input(key) => match key {
Key::Char(c) => app.on_key(c),
Key::Up => app.on_up(),
Key::Down => app.on_down(),
Key::Left => app.on_left(),
Key::Right => app.on_right(),
_ => {}
},
Event::Tick => app.on_tick(),
}
if app.should_quit {
return Ok(());
}
}
}
enum Event {
Input(Key),
Tick,
}
fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
let (tx, rx) = mpsc::channel();
let keys_tx = tx.clone();
thread::spawn(move || {
let stdin = io::stdin();
for key in stdin.keys().flatten() {
if let Err(err) = keys_tx.send(Event::Input(key)) {
eprintln!("{}", err);
return;
}
}
});
thread::spawn(move || loop {
if let Err(err) = tx.send(Event::Tick) {
eprintln!("{}", err);
break;
}
thread::sleep(tick_rate);
});
rx
}

View File

@@ -1,32 +1,37 @@
use crate::app::App;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans},
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
widgets::{
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Paragraph, Row, Sparkline,
Table, Tabs, Text,
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
},
Frame,
};
use crate::demo::App;
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size());
let tabs = Tabs::default()
let titles = app
.tabs
.titles
.iter()
.map(|t| Spans::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title(app.title))
.titles(&app.tabs.titles)
.style(Style::default().fg(Color::Green))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index);
f.render_widget(tabs, chunks[0]);
match app.tabs.index {
0 => draw_first_tab(f, app, chunks[1]),
1 => draw_second_tab(f, app, chunks[1]),
2 => draw_third_tab(f, app, chunks[1]),
_ => {}
};
}
@@ -38,8 +43,8 @@ where
let chunks = Layout::default()
.constraints(
[
Constraint::Length(7),
Constraint::Min(7),
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
]
.as_ref(),
@@ -55,7 +60,14 @@ where
B: Backend,
{
let chunks = Layout::default()
.constraints([Constraint::Length(2), Constraint::Length(3)].as_ref())
.constraints(
[
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(1),
]
.as_ref(),
)
.margin(1)
.split(area);
let block = Block::default().borders(Borders::ALL).title("Graphs");
@@ -64,13 +76,13 @@ where
let label = format!("{:.2}%", app.progress * 100.0);
let gauge = Gauge::default()
.block(Block::default().title("Gauge:"))
.style(
.gauge_style(
Style::default()
.fg(Color::Magenta)
.bg(Color::Black)
.modifier(Modifier::ITALIC | Modifier::BOLD),
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
)
.label(&label)
.label(label)
.ratio(app.progress);
f.render_widget(gauge, chunks[0]);
@@ -84,6 +96,17 @@ where
symbols::bar::THREE_LEVELS
});
f.render_widget(sparkline, chunks[1]);
let line_gauge = LineGauge::default()
.block(Block::default().title("LineGauge:"))
.gauge_style(Style::default().fg(Color::Magenta))
.line_set(if app.enhanced_graphics {
symbols::line::THICK
} else {
symbols::line::NORMAL
})
.ratio(app.progress);
f.render_widget(line_gauge, chunks[2]);
}
fn draw_charts<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
@@ -110,29 +133,41 @@ where
.split(chunks[0]);
// Draw tasks
let tasks = app.tasks.items.iter().map(|i| Text::raw(*i));
let tasks: Vec<ListItem> = app
.tasks
.items
.iter()
.map(|i| ListItem::new(vec![Spans::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks)
.block(Block::default().borders(Borders::ALL).title("List"))
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::BOLD))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> ");
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
// Draw logs
let info_style = Style::default().fg(Color::White);
let info_style = Style::default().fg(Color::Blue);
let warning_style = Style::default().fg(Color::Yellow);
let error_style = Style::default().fg(Color::Magenta);
let critical_style = Style::default().fg(Color::Red);
let logs = app.logs.items.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
let logs: Vec<ListItem> = app
.logs
.items
.iter()
.map(|&(evt, level)| {
let s = match level {
"ERROR" => error_style,
"CRITICAL" => critical_style,
"WARNING" => warning_style,
_ => info_style,
},
)
});
};
let content = vec![Spans::from(vec![
Span::styled(format!("{:<9}", level), s),
Span::raw(evt),
])];
ListItem::new(content)
})
.collect();
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
}
@@ -151,19 +186,28 @@ where
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.modifier(Modifier::ITALIC),
.add_modifier(Modifier::ITALIC),
)
.label_style(Style::default().fg(Color::Yellow))
.style(Style::default().fg(Color::Green));
.bar_style(Style::default().fg(Color::Green));
f.render_widget(barchart, chunks[1]);
}
if app.show_chart {
let x_labels = [
format!("{}", app.signals.window[0]),
format!("{}", (app.signals.window[0] + app.signals.window[1]) / 2.0),
format!("{}", app.signals.window[1]),
let x_labels = vec![
Span::styled(
format!("{}", app.signals.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!(
"{}",
(app.signals.window[0] + app.signals.window[1]) / 2.0
)),
Span::styled(
format!("{}", app.signals.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = [
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
@@ -179,30 +223,35 @@ where
.style(Style::default().fg(Color::Yellow))
.data(&app.signals.sin2.points),
];
let chart = Chart::default()
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds(app.signals.window)
.labels(&x_labels),
.labels(x_labels),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([-20.0, 20.0])
.labels(&["-20", "0", "20"]),
)
.datasets(&datasets);
.labels(vec![
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("0"),
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[1]);
}
}
@@ -211,28 +260,43 @@ fn draw_text<B>(f: &mut Frame<B>, area: Rect)
where
B: Backend,
{
let text = [
Text::raw("This is a paragraph with several lines. You can change style your text the way you want.\n\nFox example: "),
Text::styled("under", Style::default().fg(Color::Red)),
Text::raw(" "),
Text::styled("the", Style::default().fg(Color::Green)),
Text::raw(" "),
Text::styled("rainbow", Style::default().fg(Color::Blue)),
Text::raw(".\nOh and if you didn't "),
Text::styled("notice", Style::default().modifier(Modifier::ITALIC)),
Text::raw(" you can "),
Text::styled("automatically", Style::default().modifier(Modifier::BOLD)),
Text::raw(" "),
Text::styled("wrap", Style::default().modifier(Modifier::REVERSED)),
Text::raw(" your "),
Text::styled("text", Style::default().modifier(Modifier::UNDERLINED)),
Text::raw(".\nOne more thing is that it should display unicode characters: 10€")
let text = vec![
Spans::from("This is a paragraph with several lines. You can change style your text the way you want"),
Spans::from(""),
Spans::from(vec![
Span::from("For example: "),
Span::styled("under", Style::default().fg(Color::Red)),
Span::raw(" "),
Span::styled("the", Style::default().fg(Color::Green)),
Span::raw(" "),
Span::styled("rainbow", Style::default().fg(Color::Blue)),
Span::raw("."),
]),
Spans::from(vec![
Span::raw("Oh and if you didn't "),
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
Span::raw(" you can "),
Span::styled("automatically", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" "),
Span::styled("wrap", Style::default().add_modifier(Modifier::REVERSED)),
Span::raw(" your "),
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
Span::raw(".")
]),
Spans::from(
"One more thing is that it should display unicode characters: 10€"
),
];
let block = Block::default()
.borders(Borders::ALL)
.title("Footer")
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::BOLD));
let paragraph = Paragraph::new(text.iter()).block(block).wrap(true);
let block = Block::default().borders(Borders::ALL).title(Span::styled(
"Footer",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
let paragraph = Paragraph::new(text).block(block).wrap(Wrap {
trim: true,
break_words: false,
});
f.render_widget(paragraph, area);
}
@@ -247,19 +311,22 @@ where
let up_style = Style::default().fg(Color::Green);
let failure_style = Style::default()
.fg(Color::Red)
.modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
let header = ["Server", "Location", "Status"];
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
let rows = app.servers.iter().map(|s| {
let style = if s.status == "Up" {
up_style
} else {
failure_style
};
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
Row::new(vec![s.name, s.location, s.status]).style(style)
});
let table = Table::new(header.iter(), rows)
let table = Table::new(rows)
.header(
Row::new(vec!["Server", "Location", "Status"])
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::default().title("Servers").borders(Borders::ALL))
.header_style(Style::default().fg(Color::Yellow))
.widths(&[
Constraint::Length(15),
Constraint::Length(15),
@@ -299,7 +366,11 @@ where
} else {
Color::Red
};
ctx.print(server.coords.1, server.coords.0, "X", color);
ctx.print(
server.coords.1,
server.coords.0,
Span::styled("X", Style::default().fg(color)),
);
}
})
.marker(if app.enhanced_graphics {
@@ -311,3 +382,51 @@ where
.y_bounds([-90.0, 90.0]);
f.render_widget(map, chunks[1]);
}
fn draw_third_tab<B>(f: &mut Frame<B>, _app: &mut App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.split(area);
let colors = [
Color::Reset,
Color::Black,
Color::Red,
Color::Green,
Color::Yellow,
Color::Blue,
Color::Magenta,
Color::Cyan,
Color::Gray,
Color::DarkGray,
Color::LightRed,
Color::LightGreen,
Color::LightYellow,
Color::LightBlue,
Color::LightMagenta,
Color::LightCyan,
Color::White,
];
let items: Vec<Row> = colors
.iter()
.map(|c| {
let cells = vec![
Cell::from(Span::raw(format!("{:?}: ", c))),
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
Cell::from(Span::styled("Background", Style::default().bg(*c))),
];
Row::new(cells)
})
.collect();
let table = Table::new(items)
.block(Block::default().title("Colors").borders(Borders::ALL))
.widths(&[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]);
f.render_widget(table, chunks[0]);
}

View File

@@ -1,15 +1,20 @@
#[allow(dead_code)]
mod util;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::TermionBackend,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Gauge},
Terminal,
Frame, Terminal,
};
struct App {
@@ -24,17 +29,17 @@ impl App {
App {
progress1: 0,
progress2: 0,
progress3: 0.0,
progress3: 0.45,
progress4: 0,
}
}
fn update(&mut self) {
self.progress1 += 5;
fn on_tick(&mut self) {
self.progress1 += 1;
if self.progress1 > 100 {
self.progress1 = 0;
}
self.progress2 += 10;
self.progress2 += 2;
if self.progress2 > 100 {
self.progress2 = 0;
}
@@ -42,7 +47,7 @@ impl App {
if self.progress3 > 1.0 {
self.progress3 = 0.0;
}
self.progress4 += 3;
self.progress4 += 1;
if self.progress4 > 100 {
self.progress4 = 0;
}
@@ -50,74 +55,112 @@ impl App {
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
let mut app = App::new();
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(f.size());
let gauge = Gauge::default()
.block(Block::default().title("Gauge1").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow))
.percent(app.progress1);
f.render_widget(gauge, chunks[0]);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge2").borders(Borders::ALL))
.style(Style::default().fg(Color::Magenta).bg(Color::Green))
.percent(app.progress2)
.label(&label);
f.render_widget(gauge, chunks[1]);
let gauge = Gauge::default()
.block(Block::default().title("Gauge3").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow))
.ratio(app.progress3);
f.render_widget(gauge, chunks[2]);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge4").borders(Borders::ALL))
.style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC))
.percent(app.progress4)
.label(&label);
f.render_widget(gauge, chunks[3]);
})?;
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
}
}
Event::Tick => {
app.update();
}
}
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(f.size());
let gauge = Gauge::default()
.block(Block::default().title("Gauge1").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Yellow))
.percent(app.progress1);
f.render_widget(gauge, chunks[0]);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge2").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green))
.percent(app.progress2)
.label(label);
f.render_widget(gauge, chunks[1]);
let label = Span::styled(
format!("{:.2}%", app.progress3 * 100.0),
Style::default()
.fg(Color::Red)
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
);
let gauge = Gauge::default()
.block(Block::default().title("Gauge3").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(app.progress3)
.label(label)
.use_unicode(true);
f.render_widget(gauge, chunks[2]);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge4"))
.gauge_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::ITALIC),
)
.percent(app.progress4)
.label(label);
f.render_widget(gauge, chunks[3]);
}

View File

@@ -1,53 +1,70 @@
#[allow(dead_code)]
mod util;
use crate::util::event::{Event, Events};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders},
Terminal,
Frame, Terminal,
};
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
// create app and run it
let res = run_app(&mut terminal);
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Percentage(80),
Constraint::Percentage(10),
]
.as_ref(),
)
.split(f.size());
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
let block = Block::default().title("Block").borders(Borders::ALL);
f.render_widget(block, chunks[0]);
let block = Block::default().title("Block 2").borders(Borders::ALL);
f.render_widget(block, chunks[2]);
})?;
if let Event::Input(input) = events.next()? {
if let Key::Char('q') = input {
break;
}
}
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f))?;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Percentage(80),
Constraint::Percentage(10),
]
.as_ref(),
)
.split(f.size());
let block = Block::default().title("Block").borders(Borders::ALL);
f.render_widget(block, chunks[0]);
let block = Block::default().title("Block 2").borders(Borders::ALL);
f.render_widget(block, chunks[2]);
}

View File

@@ -1,36 +1,108 @@
#[allow(dead_code)]
mod util;
use crate::util::{
event::{Event, Events},
StatefulList,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, List, Text},
Terminal,
text::{Span, Spans},
widgets::{Block, Borders, List, ListItem, ListState},
Frame, Terminal,
};
struct StatefulList<T> {
state: ListState,
items: Vec<T>,
}
impl<T> StatefulList<T> {
fn with_items(items: Vec<T>) -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items,
}
}
fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
fn unselect(&mut self) {
self.state.select(None);
}
}
/// This struct holds the current state of the app. In particular, it has the `items` field which is a wrapper
/// around `ListState`. Keeping track of the items state let us render the associated widget with its state
/// and have access to features such as natural scrolling.
///
/// Check the event handling at the bottom to see how to change the state on incoming events.
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
struct App<'a> {
items: StatefulList<&'a str>,
items: StatefulList<(&'a str, usize)>,
events: Vec<(&'a str, &'a str)>,
info_style: Style,
warning_style: Style,
error_style: Style,
critical_style: Style,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
items: StatefulList::with_items(vec![
"Item0", "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8",
"Item9", "Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16",
"Item17", "Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
("Item0", 1),
("Item1", 2),
("Item2", 1),
("Item3", 3),
("Item4", 1),
("Item5", 4),
("Item6", 1),
("Item7", 3),
("Item8", 1),
("Item9", 6),
("Item10", 1),
("Item11", 3),
("Item12", 1),
("Item13", 2),
("Item14", 1),
("Item15", 1),
("Item16", 4),
("Item17", 1),
("Item18", 5),
("Item19", 4),
("Item20", 1),
("Item21", 2),
("Item22", 1),
("Item23", 3),
("Item24", 1),
]),
events: vec![
("Event1", "INFO"),
@@ -60,88 +132,155 @@ impl<'a> App<'a> {
("Event25", "INFO"),
("Event26", "INFO"),
],
info_style: Style::default().fg(Color::White),
warning_style: Style::default().fg(Color::Yellow),
error_style: Style::default().fg(Color::Magenta),
critical_style: Style::default().fg(Color::Red),
}
}
fn advance(&mut self) {
let event = self.events.pop().unwrap();
self.events.insert(0, event);
/// Rotate through the event list.
/// This only exists to simulate some kind of "progress"
fn on_tick(&mut self) {
let event = self.events.remove(0);
self.events.push(event);
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// App
let mut app = App::new();
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let style = Style::default().fg(Color::Black).bg(Color::White);
let items = app.items.items.iter().map(|i| Text::raw(*i));
let items = List::new(items)
.block(Block::default().borders(Borders::ALL).title("List"))
.style(style)
.highlight_style(style.fg(Color::LightGreen).modifier(Modifier::BOLD))
.highlight_symbol(">");
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
let events = app.events.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
"ERROR" => app.error_style,
"CRITICAL" => app.critical_style,
"WARNING" => app.warning_style,
_ => app.info_style,
},
)
});
let events_list = List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.start_corner(Corner::BottomLeft);
f.render_widget(events_list, chunks[1]);
})?;
match events.next()? {
Event::Input(input) => match input {
Key::Char('q') => {
break;
}
Key::Left => {
app.items.unselect();
}
Key::Down => {
app.items.next();
}
Key::Up => {
app.items.previous();
}
_ => {}
},
Event::Tick => {
app.advance();
}
}
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Left => app.items.unselect(),
KeyCode::Down => app.items.next(),
KeyCode::Up => app.items.previous(),
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
// Create two chunks with equal horizontal screen space
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
// Iterate through all elements in the `items` app and append some debug text to it.
let items: Vec<ListItem> = app
.items
.items
.iter()
.map(|i| {
let mut lines = vec![Spans::from(i.0)];
for _ in 0..i.1 {
lines.push(Spans::from(Span::styled(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
Style::default().add_modifier(Modifier::ITALIC),
)));
}
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
})
.collect();
// Create a List from all list items and highlight the currently selected one
let items = List::new(items)
.block(Block::default().borders(Borders::ALL).title("List"))
.highlight_style(
Style::default()
.bg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
// We can now render the item list
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
// Let's do the same for the events.
// The event list doesn't have any state and only displays the current state of the list.
let events: Vec<ListItem> = app
.events
.iter()
.rev()
.map(|&(event, level)| {
// Colorcode the level depending on its type
let s = match level {
"CRITICAL" => Style::default().fg(Color::Red),
"ERROR" => Style::default().fg(Color::Magenta),
"WARNING" => Style::default().fg(Color::Yellow),
"INFO" => Style::default().fg(Color::Blue),
_ => Style::default(),
};
// Add a example datetime and apply proper spacing between them
let header = Spans::from(vec![
Span::styled(format!("{:<9}", level), s),
Span::raw(" "),
Span::styled(
"2020-01-01 10:00:00",
Style::default().add_modifier(Modifier::ITALIC),
),
]);
// The event gets its own line
let log = Spans::from(vec![Span::raw(event)]);
// Here several things happen:
// 1. Add a `---` spacing line above the final list entry
// 2. Add the Level + datetime
// 3. Add a spacer line
// 4. Add the actual event
ListItem::new(vec![
Spans::from("-".repeat(chunks[1].width as usize)),
header,
Spans::from(""),
log,
])
})
.collect();
let events_list = List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.start_corner(Corner::BottomLeft);
f.render_widget(events_list, chunks[1]);
}

142
examples/panic.rs Normal file
View File

@@ -0,0 +1,142 @@
//! How to use a panic hook to reset the terminal before printing the panic to
//! the terminal.
//!
//! When exiting normally or when handling `Result::Err`, we can reset the
//! terminal manually at the end of `main` just before we print the error.
//!
//! Because a panic interrupts the normal control flow, manually resetting the
//! terminal at the end of `main` won't do us any good. Instead, we need to
//! make sure to set up a panic hook that first resets the terminal before
//! handling the panic. This both reuses the standard panic hook to ensure a
//! consistent panic handling UX and properly resets the terminal to not
//! distort the output.
//!
//! That's why this example is set up to show both situations, with and without
//! the chained panic hook, to see the difference.
#![deny(clippy::all)]
#![warn(clippy::pedantic, clippy::nursery)]
use std::error::Error;
use std::io;
use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use tui::backend::{Backend, CrosstermBackend};
use tui::layout::Alignment;
use tui::text::Spans;
use tui::widgets::{Block, Borders, Paragraph};
use tui::{Frame, Terminal};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
#[derive(Default)]
struct App {
hook_enabled: bool,
}
impl App {
fn chain_hook(&mut self) {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic| {
reset_terminal().unwrap();
original_hook(panic);
}));
self.hook_enabled = true;
}
}
fn main() -> Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::default();
let res = run_tui(&mut terminal, &mut app);
reset_terminal()?;
if let Err(err) = res {
println!("{:?}", err);
}
Ok(())
}
/// Initializes the terminal.
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
enable_raw_mode()?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
/// Resets the terminal.
fn reset_terminal() -> Result<()> {
disable_raw_mode()?;
crossterm::execute!(io::stdout(), LeaveAlternateScreen)?;
Ok(())
}
/// Runs the TUI loop.
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, app))?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('p') => {
panic!("intentional demo panic");
}
KeyCode::Char('e') => {
app.chain_hook();
}
_ => {
return Ok(());
}
}
}
}
}
/// Render the TUI.
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let text = vec![
if app.hook_enabled {
Spans::from("HOOK IS CURRENTLY **ENABLED**")
} else {
Spans::from("HOOK IS CURRENTLY **DISABLED**")
},
Spans::from(""),
Spans::from("press `p` to panic"),
Spans::from("press `e` to enable the terminal-resetting panic hook"),
Spans::from("press any other key to quit without panic"),
Spans::from(""),
Spans::from("when you panic without the chained hook,"),
Spans::from("you will likely have to reset your terminal afterwards"),
Spans::from("with the `reset` command"),
Spans::from(""),
Spans::from("with the chained panic hook enabled,"),
Spans::from("you should see the panic report as you would without tui"),
Spans::from(""),
Spans::from("try first without the panic handler to see the difference"),
];
let b = Block::default()
.title("Panic Handler Demo")
.borders(Borders::ALL);
let p = Paragraph::new(text).block(b).alignment(Alignment::Center);
f.render_widget(p, f.size());
}

View File

@@ -1,104 +1,180 @@
#[allow(dead_code)]
mod util;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::TermionBackend,
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Paragraph, Text},
Terminal,
text::{Span, Spans},
widgets::{Block, Borders, Paragraph, Wrap},
Frame, Terminal,
};
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
struct App {
scroll: u16,
}
let events = Events::new();
let mut scroll: u16 = 0;
loop {
terminal.draw(|mut f| {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let block = Block::default()
.style(Style::default().bg(Color::White));
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(5)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(size);
let text = [
Text::raw("This is a line \n"),
Text::styled("This is a line \n", Style::default().fg(Color::Red)),
Text::styled("This is a line\n", Style::default().bg(Color::Blue)),
Text::styled(
"This is a longer line\n",
Style::default().modifier(Modifier::CROSSED_OUT),
),
Text::styled(&long_line, Style::default().bg(Color::Green)),
Text::styled(
"This is a line\n",
Style::default().fg(Color::Green).modifier(Modifier::ITALIC),
),
];
let block = Block::default()
.borders(Borders::ALL)
.title_style(Style::default().modifier(Modifier::BOLD));
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Left, no wrap"))
.alignment(Alignment::Left);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Left, wrap"))
.alignment(Alignment::Left)
.wrap(true);
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Center, wrap"))
.alignment(Alignment::Center)
.wrap(true)
.scroll(scroll);
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Right, wrap"))
.alignment(Alignment::Right)
.wrap(true);
f.render_widget(paragraph, chunks[3]);
})?;
scroll += 1;
scroll %= 10;
if let Event::Input(key) = events.next()? {
if key == Key::Char('q') {
break;
}
}
impl App {
fn new() -> App {
App { scroll: 0 }
}
fn on_tick(&mut self) {
self.scroll += 1;
self.scroll %= 10;
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(5)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(size);
let text = vec![
Spans::from("This is a line "),
Spans::from(Span::styled(
"This is a line ",
Style::default().fg(Color::Red),
)),
Spans::from(Span::styled(
"This is a line",
Style::default().bg(Color::Blue),
)),
Spans::from(Span::styled(
"This is a longer line",
Style::default().add_modifier(Modifier::CROSSED_OUT),
)),
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
Spans::from(Span::styled(
"This is a line",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::ITALIC),
)),
];
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.style(Style::default().bg(Color::White).fg(Color::Black))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Left, no wrap"))
.alignment(Alignment::Left);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Left, wrap"))
.alignment(Alignment::Left)
.wrap(Wrap {
trim: true,
break_words: false,
});
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Center, wrap"))
.alignment(Alignment::Center)
.wrap(Wrap {
trim: true,
break_words: false,
})
.scroll((app.scroll, 0));
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Right, wrap (break words)"))
.alignment(Alignment::Right)
.wrap(Wrap {
trim: true,
break_words: true,
});
f.render_widget(paragraph, chunks[3]);
}

View File

@@ -1,21 +1,109 @@
#[allow(dead_code)]
mod util;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::layout::Rect;
use tui::widgets::Clear;
use tui::{
backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout},
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Paragraph, Text},
Terminal,
text::Span,
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame, Terminal,
};
/// helper function to create a centered rect using up
/// certain percentage of the available rect `r`
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
struct App {
show_popup: bool,
}
impl App {
fn new() -> App {
App { show_popup: false }
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &app))?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('p') => app.show_popup = !app.show_popup,
_ => {}
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let size = f.size();
let chunks = Layout::default()
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref())
.split(size);
let text = if app.show_popup {
"Press p to close the popup"
} else {
"Press p to show the popup"
};
let paragraph = Paragraph::new(Span::styled(
text,
Style::default().add_modifier(Modifier::SLOW_BLINK),
))
.alignment(Alignment::Center)
.wrap(Wrap {
trim: true,
break_words: false,
});
f.render_widget(paragraph, chunks[0]);
let block = Block::default()
.title("Content")
.borders(Borders::ALL)
.style(Style::default().bg(Color::Blue));
f.render_widget(block, chunks[1]);
if app.show_popup {
let block = Block::default().title("Popup").borders(Borders::ALL);
let area = centered_rect(60, 20, size);
f.render_widget(Clear, area); //this clears out the background
f.render_widget(block, area);
}
}
/// helper function to create a centered rect using up certain percentage of the available rect `r`
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
@@ -41,68 +129,3 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
)
.split(popup_layout[1])[1]
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
loop {
terminal.draw(|mut f| {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(size);
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width)*usize::from(size.height)/300);
long_line.push('\n');
let text = [
Text::raw("This is a line \n"),
Text::styled("This is a line \n", Style::default().fg(Color::Red)),
Text::styled("This is a line\n", Style::default().bg(Color::Blue)),
Text::styled(
"This is a longer line\n",
Style::default().modifier(Modifier::CROSSED_OUT),
),
Text::styled(&long_line, Style::default().bg(Color::Green)),
Text::styled(
"This is a line\n",
Style::default().fg(Color::Green).modifier(Modifier::ITALIC),
),
];
let paragraph = Paragraph::new(text.iter())
.block(Block::default().title("Left Block").borders(Borders::ALL))
.alignment(Alignment::Left).wrap(true);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.iter())
.block(Block::default().title("Right Block").borders(Borders::ALL))
.alignment(Alignment::Left).wrap(true);
f.render_widget(paragraph, chunks[1]);
let block = Block::default().title("Popup").borders(Borders::ALL);
let area = centered_rect(60, 20, size);
f.render_widget(Clear, area); //this clears out the background
f.render_widget(block, area);
})?;
if let Event::Input(input) = events.next()? {
if let Key::Char('q') = input {
break;
}
}
}
Ok(())
}

View File

@@ -1,69 +0,0 @@
mod demo;
#[allow(dead_code)]
mod util;
use crate::demo::{ui, App};
use argh::FromArgs;
use rustbox::keyboard::Key;
use std::{
error::Error,
time::{Duration, Instant},
};
use tui::{backend::RustboxBackend, Terminal};
/// Rustbox demo
#[derive(Debug, FromArgs)]
struct Cli {
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let backend = RustboxBackend::new()?;
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Rustbox demo", cli.enhanced_graphics);
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(cli.tick_rate);
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
if let Ok(rustbox::Event::KeyEvent(key)) =
terminal.backend().rustbox().peek_event(tick_rate, false)
{
match key {
Key::Char(c) => {
app.on_key(c);
}
Key::Up => {
app.on_up();
}
Key::Down => {
app.on_down();
}
Key::Left => {
app.on_left();
}
Key::Right => {
app.on_right();
}
_ => {}
}
}
if last_tick.elapsed() > tick_rate {
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
break;
}
}
Ok(())
}

View File

@@ -1,20 +1,47 @@
#[allow(dead_code)]
mod util;
use crate::util::{
event::{Event, Events},
RandomSignal,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, Sparkline},
Terminal,
Frame, Terminal,
};
#[derive(Clone)]
pub struct RandomSignal {
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> RandomSignal {
RandomSignal {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
}
}
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.distribution.sample(&mut self.rng))
}
}
struct App {
signal: RandomSignal,
data1: Vec<u64>,
@@ -36,7 +63,7 @@ impl App {
}
}
fn update(&mut self) {
fn on_tick(&mut self) {
let value = self.signal.next().unwrap();
self.data1.pop();
self.data1.insert(0, value);
@@ -50,76 +77,100 @@ impl App {
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup event handlers
let events = Events::new();
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// Create default app state
let mut app = App::new();
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(7),
Constraint::Min(0),
]
.as_ref(),
)
.split(f.size());
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data1")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data1)
.style(Style::default().fg(Color::Yellow));
f.render_widget(sparkline, chunks[0]);
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data2")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data2)
.style(Style::default().bg(Color::Green));
f.render_widget(sparkline, chunks[1]);
// Multiline
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data3")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data3)
.style(Style::default().fg(Color::Red));
f.render_widget(sparkline, chunks[2]);
})?;
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
}
}
Event::Tick => {
app.update();
}
}
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(7),
Constraint::Min(0),
]
.as_ref(),
)
.split(f.size());
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data1")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data1)
.style(Style::default().fg(Color::Yellow));
f.render_widget(sparkline, chunks[0]);
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data2")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data2)
.style(Style::default().bg(Color::Green));
f.render_widget(sparkline, chunks[1]);
// Multiline
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data3")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data3)
.style(Style::default().fg(Color::Red));
f.render_widget(sparkline, chunks[2]);
}

View File

@@ -1,25 +1,25 @@
#[allow(dead_code)]
mod util;
use crate::util::event::{Event, Events};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Row, Table, TableState},
Terminal,
widgets::{Block, Borders, Cell, Row, Table, TableState},
Frame, Terminal,
};
pub struct StatefulTable<'a> {
struct App<'a> {
state: TableState,
items: Vec<Vec<&'a str>>,
}
impl<'a> StatefulTable<'a> {
fn new() -> StatefulTable<'a> {
StatefulTable {
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
state: TableState::default(),
items: vec![
vec!["Row11", "Row12", "Row13"],
@@ -27,7 +27,7 @@ impl<'a> StatefulTable<'a> {
vec!["Row31", "Row32", "Row33"],
vec!["Row41", "Row42", "Row43"],
vec!["Row51", "Row52", "Row53"],
vec!["Row61", "Row62", "Row63"],
vec!["Row61", "Row62\nTest", "Row63"],
vec!["Row71", "Row72", "Row73"],
vec!["Row81", "Row82", "Row83"],
vec!["Row91", "Row92", "Row93"],
@@ -74,60 +74,82 @@ impl<'a> StatefulTable<'a> {
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
let mut table = StatefulTable::new();
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
// Input
loop {
terminal.draw(|mut f| {
let rects = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.margin(5)
.split(f.size());
let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::BOLD);
let normal_style = Style::default().fg(Color::White);
let header = ["Header1", "Header2", "Header3"];
let rows = table
.items
.iter()
.map(|i| Row::StyledData(i.iter(), normal_style));
let t = Table::new(header.iter(), rows)
.block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
.highlight_symbol(">> ")
.widths(&[
Constraint::Percentage(50),
Constraint::Length(30),
Constraint::Max(10),
]);
f.render_stateful_widget(t, rects[0], &mut table.state);
})?;
if let Event::Input(key) = events.next()? {
match key {
Key::Char('q') => {
break;
}
Key::Down => {
table.next();
}
Key::Up => {
table.previous();
}
_ => {}
}
};
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &mut app))?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
_ => {}
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let rects = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.margin(5)
.split(f.size());
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
let normal_style = Style::default().bg(Color::Blue);
let header_cells = ["Header1", "Header2", "Header3"]
.iter()
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
let header = Row::new(header_cells)
.style(normal_style)
.height(1)
.bottom_margin(1);
let rows = app.items.iter().map(|item| {
let height = item
.iter()
.map(|content| content.chars().filter(|c| *c == '\n').count())
.max()
.unwrap_or(0)
+ 1;
let cells = item.iter().map(|c| Cell::from(*c));
Row::new(cells).height(height as u16).bottom_margin(1)
});
let t = Table::new(rows)
.header(header)
.block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
.highlight_symbol(">> ")
.widths(&[
Constraint::Percentage(50),
Constraint::Length(30),
Constraint::Min(10),
]);
f.render_stateful_widget(t, rects[0], &mut app.state);
}

View File

@@ -1,79 +1,124 @@
#[allow(dead_code)]
mod util;
use crate::util::{
event::{Event, Events},
TabsState,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, Tabs},
Terminal,
Frame, Terminal,
};
struct App<'a> {
tabs: TabsState<'a>,
pub titles: Vec<&'a str>,
pub index: usize,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
titles: vec!["Tab0", "Tab1", "Tab2", "Tab3"],
index: 0,
}
}
pub fn next(&mut self) {
self.index = (self.index + 1) % self.titles.len();
}
pub fn previous(&mut self) {
if self.index > 0 {
self.index -= 1;
} else {
self.index = self.titles.len() - 1;
}
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
// App
let mut app = App {
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2", "Tab3"]),
};
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
// Main loop
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|mut f| {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(5)
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(size);
terminal.draw(|f| ui(f, &app))?;
let block = Block::default().style(Style::default().bg(Color::White));
f.render_widget(block, size);
let tabs = Tabs::default()
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.titles(&app.tabs.titles)
.select(app.tabs.index)
.style(Style::default().fg(Color::Cyan))
.highlight_style(Style::default().fg(Color::Yellow));
f.render_widget(tabs, chunks[0]);
let inner = match app.tabs.index {
0 => Block::default().title("Inner 0").borders(Borders::ALL),
1 => Block::default().title("Inner 1").borders(Borders::ALL),
2 => Block::default().title("Inner 2").borders(Borders::ALL),
3 => Block::default().title("Inner 3").borders(Borders::ALL),
_ => unreachable!(),
};
f.render_widget(inner, chunks[1]);
})?;
if let Event::Input(input) = events.next()? {
match input {
Key::Char('q') => {
break;
}
Key::Right => app.tabs.next(),
Key::Left => app.tabs.previous(),
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Right => app.next(),
KeyCode::Left => app.previous(),
_ => {}
}
}
}
Ok(())
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(5)
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(size);
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
f.render_widget(block, size);
let titles = app
.titles
.iter()
.map(|t| {
let (first, rest) = t.split_at(1);
Spans::from(vec![
Span::styled(first, Style::default().fg(Color::Yellow)),
Span::styled(rest, Style::default().fg(Color::Green)),
])
})
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.select(app.index)
.style(Style::default().fg(Color::Cyan))
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Black),
);
f.render_widget(tabs, chunks[0]);
let inner = match app.index {
0 => Block::default().title("Inner 0").borders(Borders::ALL),
1 => Block::default().title("Inner 1").borders(Borders::ALL),
2 => Block::default().title("Inner 2").borders(Borders::ALL),
3 => Block::default().title("Inner 3").borders(Borders::ALL),
_ => unreachable!(),
};
f.render_widget(inner, chunks[1]);
}

View File

@@ -1,73 +0,0 @@
mod demo;
#[allow(dead_code)]
mod util;
use crate::{
demo::{ui, App},
util::event::{Config, Event, Events},
};
use argh::FromArgs;
use std::{error::Error, io, time::Duration};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{backend::TermionBackend, Terminal};
/// Termion demo
#[derive(Debug, FromArgs)]
struct Cli {
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let events = Events::with_config(Config {
tick_rate: Duration::from_millis(cli.tick_rate),
..Config::default()
});
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Termion demo", cli.enhanced_graphics);
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
match events.next()? {
Event::Input(key) => match key {
Key::Char(c) => {
app.on_key(c);
}
Key::Up => {
app.on_up();
}
Key::Down => {
app.on_down();
}
Key::Left => {
app.on_left();
}
Key::Right => {
app.on_right();
}
_ => {}
},
Event::Tick => {
app.on_tick();
}
}
if app.should_quit {
break;
}
}
Ok(())
}

View File

@@ -9,24 +9,19 @@
/// * Pressing Backspace erases a character
/// * Pressing Enter pushes the current input in the history of previous
/// messages
#[allow(dead_code)]
mod util;
use crate::util::event::{Event, Events};
use std::{
error::Error,
io::{self, Write},
};
use termion::{
cursor::Goto, event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{
backend::TermionBackend,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, List, Paragraph, Text},
Terminal,
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, Borders, List, ListItem, Paragraph},
Frame, Terminal,
};
use unicode_width::UnicodeWidthStr;
@@ -56,98 +51,142 @@ impl Default for App {
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// Setup event handlers
let mut events = Events::new();
// create app and run it
let app = App::default();
let res = run_app(&mut terminal, app);
// Create default app state
let mut app = App::default();
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
// Draw UI
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
]
.as_ref(),
)
.split(f.size());
terminal.draw(|f| ui(f, &app))?;
let msg = match app.input_mode {
InputMode::Normal => "Press q to exit, e to start editing.",
InputMode::Editing => "Press Esc to stop editing, Enter to record the message",
};
let text = [Text::raw(msg)];
let help_message = Paragraph::new(text.iter());
f.render_widget(help_message, chunks[0]);
let text = [Text::raw(&app.input)];
let input = Paragraph::new(text.iter())
.style(Style::default().fg(Color::Yellow))
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, chunks[1]);
let messages = app
.messages
.iter()
.enumerate()
.map(|(i, m)| Text::raw(format!("{}: {}", i, m)));
let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, chunks[2]);
})?;
// Put the cursor back inside the input box
write!(
terminal.backend_mut(),
"{}",
Goto(4 + app.input.width() as u16, 5)
)?;
// stdout is buffered, flush it to see the effect immediately when hitting backspace
io::stdout().flush().ok();
// Handle input
if let Event::Input(input) = events.next()? {
if let Event::Key(key) = event::read()? {
match app.input_mode {
InputMode::Normal => match input {
Key::Char('e') => {
InputMode::Normal => match key.code {
KeyCode::Char('e') => {
app.input_mode = InputMode::Editing;
events.disable_exit_key();
}
Key::Char('q') => {
break;
KeyCode::Char('q') => {
return Ok(());
}
_ => {}
},
InputMode::Editing => match input {
Key::Char('\n') => {
InputMode::Editing => match key.code {
KeyCode::Enter => {
app.messages.push(app.input.drain(..).collect());
}
Key::Char(c) => {
KeyCode::Char(c) => {
app.input.push(c);
}
Key::Backspace => {
KeyCode::Backspace => {
app.input.pop();
}
Key::Esc => {
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
events.enable_exit_key();
}
_ => {}
},
}
}
}
Ok(())
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
]
.as_ref(),
)
.split(f.size());
let (msg, style) = match app.input_mode {
InputMode::Normal => (
vec![
Span::raw("Press "),
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit, "),
Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to start editing."),
],
Style::default().add_modifier(Modifier::RAPID_BLINK),
),
InputMode::Editing => (
vec![
Span::raw("Press "),
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to stop editing, "),
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to record the message"),
],
Style::default(),
),
};
let mut text = Text::from(Spans::from(msg));
text.patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[0]);
let input = Paragraph::new(app.input.as_ref())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, chunks[1]);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
{}
InputMode::Editing => {
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
f.set_cursor(
// Put cursor past the end of the input text
chunks[1].x + app.input.width() as u16 + 1,
// Move one line down, from the border to the input line
chunks[1].y + 1,
)
}
}
let messages: Vec<ListItem> = app
.messages
.iter()
.enumerate()
.map(|(i, m)| {
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
ListItem::new(content)
})
.collect();
let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, chunks[2]);
}

View File

@@ -1,93 +0,0 @@
use std::io;
use std::sync::mpsc;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::thread;
use std::time::Duration;
use termion::event::Key;
use termion::input::TermRead;
pub enum Event<I> {
Input(I),
Tick,
}
/// A small event handler that wrap termion input and tick events. Each event
/// type is handled in its own thread and returned to a common `Receiver`
pub struct Events {
rx: mpsc::Receiver<Event<Key>>,
input_handle: thread::JoinHandle<()>,
ignore_exit_key: Arc<AtomicBool>,
tick_handle: thread::JoinHandle<()>,
}
#[derive(Debug, Clone, Copy)]
pub struct Config {
pub exit_key: Key,
pub tick_rate: Duration,
}
impl Default for Config {
fn default() -> Config {
Config {
exit_key: Key::Char('q'),
tick_rate: Duration::from_millis(250),
}
}
}
impl Events {
pub fn new() -> Events {
Events::with_config(Config::default())
}
pub fn with_config(config: Config) -> Events {
let (tx, rx) = mpsc::channel();
let ignore_exit_key = Arc::new(AtomicBool::new(false));
let input_handle = {
let tx = tx.clone();
let ignore_exit_key = ignore_exit_key.clone();
thread::spawn(move || {
let stdin = io::stdin();
for evt in stdin.keys() {
if let Ok(key) = evt {
if let Err(err) = tx.send(Event::Input(key)) {
eprintln!("{}", err);
return;
}
if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key {
return;
}
}
}
})
};
let tick_handle = {
thread::spawn(move || loop {
tx.send(Event::Tick).unwrap();
thread::sleep(config.tick_rate);
})
};
Events {
rx,
ignore_exit_key,
input_handle,
tick_handle,
}
}
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
self.rx.recv()
}
pub fn disable_exit_key(&mut self) {
self.ignore_exit_key.store(true, Ordering::Relaxed);
}
pub fn enable_exit_key(&mut self) {
self.ignore_exit_key.store(false, Ordering::Relaxed);
}
}

View File

@@ -1,131 +0,0 @@
#[cfg(feature = "termion")]
pub mod event;
use rand::distributions::{Distribution, Uniform};
use rand::rngs::ThreadRng;
use tui::widgets::ListState;
#[derive(Clone)]
pub struct RandomSignal {
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> RandomSignal {
RandomSignal {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
}
}
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.distribution.sample(&mut self.rng))
}
}
#[derive(Clone)]
pub struct SinSignal {
x: f64,
interval: f64,
period: f64,
scale: f64,
}
impl SinSignal {
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
SinSignal {
x: 0.0,
interval,
period,
scale,
}
}
}
impl Iterator for SinSignal {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
self.x += self.interval;
Some(point)
}
}
pub struct TabsState<'a> {
pub titles: Vec<&'a str>,
pub index: usize,
}
impl<'a> TabsState<'a> {
pub fn new(titles: Vec<&'a str>) -> TabsState {
TabsState { titles, index: 0 }
}
pub fn next(&mut self) {
self.index = (self.index + 1) % self.titles.len();
}
pub fn previous(&mut self) {
if self.index > 0 {
self.index -= 1;
} else {
self.index = self.titles.len() - 1;
}
}
}
pub struct StatefulList<T> {
pub state: ListState,
pub items: Vec<T>,
}
impl<T> StatefulList<T> {
pub fn new() -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items: Vec::new(),
}
}
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items,
}
}
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn unselect(&mut self) {
self.state.select(None);
}
}

View File

@@ -1,8 +1,9 @@
use std::{
fmt,
io::{self, Write},
use crate::{
backend::Backend,
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
};
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
@@ -12,10 +13,7 @@ use crossterm::{
},
terminal::{self, Clear, ClearType},
};
use crate::backend::Backend;
use crate::style::{Color, Modifier};
use crate::{buffer::Cell, layout::Rect, style};
use std::io::{self, Write};
pub struct CrosstermBackend<W: Write> {
buffer: W,
@@ -51,49 +49,40 @@ where
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
use fmt::Write;
let mut string = String::with_capacity(content.size_hint().0 * 3);
let mut style = style::Style::default();
let mut last_y = 0;
let mut last_x = 0;
let mut inst = 0;
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
for (x, y, cell) in content {
if y != last_y || x != last_x + 1 || inst == 0 {
map_error(queue!(string, MoveTo(x, y)))?;
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
map_error(queue!(self.buffer, MoveTo(x, y)))?;
}
last_x = x;
last_y = y;
if cell.style.modifier != style.modifier {
last_pos = Some((x, y));
if cell.modifier != modifier {
let diff = ModifierDiff {
from: style.modifier,
to: cell.style.modifier,
from: modifier,
to: cell.modifier,
};
diff.queue(&mut string)?;
inst += 1;
style.modifier = cell.style.modifier;
diff.queue(&mut self.buffer)?;
modifier = cell.modifier;
}
if cell.style.fg != style.fg {
let color = CColor::from(cell.style.fg);
map_error(queue!(string, SetForegroundColor(color)))?;
style.fg = cell.style.fg;
inst += 1;
if cell.fg != fg {
let color = CColor::from(cell.fg);
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
fg = cell.fg;
}
if cell.style.bg != style.bg {
let color = CColor::from(cell.style.bg);
map_error(queue!(string, SetBackgroundColor(color)))?;
style.bg = cell.style.bg;
inst += 1;
if cell.bg != bg {
let color = CColor::from(cell.bg);
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
bg = cell.bg;
}
string.push_str(&cell.symbol);
inst += 1;
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
}
map_error(queue!(
self.buffer,
Print(string),
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
@@ -169,11 +158,10 @@ struct ModifierDiff {
pub to: Modifier,
}
#[cfg(unix)]
impl ModifierDiff {
fn queue<W>(&self, mut w: W) -> io::Result<()>
where
W: fmt::Write,
W: io::Write,
{
//use crossterm::Attribute;
let removed = self.from - self.to;
@@ -231,28 +219,3 @@ impl ModifierDiff {
Ok(())
}
}
#[cfg(windows)]
impl ModifierDiff {
fn queue<W>(&self, mut w: W) -> io::Result<()>
where
W: fmt::Write,
{
let removed = self.from - self.to;
if removed.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
}
if removed.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
}
let added = self.to - self.from;
if added.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
}
if added.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
}
Ok(())
}
}

View File

@@ -1,283 +0,0 @@
use std::io;
use crate::backend::Backend;
use crate::buffer::Cell;
use crate::layout::Rect;
use crate::style::{Color, Modifier, Style};
use crate::symbols::{bar, block};
#[cfg(unix)]
use crate::symbols::{line, DOT};
#[cfg(unix)]
use pancurses::{chtype, ToChtype};
use unicode_segmentation::UnicodeSegmentation;
pub struct CursesBackend {
curses: easycurses::EasyCurses,
}
impl CursesBackend {
pub fn new() -> Option<CursesBackend> {
let curses = easycurses::EasyCurses::initialize_system()?;
Some(CursesBackend { curses })
}
pub fn with_curses(curses: easycurses::EasyCurses) -> CursesBackend {
CursesBackend { curses }
}
pub fn get_curses(&self) -> &easycurses::EasyCurses {
&self.curses
}
pub fn get_curses_mut(&mut self) -> &mut easycurses::EasyCurses {
&mut self.curses
}
}
impl Backend for CursesBackend {
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
let mut last_col = 0;
let mut last_row = 0;
let mut style = Style {
fg: Color::Reset,
bg: Color::Reset,
modifier: Modifier::empty(),
};
let mut curses_style = CursesStyle {
fg: easycurses::Color::White,
bg: easycurses::Color::Black,
};
let mut update_color = false;
for (col, row, cell) in content {
// eprintln!("{:?}", cell);
if row != last_row || col != last_col + 1 {
self.curses.move_rc(i32::from(row), i32::from(col));
}
last_col = col;
last_row = row;
if cell.style.modifier != style.modifier {
apply_modifier_diff(&mut self.curses.win, style.modifier, cell.style.modifier);
style.modifier = cell.style.modifier;
};
if cell.style.fg != style.fg {
update_color = true;
if let Some(ccolor) = cell.style.fg.into() {
style.fg = cell.style.fg;
curses_style.fg = ccolor;
} else {
style.fg = Color::White;
curses_style.fg = easycurses::Color::White;
}
};
if cell.style.bg != style.bg {
update_color = true;
if let Some(ccolor) = cell.style.bg.into() {
style.bg = cell.style.bg;
curses_style.bg = ccolor;
} else {
style.bg = Color::Black;
curses_style.bg = easycurses::Color::Black;
}
};
if update_color {
self.curses
.set_color_pair(easycurses::ColorPair::new(curses_style.fg, curses_style.bg));
};
update_color = false;
draw(&mut self.curses, cell.symbol.as_str());
}
self.curses.win.attrset(pancurses::Attribute::Normal);
self.curses.set_color_pair(easycurses::ColorPair::new(
easycurses::Color::White,
easycurses::Color::Black,
));
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.curses
.set_cursor_visibility(easycurses::CursorVisibility::Invisible);
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
self.curses
.set_cursor_visibility(easycurses::CursorVisibility::Visible);
Ok(())
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
let (y, x) = self.curses.get_cursor_rc();
Ok((x as u16, y as u16))
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.curses.move_rc(i32::from(y), i32::from(x));
Ok(())
}
fn clear(&mut self) -> io::Result<()> {
self.curses.clear();
// self.curses.refresh();
Ok(())
}
fn size(&self) -> Result<Rect, io::Error> {
let (nrows, ncols) = self.curses.get_row_col_count();
Ok(Rect::new(0, 0, ncols as u16, nrows as u16))
}
fn flush(&mut self) -> io::Result<()> {
self.curses.refresh();
Ok(())
}
}
struct CursesStyle {
fg: easycurses::Color,
bg: easycurses::Color,
}
#[cfg(unix)]
/// Deals with lack of unicode support for ncurses on unix
fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) {
for grapheme in symbol.graphemes(true) {
let ch = match grapheme {
line::TOP_RIGHT => pancurses::ACS_URCORNER(),
line::VERTICAL => pancurses::ACS_VLINE(),
line::HORIZONTAL => pancurses::ACS_HLINE(),
line::TOP_LEFT => pancurses::ACS_ULCORNER(),
line::BOTTOM_RIGHT => pancurses::ACS_LRCORNER(),
line::BOTTOM_LEFT => pancurses::ACS_LLCORNER(),
line::VERTICAL_LEFT => pancurses::ACS_RTEE(),
line::VERTICAL_RIGHT => pancurses::ACS_LTEE(),
line::HORIZONTAL_DOWN => pancurses::ACS_TTEE(),
line::HORIZONTAL_UP => pancurses::ACS_BTEE(),
block::FULL => pancurses::ACS_BLOCK(),
block::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(),
block::THREE_QUARTERS => pancurses::ACS_BLOCK(),
block::FIVE_EIGHTHS => pancurses::ACS_BLOCK(),
block::HALF => pancurses::ACS_BLOCK(),
block::THREE_EIGHTHS => ' ' as chtype,
block::ONE_QUARTER => ' ' as chtype,
block::ONE_EIGHTH => ' ' as chtype,
bar::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(),
bar::THREE_QUARTERS => pancurses::ACS_BLOCK(),
bar::FIVE_EIGHTHS => pancurses::ACS_BLOCK(),
bar::HALF => pancurses::ACS_BLOCK(),
bar::THREE_EIGHTHS => pancurses::ACS_S9(),
bar::ONE_QUARTER => pancurses::ACS_S9(),
bar::ONE_EIGHTH => pancurses::ACS_S9(),
DOT => pancurses::ACS_BULLET(),
unicode_char => {
if unicode_char.is_ascii() {
let mut chars = unicode_char.chars();
if let Some(ch) = chars.next() {
ch.to_chtype()
} else {
pancurses::ACS_BLOCK()
}
} else {
pancurses::ACS_BLOCK()
}
}
};
curses.win.addch(ch);
}
}
#[cfg(windows)]
fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) {
for grapheme in symbol.graphemes(true) {
let ch = match grapheme {
block::SEVEN_EIGHTHS => block::FULL,
block::THREE_QUARTERS => block::FULL,
block::FIVE_EIGHTHS => block::HALF,
block::THREE_EIGHTHS => block::HALF,
block::ONE_QUARTER => block::HALF,
block::ONE_EIGHTH => " ",
bar::SEVEN_EIGHTHS => bar::FULL,
bar::THREE_QUARTERS => bar::FULL,
bar::FIVE_EIGHTHS => bar::HALF,
bar::THREE_EIGHTHS => bar::HALF,
bar::ONE_QUARTER => bar::HALF,
bar::ONE_EIGHTH => " ",
ch => ch,
};
// curses.win.addch(ch);
curses.print(ch);
}
}
impl From<Color> for Option<easycurses::Color> {
fn from(color: Color) -> Option<easycurses::Color> {
match color {
Color::Reset => None,
Color::Black => Some(easycurses::Color::Black),
Color::Red | Color::LightRed => Some(easycurses::Color::Red),
Color::Green | Color::LightGreen => Some(easycurses::Color::Green),
Color::Yellow | Color::LightYellow => Some(easycurses::Color::Yellow),
Color::Magenta | Color::LightMagenta => Some(easycurses::Color::Magenta),
Color::Cyan | Color::LightCyan => Some(easycurses::Color::Cyan),
Color::White | Color::Gray | Color::DarkGray => Some(easycurses::Color::White),
Color::Blue | Color::LightBlue => Some(easycurses::Color::Blue),
Color::Indexed(_) => None,
Color::Rgb(_, _, _) => None,
}
}
}
fn apply_modifier_diff(win: &mut pancurses::Window, from: Modifier, to: Modifier) {
remove_modifier(win, from - to);
add_modifier(win, to - from);
}
fn remove_modifier(win: &mut pancurses::Window, remove: Modifier) {
if remove.contains(Modifier::BOLD) {
win.attroff(pancurses::Attribute::Bold);
}
if remove.contains(Modifier::DIM) {
win.attroff(pancurses::Attribute::Dim);
}
if remove.contains(Modifier::ITALIC) {
win.attroff(pancurses::Attribute::Italic);
}
if remove.contains(Modifier::UNDERLINED) {
win.attroff(pancurses::Attribute::Underline);
}
if remove.contains(Modifier::SLOW_BLINK) || remove.contains(Modifier::RAPID_BLINK) {
win.attroff(pancurses::Attribute::Blink);
}
if remove.contains(Modifier::REVERSED) {
win.attroff(pancurses::Attribute::Reverse);
}
if remove.contains(Modifier::HIDDEN) {
win.attroff(pancurses::Attribute::Invisible);
}
if remove.contains(Modifier::CROSSED_OUT) {
win.attroff(pancurses::Attribute::Strikeout);
}
}
fn add_modifier(win: &mut pancurses::Window, add: Modifier) {
if add.contains(Modifier::BOLD) {
win.attron(pancurses::Attribute::Bold);
}
if add.contains(Modifier::DIM) {
win.attron(pancurses::Attribute::Dim);
}
if add.contains(Modifier::ITALIC) {
win.attron(pancurses::Attribute::Italic);
}
if add.contains(Modifier::UNDERLINED) {
win.attron(pancurses::Attribute::Underline);
}
if add.contains(Modifier::SLOW_BLINK) || add.contains(Modifier::RAPID_BLINK) {
win.attron(pancurses::Attribute::Blink);
}
if add.contains(Modifier::REVERSED) {
win.attron(pancurses::Attribute::Reverse);
}
if add.contains(Modifier::HIDDEN) {
win.attron(pancurses::Attribute::Invisible);
}
if add.contains(Modifier::CROSSED_OUT) {
win.attron(pancurses::Attribute::Strikeout);
}
}

View File

@@ -3,11 +3,6 @@ use std::io;
use crate::buffer::Cell;
use crate::layout::Rect;
#[cfg(feature = "rustbox")]
mod rustbox;
#[cfg(feature = "rustbox")]
pub use self::rustbox::RustboxBackend;
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termion")]
@@ -18,11 +13,6 @@ mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "curses")]
mod curses;
#[cfg(feature = "curses")]
pub use self::curses::CursesBackend;
mod test;
pub use self::test::TestBackend;

View File

@@ -1,123 +0,0 @@
use crate::{
backend::Backend,
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
};
use std::io;
pub struct RustboxBackend {
rustbox: rustbox::RustBox,
}
impl RustboxBackend {
pub fn new() -> Result<RustboxBackend, rustbox::InitError> {
let rustbox = rustbox::RustBox::init(Default::default())?;
Ok(RustboxBackend { rustbox })
}
pub fn with_rustbox(instance: rustbox::RustBox) -> RustboxBackend {
RustboxBackend { rustbox: instance }
}
pub fn rustbox(&self) -> &rustbox::RustBox {
&self.rustbox
}
}
impl Backend for RustboxBackend {
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, cell) in content {
self.rustbox.print(
x as usize,
y as usize,
cell.style.modifier.into(),
cell.style.fg.into(),
cell.style.bg.into(),
&cell.symbol,
);
}
Ok(())
}
fn hide_cursor(&mut self) -> Result<(), io::Error> {
Ok(())
}
fn show_cursor(&mut self) -> Result<(), io::Error> {
Ok(())
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
Err(io::Error::from(io::ErrorKind::Other))
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.rustbox.set_cursor(x as isize, y as isize);
Ok(())
}
fn clear(&mut self) -> Result<(), io::Error> {
self.rustbox.clear();
Ok(())
}
fn size(&self) -> Result<Rect, io::Error> {
let term_width = self.rustbox.width();
let term_height = self.rustbox.height();
let max = u16::max_value();
Ok(Rect::new(
0,
0,
if term_width > usize::from(max) {
max
} else {
term_width as u16
},
if term_height > usize::from(max) {
max
} else {
term_height as u16
},
))
}
fn flush(&mut self) -> Result<(), io::Error> {
self.rustbox.present();
Ok(())
}
}
fn rgb_to_byte(r: u8, g: u8, b: u8) -> u16 {
u16::from((r & 0xC0) + ((g & 0xE0) >> 2) + ((b & 0xE0) >> 5))
}
impl Into<rustbox::Color> for Color {
fn into(self) -> rustbox::Color {
match self {
Color::Reset => rustbox::Color::Default,
Color::Black | Color::Gray | Color::DarkGray => rustbox::Color::Black,
Color::Red | Color::LightRed => rustbox::Color::Red,
Color::Green | Color::LightGreen => rustbox::Color::Green,
Color::Yellow | Color::LightYellow => rustbox::Color::Yellow,
Color::Magenta | Color::LightMagenta => rustbox::Color::Magenta,
Color::Cyan | Color::LightCyan => rustbox::Color::Cyan,
Color::White => rustbox::Color::White,
Color::Blue | Color::LightBlue => rustbox::Color::Blue,
Color::Indexed(i) => rustbox::Color::Byte(u16::from(i)),
Color::Rgb(r, g, b) => rustbox::Color::Byte(rgb_to_byte(r, g, b)),
}
}
}
impl Into<rustbox::Style> for Modifier {
fn into(self) -> rustbox::Style {
let mut result = rustbox::Style::empty();
if self.contains(Modifier::BOLD) {
result.insert(rustbox::RB_BOLD);
}
if self.contains(Modifier::UNDERLINED) {
result.insert(rustbox::RB_UNDERLINE);
}
if self.contains(Modifier::REVERSED) {
result.insert(rustbox::RB_REVERSE);
}
result
}
}

View File

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

View File

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

View File

@@ -1,18 +1,19 @@
use crate::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Span, Spans},
};
use std::cmp::min;
use std::fmt;
use std::usize;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::layout::Rect;
use crate::style::{Color, Modifier, Style};
/// A buffer cell
#[derive(Debug, Clone, PartialEq)]
pub struct Cell {
pub symbol: String,
pub style: Style,
pub fg: Color,
pub bg: Color,
pub modifier: Modifier,
}
impl Cell {
@@ -29,29 +30,40 @@ impl Cell {
}
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
self.style.fg = color;
self.fg = color;
self
}
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
self.style.bg = color;
self
}
pub fn set_modifier(&mut self, modifier: Modifier) -> &mut Cell {
self.style.modifier = modifier;
self.bg = color;
self
}
pub fn set_style(&mut self, style: Style) -> &mut Cell {
self.style = style;
if let Some(c) = style.fg {
self.fg = c;
}
if let Some(c) = style.bg {
self.bg = c;
}
self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
self
}
pub fn style(&self) -> Style {
Style::default()
.fg(self.fg)
.bg(self.bg)
.add_modifier(self.modifier)
}
pub fn reset(&mut self) {
self.symbol.clear();
self.symbol.push(' ');
self.style.reset();
self.fg = Color::Reset;
self.bg = Color::Reset;
self.modifier = Modifier::empty();
}
}
@@ -59,7 +71,9 @@ impl Default for Cell {
fn default() -> Cell {
Cell {
symbol: " ".into(),
style: Default::default(),
fg: Color::Reset,
bg: Color::Reset,
modifier: Modifier::empty(),
}
}
}
@@ -84,15 +98,14 @@ impl Default for Cell {
/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
/// assert_eq!(buf.get(5, 0), &Cell{
/// symbol: String::from("r"),
/// style: Style {
/// fg: Color::Red,
/// bg: Color::White,
/// modifier: Modifier::empty()
/// }});
/// fg: Color::Red,
/// bg: Color::White,
/// modifier: Modifier::empty()
/// });
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol, "x");
/// ```
#[derive(Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
@@ -101,58 +114,6 @@ pub struct Buffer {
pub content: Vec<Cell>,
}
impl Default for Buffer {
fn default() -> Buffer {
Buffer {
area: Default::default(),
content: Vec::new(),
}
}
}
impl fmt::Debug for Buffer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Buffer: {:?}", self.area)?;
f.write_str("Content (quoted lines):\n")?;
for cells in self.content.chunks(self.area.width as usize) {
let mut line = String::new();
let mut overwritten = vec![];
let mut skip: usize = 0;
for (x, c) in cells.iter().enumerate() {
if skip == 0 {
line.push_str(&c.symbol);
} else {
overwritten.push((x, &c.symbol))
}
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
}
f.write_fmt(format_args!("{:?},", line))?;
if !overwritten.is_empty() {
f.write_fmt(format_args!(
" Hidden by multi-width symbols: {:?}",
overwritten
))?;
}
f.write_str("\n")?;
}
f.write_str("Style:\n")?;
for cells in self.content.chunks(self.area.width as usize) {
f.write_str("|")?;
for cell in cells {
write!(
f,
"{} {} {}|",
cell.style.fg.code(),
cell.style.bg.code(),
cell.style.modifier.code()
)?;
}
f.write_str("\n")?;
}
Ok(())
}
}
impl Buffer {
/// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer {
@@ -176,9 +137,11 @@ impl Buffer {
S: AsRef<str>,
{
let height = lines.len() as u16;
let width = lines.iter().fold(0, |acc, item| {
std::cmp::max(acc, item.as_ref().width() as u16)
});
let width = lines
.iter()
.map(|i| i.as_ref().width() as u16)
.max()
.unwrap_or_default();
let mut buffer = Buffer::empty(Rect {
x: 0,
y: 0,
@@ -343,6 +306,35 @@ impl Buffer {
(x_offset as u16, y)
}
pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &spans.0 {
if remaining_width == 0 {
break;
}
let pos = self.set_stringn(
x,
y,
span.content.as_ref(),
remaining_width as usize,
span.style,
);
let w = pos.0.saturating_sub(x);
x = pos.0;
remaining_width = remaining_width.saturating_sub(w);
}
(x, y)
}
pub fn set_span<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) {
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
}
#[deprecated(
since = "0.10.0",
note = "You should use styling capabilities of `Buffer::set_style`"
)]
pub fn set_background(&mut self, area: Rect, color: Color) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
@@ -351,6 +343,14 @@ impl Buffer {
}
}
pub fn set_style(&mut self, area: Rect, style: Style) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
self.get_mut(x, y).set_style(style);
}
}
}
/// Resize the buffer so that the mapped area matches the given area and that the buffer
/// length is equal to area.width * area.height
pub fn resize(&mut self, area: Rect) {

View File

@@ -63,6 +63,9 @@ pub struct Layout {
direction: Direction,
margin: Margin,
constraints: Vec<Constraint>,
/// Whether the last chunk of the computed layout should be expanded to fill the available
/// space.
expand_to_fill: bool,
}
thread_local! {
@@ -78,6 +81,7 @@ impl Default for Layout {
vertical: 0,
},
constraints: Vec::new(),
expand_to_fill: true,
}
}
}
@@ -114,6 +118,11 @@ impl Layout {
self
}
pub(crate) fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
self.expand_to_fill = expand_to_fill;
self
}
/// Wrapper function around the cassowary-rs solver to be able to split a given
/// area into smaller ones based on the preferred widths or heights and the direction.
///
@@ -174,12 +183,12 @@ impl Layout {
/// ]
/// );
/// ```
pub fn split(self, area: Rect) -> Vec<Rect> {
pub fn split(&self, area: Rect) -> Vec<Rect> {
// TODO: Maybe use a fixed size cache ?
LAYOUT_CACHE.with(|c| {
c.borrow_mut()
.entry((area, self.clone()))
.or_insert_with(|| split(area, &self))
.or_insert_with(|| split(area, self))
.clone()
})
}
@@ -209,6 +218,8 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
let mut ccs: Vec<CassowaryConstraint> =
Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6);
for elt in &elements {
ccs.push(elt.width | GE(REQUIRED) | 0f64);
ccs.push(elt.height | GE(REQUIRED) | 0f64);
ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
@@ -220,11 +231,13 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
});
}
if let Some(last) = elements.last() {
ccs.push(match layout.direction {
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
});
if layout.expand_to_fill {
if let Some(last) = elements.last() {
ccs.push(match layout.direction {
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
});
}
}
match layout.direction {
Direction::Horizontal => {
@@ -297,14 +310,16 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
}
}
// Fix imprecision by extending the last item a bit if necessary
if let Some(last) = results.last_mut() {
match layout.direction {
Direction::Vertical => {
last.height = dest_area.bottom() - last.y;
}
Direction::Horizontal => {
last.width = dest_area.right() - last.x;
if layout.expand_to_fill {
// Fix imprecision by extending the last item a bit if necessary
if let Some(last) = results.last_mut() {
match layout.direction {
Direction::Vertical => {
last.height = dest_area.bottom() - last.y;
}
Direction::Horizontal => {
last.width = dest_area.right() - last.x;
}
}
}
}
@@ -348,7 +363,7 @@ impl Element {
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
/// area they are supposed to render to.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
pub struct Rect {
pub x: u16,
pub y: u16,
@@ -356,17 +371,6 @@ pub struct Rect {
pub height: u16,
}
impl Default for Rect {
fn default() -> Rect {
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}
}
}
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16.
/// If clipped, aspect ratio will be preserved.
@@ -399,7 +403,7 @@ impl Rect {
}
pub fn right(self) -> u16 {
self.x + self.width
self.x.saturating_add(self.width)
}
pub fn top(self) -> u16 {
@@ -407,7 +411,7 @@ impl Rect {
}
pub fn bottom(self) -> u16 {
self.y + self.height
self.y.saturating_add(self.height)
}
pub fn inner(self, margin: &Margin) -> Rect {
@@ -461,6 +465,31 @@ impl Rect {
mod tests {
use super::*;
#[test]
fn test_vertical_split_by_height() {
let target = Rect {
x: 2,
y: 2,
width: 10,
height: 10,
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Max(5),
Constraint::Min(1),
]
.as_ref(),
)
.split(target);
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
}
#[test]
fn test_rect_size_truncation() {
for width in 256u16..300u16 {

View File

@@ -9,18 +9,19 @@
//!
//! ```toml
//! [dependencies]
//! tui = "0.9"
//! termion = "1.5"
//! tui = "0.18"
//! crossterm = "0.23"
//! ```
//!
//! The crate is using the `termion` backend by default but if for example you want your
//! application to work on Windows, you might want to use the `crossterm` backend instead. This can
//! be done by changing your dependencies specification to the following:
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
//! example you want to use the `termion` backend instead. This can be done by changing your
//! dependencies specification to the following:
//!
//! ```toml
//! [dependencies]
//! crossterm = "0.17"
//! tui = { version = "0.9", default-features = false, features = ['crossterm'] }
//! termion = "1.5"
//! tui = { version = "0.18", default-features = false, features = ['termion'] }
//!
//! ```
//!
//! The same logic applies for all other available backends.
@@ -33,29 +34,27 @@
//!
//! ```rust,no_run
//! use std::io;
//! use tui::Terminal;
//! use tui::backend::TermionBackend;
//! use termion::raw::IntoRawMode;
//! use tui::{backend::CrosstermBackend, Terminal};
//!
//! fn main() -> Result<(), io::Error> {
//! let stdout = io::stdout().into_raw_mode()?;
//! let backend = TermionBackend::new(stdout);
//! let stdout = io::stdout();
//! let backend = CrosstermBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! Ok(())
//! }
//! ```
//!
//! If you had previously chosen `crossterm` as a backend, the terminal can be created in a similar
//! If you had previously chosen `termion` as a backend, the terminal can be created in a similar
//! way:
//!
//! ```rust,ignore
//! use std::io;
//! use tui::Terminal;
//! use tui::backend::CrosstermBackend;
//! use tui::{backend::TermionBackend, Terminal};
//! use termion::raw::IntoRawMode;
//!
//! fn main() -> Result<(), io::Error> {
//! let stdout = io::stdout();
//! let backend = CrosstermBackend::new(stdout);
//! let stdout = io::stdout().into_raw_mode()?;
//! let backend = TermionBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! Ok(())
//! }
@@ -71,30 +70,53 @@
//! implement your own.
//!
//! Each widget follows a builder pattern API providing a default configuration along with methods
//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take
//! your widget instance an area to draw to.
//! to customize them. The widget is then rendered using [`Frame::render_widget`] which takes
//! your widget instance and an area to draw to.
//!
//! The following example renders a block of the size of the terminal:
//!
//! ```rust,no_run
//! use std::io;
//! use termion::raw::IntoRawMode;
//! use tui::Terminal;
//! use tui::backend::TermionBackend;
//! use tui::widgets::{Widget, Block, Borders};
//! use tui::layout::{Layout, Constraint, Direction};
//! use std::{io, thread, time::Duration};
//! use tui::{
//! backend::CrosstermBackend,
//! widgets::{Widget, Block, Borders},
//! layout::{Layout, Constraint, Direction},
//! Terminal
//! };
//! use crossterm::{
//! event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
//! execute,
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
//! };
//!
//! fn main() -> Result<(), io::Error> {
//! let stdout = io::stdout().into_raw_mode()?;
//! let backend = TermionBackend::new(stdout);
//! // setup terminal
//! enable_raw_mode()?;
//! let mut stdout = io::stdout();
//! execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
//! let backend = CrosstermBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! terminal.draw(|mut f| {
//!
//! terminal.draw(|f| {
//! let size = f.size();
//! let block = Block::default()
//! .title("Block")
//! .borders(Borders::ALL);
//! f.render_widget(block, size);
//! })
//! })?;
//!
//! thread::sleep(Duration::from_millis(5000));
//!
//! // restore terminal
//! disable_raw_mode()?;
//! execute!(
//! terminal.backend_mut(),
//! LeaveAlternateScreen,
//! DisableMouseCapture
//! )?;
//! terminal.show_cursor()?;
//!
//! Ok(())
//! }
//! ```
//!
@@ -105,38 +127,32 @@
//! full customization. And `Layout` is no exception:
//!
//! ```rust,no_run
//! use std::io;
//! use termion::raw::IntoRawMode;
//! use tui::Terminal;
//! use tui::backend::TermionBackend;
//! use tui::widgets::{Widget, Block, Borders};
//! use tui::layout::{Layout, Constraint, Direction};
//!
//! fn main() -> Result<(), io::Error> {
//! let stdout = io::stdout().into_raw_mode()?;
//! let backend = TermionBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! terminal.draw(|mut f| {
//! let chunks = Layout::default()
//! .direction(Direction::Vertical)
//! .margin(1)
//! .constraints(
//! [
//! Constraint::Percentage(10),
//! Constraint::Percentage(80),
//! Constraint::Percentage(10)
//! ].as_ref()
//! )
//! .split(f.size());
//! let block = Block::default()
//! .title("Block")
//! .borders(Borders::ALL);
//! f.render_widget(block, chunks[0]);
//! let block = Block::default()
//! .title("Block 2")
//! .borders(Borders::ALL);
//! f.render_widget(block, chunks[1]);
//! })
//! use tui::{
//! backend::Backend,
//! layout::{Constraint, Direction, Layout},
//! widgets::{Block, Borders},
//! Frame,
//! };
//! fn ui<B: Backend>(f: &mut Frame<B>) {
//! let chunks = Layout::default()
//! .direction(Direction::Vertical)
//! .margin(1)
//! .constraints(
//! [
//! Constraint::Percentage(10),
//! Constraint::Percentage(80),
//! Constraint::Percentage(10)
//! ].as_ref()
//! )
//! .split(f.size());
//! let block = Block::default()
//! .title("Block")
//! .borders(Borders::ALL);
//! f.render_widget(block, chunks[0]);
//! let block = Block::default()
//! .title("Block 2")
//! .borders(Borders::ALL);
//! f.render_widget(block, chunks[1]);
//! }
//! ```
//!
@@ -151,6 +167,7 @@ pub mod layout;
pub mod style;
pub mod symbols;
pub mod terminal;
pub mod text;
pub mod widgets;
pub use self::terminal::{Frame, Terminal};
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};

View File

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

View File

@@ -224,8 +224,10 @@ pub mod braille {
/// Marker to use when plotting data points
#[derive(Debug, Clone, Copy)]
pub enum Marker {
/// One point per cell
/// One point per cell in shape of dot
Dot,
/// One point per cell in shape of a block
Block,
/// Up to 8 points per cell
Braille,
}

View File

@@ -1,9 +1,41 @@
use crate::{
backend::Backend,
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
};
use std::io;
use crate::backend::Backend;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::widgets::{StatefulWidget, Widget};
#[derive(Debug, Clone, PartialEq)]
/// UNSTABLE
enum ResizeBehavior {
Fixed,
Auto,
}
#[derive(Debug, Clone, PartialEq)]
/// UNSTABLE
pub struct Viewport {
area: Rect,
resize_behavior: ResizeBehavior,
}
impl Viewport {
/// UNSTABLE
pub fn fixed(area: Rect) -> Viewport {
Viewport {
area,
resize_behavior: ResizeBehavior::Fixed,
}
}
}
#[derive(Debug, Clone, PartialEq)]
/// Options to pass to [`Terminal::with_options`]
pub struct TerminalOptions {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
/// Interface to the terminal backed by Termion
#[derive(Debug)]
@@ -19,8 +51,8 @@ where
current: usize,
/// Whether the cursor is currently hidden
hidden_cursor: bool,
/// Terminal size used for rendering.
known_size: Rect,
/// Viewport
viewport: Viewport,
}
/// Represents a consistent terminal interface for rendering.
@@ -29,6 +61,12 @@ where
B: Backend,
{
terminal: &'a mut Terminal<B>,
/// Where should the cursor be after drawing this frame?
///
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
cursor_position: Option<(u16, u16)>,
}
impl<'a, B> Frame<'a, B>
@@ -37,21 +75,19 @@ where
{
/// Terminal size, guaranteed not to change when rendering.
pub fn size(&self) -> Rect {
self.terminal.known_size
self.terminal.viewport.area
}
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
///
/// # Examples
///
/// ```rust,no_run
/// # use std::io;
/// ```rust
/// # use tui::Terminal;
/// # use tui::backend::TermionBackend;
/// # use tui::backend::TestBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::Block;
/// # let stdout = io::stdout();
/// # let backend = TermionBackend::new(stdout);
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let block = Block::default();
/// let area = Rect::new(0, 0, 5, 5);
@@ -72,19 +108,20 @@ where
///
/// # Examples
///
/// ```rust,no_run
/// # use std::io;
/// ```rust
/// # use tui::Terminal;
/// # use tui::backend::TermionBackend;
/// # use tui::backend::TestBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::{List, ListState, Text};
/// # let stdout = io::stdout();
/// # let backend = TermionBackend::new(stdout);
/// # use tui::widgets::{List, ListItem, ListState};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let mut state = ListState::default();
/// state.select(Some(1));
/// let items = vec![Text::raw("Item 1"), Text::raw("Item 2")];
/// let list = List::new(items.into_iter());
/// let items = vec![
/// ListItem::new("Item 1"),
/// ListItem::new("Item 2"),
/// ];
/// let list = List::new(items);
/// let area = Rect::new(0, 0, 5, 5);
/// let mut frame = terminal.get_frame();
/// frame.render_stateful_widget(list, area, &mut state);
@@ -95,6 +132,24 @@ where
{
widget.render(area, self.terminal.current_buffer_mut(), state);
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
/// with it.
pub fn set_cursor(&mut self, x: u16, y: u16) {
self.cursor_position = Some((x, y));
}
}
/// CompletedFrame represents the state of the terminal after all changes performed in the last
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
/// [`Terminal::draw`].
pub struct CompletedFrame<'a> {
pub buffer: &'a Buffer,
pub area: Rect,
}
impl<B> Drop for Terminal<B>
@@ -115,22 +170,41 @@ impl<B> Terminal<B>
where
B: Backend,
{
/// Wrapper around Termion initialization. Each buffer is initialized with a blank string and
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
/// default colors for the foreground and the background
pub fn new(backend: B) -> io::Result<Terminal<B>> {
let size = backend.size()?;
Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport {
area: size,
resize_behavior: ResizeBehavior::Auto,
},
},
)
}
/// UNSTABLE
pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
Ok(Terminal {
backend,
buffers: [Buffer::empty(size), Buffer::empty(size)],
buffers: [
Buffer::empty(options.viewport.area),
Buffer::empty(options.viewport.area),
],
current: 0,
hidden_cursor: false,
known_size: size,
viewport: options.viewport,
})
}
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
pub fn get_frame(&mut self) -> Frame<B> {
Frame { terminal: self }
Frame {
terminal: self,
cursor_position: None,
}
}
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
@@ -159,43 +233,60 @@ where
/// This leads to a full clear of the screen.
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].reset();
self.buffers[1 - self.current].resize(area);
self.known_size = area;
self.backend.clear()
self.viewport.area = area;
self.clear()
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
let size = self.size()?;
if self.known_size != size {
self.resize(size)?;
}
if self.viewport.resize_behavior == ResizeBehavior::Auto {
let size = self.size()?;
if size != self.viewport.area {
self.resize(size)?;
}
};
Ok(())
}
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
/// and prepares for the next draw call.
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
where
F: FnOnce(Frame<B>),
F: FnOnce(&mut Frame<B>),
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;
f(self.get_frame());
let mut frame = self.get_frame();
f(&mut frame);
// We can't change the cursor position right away because we have to flush the frame to
// stdout first. But we also can't keep the frame around, since it holds a &mut to
// Terminal. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;
// Draw to stdout
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some((x, y)) => {
self.show_cursor()?;
self.set_cursor(x, y)?;
}
}
// Swap buffers
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
// Flush
self.backend.flush()?;
Ok(())
Ok(CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.viewport.area,
})
}
pub fn hide_cursor(&mut self) -> io::Result<()> {
@@ -203,20 +294,29 @@ where
self.hidden_cursor = true;
Ok(())
}
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
}
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
self.backend.clear()
self.backend.clear()?;
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
}
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Rect> {
self.backend.size()

428
src/text.rs Normal file
View File

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

View File

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

View File

@@ -1,10 +1,13 @@
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::symbols::line;
use crate::widgets::{Borders, Widget};
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::Style,
symbols::line,
text::{Span, Spans},
widgets::{Borders, Widget},
};
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BorderType {
Plain,
Rounded,
@@ -33,18 +36,18 @@ impl BorderType {
/// # use tui::style::{Style, Color};
/// Block::default()
/// .title("Block")
/// .title_style(Style::default().fg(Color::Red))
/// .borders(Borders::LEFT | Borders::RIGHT)
/// .border_style(Style::default().fg(Color::White))
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// ```
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, PartialEq)]
pub struct Block<'a> {
/// Optional title place on the upper left of the block
title: Option<&'a str>,
/// Title style
title_style: Style,
title: Option<Spans<'a>>,
/// Title alignment. The default is top left of the block, but one can choose to place
/// title in the top middle, or top right of the block
title_alignment: Alignment,
/// Visible borders
borders: Borders,
/// Border style
@@ -60,7 +63,7 @@ impl<'a> Default for Block<'a> {
fn default() -> Block<'a> {
Block {
title: None,
title_style: Default::default(),
title_alignment: Alignment::Left,
borders: Borders::NONE,
border_style: Default::default(),
border_type: BorderType::Plain,
@@ -70,13 +73,28 @@ impl<'a> Default for Block<'a> {
}
impl<'a> Block<'a> {
pub fn title(mut self, title: &'a str) -> Block<'a> {
self.title = Some(title);
pub fn title<T>(mut self, title: T) -> Block<'a>
where
T: Into<Spans<'a>>,
{
self.title = Some(title.into());
self
}
#[deprecated(
since = "0.10.0",
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
)]
pub fn title_style(mut self, style: Style) -> Block<'a> {
self.title_style = style;
if let Some(t) = self.title {
let title = String::from(t);
self.title = Some(Spans::from(Span::styled(title, style)));
}
self
}
pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
self.title_alignment = alignment;
self
}
@@ -102,23 +120,20 @@ impl<'a> Block<'a> {
/// Compute the inner area of a block based on its border visibility rules.
pub fn inner(&self, area: Rect) -> Rect {
if area.width < 2 || area.height < 2 {
return Rect::default();
}
let mut inner = area;
if self.borders.intersects(Borders::LEFT) {
inner.x += 1;
inner.width -= 1;
inner.x = inner.x.saturating_add(1).min(inner.right());
inner.width = inner.width.saturating_sub(1);
}
if self.borders.intersects(Borders::TOP) || self.title.is_some() {
inner.y += 1;
inner.height -= 1;
inner.y = inner.y.saturating_add(1).min(inner.bottom());
inner.height = inner.height.saturating_sub(1);
}
if self.borders.intersects(Borders::RIGHT) {
inner.width -= 1;
inner.width = inner.width.saturating_sub(1);
}
if self.borders.intersects(Borders::BOTTOM) {
inner.height -= 1;
inner.height = inner.height.saturating_sub(1);
}
inner
}
@@ -126,13 +141,12 @@ impl<'a> Block<'a> {
impl<'a> Widget for Block<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width < 2 || area.height < 2 {
if area.area() == 0 {
return;
}
buf.set_background(area, self.style.bg);
buf.set_style(area, self.style);
let symbols = BorderType::line_symbols(self.border_type);
// Sides
if self.borders.intersects(Borders::LEFT) {
for y in area.top()..area.bottom() {
@@ -166,9 +180,9 @@ impl<'a> Widget for Block<'a> {
}
// Corners
if self.borders.contains(Borders::LEFT | Borders::TOP) {
buf.get_mut(area.left(), area.top())
.set_symbol(symbols.top_left)
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
buf.get_mut(area.right() - 1, area.bottom() - 1)
.set_symbol(symbols.bottom_right)
.set_style(self.border_style);
}
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
@@ -181,31 +195,379 @@ impl<'a> Widget for Block<'a> {
.set_symbol(symbols.bottom_left)
.set_style(self.border_style);
}
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
buf.get_mut(area.right() - 1, area.bottom() - 1)
.set_symbol(symbols.bottom_right)
if self.borders.contains(Borders::LEFT | Borders::TOP) {
buf.get_mut(area.left(), area.top())
.set_symbol(symbols.top_left)
.set_style(self.border_style);
}
// Title
if let Some(title) = self.title {
let lx = if self.borders.intersects(Borders::LEFT) {
let left_border_dx = if self.borders.intersects(Borders::LEFT) {
1
} else {
0
};
let rx = if self.borders.intersects(Borders::RIGHT) {
let right_border_dx = if self.borders.intersects(Borders::RIGHT) {
1
} else {
0
};
let width = area.width - lx - rx;
buf.set_stringn(
area.left() + lx,
area.top(),
title,
width as usize,
self.title_style,
);
let title_area_width = area
.width
.saturating_sub(left_border_dx)
.saturating_sub(right_border_dx);
let title_dx = match self.title_alignment {
Alignment::Left => left_border_dx,
Alignment::Center => area.width.saturating_sub(title.width() as u16) / 2,
Alignment::Right => area
.width
.saturating_sub(title.width() as u16)
.saturating_sub(right_border_dx),
};
let title_x = area.left() + title_dx;
let title_y = area.top();
buf.set_spans(title_x, title_y, &title, title_area_width);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Rect;
#[test]
fn inner_takes_into_account_the_borders() {
// No borders
assert_eq!(
Block::default().inner(Rect::default()),
Rect {
x: 0,
y: 0,
width: 0,
height: 0
},
"no borders, width=0, height=0"
);
assert_eq!(
Block::default().inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 1
},
"no borders, width=1, height=1"
);
// Left border
assert_eq!(
Block::default().borders(Borders::LEFT).inner(Rect {
x: 0,
y: 0,
width: 0,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 0,
height: 1
},
"left, width=0"
);
assert_eq!(
Block::default().borders(Borders::LEFT).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 1,
y: 0,
width: 0,
height: 1
},
"left, width=1"
);
assert_eq!(
Block::default().borders(Borders::LEFT).inner(Rect {
x: 0,
y: 0,
width: 2,
height: 1
}),
Rect {
x: 1,
y: 0,
width: 1,
height: 1
},
"left, width=2"
);
// Top border
assert_eq!(
Block::default().borders(Borders::TOP).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 0
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 0
},
"top, height=0"
);
assert_eq!(
Block::default().borders(Borders::TOP).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 0,
y: 1,
width: 1,
height: 0
},
"top, height=1"
);
assert_eq!(
Block::default().borders(Borders::TOP).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 2
}),
Rect {
x: 0,
y: 1,
width: 1,
height: 1
},
"top, height=2"
);
// Right border
assert_eq!(
Block::default().borders(Borders::RIGHT).inner(Rect {
x: 0,
y: 0,
width: 0,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 0,
height: 1
},
"right, width=0"
);
assert_eq!(
Block::default().borders(Borders::RIGHT).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 0,
height: 1
},
"right, width=1"
);
assert_eq!(
Block::default().borders(Borders::RIGHT).inner(Rect {
x: 0,
y: 0,
width: 2,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 1
},
"right, width=2"
);
// Bottom border
assert_eq!(
Block::default().borders(Borders::BOTTOM).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 0
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 0
},
"bottom, height=0"
);
assert_eq!(
Block::default().borders(Borders::BOTTOM).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 0
},
"bottom, height=1"
);
assert_eq!(
Block::default().borders(Borders::BOTTOM).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 2
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 1
},
"bottom, height=2"
);
// All borders
assert_eq!(
Block::default()
.borders(Borders::ALL)
.inner(Rect::default()),
Rect {
x: 0,
y: 0,
width: 0,
height: 0
},
"all borders, width=0, height=0"
);
assert_eq!(
Block::default().borders(Borders::ALL).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 1,
y: 1,
width: 0,
height: 0,
},
"all borders, width=1, height=1"
);
assert_eq!(
Block::default().borders(Borders::ALL).inner(Rect {
x: 0,
y: 0,
width: 2,
height: 2,
}),
Rect {
x: 1,
y: 1,
width: 0,
height: 0,
},
"all borders, width=2, height=2"
);
assert_eq!(
Block::default().borders(Borders::ALL).inner(Rect {
x: 0,
y: 0,
width: 3,
height: 3,
}),
Rect {
x: 1,
y: 1,
width: 1,
height: 1,
},
"all borders, width=3, height=3"
);
}
#[test]
fn inner_takes_into_account_the_title() {
assert_eq!(
Block::default().title("Test").inner(Rect {
x: 0,
y: 0,
width: 0,
height: 1,
}),
Rect {
x: 0,
y: 1,
width: 0,
height: 0,
},
);
assert_eq!(
Block::default()
.title("Test")
.title_alignment(Alignment::Center)
.inner(Rect {
x: 0,
y: 0,
width: 0,
height: 1,
}),
Rect {
x: 0,
y: 1,
width: 0,
height: 0,
},
);
assert_eq!(
Block::default()
.title("Test")
.title_alignment(Alignment::Right)
.inner(Rect {
x: 0,
y: 0,
width: 0,
height: 1,
}),
Rect {
x: 0,
y: 1,
width: 0,
height: 0,
},
);
}
}

View File

@@ -14,6 +14,7 @@ use crate::{
layout::Rect,
style::{Color, Style},
symbols,
text::Spans,
widgets::{Block, Widget},
};
use std::fmt::Debug;
@@ -26,10 +27,9 @@ pub trait Shape {
/// Label to draw some text on the canvas
#[derive(Debug, Clone)]
pub struct Label<'a> {
pub x: f64,
pub y: f64,
pub text: &'a str,
pub color: Color,
x: f64,
y: f64,
spans: Spans<'a>,
}
#[derive(Debug, Clone)]
@@ -111,26 +111,28 @@ impl Grid for BrailleGrid {
}
#[derive(Debug, Clone)]
struct DotGrid {
struct CharGrid {
width: u16,
height: u16,
cells: Vec<char>,
colors: Vec<Color>,
cell_char: char,
}
impl DotGrid {
fn new(width: u16, height: u16) -> DotGrid {
impl CharGrid {
fn new(width: u16, height: u16, cell_char: char) -> CharGrid {
let length = usize::from(width * height);
DotGrid {
CharGrid {
width,
height,
cells: vec![' '; length],
colors: vec![Color::Reset; length],
cell_char,
}
}
}
impl Grid for DotGrid {
impl Grid for CharGrid {
fn width(&self) -> u16 {
self.width
}
@@ -162,7 +164,7 @@ impl Grid for DotGrid {
fn paint(&mut self, x: usize, y: usize, color: Color) {
let index = y * self.width as usize + x;
if let Some(c) = self.cells.get_mut(index) {
*c = '•';
*c = self.cell_char;
}
if let Some(c) = self.colors.get_mut(index) {
*c = color;
@@ -206,6 +208,9 @@ impl<'a, 'b> Painter<'a, 'b> {
}
let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs();
let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs();
if width == 0.0 || height == 0.0 {
return None;
}
let x = ((x - left) * self.resolution.0 / width) as usize;
let y = ((top - y) * self.resolution.1 / height) as usize;
Some((x, y))
@@ -256,7 +261,8 @@ impl<'a> Context<'a> {
marker: symbols::Marker,
) -> Context<'a> {
let grid: Box<dyn Grid> = match marker {
symbols::Marker::Dot => Box::new(DotGrid::new(width, height)),
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, '•')),
symbols::Marker::Block => Box::new(CharGrid::new(width, height, '▄')),
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
};
Context {
@@ -287,8 +293,15 @@ impl<'a> Context<'a> {
}
/// Print a string on the canvas at the given position
pub fn print(&mut self, x: f64, y: f64, text: &'a str, color: Color) {
self.labels.push(Label { x, y, text, color });
pub fn print<T>(&mut self, x: f64, y: f64, spans: T)
where
T: Into<Spans<'a>>,
{
self.labels.push(Label {
x,
y,
spans: spans.into(),
});
}
/// Push the last layer if necessary
@@ -393,8 +406,8 @@ where
}
/// Change the type of points used to draw the shapes. By default the braille patterns are used
/// as they provide a more fine grained result but you might want to use the simple dot instead
/// if the targeted terminal does not support those symbols.
/// as they provide a more fine grained result but you might want to use the simple dot or
/// block instead if the targeted terminal does not support those symbols.
///
/// # Examples
///
@@ -404,6 +417,8 @@ where
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
///
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
///
/// Canvas::default().marker(symbols::Marker::Block).paint(|ctx| {});
/// ```
pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> {
self.marker = marker;
@@ -416,14 +431,17 @@ where
F: Fn(&mut Context),
{
fn render(mut self, area: Rect, buf: &mut Buffer) {
let canvas_area = match self.block {
Some(ref mut b) => {
let canvas_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
buf.set_style(canvas_area, Style::default().bg(self.background_color));
let width = canvas_area.width as usize;
let painter = match self.painter {
@@ -455,14 +473,12 @@ where
let (x, y) = (i % width, i / width);
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
.set_char(ch)
.set_fg(color)
.set_bg(self.background_color);
.set_fg(color);
}
}
}
// Finally draw the labels
let style = Style::default().bg(self.background_color);
let left = self.x_bounds[0];
let right = self.x_bounds[1];
let top = self.y_bounds[1];
@@ -481,13 +497,7 @@ where
{
let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
buf.set_stringn(
x,
y,
label.text,
(canvas_area.right() - x) as usize,
style.fg(label.color),
);
buf.set_spans(x, y, &label.spans, canvas_area.right() - x);
}
}
}

View File

@@ -6122,6 +6122,7 @@ pub static WORLD_LOW_RESOLUTION: [(f64, f64); 1166] = [
(120.43, 16.43),
(121.72, 18.40),
(125.34, 9.79),
#[allow(clippy::approx_constant)]
(125.56, 6.28),
(122.38, 7.00),
(125.10, 9.38),

View File

@@ -1,83 +1,89 @@
use std::{borrow::Cow, cmp::max};
use unicode_width::UnicodeWidthStr;
use crate::layout::Alignment;
use crate::{
buffer::Buffer,
layout::{Constraint, Rect},
style::Style,
style::{Color, Style},
symbols,
text::{Span, Spans},
widgets::{
canvas::{Canvas, Line, Points},
Block, Borders, Widget,
},
};
use std::{borrow::Cow, cmp::max};
use unicode_width::UnicodeWidthStr;
/// An X or Y axis for the chart widget
#[derive(Debug, Clone)]
pub struct Axis<'a, L>
where
L: AsRef<str> + 'a,
{
pub struct Axis<'a> {
/// Title displayed next to axis end
title: Option<&'a str>,
/// Style of the title
title_style: Style,
title: Option<Spans<'a>>,
/// Bounds for the axis (all data points outside these limits will not be represented)
bounds: [f64; 2],
/// A list of labels to put to the left or below the axis
labels: Option<&'a [L]>,
/// The labels' style
labels_style: Style,
labels: Option<Vec<Span<'a>>>,
/// The style used to draw the axis itself
style: Style,
/// The alignment of the labels of the Axis
labels_alignment: Alignment,
}
impl<'a, L> Default for Axis<'a, L>
where
L: AsRef<str>,
{
fn default() -> Axis<'a, L> {
impl<'a> Default for Axis<'a> {
fn default() -> Axis<'a> {
Axis {
title: None,
title_style: Default::default(),
bounds: [0.0, 0.0],
labels: None,
labels_style: Default::default(),
style: Default::default(),
labels_alignment: Alignment::Left,
}
}
}
impl<'a, L> Axis<'a, L>
where
L: AsRef<str>,
{
pub fn title(mut self, title: &'a str) -> Axis<'a, L> {
self.title = Some(title);
impl<'a> Axis<'a> {
pub fn title<T>(mut self, title: T) -> Axis<'a>
where
T: Into<Spans<'a>>,
{
self.title = Some(title.into());
self
}
pub fn title_style(mut self, style: Style) -> Axis<'a, L> {
self.title_style = style;
#[deprecated(
since = "0.10.0",
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
)]
pub fn title_style(mut self, style: Style) -> Axis<'a> {
if let Some(t) = self.title {
let title = String::from(t);
self.title = Some(Spans::from(Span::styled(title, style)));
}
self
}
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a, L> {
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
self.bounds = bounds;
self
}
pub fn labels(mut self, labels: &'a [L]) -> Axis<'a, L> {
pub fn labels(mut self, labels: Vec<Span<'a>>) -> Axis<'a> {
self.labels = Some(labels);
self
}
pub fn labels_style(mut self, style: Style) -> Axis<'a, L> {
self.labels_style = style;
pub fn style(mut self, style: Style) -> Axis<'a> {
self.style = style;
self
}
pub fn style(mut self, style: Style) -> Axis<'a, L> {
self.style = style;
/// Defines the alignment of the labels of the axis.
/// The alignment behaves differently based on the axis:
/// - Y-Axis: The labels are aligned within the area on the left of the axis
/// - X-Axis: The first X-axis label is aligned relative to the Y-axis
pub fn labels_alignment(mut self, alignment: Alignment) -> Axis<'a> {
self.labels_alignment = alignment;
self
}
}
@@ -150,7 +156,7 @@ impl<'a> Dataset<'a> {
/// A container that holds all the infos about where to display each elements of the chart (axis,
/// labels, legend, ...).
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Default)]
struct ChartLayout {
/// Location of the title of the x axis
title_x: Option<(u16, u16)>,
@@ -170,21 +176,6 @@ struct ChartLayout {
graph_area: Rect,
}
impl Default for ChartLayout {
fn default() -> ChartLayout {
ChartLayout {
title_x: None,
title_y: None,
label_x: None,
label_y: None,
axis_x: None,
axis_y: None,
legend_area: None,
graph_area: Rect::default(),
}
}
}
/// A widget to plot one or more dataset in a cartesian coordinate system
///
/// # Examples
@@ -193,103 +184,83 @@ impl Default for ChartLayout {
/// # use tui::symbols;
/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
/// # use tui::style::{Style, Color};
/// Chart::default()
/// # use tui::text::Span;
/// let datasets = vec![
/// Dataset::default()
/// .name("data1")
/// .marker(symbols::Marker::Dot)
/// .graph_type(GraphType::Scatter)
/// .style(Style::default().fg(Color::Cyan))
/// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
/// Dataset::default()
/// .name("data2")
/// .marker(symbols::Marker::Braille)
/// .graph_type(GraphType::Line)
/// .style(Style::default().fg(Color::Magenta))
/// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)]),
/// ];
/// Chart::new(datasets)
/// .block(Block::default().title("Chart"))
/// .x_axis(Axis::default()
/// .title("X Axis")
/// .title_style(Style::default().fg(Color::Red))
/// .title(Span::styled("X Axis", Style::default().fg(Color::Red)))
/// .style(Style::default().fg(Color::White))
/// .bounds([0.0, 10.0])
/// .labels(&["0.0", "5.0", "10.0"]))
/// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()))
/// .y_axis(Axis::default()
/// .title("Y Axis")
/// .title_style(Style::default().fg(Color::Red))
/// .title(Span::styled("Y Axis", Style::default().fg(Color::Red)))
/// .style(Style::default().fg(Color::White))
/// .bounds([0.0, 10.0])
/// .labels(&["0.0", "5.0", "10.0"]))
/// .datasets(&[Dataset::default()
/// .name("data1")
/// .marker(symbols::Marker::Dot)
/// .graph_type(GraphType::Scatter)
/// .style(Style::default().fg(Color::Cyan))
/// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
/// Dataset::default()
/// .name("data2")
/// .marker(symbols::Marker::Braille)
/// .graph_type(GraphType::Line)
/// .style(Style::default().fg(Color::Magenta))
/// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)])]);
/// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()));
/// ```
#[derive(Debug, Clone)]
pub struct Chart<'a, LX, LY>
where
LX: AsRef<str> + 'a,
LY: AsRef<str> + 'a,
{
pub struct Chart<'a> {
/// A block to display around the widget eventually
block: Option<Block<'a>>,
/// The horizontal axis
x_axis: Axis<'a, LX>,
x_axis: Axis<'a>,
/// The vertical axis
y_axis: Axis<'a, LY>,
y_axis: Axis<'a>,
/// A reference to the datasets
datasets: &'a [Dataset<'a>],
datasets: Vec<Dataset<'a>>,
/// The widget base style
style: Style,
/// Constraints used to determine whether the legend should be shown or
/// not
/// Constraints used to determine whether the legend should be shown or not
hidden_legend_constraints: (Constraint, Constraint),
}
impl<'a, LX, LY> Default for Chart<'a, LX, LY>
where
LX: AsRef<str>,
LY: AsRef<str>,
{
fn default() -> Chart<'a, LX, LY> {
impl<'a> Chart<'a> {
pub fn new(datasets: Vec<Dataset<'a>>) -> Chart<'a> {
Chart {
block: None,
x_axis: Axis::default(),
y_axis: Axis::default(),
style: Default::default(),
datasets: &[],
datasets,
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
}
}
}
impl<'a, LX, LY> Chart<'a, LX, LY>
where
LX: AsRef<str>,
LY: AsRef<str>,
{
pub fn block(mut self, block: Block<'a>) -> Chart<'a, LX, LY> {
pub fn block(mut self, block: Block<'a>) -> Chart<'a> {
self.block = Some(block);
self
}
pub fn style(mut self, style: Style) -> Chart<'a, LX, LY> {
pub fn style(mut self, style: Style) -> Chart<'a> {
self.style = style;
self
}
pub fn x_axis(mut self, axis: Axis<'a, LX>) -> Chart<'a, LX, LY> {
pub fn x_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
self.x_axis = axis;
self
}
pub fn y_axis(mut self, axis: Axis<'a, LY>) -> Chart<'a, LX, LY> {
pub fn y_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
self.y_axis = axis;
self
}
pub fn datasets(mut self, datasets: &'a [Dataset<'a>]) -> Chart<'a, LX, LY> {
self.datasets = datasets;
self
}
/// Set the constraints used to determine whether the legend should be shown or
/// not.
/// Set the constraints used to determine whether the legend should be shown or not.
///
/// # Examples
///
@@ -302,13 +273,10 @@ where
/// );
/// // Hide the legend when either its width is greater than 33% of the total widget width
/// // or if its height is greater than 25% of the total widget height.
/// let _chart: Chart<String, String> = Chart::default()
/// let _chart: Chart = Chart::new(vec![])
/// .hidden_legend_constraints(constraints);
/// ```
pub fn hidden_legend_constraints(
mut self,
constraints: (Constraint, Constraint),
) -> Chart<'a, LX, LY> {
pub fn hidden_legend_constraints(mut self, constraints: (Constraint, Constraint)) -> Chart<'a> {
self.hidden_legend_constraints = constraints;
self
}
@@ -328,21 +296,8 @@ where
y -= 1;
}
if let Some(y_labels) = self.y_axis.labels {
let mut max_width = y_labels
.iter()
.fold(0, |acc, l| max(l.as_ref().width(), acc))
as u16;
if let Some(x_labels) = self.x_axis.labels {
if !x_labels.is_empty() {
max_width = max(max_width, x_labels[0].as_ref().width() as u16);
}
}
if x + max_width < area.right() {
layout.label_y = Some(x);
x += max_width;
}
}
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
x += self.max_width_of_labels_left_of_y_axis(area, self.y_axis.labels.is_some());
if self.x_axis.labels.is_some() && y > area.top() {
layout.axis_x = Some(y);
@@ -358,17 +313,17 @@ where
layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
}
if let Some(title) = self.x_axis.title {
if let Some(ref title) = self.x_axis.title {
let w = title.width() as u16;
if w < layout.graph_area.width && layout.graph_area.height > 2 {
layout.title_x = Some((x + layout.graph_area.width - w, y));
}
}
if let Some(title) = self.y_axis.title {
if let Some(ref title) = self.y_axis.title {
let w = title.width() as u16;
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
layout.title_y = Some((x + 1, area.top()));
layout.title_y = Some((x, area.top()));
}
}
@@ -397,18 +352,161 @@ where
}
layout
}
fn max_width_of_labels_left_of_y_axis(&self, area: Rect, has_y_axis: bool) -> u16 {
let mut max_width = self
.y_axis
.labels
.as_ref()
.map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
.unwrap_or_default();
if let Some(first_x_label) = self.x_axis.labels.as_ref().and_then(|labels| labels.get(0)) {
let first_label_width = first_x_label.content.width() as u16;
let width_left_of_y_axis = match self.x_axis.labels_alignment {
Alignment::Left => {
// The last character of the label should be below the Y-Axis when it exists, not on its left
let y_axis_offset = if has_y_axis { 1 } else { 0 };
first_label_width.saturating_sub(y_axis_offset)
}
Alignment::Center => first_label_width / 2,
Alignment::Right => 0,
};
max_width = max(max_width, width_left_of_y_axis);
}
// labels of y axis and first label of x axis can take at most 1/3rd of the total width
max_width.min(area.width / 3)
}
fn render_x_labels(
&mut self,
buf: &mut Buffer,
layout: &ChartLayout,
chart_area: Rect,
graph_area: Rect,
) {
let y = match layout.label_x {
Some(y) => y,
None => return,
};
let labels = self.x_axis.labels.as_ref().unwrap();
let labels_len = labels.len() as u16;
if labels_len < 2 {
return;
}
let width_between_ticks = graph_area.width / labels_len;
let label_area = self.first_x_label_area(
y,
labels.first().unwrap().width() as u16,
width_between_ticks,
chart_area,
graph_area,
);
let label_alignment = match self.x_axis.labels_alignment {
Alignment::Left => Alignment::Right,
Alignment::Center => Alignment::Center,
Alignment::Right => Alignment::Left,
};
Self::render_label(buf, labels.first().unwrap(), label_area, label_alignment);
for (i, label) in labels[1..labels.len() - 1].iter().enumerate() {
// We add 1 to x (and width-1 below) to leave at least one space before each intermediate labels
let x = graph_area.left() + (i + 1) as u16 * width_between_ticks + 1;
let label_area = Rect::new(x, y, width_between_ticks.saturating_sub(1), 1);
Self::render_label(buf, label, label_area, Alignment::Center);
}
let x = graph_area.right() - width_between_ticks;
let label_area = Rect::new(x, y, width_between_ticks, 1);
// The last label should be aligned Right to be at the edge of the graph area
Self::render_label(buf, labels.last().unwrap(), label_area, Alignment::Right);
}
fn first_x_label_area(
&self,
y: u16,
label_width: u16,
max_width_after_y_axis: u16,
chart_area: Rect,
graph_area: Rect,
) -> Rect {
let (min_x, max_x) = match self.x_axis.labels_alignment {
Alignment::Left => (chart_area.left(), graph_area.left()),
Alignment::Center => (
chart_area.left(),
graph_area.left() + max_width_after_y_axis.min(label_width),
),
Alignment::Right => (
graph_area.left().saturating_sub(1),
graph_area.left() + max_width_after_y_axis,
),
};
Rect::new(min_x, y, max_x - min_x, 1)
}
fn render_label(buf: &mut Buffer, label: &Span, label_area: Rect, alignment: Alignment) {
let label_width = label.width() as u16;
let bounded_label_width = label_area.width.min(label_width);
let x = match alignment {
Alignment::Left => label_area.left(),
Alignment::Center => label_area.left() + label_area.width / 2 - bounded_label_width / 2,
Alignment::Right => label_area.right() - bounded_label_width,
};
buf.set_span(x, label_area.top(), label, bounded_label_width);
}
fn render_y_labels(
&mut self,
buf: &mut Buffer,
layout: &ChartLayout,
chart_area: Rect,
graph_area: Rect,
) {
let x = match layout.label_y {
Some(x) => x,
None => return,
};
let labels = self.y_axis.labels.as_ref().unwrap();
let labels_len = labels.len() as u16;
for (i, label) in labels.iter().enumerate() {
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
if dy < graph_area.bottom() {
let label_area = Rect::new(
x,
graph_area.bottom().saturating_sub(1) - dy,
(graph_area.left() - chart_area.left()).saturating_sub(1),
1,
);
Self::render_label(buf, label, label_area, self.y_axis.labels_alignment);
}
}
}
}
impl<'a, LX, LY> Widget for Chart<'a, LX, LY>
where
LX: AsRef<str>,
LY: AsRef<str>,
{
impl<'a> Widget for Chart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let chart_area = match self.block {
Some(ref mut b) => {
if area.area() == 0 {
return;
}
buf.set_style(area, self.style);
// Sample the style of the entire widget. This sample will be used to reset the style of
// the cells that are part of the components put on top of the grah area (i.e legend and
// axis names).
let original_style = buf.get(area.left(), area.top()).style();
let chart_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
@@ -419,50 +517,8 @@ where
return;
}
buf.set_background(chart_area, self.style.bg);
if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap();
buf.set_string(x, y, title, self.x_axis.title_style);
}
if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap();
buf.set_string(x, y, title, self.y_axis.title_style);
}
if let Some(y) = layout.label_x {
let labels = self.x_axis.labels.unwrap();
let total_width = labels.iter().fold(0, |acc, l| l.as_ref().width() + acc) as u16;
let labels_len = labels.len() as u16;
if total_width < graph_area.width && labels_len > 1 {
for (i, label) in labels.iter().enumerate() {
buf.set_string(
graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1)
- label.as_ref().width() as u16,
y,
label.as_ref(),
self.x_axis.labels_style,
);
}
}
}
if let Some(x) = layout.label_y {
let labels = self.y_axis.labels.unwrap();
let labels_len = labels.len() as u16;
for (i, label) in labels.iter().enumerate() {
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
if dy < graph_area.bottom() {
buf.set_string(
x,
graph_area.bottom() - 1 - dy,
label.as_ref(),
self.y_axis.labels_style,
);
}
}
}
self.render_x_labels(buf, &layout, chart_area, graph_area);
self.render_y_labels(buf, &layout, chart_area, graph_area);
if let Some(y) = layout.axis_x {
for x in graph_area.left()..graph_area.right() {
@@ -488,16 +544,16 @@ where
}
}
for dataset in self.datasets {
for dataset in &self.datasets {
Canvas::default()
.background_color(self.style.bg)
.background_color(self.style.bg.unwrap_or(Color::Reset))
.x_bounds(self.x_axis.bounds)
.y_bounds(self.y_axis.bounds)
.marker(dataset.marker)
.paint(|ctx| {
ctx.draw(&Points {
coords: dataset.data,
color: dataset.style.fg,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
if let GraphType::Line = dataset.graph_type {
for data in dataset.data.windows(2) {
@@ -506,7 +562,7 @@ where
y1: data[0].1,
x2: data[1].0,
y2: data[1].1,
color: dataset.style.fg,
color: dataset.style.fg.unwrap_or(Color::Reset),
})
}
}
@@ -515,6 +571,7 @@ where
}
if let Some(legend_area) = layout.legend_area {
buf.set_style(legend_area, original_style);
Block::default()
.borders(Borders::ALL)
.render(legend_area, buf);
@@ -527,6 +584,36 @@ where
);
}
}
if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap();
let width = graph_area.right().saturating_sub(x);
buf.set_style(
Rect {
x,
y,
width,
height: 1,
},
original_style,
);
buf.set_spans(x, y, &title, width);
}
if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap();
let width = graph_area.right().saturating_sub(x);
buf.set_style(
Rect {
x,
y,
width,
height: 1,
},
original_style,
);
buf.set_spans(x, y, &title, width);
}
}
}
@@ -543,12 +630,6 @@ mod tests {
#[test]
fn it_should_hide_the_legend() {
let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)];
let datasets = (0..10)
.map(|i| {
let name = format!("Dataset #{}", i);
Dataset::default().name(name).data(&data)
})
.collect::<Vec<_>>();
let cases = [
LegendTestCase {
chart_area: Rect::new(0, 0, 100, 100),
@@ -562,11 +643,16 @@ mod tests {
},
];
for case in &cases {
let chart: Chart<String, String> = Chart::default()
let datasets = (0..10)
.map(|i| {
let name = format!("Dataset #{}", i);
Dataset::default().name(name).data(&data)
})
.collect::<Vec<_>>();
let chart = Chart::new(datasets)
.x_axis(Axis::default().title("X axis"))
.y_axis(Axis::default().title("Y axis"))
.hidden_legend_constraints(case.hidden_legend_constraints)
.datasets(datasets.as_slice());
.hidden_legend_constraints(case.hidden_legend_constraints);
let layout = chart.layout(case.chart_area);
assert_eq!(layout.legend_area, case.legend_area);
}

View File

@@ -1,8 +1,9 @@
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::widgets::Widget;
use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
/// A widget to to clear/reset a certain area to allow overdrawing (e.g. for popups)
/// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups).
///
/// This widget **cannot be used to clear the terminal on the first render** as `tui` assumes the
/// render area is empty. Use [`crate::Terminal::clear`] instead.
///
/// # Examples
///

View File

@@ -1,9 +1,11 @@
use unicode_width::UnicodeWidthStr;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::{Color, Style};
use crate::widgets::{Block, Widget};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols,
text::{Span, Spans},
widgets::{Block, Widget},
};
/// A widget to display a task progress.
///
@@ -14,15 +16,17 @@ use crate::widgets::{Block, Widget};
/// # use tui::style::{Style, Color, Modifier};
/// Gauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .style(Style::default().fg(Color::White).bg(Color::Black).modifier(Modifier::ITALIC))
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::ITALIC))
/// .percent(20);
/// ```
#[derive(Debug, Clone)]
pub struct Gauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
label: Option<&'a str>,
label: Option<Span<'a>>,
use_unicode: bool,
style: Style,
gauge_style: Style,
}
impl<'a> Default for Gauge<'a> {
@@ -31,7 +35,9 @@ impl<'a> Default for Gauge<'a> {
block: None,
ratio: 0.0,
label: None,
style: Default::default(),
use_unicode: false,
style: Style::default(),
gauge_style: Style::default(),
}
}
}
@@ -54,15 +60,18 @@ impl<'a> Gauge<'a> {
/// Sets ratio ([0.0, 1.0]) directly.
pub fn ratio(mut self, ratio: f64) -> Gauge<'a> {
assert!(
ratio <= 1.0 && ratio >= 0.0,
(0.0..=1.0).contains(&ratio),
"Ratio should be between 0 and 1 inclusively."
);
self.ratio = ratio;
self
}
pub fn label(mut self, string: &'a str) -> Gauge<'a> {
self.label = Some(string);
pub fn label<T>(mut self, label: T) -> Gauge<'a>
where
T: Into<Span<'a>>,
{
self.label = Some(label.into());
self
}
@@ -70,49 +79,212 @@ impl<'a> Gauge<'a> {
self.style = style;
self
}
pub fn gauge_style(mut self, style: Style) -> Gauge<'a> {
self.gauge_style = style;
self
}
pub fn use_unicode(mut self, unicode: bool) -> Gauge<'a> {
self.use_unicode = unicode;
self
}
}
impl<'a> Widget for Gauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let gauge_area = match self.block {
Some(ref mut b) => {
buf.set_style(area, self.style);
let gauge_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
buf.set_style(gauge_area, self.gauge_style);
if gauge_area.height < 1 {
return;
}
if self.style.bg != Color::Reset {
buf.set_background(gauge_area, self.style.bg);
// compute label value and its position
// label is put at the center of the gauge_area
let label = {
let pct = f64::round(self.ratio * 100.0);
self.label
.unwrap_or_else(|| Span::from(format!("{}%", pct)))
};
let clamped_label_width = gauge_area.width.min(label.width() as u16);
let label_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2;
let label_row = gauge_area.top() + gauge_area.height / 2;
// the gauge will be filled proportionally to the ratio
let filled_width = f64::from(gauge_area.width) * self.ratio;
let end = if self.use_unicode {
gauge_area.left() + filled_width.floor() as u16
} else {
gauge_area.left() + filled_width.round() as u16
};
for y in gauge_area.top()..gauge_area.bottom() {
// render the filled area (left to end)
for x in gauge_area.left()..end {
// spaces are needed to apply the background styling
buf.get_mut(x, y)
.set_symbol(" ")
.set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
.set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
}
if self.use_unicode && self.ratio < 1.0 {
buf.get_mut(end, y)
.set_symbol(get_unicode_block(filled_width % 1.0));
}
}
// set the span
buf.set_span(label_col, label_row, &label, clamped_label_width);
}
}
fn get_unicode_block<'a>(frac: f64) -> &'a str {
match (frac * 8.0).round() as u16 {
1 => symbols::block::ONE_EIGHTH,
2 => symbols::block::ONE_QUARTER,
3 => symbols::block::THREE_EIGHTHS,
4 => symbols::block::HALF,
5 => symbols::block::FIVE_EIGHTHS,
6 => symbols::block::THREE_QUARTERS,
7 => symbols::block::SEVEN_EIGHTHS,
8 => symbols::block::FULL,
_ => " ",
}
}
/// A compact widget to display a task progress over a single line.
///
/// # Examples:
///
/// ```
/// # use tui::widgets::{Widget, LineGauge, Block, Borders};
/// # use tui::style::{Style, Color, Modifier};
/// # use tui::symbols;
/// LineGauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::BOLD))
/// .line_set(symbols::line::THICK)
/// .ratio(0.4);
/// ```
pub struct LineGauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
label: Option<Spans<'a>>,
line_set: symbols::line::Set,
style: Style,
gauge_style: Style,
}
impl<'a> Default for LineGauge<'a> {
fn default() -> Self {
Self {
block: None,
ratio: 0.0,
label: None,
style: Style::default(),
line_set: symbols::line::NORMAL,
gauge_style: Style::default(),
}
}
}
impl<'a> LineGauge<'a> {
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn ratio(mut self, ratio: f64) -> Self {
assert!(
(0.0..=1.0).contains(&ratio),
"Ratio should be between 0 and 1 inclusively."
);
self.ratio = ratio;
self
}
pub fn line_set(mut self, set: symbols::line::Set) -> Self {
self.line_set = set;
self
}
pub fn label<T>(mut self, label: T) -> Self
where
T: Into<Spans<'a>>,
{
self.label = Some(label.into());
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn gauge_style(mut self, style: Style) -> Self {
self.gauge_style = style;
self
}
}
impl<'a> Widget for LineGauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let gauge_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if gauge_area.height < 1 {
return;
}
let center = gauge_area.height / 2 + gauge_area.top();
let width = (f64::from(gauge_area.width) * self.ratio).round() as u16;
let end = gauge_area.left() + width;
for y in gauge_area.top()..gauge_area.bottom() {
// Gauge
for x in gauge_area.left()..end {
buf.get_mut(x, y).set_symbol(" ");
}
let ratio = self.ratio;
let label = self
.label
.unwrap_or_else(move || Spans::from(format!("{:.0}%", ratio * 100.0)));
let (col, row) = buf.set_spans(
gauge_area.left(),
gauge_area.top(),
&label,
gauge_area.width,
);
let start = col + 1;
if start >= gauge_area.right() {
return;
}
if y == center {
// Label
let precent_label = format!("{}%", (self.ratio * 100.0).round());
let label = self.label.unwrap_or(&precent_label);
let label_width = label.width() as u16;
let middle = (gauge_area.width - label_width) / 2 + gauge_area.left();
buf.set_string(middle, y, label, self.style);
}
// Fix colors
for x in gauge_area.left()..end {
buf.get_mut(x, y)
.set_fg(self.style.bg)
.set_bg(self.style.fg);
}
let end = start
+ (f64::from(gauge_area.right().saturating_sub(start)) * self.ratio).floor() as u16;
for col in start..end {
buf.get_mut(col, row)
.set_symbol(self.line_set.horizontal)
.set_style(Style {
fg: self.gauge_style.fg,
bg: None,
add_modifier: self.gauge_style.add_modifier,
sub_modifier: self.gauge_style.sub_modifier,
});
}
for col in end..gauge_area.right() {
buf.get_mut(col, row)
.set_symbol(self.line_set.horizontal)
.set_style(Style {
fg: self.gauge_style.bg,
bg: None,
add_modifier: self.gauge_style.add_modifier,
sub_modifier: self.gauge_style.sub_modifier,
});
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,346 @@
use crate::{
buffer::Buffer,
layout::{Constraint, Rect},
layout::{Constraint, Direction, Layout, Rect},
style::Style,
text::Text,
widgets::{Block, StatefulWidget, Widget},
};
use cassowary::{
strength::{MEDIUM, REQUIRED, WEAK},
WeightedRelation::*,
{Expression, Solver},
};
use std::{
collections::HashMap,
fmt::Display,
iter::{self, Iterator},
};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone)]
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
///
/// It can be created from anything that can be converted to a [`Text`].
/// ```rust
/// # use tui::widgets::Cell;
/// # use tui::style::{Style, Modifier};
/// # use tui::text::{Span, Spans, Text};
/// # use std::borrow::Cow;
/// Cell::from("simple string");
///
/// Cell::from(Span::from("span"));
///
/// Cell::from(Spans::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
/// ]));
///
/// Cell::from(Text::from("a text"));
///
/// Cell::from(Text::from(Cow::Borrowed("hello")));
/// ```
///
/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
/// capabilities of [`Text`].
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Cell<'a> {
content: Text<'a>,
style: Style,
}
impl<'a> Cell<'a> {
/// Set the `Style` of this cell.
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl<'a, T> From<T> for Cell<'a>
where
T: Into<Text<'a>>,
{
fn from(content: T) -> Cell<'a> {
Cell {
content: content.into(),
style: Style::default(),
}
}
}
/// Holds data to be displayed in a [`Table`] widget.
///
/// A [`Row`] is a collection of cells. It can be created from simple strings:
/// ```rust
/// # use tui::widgets::Row;
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
/// ```
///
/// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s:
/// ```rust
/// # use tui::widgets::{Row, Cell};
/// # use tui::style::{Style, Color};
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
/// ]);
/// ```
///
/// You can also construct a row from any type that can be converted into [`Text`]:
/// ```rust
/// # use std::borrow::Cow;
/// # use tui::widgets::Row;
/// Row::new(vec![
/// Cow::Borrowed("hello"),
/// Cow::Owned("world".to_uppercase()),
/// ]);
/// ```
///
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Row<'a> {
cells: Vec<Cell<'a>>,
height: u16,
style: Style,
bottom_margin: u16,
}
impl<'a> Row<'a> {
/// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
pub fn new<T>(cells: T) -> Self
where
T: IntoIterator,
T::Item: Into<Cell<'a>>,
{
Self {
height: 1,
cells: cells.into_iter().map(|c| c.into()).collect(),
style: Style::default(),
bottom_margin: 0,
}
}
/// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
/// height will see its content truncated.
pub fn height(mut self, height: u16) -> Self {
self.height = height;
self
}
/// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a
/// any individual [`Cell`] or event by their [`Text`] content.
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
/// Set the bottom margin. By default, the bottom margin is `0`.
pub fn bottom_margin(mut self, margin: u16) -> Self {
self.bottom_margin = margin;
self
}
/// Returns the total height of the row.
fn total_height(&self) -> u16 {
self.height.saturating_add(self.bottom_margin)
}
}
/// A widget to display data in formatted columns.
///
/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
/// ```rust
/// # use tui::widgets::{Block, Borders, Table, Row, Cell};
/// # use tui::layout::Constraint;
/// # use tui::style::{Style, Color, Modifier};
/// # use tui::text::{Text, Spans, Span};
/// Table::new(vec![
/// // Row can be created from simple strings.
/// Row::new(vec!["Row11", "Row12", "Row13"]),
/// // You can style the entire row.
/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
/// // If you need more control over the styling you may need to create Cells directly
/// Row::new(vec![
/// Cell::from("Row31"),
/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
/// Cell::from(Spans::from(vec![
/// Span::raw("Row"),
/// Span::styled("33", Style::default().fg(Color::Green))
/// ])),
/// ]),
/// // If a Row need to display some content over multiple lines, you just have to change
/// // its height.
/// Row::new(vec![
/// Cell::from("Row\n41"),
/// Cell::from("Row\n42"),
/// Cell::from("Row\n43"),
/// ]).height(2),
/// ])
/// // You can set the style of the entire Table.
/// .style(Style::default().fg(Color::White))
/// // It has an optional header, which is simply a Row always visible at the top.
/// .header(
/// Row::new(vec!["Col1", "Col2", "Col3"])
/// .style(Style::default().fg(Color::Yellow))
/// // If you want some space between the header and the rest of the rows, you can always
/// // specify some margin at the bottom.
/// .bottom_margin(1)
/// )
/// // As any other widget, a Table can be wrapped in a Block.
/// .block(Block::default().title("Table"))
/// // Columns widths are constrained in the same way as Layout...
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
/// // ...and they can be separated by a fixed spacing.
/// .column_spacing(1)
/// // If you wish to highlight a row in any specific way when it is selected...
/// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
/// // ...and potentially show a symbol in front of the selection.
/// .highlight_symbol(">>");
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct Table<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
/// Base style for the widget
style: Style,
/// Width constraints for each column
widths: &'a [Constraint],
/// Space between each column
column_spacing: u16,
/// Style used to render the selected row
highlight_style: Style,
/// Symbol in front of the selected rom
highlight_symbol: Option<&'a str>,
/// Optional header
header: Option<Row<'a>>,
/// Data to display in each row
rows: Vec<Row<'a>>,
}
impl<'a> Table<'a> {
pub fn new<T>(rows: T) -> Self
where
T: IntoIterator<Item = Row<'a>>,
{
Self {
block: None,
style: Style::default(),
widths: &[],
column_spacing: 1,
highlight_style: Style::default(),
highlight_symbol: None,
header: None,
rows: rows.into_iter().collect(),
}
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn header(mut self, header: Row<'a>) -> Self {
self.header = Some(header);
self
}
pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
let between_0_and_100 = |&w| match w {
Constraint::Percentage(p) => p <= 100,
_ => true,
};
assert!(
widths.iter().all(between_0_and_100),
"Percentages should be between 0 and 100 inclusively."
);
self.widths = widths;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
self.highlight_symbol = Some(highlight_symbol);
self
}
pub fn highlight_style(mut self, highlight_style: Style) -> Self {
self.highlight_style = highlight_style;
self
}
pub fn column_spacing(mut self, spacing: u16) -> Self {
self.column_spacing = spacing;
self
}
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
if has_selection {
let highlight_symbol_width =
self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
constraints.push(Constraint::Length(highlight_symbol_width));
}
for constraint in self.widths {
constraints.push(*constraint);
constraints.push(Constraint::Length(self.column_spacing));
}
if !self.widths.is_empty() {
constraints.pop();
}
let mut chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.expand_to_fill(false)
.split(Rect {
x: 0,
y: 0,
width: max_width,
height: 1,
});
if has_selection {
chunks.remove(0);
}
chunks.iter().step_by(2).map(|c| c.width).collect()
}
fn get_row_bounds(
&self,
selected: Option<usize>,
offset: usize,
max_height: u16,
) -> (usize, usize) {
let offset = offset.min(self.rows.len().saturating_sub(1));
let mut start = offset;
let mut end = offset;
let mut height = 0;
for item in self.rows.iter().skip(offset) {
if height + item.height > max_height {
break;
}
height += item.total_height();
end += 1;
}
let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
while selected >= end {
height = height.saturating_add(self.rows[end].total_height());
end += 1;
while height > max_height {
height = height.saturating_sub(self.rows[start].total_height());
start += 1;
}
}
while selected < start {
start -= 1;
height = height.saturating_add(self.rows[start].total_height());
while height > max_height {
end -= 1;
height = height.saturating_sub(self.rows[end].total_height());
}
}
(start, end)
}
}
#[derive(Debug, Clone, Default)]
pub struct TableState {
offset: usize,
selected: Option<usize>,
}
impl Default for TableState {
fn default() -> TableState {
TableState {
offset: 0,
selected: None,
}
}
}
impl TableState {
pub fn selected(&self) -> Option<usize> {
self.selected
@@ -44,311 +354,130 @@ impl TableState {
}
}
/// Holds data to be displayed in a Table widget
#[derive(Debug, Clone)]
pub enum Row<D>
where
D: Iterator,
D::Item: Display,
{
Data(D),
StyledData(D, Style),
}
/// A widget to display data in formatted columns
///
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, Table, Row};
/// # use tui::layout::Constraint;
/// # use tui::style::{Style, Color};
/// let row_style = Style::default().fg(Color::White);
/// Table::new(
/// ["Col1", "Col2", "Col3"].into_iter(),
/// vec![
/// Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), row_style),
/// Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), row_style),
/// Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), row_style),
/// Row::Data(["Row41", "Row42", "Row43"].into_iter())
/// ].into_iter()
/// )
/// .block(Block::default().title("Table"))
/// .header_style(Style::default().fg(Color::Yellow))
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
/// .style(Style::default().fg(Color::White))
/// .column_spacing(1);
/// ```
#[derive(Debug, Clone)]
pub struct Table<'a, H, R> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
/// Base style for the widget
style: Style,
/// Header row for all columns
header: H,
/// Style for the header
header_style: Style,
/// Width constraints for each column
widths: &'a [Constraint],
/// Space between each column
column_spacing: u16,
/// Space between the header and the rows
header_gap: u16,
/// Style used to render the selected row
highlight_style: Style,
/// Symbol in front of the selected rom
highlight_symbol: Option<&'a str>,
/// Data to display in each row
rows: R,
}
impl<'a, H, R> Default for Table<'a, H, R>
where
H: Iterator + Default,
R: Iterator + Default,
{
fn default() -> Table<'a, H, R> {
Table {
block: None,
style: Style::default(),
header: H::default(),
header_style: Style::default(),
widths: &[],
column_spacing: 1,
header_gap: 1,
highlight_style: Style::default(),
highlight_symbol: None,
rows: R::default(),
}
}
}
impl<'a, H, D, R> Table<'a, H, R>
where
H: Iterator,
D: Iterator,
D::Item: Display,
R: Iterator<Item = Row<D>>,
{
pub fn new(header: H, rows: R) -> Table<'a, H, R> {
Table {
block: None,
style: Style::default(),
header,
header_style: Style::default(),
widths: &[],
column_spacing: 1,
header_gap: 1,
highlight_style: Style::default(),
highlight_symbol: None,
rows,
}
}
pub fn block(mut self, block: Block<'a>) -> Table<'a, H, R> {
self.block = Some(block);
self
}
pub fn header<II>(mut self, header: II) -> Table<'a, H, R>
where
II: IntoIterator<Item = H::Item, IntoIter = H>,
{
self.header = header.into_iter();
self
}
pub fn header_style(mut self, style: Style) -> Table<'a, H, R> {
self.header_style = style;
self
}
pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, H, R> {
let between_0_and_100 = |&w| match w {
Constraint::Percentage(p) => p <= 100,
_ => true,
};
assert!(
widths.iter().all(between_0_and_100),
"Percentages should be between 0 and 100 inclusively."
);
self.widths = widths;
self
}
pub fn rows<II>(mut self, rows: II) -> Table<'a, H, R>
where
II: IntoIterator<Item = Row<D>, IntoIter = R>,
{
self.rows = rows.into_iter();
self
}
pub fn style(mut self, style: Style) -> Table<'a, H, R> {
self.style = style;
self
}
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Table<'a, H, R> {
self.highlight_symbol = Some(highlight_symbol);
self
}
pub fn highlight_style(mut self, highlight_style: Style) -> Table<'a, H, R> {
self.highlight_style = highlight_style;
self
}
pub fn column_spacing(mut self, spacing: u16) -> Table<'a, H, R> {
self.column_spacing = spacing;
self
}
pub fn header_gap(mut self, gap: u16) -> Table<'a, H, R> {
self.header_gap = gap;
self
}
}
impl<'a, H, D, R> StatefulWidget for Table<'a, H, R>
where
H: Iterator,
H::Item: Display,
D: Iterator,
D::Item: Display,
R: Iterator<Item = Row<D>>,
{
impl<'a> StatefulWidget for Table<'a> {
type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// Render block if necessary and get the drawing area
let table_area = match self.block {
Some(ref mut b) => {
if area.area() == 0 {
return;
}
buf.set_style(area, self.style);
let table_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
buf.set_background(table_area, self.style.bg);
let mut solver = Solver::new();
let mut var_indices = HashMap::new();
let mut ccs = Vec::new();
let mut variables = Vec::new();
for i in 0..self.widths.len() {
let var = cassowary::Variable::new();
variables.push(var);
var_indices.insert(var, i);
}
for (i, constraint) in self.widths.iter().enumerate() {
ccs.push(variables[i] | GE(WEAK) | 0.);
ccs.push(match *constraint {
Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
Constraint::Percentage(v) => {
variables[i] | EQ(WEAK) | (f64::from(v * area.width) / 100.0)
}
Constraint::Ratio(n, d) => {
variables[i] | EQ(WEAK) | (f64::from(area.width) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
})
}
solver
.add_constraint(
variables
.iter()
.fold(Expression::from_constant(0.), |acc, v| acc + *v)
| LE(REQUIRED)
| f64::from(
area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1)),
),
)
.unwrap();
solver.add_constraints(&ccs).unwrap();
let mut solved_widths = vec![0; variables.len()];
for &(var, value) in solver.fetch_changes() {
let index = var_indices[&var];
let value = if value.is_sign_negative() {
0
} else {
value as u16
};
solved_widths[index] = value
}
let mut y = table_area.top();
let mut x = table_area.left();
let has_selection = state.selected.is_some();
let columns_widths = self.get_columns_widths(table_area.width, has_selection);
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = " ".repeat(highlight_symbol.width());
let mut current_height = 0;
let mut rows_height = table_area.height;
// Draw header
if y < table_area.bottom() {
for (w, t) in solved_widths.iter().zip(self.header.by_ref()) {
buf.set_stringn(x, y, format!("{}", t), *w as usize, self.header_style);
x += *w + self.column_spacing;
if let Some(ref header) = self.header {
let max_header_height = table_area.height.min(header.total_height());
buf.set_style(
Rect {
x: table_area.left(),
y: table_area.top(),
width: table_area.width,
height: table_area.height.min(header.height),
},
header.style,
);
let mut col = table_area.left();
if has_selection {
col += (highlight_symbol.width() as u16).min(table_area.width);
}
for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
render_cell(
buf,
cell,
Rect {
x: col,
y: table_area.top(),
width: *width,
height: max_header_height,
},
);
col += *width + self.column_spacing;
}
current_height += max_header_height;
rows_height = rows_height.saturating_sub(max_header_height);
}
y += 1 + self.header_gap;
// Use highlight_style only if something is selected
let (selected, highlight_style) = match state.selected {
Some(i) => (Some(i), self.highlight_style),
None => (None, self.style),
};
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
// Draw rows
let default_style = Style::default();
if y < table_area.bottom() {
let remaining = (table_area.bottom() - y) as usize;
// Make sure the table shows the selected item
state.offset = if let Some(selected) = selected {
if selected >= remaining + state.offset - 1 {
selected + 1 - remaining
} else if selected < state.offset {
selected
} else {
state.offset
}
} else {
0
if self.rows.is_empty() {
return;
}
let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
state.offset = start;
for (i, table_row) in self
.rows
.iter_mut()
.enumerate()
.skip(state.offset)
.take(end - start)
{
let (row, col) = (table_area.top() + current_height, table_area.left());
current_height += table_row.total_height();
let table_row_area = Rect {
x: col,
y: row,
width: table_area.width,
height: table_row.height,
};
for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() {
let (data, style, symbol) = match row {
Row::Data(d) | Row::StyledData(d, _)
if Some(i) == state.selected.map(|s| s - state.offset) =>
{
(d, highlight_style, highlight_symbol)
}
Row::Data(d) => (d, default_style, blank_symbol.as_ref()),
Row::StyledData(d, s) => (d, s, blank_symbol.as_ref()),
buf.set_style(table_row_area, table_row.style);
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
let table_row_start_col = if has_selection {
let symbol = if is_selected {
highlight_symbol
} else {
&blank_symbol
};
x = table_area.left();
for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() {
let s = if c == 0 {
format!("{}{}", symbol, elt)
} else {
format!("{}", elt)
};
buf.set_stringn(x, y + i as u16, s, *w as usize, style);
x += *w + self.column_spacing;
}
let (col, _) =
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
col
} else {
col
};
let mut col = table_row_start_col;
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
render_cell(
buf,
cell,
Rect {
x: col,
y: row,
width: *width,
height: table_row.height,
},
);
col += *width + self.column_spacing;
}
if is_selected {
buf.set_style(table_row_area, self.highlight_style);
}
}
}
}
impl<'a, H, D, R> Widget for Table<'a, H, R>
where
H: Iterator,
H::Item: Display,
D: Iterator,
D::Item: Display,
R: Iterator<Item = Row<D>>,
{
fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
buf.set_style(area, cell.style);
for (i, spans) in cell.content.lines.iter().enumerate() {
if i as u16 >= area.height {
break;
}
buf.set_spans(area.x, area.y + i as u16, spans, area.width);
}
}
impl<'a> Widget for Table<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state);
@@ -362,7 +491,6 @@ mod tests {
#[test]
#[should_panic]
fn table_invalid_percentages() {
Table::new([""].iter(), vec![Row::Data([""].iter())].into_iter())
.widths(&[Constraint::Percentage(110)]);
Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
}
}

View File

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

63
tests/backend_termion.rs Normal file
View File

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

View File

@@ -1,45 +0,0 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders};
use tui::Terminal;
#[test]
fn it_draws_a_block() {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
let block = Block::default()
.title("Title")
.borders(Borders::ALL)
.title_style(Style::default().fg(Color::LightBlue));
f.render_widget(
block,
Rect {
x: 0,
y: 0,
width: 8,
height: 8,
},
);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
"┌Title─┐ ",
"│ │ ",
"│ │ ",
"│ │ ",
"│ │ ",
"│ │ ",
"│ │ ",
"└──────┘ ",
" ",
" ",
]);
for x in 1..=5 {
expected.get_mut(x, 0).set_fg(Color::LightBlue);
}
assert_eq!(&expected, terminal.backend().buffer());
}

View File

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

View File

@@ -1,42 +0,0 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::{Constraint, Direction, Layout};
use tui::widgets::{Block, Borders, Gauge};
use tui::Terminal;
#[test]
fn gauge_render() {
let backend = TestBackend::new(40, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let gauge = Gauge::default()
.block(Block::default().title("Percentage").borders(Borders::ALL))
.percent(43);
f.render_widget(gauge, chunks[0]);
let gauge = Gauge::default()
.block(Block::default().title("Ratio").borders(Borders::ALL))
.ratio(0.211_313_934_313_1);
f.render_widget(gauge, chunks[1]);
})
.unwrap();
let expected = Buffer::with_lines(vec![
" ",
" ",
" ┌Percentage────────────────────────┐ ",
" │ 43% │ ",
" └──────────────────────────────────┘ ",
" ┌Ratio─────────────────────────────┐ ",
" │ 21% │ ",
" └──────────────────────────────────┘ ",
" ",
" ",
]);
assert_eq!(&expected, terminal.backend().buffer());
}

View File

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

View File

@@ -1,139 +0,0 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::Alignment;
use tui::widgets::{Block, Borders, Paragraph, Text};
use tui::Terminal;
const SAMPLE_STRING: &str = "The library is based on the principle of immediate rendering with \
intermediate buffers. This means that at each new frame you should build all widgets that are \
supposed to be part of the UI. While providing a great flexibility for rich and \
interactive UI, this may introduce overhead for highly dynamic content.";
#[test]
fn paragraph_render_wrap() {
let render = |alignment| {
let backend = TestBackend::new(20, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
let size = f.size();
let text = [Text::raw(SAMPLE_STRING)];
let paragraph = Paragraph::new(text.iter())
.block(Block::default().borders(Borders::ALL))
.alignment(alignment)
.wrap(true);
f.render_widget(paragraph, size);
})
.unwrap();
terminal.backend().buffer().clone()
};
assert_eq!(
render(Alignment::Left),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│The library is │",
"│based on the │",
"│principle of │",
"│immediate │",
"│rendering with │",
"│intermediate │",
"│buffers. This │",
"│means that at each│",
"└──────────────────┘",
])
);
assert_eq!(
render(Alignment::Right),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│ The library is│",
"│ based on the│",
"│ principle of│",
"│ immediate│",
"│ rendering with│",
"│ intermediate│",
"│ buffers. This│",
"│means that at each│",
"└──────────────────┘",
])
);
assert_eq!(
render(Alignment::Center),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│ The library is │",
"│ based on the │",
"│ principle of │",
"│ immediate │",
"│ rendering with │",
"│ intermediate │",
"│ buffers. This │",
"│means that at each│",
"└──────────────────┘",
])
);
}
#[test]
fn paragraph_render_double_width() {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend).unwrap();
let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、";
terminal
.draw(|mut f| {
let size = f.size();
let text = [Text::raw(s)];
let paragraph = Paragraph::new(text.iter())
.block(Block::default().borders(Borders::ALL))
.wrap(true);
f.render_widget(paragraph, size);
})
.unwrap();
let expected = Buffer::with_lines(vec![
"┌────────┐",
"│コンピュ│",
"│ータ上で│",
"│文字を扱│",
"│う場合、│",
"│典型的に│",
"│は文字に│",
"│よる通信│",
"│を行う場│",
"└────────┘",
]);
assert_eq!(&expected, terminal.backend().buffer());
}
#[test]
fn paragraph_render_mixed_width() {
let backend = TestBackend::new(10, 7);
let mut terminal = Terminal::new(backend).unwrap();
let s = "aコンピュータ上で文字を扱う場合、";
terminal
.draw(|mut f| {
let size = f.size();
let text = [Text::raw(s)];
let paragraph = Paragraph::new(text.iter())
.block(Block::default().borders(Borders::ALL))
.wrap(true);
f.render_widget(paragraph, size);
})
.unwrap();
let expected = Buffer::with_lines(vec![
// The internal width is 8 so only 4 slots for double-width characters.
"┌────────┐",
"│aコンピ │", // Here we have 1 latin character so only 3 double-width ones can fit.
"│ュータ上│",
"│で文字を│",
"│扱う場合│",
"│、 │",
"└────────┘",
]);
assert_eq!(&expected, terminal.backend().buffer());
}

View File

@@ -1,11 +0,0 @@
use tui::backend::{Backend, TestBackend};
use tui::Terminal;
#[test]
fn buffer_size_limited() {
let backend = TestBackend::new(400, 400);
let terminal = Terminal::new(backend).unwrap();
let size = terminal.backend().size().unwrap();
assert_eq!(size.width, 255);
assert_eq!(size.height, 255);
}

View File

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

36
tests/terminal.rs Normal file
View File

@@ -0,0 +1,36 @@
use std::error::Error;
use tui::{
backend::{Backend, TestBackend},
layout::Rect,
widgets::Paragraph,
Terminal,
};
#[test]
fn terminal_buffer_size_should_be_limited() {
let backend = TestBackend::new(400, 400);
let terminal = Terminal::new(backend).unwrap();
let size = terminal.backend().size().unwrap();
assert_eq!(size.width, 255);
assert_eq!(size.height, 255);
}
#[test]
fn terminal_draw_returns_the_completed_frame() -> Result<(), Box<dyn Error>> {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend)?;
let frame = terminal.draw(|f| {
let paragrah = Paragraph::new("Test");
f.render_widget(paragrah, f.size());
})?;
assert_eq!(frame.buffer.get(0, 0).symbol, "T");
assert_eq!(frame.area, Rect::new(0, 0, 10, 10));
terminal.backend_mut().resize(8, 8);
let frame = terminal.draw(|f| {
let paragrah = Paragraph::new("test");
f.render_widget(paragrah, f.size());
})?;
assert_eq!(frame.buffer.get(0, 0).symbol, "t");
assert_eq!(frame.area, Rect::new(0, 0, 8, 8));
Ok(())
}

40
tests/widgets_barchart.rs Normal file
View File

@@ -0,0 +1,40 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::widgets::{BarChart, Block, Borders};
use tui::Terminal;
#[test]
fn widgets_barchart_not_full_below_max_value() {
let test_case = |expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let barchart = BarChart::default()
.block(Block::default().borders(Borders::ALL))
.data(&[("empty", 0), ("half", 50), ("almost", 99), ("full", 100)])
.max(100)
.bar_width(7)
.bar_gap(0);
f.render_widget(barchart, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
// check that bars fill up correctly up to max value
test_case(Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ ▇▇▇▇▇▇▇███████│",
"│ ██████████████│",
"│ ██████████████│",
"│ ▄▄▄▄▄▄▄██████████████│",
"│ █████████████████████│",
"│ █████████████████████│",
"│ ██50█████99█████100██│",
"│empty half almost full │",
"└────────────────────────────┘",
]));
}

346
tests/widgets_block.rs Normal file
View File

@@ -0,0 +1,346 @@
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::{Alignment, Rect},
style::{Color, Style},
text::Span,
widgets::{Block, Borders},
Terminal,
};
#[test]
fn widgets_block_renders() {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let block = Block::default()
.title(Span::styled("Title", Style::default().fg(Color::LightBlue)))
.borders(Borders::ALL);
f.render_widget(
block,
Rect {
x: 0,
y: 0,
width: 8,
height: 8,
},
);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
"┌Title─┐ ",
"│ │ ",
"│ │ ",
"│ │ ",
"│ │ ",
"│ │ ",
"│ │ ",
"└──────┘ ",
" ",
" ",
]);
for x in 1..=5 {
expected.get_mut(x, 0).set_fg(Color::LightBlue);
}
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_block_renders_on_small_areas() {
let test_case = |block, area: Rect, expected| {
let backend = TestBackend::new(area.width, area.height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
f.render_widget(block, area);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
let one_cell_test_cases = [
(Borders::NONE, "T"),
(Borders::LEFT, ""),
(Borders::TOP, "T"),
(Borders::RIGHT, ""),
(Borders::BOTTOM, "T"),
(Borders::ALL, ""),
];
for (borders, symbol) in one_cell_test_cases.iter().cloned() {
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
},
Buffer::empty(Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}),
);
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 1,
height: 0,
},
Buffer::empty(Rect {
x: 0,
y: 0,
width: 1,
height: 0,
}),
);
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 0,
height: 1,
},
Buffer::empty(Rect {
x: 0,
y: 0,
width: 0,
height: 1,
}),
);
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 1,
height: 1,
},
Buffer::with_lines(vec![symbol]),
);
}
test_case(
Block::default().title("Test").borders(Borders::LEFT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Buffer::with_lines(vec!["│Tes"]),
);
test_case(
Block::default().title("Test").borders(Borders::RIGHT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Buffer::with_lines(vec!["Tes│"]),
);
test_case(
Block::default().title("Test").borders(Borders::RIGHT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Buffer::with_lines(vec!["Tes│"]),
);
test_case(
Block::default()
.title("Test")
.borders(Borders::LEFT | Borders::RIGHT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Buffer::with_lines(vec!["│Te│"]),
);
test_case(
Block::default().title("Test").borders(Borders::TOP),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Buffer::with_lines(vec!["Test"]),
);
test_case(
Block::default().title("Test").borders(Borders::TOP),
Rect {
x: 0,
y: 0,
width: 5,
height: 1,
},
Buffer::with_lines(vec!["Test─"]),
);
test_case(
Block::default()
.title("Test")
.borders(Borders::LEFT | Borders::TOP),
Rect {
x: 0,
y: 0,
width: 5,
height: 1,
},
Buffer::with_lines(vec!["┌Test"]),
);
test_case(
Block::default()
.title("Test")
.borders(Borders::LEFT | Borders::TOP),
Rect {
x: 0,
y: 0,
width: 6,
height: 1,
},
Buffer::with_lines(vec!["┌Test─"]),
);
}
#[test]
fn widgets_block_title_alignment() {
let test_case = |alignment, borders, expected| {
let backend = TestBackend::new(15, 2);
let mut terminal = Terminal::new(backend).unwrap();
let block = Block::default()
.title(Span::styled("Title", Style::default()))
.title_alignment(alignment)
.borders(borders);
let area = Rect {
x: 1,
y: 0,
width: 13,
height: 2,
};
terminal
.draw(|f| {
f.render_widget(block, area);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
// title top-left with all borders
test_case(
Alignment::Left,
Borders::ALL,
Buffer::with_lines(vec![" ┌Title──────┐ ", " └───────────┘ "]),
);
// title top-left without top border
test_case(
Alignment::Left,
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │Title │ ", " └───────────┘ "]),
);
// title top-left with no left border
test_case(
Alignment::Left,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" Title───────┐ ", " ────────────┘ "]),
);
// title top-left without right border
test_case(
Alignment::Left,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌Title─────── ", " └──────────── "]),
);
// title top-left without borders
test_case(
Alignment::Left,
Borders::NONE,
Buffer::with_lines(vec![" Title ", " "]),
);
// title center with all borders
test_case(
Alignment::Center,
Borders::ALL,
Buffer::with_lines(vec![" ┌───Title───┐ ", " └───────────┘ "]),
);
// title center without top border
test_case(
Alignment::Center,
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │ Title │ ", " └───────────┘ "]),
);
// title center with no left border
test_case(
Alignment::Center,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ────Title───┐ ", " ────────────┘ "]),
);
// title center without right border
test_case(
Alignment::Center,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌───Title──── ", " └──────────── "]),
);
// title center without borders
test_case(
Alignment::Center,
Borders::NONE,
Buffer::with_lines(vec![" Title ", " "]),
);
// title top-right with all borders
test_case(
Alignment::Right,
Borders::ALL,
Buffer::with_lines(vec![" ┌──────Title┐ ", " └───────────┘ "]),
);
// title top-right without top border
test_case(
Alignment::Right,
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │ Title│ ", " └───────────┘ "]),
);
// title top-right with no left border
test_case(
Alignment::Right,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ───────Title┐ ", " ────────────┘ "]),
);
// title top-right without right border
test_case(
Alignment::Right,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌───────Title ", " └──────────── "]),
);
// title top-right without borders
test_case(
Alignment::Right,
Borders::NONE,
Buffer::with_lines(vec![" Title ", " "]),
);
}

42
tests/widgets_canvas.rs Normal file
View File

@@ -0,0 +1,42 @@
use tui::{
backend::TestBackend,
buffer::Buffer,
style::{Color, Style},
text::Span,
widgets::canvas::Canvas,
Terminal,
};
#[test]
fn widgets_canvas_draw_labels() {
let backend = TestBackend::new(5, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let label = String::from("test");
let canvas = Canvas::default()
.background_color(Color::Yellow)
.x_bounds([0.0, 5.0])
.y_bounds([0.0, 5.0])
.paint(|ctx| {
ctx.print(
0.0,
0.0,
Span::styled(label.clone(), Style::default().fg(Color::Blue)),
);
});
f.render_widget(canvas, f.size());
})
.unwrap();
let mut expected = Buffer::with_lines(vec![" ", " ", " ", " ", "test "]);
for row in 0..5 {
for col in 0..5 {
expected.get_mut(col, row).set_bg(Color::Yellow);
}
}
for col in 0..4 {
expected.get_mut(col, 4).set_fg(Color::Blue);
}
terminal.backend().assert_buffer(&expected)
}

621
tests/widgets_chart.rs Normal file
View File

@@ -0,0 +1,621 @@
use tui::layout::Alignment;
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols,
text::Span,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType::Line},
Terminal,
};
fn create_labels<'a>(labels: &'a [&'a str]) -> Vec<Span<'a>> {
labels.iter().map(|l| Span::from(*l)).collect()
}
fn axis_test_case<S>(width: u16, height: u16, x_axis: Axis, y_axis: Axis, lines: Vec<S>)
where
S: AsRef<str>,
{
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let chart = Chart::new(vec![]).x_axis(x_axis).y_axis(y_axis);
f.render_widget(chart, f.size());
})
.unwrap();
let expected = Buffer::with_lines(lines);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_chart_can_render_on_small_areas() {
let test_case = |width, height| {
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let datasets = vec![Dataset::default()
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta))
.data(&[(0.0, 0.0)])];
let chart = Chart::new(datasets)
.block(Block::default().title("Plot").borders(Borders::ALL))
.x_axis(
Axis::default()
.bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
);
f.render_widget(chart, f.size());
})
.unwrap();
};
test_case(0, 0);
test_case(0, 1);
test_case(1, 0);
test_case(1, 1);
test_case(2, 2);
}
#[test]
fn widgets_chart_handles_long_labels() {
let test_case = |x_labels, y_labels, x_alignment, lines| {
let mut x_axis = Axis::default().bounds([0.0, 1.0]);
if let Some((left_label, right_label)) = x_labels {
x_axis = x_axis
.labels(vec![Span::from(left_label), Span::from(right_label)])
.labels_alignment(x_alignment);
}
let mut y_axis = Axis::default().bounds([0.0, 1.0]);
if let Some((left_label, right_label)) = y_labels {
y_axis = y_axis.labels(vec![Span::from(left_label), Span::from(right_label)]);
}
axis_test_case(10, 5, x_axis, y_axis, lines);
};
test_case(
Some(("AAAA", "B")),
None,
Alignment::Left,
vec![
" ",
" ",
" ",
" ───────",
"AAA B",
],
);
test_case(
Some(("A", "BBBB")),
None,
Alignment::Left,
vec![
" ",
" ",
" ",
" ─────────",
"A BBBB",
],
);
test_case(
Some(("AAAAAAAAAAA", "B")),
None,
Alignment::Left,
vec![
" ",
" ",
" ",
" ───────",
"AAA B",
],
);
test_case(
Some(("A", "B")),
Some(("CCCCCCC", "D")),
Alignment::Left,
vec![
"D │ ",
"",
"CCC│ ",
" └──────",
" A B",
],
);
test_case(
Some(("AAAAAAAAAA", "B")),
Some(("C", "D")),
Alignment::Center,
vec![
"D │ ",
"",
"C │ ",
" └──────",
"AAAAAAA B",
],
);
test_case(
Some(("AAAAAAA", "B")),
Some(("C", "D")),
Alignment::Right,
vec![
"D│ ",
"",
"C│ ",
" └────────",
" AAAAA B",
],
);
test_case(
Some(("AAAAAAA", "BBBBBBB")),
Some(("C", "D")),
Alignment::Right,
vec![
"D│ ",
"",
"C│ ",
" └────────",
" AAAAABBBB",
],
);
}
#[test]
fn widgets_chart_handles_x_axis_labels_alignments() {
let test_case = |y_alignment, lines| {
let x_axis = Axis::default()
.labels(vec![Span::from("AAAA"), Span::from("B"), Span::from("C")])
.labels_alignment(y_alignment);
let y_axis = Axis::default();
axis_test_case(10, 5, x_axis, y_axis, lines);
};
test_case(
Alignment::Left,
vec![
" ",
" ",
" ",
" ───────",
"AAA B C",
],
);
test_case(
Alignment::Center,
vec![
" ",
" ",
" ",
" ────────",
"AAAA B C",
],
);
test_case(
Alignment::Right,
vec![
" ",
" ",
" ",
"──────────",
"AAA B C",
],
);
}
#[test]
fn widgets_chart_handles_y_axis_labels_alignments() {
let test_case = |y_alignment, lines| {
let x_axis = Axis::default().labels(create_labels(&["AAAAA", "B"]));
let y_axis = Axis::default()
.labels(create_labels(&["C", "D"]))
.labels_alignment(y_alignment);
axis_test_case(20, 5, x_axis, y_axis, lines);
};
test_case(
Alignment::Left,
vec![
"D │ ",
"",
"C │ ",
" └───────────────",
"AAAAA B",
],
);
test_case(
Alignment::Center,
vec![
" D │ ",
"",
" C │ ",
" └───────────────",
"AAAAA B",
],
);
test_case(
Alignment::Right,
vec![
" D│ ",
"",
" C│ ",
" └───────────────",
"AAAAA B",
],
);
}
#[test]
fn widgets_chart_can_have_axis_with_zero_length_bounds() {
let backend = TestBackend::new(100, 100);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let datasets = vec![Dataset::default()
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta))
.data(&[(0.0, 0.0)])];
let chart = Chart::new(datasets)
.block(Block::default().title("Plot").borders(Borders::ALL))
.x_axis(
Axis::default()
.bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
);
f.render_widget(
chart,
Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
);
})
.unwrap();
}
#[test]
fn widgets_chart_handles_overflows() {
let backend = TestBackend::new(80, 30);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let datasets = vec![Dataset::default()
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta))
.data(&[
(1_588_298_471.0, 1.0),
(1_588_298_473.0, 0.0),
(1_588_298_496.0, 1.0),
])];
let chart = Chart::new(datasets)
.block(Block::default().title("Plot").borders(Borders::ALL))
.x_axis(
Axis::default()
.bounds([1_588_298_471.0, 1_588_992_600.0])
.labels(create_labels(&["1588298471.0", "1588992600.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 1.0])
.labels(create_labels(&["0.0", "1.0"])),
);
f.render_widget(
chart,
Rect {
x: 0,
y: 0,
width: 80,
height: 30,
},
);
})
.unwrap();
}
#[test]
fn widgets_chart_can_have_empty_datasets() {
let backend = TestBackend::new(100, 100);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let datasets = vec![Dataset::default().data(&[]).graph_type(Line)];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Empty Dataset With Line")
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 1.0])
.labels(create_labels(&["0.0", "1.0"])),
);
f.render_widget(
chart,
Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
);
})
.unwrap();
}
#[test]
fn widgets_chart_can_have_a_legend() {
let backend = TestBackend::new(60, 30);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let datasets = vec![
Dataset::default()
.name("Dataset 1")
.style(Style::default().fg(Color::Blue))
.data(&[
(0.0, 0.0),
(10.0, 1.0),
(20.0, 2.0),
(30.0, 3.0),
(40.0, 4.0),
(50.0, 5.0),
(60.0, 6.0),
(70.0, 7.0),
(80.0, 8.0),
(90.0, 9.0),
(100.0, 10.0),
])
.graph_type(Line),
Dataset::default()
.name("Dataset 2")
.style(Style::default().fg(Color::Green))
.data(&[
(0.0, 10.0),
(10.0, 9.0),
(20.0, 8.0),
(30.0, 7.0),
(40.0, 6.0),
(50.0, 5.0),
(60.0, 4.0),
(70.0, 3.0),
(80.0, 2.0),
(90.0, 1.0),
(100.0, 0.0),
])
.graph_type(Line),
];
let chart = Chart::new(datasets)
.style(Style::default().bg(Color::White))
.block(Block::default().title("Chart Test").borders(Borders::ALL))
.x_axis(
Axis::default()
.bounds([0.0, 100.0])
.title(Span::styled("X Axis", Style::default().fg(Color::Yellow)))
.labels(create_labels(&["0.0", "50.0", "100.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 10.0])
.title("Y Axis")
.labels(create_labels(&["0.0", "5.0", "10.0"])),
);
f.render_widget(
chart,
Rect {
x: 0,
y: 0,
width: 60,
height: 30,
},
);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
"┌Chart Test────────────────────────────────────────────────┐",
"│10.0│Y Axis ┌─────────┐│",
"│ │ •• │Dataset 1││",
"│ │ •• │Dataset 2││",
"│ │ •• └─────────┘│",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ ••• •• │",
"│ │ ••• │",
"│5.0 │ •• •• │",
"│ │ •• •• │",
"│ │ ••• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• ••• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│0.0 │• X Axis│",
"│ └─────────────────────────────────────────────────────│",
"│ 0.0 50.0 100.0│",
"└──────────────────────────────────────────────────────────┘",
]);
// Set expected backgound color
for row in 0..30 {
for col in 0..60 {
expected.get_mut(col, row).set_bg(Color::White);
}
}
// Set expected colors of the first dataset
let line1 = vec![
(48, 5),
(49, 5),
(46, 6),
(47, 6),
(44, 7),
(45, 7),
(42, 8),
(43, 8),
(40, 9),
(41, 9),
(38, 10),
(39, 10),
(36, 11),
(37, 11),
(34, 12),
(35, 12),
(33, 13),
(30, 14),
(31, 14),
(28, 15),
(29, 15),
(25, 16),
(26, 16),
(27, 16),
(23, 17),
(24, 17),
(21, 18),
(22, 18),
(19, 19),
(20, 19),
(17, 20),
(18, 20),
(15, 21),
(16, 21),
(13, 22),
(14, 22),
(11, 23),
(12, 23),
(9, 24),
(10, 24),
(7, 25),
(8, 25),
(6, 26),
];
let legend1 = vec![
(49, 2),
(50, 2),
(51, 2),
(52, 2),
(53, 2),
(54, 2),
(55, 2),
(56, 2),
(57, 2),
];
for (col, row) in line1 {
expected.get_mut(col, row).set_fg(Color::Blue);
}
for (col, row) in legend1 {
expected.get_mut(col, row).set_fg(Color::Blue);
}
// Set expected colors of the second dataset
let line2 = vec![
(8, 2),
(9, 2),
(10, 3),
(11, 3),
(12, 4),
(13, 4),
(14, 5),
(15, 5),
(16, 6),
(17, 6),
(18, 7),
(19, 7),
(20, 8),
(21, 8),
(22, 9),
(23, 9),
(24, 10),
(25, 10),
(26, 11),
(27, 11),
(28, 12),
(29, 12),
(30, 12),
(31, 13),
(32, 13),
(33, 14),
(34, 14),
(35, 15),
(36, 15),
(37, 16),
(38, 16),
(39, 17),
(40, 17),
(41, 18),
(42, 18),
(43, 19),
(44, 19),
(45, 20),
(46, 20),
(47, 21),
(48, 21),
(49, 22),
(50, 22),
(51, 23),
(52, 23),
(53, 23),
(54, 24),
(55, 24),
(56, 25),
(57, 25),
];
let legend2 = vec![
(49, 3),
(50, 3),
(51, 3),
(52, 3),
(53, 3),
(54, 3),
(55, 3),
(56, 3),
(57, 3),
];
for (col, row) in line2 {
expected.get_mut(col, row).set_fg(Color::Green);
}
for (col, row) in legend2 {
expected.get_mut(col, row).set_fg(Color::Green);
}
// Set expected colors of the x axis
let x_axis_title = vec![(53, 26), (54, 26), (55, 26), (56, 26), (57, 26), (58, 26)];
for (col, row) in x_axis_title {
expected.get_mut(col, row).set_fg(Color::Yellow);
}
terminal.backend().assert_buffer(&expected);
}

253
tests/widgets_gauge.rs Normal file
View File

@@ -0,0 +1,253 @@
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::Span,
widgets::{Block, Borders, Gauge, LineGauge},
Terminal,
};
#[test]
fn widgets_gauge_renders() {
let backend = TestBackend::new(40, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let gauge = Gauge::default()
.block(Block::default().title("Percentage").borders(Borders::ALL))
.gauge_style(Style::default().bg(Color::Blue).fg(Color::Red))
.use_unicode(true)
.percent(43);
f.render_widget(gauge, chunks[0]);
let gauge = Gauge::default()
.block(Block::default().title("Ratio").borders(Borders::ALL))
.gauge_style(Style::default().bg(Color::Blue).fg(Color::Red))
.use_unicode(true)
.ratio(0.511_313_934_313_1);
f.render_widget(gauge, chunks[1]);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
" ",
" ",
" ┌Percentage────────────────────────┐ ",
" │ ▋43% │ ",
" └──────────────────────────────────┘ ",
" ┌Ratio─────────────────────────────┐ ",
" │ 51% │ ",
" └──────────────────────────────────┘ ",
" ",
" ",
]);
for i in 3..17 {
expected
.get_mut(i, 3)
.set_bg(Color::Red)
.set_fg(Color::Blue);
}
for i in 17..37 {
expected
.get_mut(i, 3)
.set_bg(Color::Blue)
.set_fg(Color::Red);
}
for i in 3..20 {
expected
.get_mut(i, 6)
.set_bg(Color::Red)
.set_fg(Color::Blue);
}
for i in 20..37 {
expected
.get_mut(i, 6)
.set_bg(Color::Blue)
.set_fg(Color::Red);
}
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_gauge_renders_no_unicode() {
let backend = TestBackend::new(40, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let gauge = Gauge::default()
.block(Block::default().title("Percentage").borders(Borders::ALL))
.percent(43)
.use_unicode(false);
f.render_widget(gauge, chunks[0]);
let gauge = Gauge::default()
.block(Block::default().title("Ratio").borders(Borders::ALL))
.ratio(0.211_313_934_313_1)
.use_unicode(false);
f.render_widget(gauge, chunks[1]);
})
.unwrap();
let expected = Buffer::with_lines(vec![
" ",
" ",
" ┌Percentage────────────────────────┐ ",
" │ 43% │ ",
" └──────────────────────────────────┘ ",
" ┌Ratio─────────────────────────────┐ ",
" │ 21% │ ",
" └──────────────────────────────────┘ ",
" ",
" ",
]);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_gauge_applies_styles() {
let backend = TestBackend::new(12, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let gauge = Gauge::default()
.block(
Block::default()
.title(Span::styled("Test", Style::default().fg(Color::Red)))
.borders(Borders::ALL),
)
.gauge_style(Style::default().fg(Color::Blue).bg(Color::Red))
.percent(43)
.label(Span::styled(
"43%",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
));
f.render_widget(gauge, f.size());
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
"┌Test──────┐",
"│ │",
"│ 43% │",
"│ │",
"└──────────┘",
]);
// title
expected.set_style(Rect::new(1, 0, 4, 1), Style::default().fg(Color::Red));
// gauge area
expected.set_style(
Rect::new(1, 1, 10, 3),
Style::default().fg(Color::Blue).bg(Color::Red),
);
// filled area
for y in 1..4 {
expected.set_style(
Rect::new(1, y, 4, 1),
// filled style is invert of gauge_style
Style::default().fg(Color::Red).bg(Color::Blue),
);
}
// label (foreground and modifier from label style)
expected.set_style(
Rect::new(4, 2, 1, 1),
Style::default()
.fg(Color::Green)
// "4" is in the filled area so background is gauge_style foreground
.bg(Color::Blue)
.add_modifier(Modifier::BOLD),
);
expected.set_style(
Rect::new(5, 2, 2, 1),
Style::default()
.fg(Color::Green)
// "3%" is not in the filled area so background is gauge_style background
.bg(Color::Red)
.add_modifier(Modifier::BOLD),
);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_gauge_supports_large_labels() {
let backend = TestBackend::new(10, 1);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let gauge = Gauge::default()
.percent(43)
.label("43333333333333333333333333333%");
f.render_widget(gauge, f.size());
})
.unwrap();
let expected = Buffer::with_lines(vec!["4333333333"]);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_line_gauge_renders() {
let backend = TestBackend::new(20, 4);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let gauge = LineGauge::default()
.gauge_style(Style::default().fg(Color::Green).bg(Color::White))
.ratio(0.43);
f.render_widget(
gauge,
Rect {
x: 0,
y: 0,
width: 20,
height: 1,
},
);
let gauge = LineGauge::default()
.block(Block::default().title("Gauge 2").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Green))
.line_set(symbols::line::THICK)
.ratio(0.211_313_934_313_1);
f.render_widget(
gauge,
Rect {
x: 0,
y: 1,
width: 20,
height: 3,
},
);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
"43% ────────────────",
"┌Gauge 2───────────┐",
"│21% ━━━━━━━━━━━━━━│",
"└──────────────────┘",
]);
for col in 4..10 {
expected.get_mut(col, 0).set_fg(Color::Green);
}
for col in 10..20 {
expected.get_mut(col, 0).set_fg(Color::White);
}
for col in 5..7 {
expected.get_mut(col, 2).set_fg(Color::Green);
}
terminal.backend().assert_buffer(&expected);
}

200
tests/widgets_list.rs Normal file
View File

@@ -0,0 +1,200 @@
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols,
text::Spans,
widgets::{Block, Borders, List, ListItem, ListState},
Terminal,
};
#[test]
fn widgets_list_should_highlight_the_selected_item() {
let backend = TestBackend::new(10, 3);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
state.select(Some(1));
terminal
.draw(|f| {
let size = f.size();
let items = vec![
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
];
let list = List::new(items)
.highlight_style(Style::default().bg(Color::Yellow))
.highlight_symbol(">> ");
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![" Item 1 ", ">> Item 2 ", " Item 3 "]);
for x in 0..10 {
expected.get_mut(x, 1).set_bg(Color::Yellow);
}
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_list_should_truncate_items() {
let backend = TestBackend::new(10, 2);
let mut terminal = Terminal::new(backend).unwrap();
struct TruncateTestCase<'a> {
selected: Option<usize>,
items: Vec<ListItem<'a>>,
expected: Buffer,
}
let cases = vec![
// An item is selected
TruncateTestCase {
selected: Some(0),
items: vec![
ListItem::new("A very long line"),
ListItem::new("A very long line"),
],
expected: Buffer::with_lines(vec![
format!(">> A ve{} ", symbols::line::VERTICAL),
format!(" A ve{} ", symbols::line::VERTICAL),
]),
},
// No item is selected
TruncateTestCase {
selected: None,
items: vec![
ListItem::new("A very long line"),
ListItem::new("A very long line"),
],
expected: Buffer::with_lines(vec![
format!("A very {} ", symbols::line::VERTICAL),
format!("A very {} ", symbols::line::VERTICAL),
]),
},
];
for case in cases {
let mut state = ListState::default();
state.select(case.selected);
terminal
.draw(|f| {
let list = List::new(case.items.clone())
.block(Block::default().borders(Borders::RIGHT))
.highlight_symbol(">> ");
f.render_stateful_widget(list, Rect::new(0, 0, 8, 2), &mut state);
})
.unwrap();
terminal.backend().assert_buffer(&case.expected);
}
}
#[test]
fn widgets_list_should_clamp_offset_if_items_are_removed() {
let backend = TestBackend::new(10, 4);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
// render with 6 items => offset will be at 2
state.select(Some(5));
terminal
.draw(|f| {
let size = f.size();
let items = vec![
ListItem::new("Item 0"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
ListItem::new("Item 4"),
ListItem::new("Item 5"),
];
let list = List::new(items).highlight_symbol(">> ");
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let expected = Buffer::with_lines(vec![" Item 2 ", " Item 3 ", " Item 4 ", ">> Item 5 "]);
terminal.backend().assert_buffer(&expected);
// render again with 1 items => check offset is clamped to 1
state.select(Some(1));
terminal
.draw(|f| {
let size = f.size();
let items = vec![ListItem::new("Item 3")];
let list = List::new(items).highlight_symbol(">> ");
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let expected = Buffer::with_lines(vec![" Item 3 ", " ", " ", " "]);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_list_should_display_multiline_items() {
let backend = TestBackend::new(10, 6);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
state.select(Some(1));
terminal
.draw(|f| {
let size = f.size();
let items = vec![
ListItem::new(vec![Spans::from("Item 1"), Spans::from("Item 1a")]),
ListItem::new(vec![Spans::from("Item 2"), Spans::from("Item 2b")]),
ListItem::new(vec![Spans::from("Item 3"), Spans::from("Item 3c")]),
];
let list = List::new(items)
.highlight_style(Style::default().bg(Color::Yellow))
.highlight_symbol(">> ");
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
" Item 1 ",
" Item 1a",
">> Item 2 ",
" Item 2b",
" Item 3 ",
" Item 3c",
]);
for x in 0..10 {
expected.get_mut(x, 2).set_bg(Color::Yellow);
expected.get_mut(x, 3).set_bg(Color::Yellow);
}
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_list_should_repeat_highlight_symbol() {
let backend = TestBackend::new(10, 6);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
state.select(Some(1));
terminal
.draw(|f| {
let size = f.size();
let items = vec![
ListItem::new(vec![Spans::from("Item 1"), Spans::from("Item 1a")]),
ListItem::new(vec![Spans::from("Item 2"), Spans::from("Item 2b")]),
ListItem::new(vec![Spans::from("Item 3"), Spans::from("Item 3c")]),
];
let list = List::new(items)
.highlight_style(Style::default().bg(Color::Yellow))
.highlight_symbol(">> ")
.repeat_highlight_symbol(true);
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
" Item 1 ",
" Item 1a",
">> Item 2 ",
">> Item 2b",
" Item 3 ",
" Item 3c",
]);
for x in 0..10 {
expected.get_mut(x, 2).set_bg(Color::Yellow);
expected.get_mut(x, 3).set_bg(Color::Yellow);
}
terminal.backend().assert_buffer(&expected);
}

299
tests/widgets_paragraph.rs Normal file
View File

@@ -0,0 +1,299 @@
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::Alignment,
text::{Span, Spans, Text},
widgets::{Block, Borders, Paragraph, Wrap},
Terminal,
};
const SAMPLE_STRING: &str = "The library is based on the principle of immediate rendering with \
intermediate buffers. This means that at each new frame you should build all widgets that are \
supposed to be part of the UI. While providing a great flexibility for rich and \
interactive UI, this may introduce overhead for highly dynamic content.";
#[test]
fn widgets_paragraph_can_wrap_its_content() {
let test_case = |alignment, expected| {
let backend = TestBackend::new(20, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let text = vec![Spans::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.alignment(alignment)
.wrap(Wrap {
trim: true,
break_words: false,
});
f.render_widget(paragraph, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
test_case(
Alignment::Left,
Buffer::with_lines(vec![
"┌──────────────────┐",
"│The library is │",
"│based on the │",
"│principle of │",
"│immediate │",
"│rendering with │",
"│intermediate │",
"│buffers. This │",
"│means that at each│",
"└──────────────────┘",
]),
);
test_case(
Alignment::Right,
Buffer::with_lines(vec![
"┌──────────────────┐",
"│ The library is│",
"│ based on the│",
"│ principle of│",
"│ immediate│",
"│ rendering with│",
"│ intermediate│",
"│ buffers. This│",
"│means that at each│",
"└──────────────────┘",
]),
);
test_case(
Alignment::Center,
Buffer::with_lines(vec![
"┌──────────────────┐",
"│ The library is │",
"│ based on the │",
"│ principle of │",
"│ immediate │",
"│ rendering with │",
"│ intermediate │",
"│ buffers. This │",
"│means that at each│",
"└──────────────────┘",
]),
);
}
#[test]
fn widgets_paragraph_can_wrap_its_content_with_word_break_enabled() {
let test_case = |alignment, expected| {
let backend = TestBackend::new(20, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let text = vec![Spans::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.alignment(alignment)
.wrap(Wrap {
trim: true,
break_words: true,
});
f.render_widget(paragraph, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
test_case(
Alignment::Left,
Buffer::with_lines(vec![
"┌──────────────────┐",
"│The library is bas│",
"│ed on the principl│",
"│e of immediate ren│",
"│dering with interm│",
"│ediate buffers. Th│",
"│is means that at e│",
"│ach new frame you │",
"│should build all w│",
"└──────────────────┘",
]),
);
test_case(
Alignment::Right,
Buffer::with_lines(vec![
"┌──────────────────┐",
"│The library is bas│",
"│ed on the principl│",
"│e of immediate ren│",
"│dering with interm│",
"│ediate buffers. Th│",
"│is means that at e│",
"│ach new frame you │",
"│should build all w│",
"└──────────────────┘",
]),
);
test_case(
Alignment::Center,
Buffer::with_lines(vec![
"┌──────────────────┐",
"│The library is bas│",
"│ed on the principl│",
"│e of immediate ren│",
"│dering with interm│",
"│ediate buffers. Th│",
"│is means that at e│",
"│ach new frame you │",
"│should build all w│",
"└──────────────────┘",
]),
);
}
#[test]
fn widgets_paragraph_renders_double_width_graphemes() {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend).unwrap();
let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、";
terminal
.draw(|f| {
let size = f.size();
let text = vec![Spans::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(Wrap {
trim: true,
break_words: false,
});
f.render_widget(paragraph, size);
})
.unwrap();
let expected = Buffer::with_lines(vec![
"┌────────┐",
"│コンピュ│",
"│ータ上で│",
"│文字を扱│",
"│う場合、│",
"│典型的に│",
"│は文字に│",
"│よる通信│",
"│を行う場│",
"└────────┘",
]);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_paragraph_renders_mixed_width_graphemes() {
let backend = TestBackend::new(10, 7);
let mut terminal = Terminal::new(backend).unwrap();
let s = "aコンピュータ上で文字を扱う場合、";
terminal
.draw(|f| {
let size = f.size();
let text = vec![Spans::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(Wrap {
trim: true,
break_words: false,
});
f.render_widget(paragraph, size);
})
.unwrap();
let expected = Buffer::with_lines(vec![
// The internal width is 8 so only 4 slots for double-width characters.
"┌────────┐",
"│aコンピ │", // Here we have 1 latin character so only 3 double-width ones can fit.
"│ュータ上│",
"│で文字を│",
"│扱う場合│",
"│、 │",
"└────────┘",
]);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_paragraph_can_wrap_with_a_trailing_nbsp() {
let nbsp: &str = "\u{00a0}";
let line = Spans::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]);
let backend = TestBackend::new(20, 3);
let mut terminal = Terminal::new(backend).unwrap();
let expected = Buffer::with_lines(vec![
"┌──────────────────┐",
"│NBSP\u{00a0}",
"└──────────────────┘",
]);
terminal
.draw(|f| {
let size = f.size();
let paragraph = Paragraph::new(line).block(Block::default().borders(Borders::ALL));
f.render_widget(paragraph, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_paragraph_can_scroll_horizontally() {
let test_case = |alignment, scroll, expected| {
let backend = TestBackend::new(20, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let text = Text::from(
"段落现在可以水平滚动了!\nParagraph can scroll horizontally!\nShort line",
);
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.alignment(alignment)
.scroll(scroll);
f.render_widget(paragraph, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
test_case(
Alignment::Left,
(0, 7),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│在可以水平滚动了!│",
"│ph can scroll hori│",
"│ine │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└──────────────────┘",
]),
);
// only support Alignment::Left
test_case(
Alignment::Right,
(0, 7),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│段落现在可以水平滚│",
"│Paragraph can scro│",
"│ Short line│",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└──────────────────┘",
]),
);
}

823
tests/widgets_table.rs Normal file
View File

@@ -0,0 +1,823 @@
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::Constraint,
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, Cell, Row, Table, TableState},
Terminal,
};
#[test]
fn widgets_table_column_spacing_can_be_changed() {
let test_case = |column_spacing, expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(column_spacing);
f.render_widget(table, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
// no space between columns
test_case(
0,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1Head2Head3 │",
"│ │",
"│Row11Row12Row13 │",
"│Row21Row22Row23 │",
"│Row31Row32Row33 │",
"│Row41Row42Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// one space between columns
test_case(
1,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// enough space to just not hide the third column
test_case(
6,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// enough space to hide part of the third column
test_case(
7,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head│",
"│ │",
"│Row11 Row12 Row1│",
"│Row21 Row22 Row2│",
"│Row31 Row32 Row3│",
"│Row41 Row42 Row4│",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
}
#[test]
fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
let test_case = |widths, expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths);
f.render_widget(table, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
// columns of zero width show nothing
test_case(
&[
Constraint::Length(0),
Constraint::Length(0),
Constraint::Length(0),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// columns of 1 width trim
test_case(
&[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│H H H │",
"│ │",
"│R R R │",
"│R R R │",
"│R R R │",
"│R R R │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// columns of large width just before pushing a column off
test_case(
&[
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(8),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
}
#[test]
fn widgets_table_columns_widths_can_use_percentage_constraints() {
let test_case = |widths, expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths)
.column_spacing(0);
f.render_widget(table, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
// columns of zero width show nothing
test_case(
&[
Constraint::Percentage(0),
Constraint::Percentage(0),
Constraint::Percentage(0),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// columns of not enough width trims the data
test_case(
&[
Constraint::Percentage(11),
Constraint::Percentage(11),
Constraint::Percentage(11),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│HeaHeaHea │",
"│ │",
"│RowRowRow │",
"│RowRowRow │",
"│RowRowRow │",
"│RowRowRow │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// columns of large width just before pushing a column off
test_case(
&[
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(33),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// percentages summing to 100 should give equal widths
test_case(
&[Constraint::Percentage(50), Constraint::Percentage(50)],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 │",
"│ │",
"│Row11 Row12 │",
"│Row21 Row22 │",
"│Row31 Row32 │",
"│Row41 Row42 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
}
#[test]
fn widgets_table_columns_widths_can_use_mixed_constraints() {
let test_case = |widths, expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths);
f.render_widget(table, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
// columns of zero width show nothing
test_case(
&[
Constraint::Percentage(0),
Constraint::Length(0),
Constraint::Percentage(0),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// columns of not enough width trims the data
test_case(
&[
Constraint::Percentage(11),
Constraint::Length(20),
Constraint::Percentage(11),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Hea Head2 He │",
"│ │",
"│Row Row12 Ro │",
"│Row Row22 Ro │",
"│Row Row32 Ro │",
"│Row Row42 Ro │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// columns of large width just before pushing a column off
test_case(
&[
Constraint::Percentage(33),
Constraint::Length(10),
Constraint::Percentage(33),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// columns of large size (>100% total) hide the last column
test_case(
&[
Constraint::Percentage(60),
Constraint::Length(10),
Constraint::Percentage(60),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 │",
"│ │",
"│Row11 Row12 │",
"│Row21 Row22 │",
"│Row31 Row32 │",
"│Row41 Row42 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
}
#[test]
fn widgets_table_columns_widths_can_use_ratio_constraints() {
let test_case = |widths, expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths)
.column_spacing(0);
f.render_widget(table, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
// columns of zero width show nothing
test_case(
&[
Constraint::Ratio(0, 1),
Constraint::Ratio(0, 1),
Constraint::Ratio(0, 1),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// columns of not enough width trims the data
test_case(
&[
Constraint::Ratio(1, 9),
Constraint::Ratio(1, 9),
Constraint::Ratio(1, 9),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│HeaHeaHea │",
"│ │",
"│RowRowRow │",
"│RowRowRow │",
"│RowRowRow │",
"│RowRowRow │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// columns of large width just before pushing a column off
test_case(
&[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// percentages summing to 100 should give equal widths
test_case(
&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 │",
"│ │",
"│Row11 Row12 │",
"│Row21 Row22 │",
"│Row31 Row32 │",
"│Row41 Row42 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
}
#[test]
fn widgets_table_can_have_rows_with_multi_lines() {
let test_case = |state: &mut TableState, expected: Buffer| {
let backend = TestBackend::new(30, 8);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1);
f.render_stateful_widget(table, size, state);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
let mut state = TableState::default();
// no selection
test_case(
&mut state,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│ │",
"│Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select first
state.select(Some(0));
test_case(
&mut state,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│>> Row11 Row12 Row13 │",
"│ Row21 Row22 Row23 │",
"│ │",
"│ Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select second (we don't show partially the 4th row)
state.select(Some(1));
test_case(
&mut state,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│ Row11 Row12 Row13 │",
"│>> Row21 Row22 Row23 │",
"│ │",
"│ Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select 4th (we don't show partially the 1st row)
state.select(Some(3));
test_case(
&mut state,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│ Row31 Row32 Row33 │",
"│>> Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
}
#[test]
fn widgets_table_can_have_elements_styled_individually() {
let backend = TestBackend::new(30, 4);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = TableState::default();
state.select(Some(0));
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]).style(Style::default().fg(Color::Green)),
Row::new(vec![
Cell::from("Row21"),
Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
Cell::from(Spans::from(vec![
Span::raw("Row"),
Span::styled("23", Style::default().fg(Color::Blue)),
]))
.style(Style::default().fg(Color::Red)),
])
.style(Style::default().fg(Color::LightGreen)),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.highlight_symbol(">> ")
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.widths(&[
Constraint::Length(6),
Constraint::Length(6),
Constraint::Length(6),
])
.column_spacing(1);
f.render_stateful_widget(table, size, &mut state);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
"│ Head1 Head2 Head3 │",
"│ │",
"│>> Row11 Row12 Row13 │",
"│ Row21 Row22 Row23 │",
]);
// First row = row color + highlight style
for col in 1..=28 {
expected.get_mut(col, 2).set_style(
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
);
}
// Second row:
// 1. row color
for col in 1..=28 {
expected
.get_mut(col, 3)
.set_style(Style::default().fg(Color::LightGreen));
}
// 2. cell color
for col in 11..=16 {
expected
.get_mut(col, 3)
.set_style(Style::default().fg(Color::Yellow));
}
for col in 18..=23 {
expected
.get_mut(col, 3)
.set_style(Style::default().fg(Color::Red));
}
// 3. text color
for col in 21..=22 {
expected
.get_mut(col, 3)
.set_style(Style::default().fg(Color::Blue));
}
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_table_should_render_even_if_empty() {
let backend = TestBackend::new(30, 4);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![])
.header(Row::new(vec!["Head1", "Head2", "Head3"]))
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.widths(&[
Constraint::Length(6),
Constraint::Length(6),
Constraint::Length(6),
])
.column_spacing(1);
f.render_widget(table, size);
})
.unwrap();
let expected = Buffer::with_lines(vec![
"│Head1 Head2 Head3 │",
"│ │",
"│ │",
"│ │",
]);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_table_columns_dont_panic() {
let test_case = |state: &mut TableState, table: Table, width: u16| {
let backend = TestBackend::new(width, 8);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(table, size, state);
})
.unwrap();
};
// based on https://github.com/fdehau/tui-rs/issues/470#issuecomment-852562848
let table1_width = 98;
let table1 = Table::new(vec![Row::new(vec!["r1", "r2", "r3", "r4"])])
.header(Row::new(vec!["h1", "h2", "h3", "h4"]))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.column_spacing(1)
.widths(&[
Constraint::Percentage(15),
Constraint::Percentage(15),
Constraint::Percentage(25),
Constraint::Percentage(45),
]);
let mut state = TableState::default();
// select first, which would cause a panic before fix
state.select(Some(0));
test_case(&mut state, table1.clone(), table1_width);
}
#[test]
fn widgets_table_should_clamp_offset_if_rows_are_removed() {
let backend = TestBackend::new(30, 8);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = TableState::default();
// render with 6 items => offset will be at 2
state.select(Some(5));
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row01", "Row02", "Row03"]),
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
Row::new(vec!["Row51", "Row52", "Row53"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1);
f.render_stateful_widget(table, size, &mut state);
})
.unwrap();
let expected = Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│Row51 Row52 Row53 │",
"└────────────────────────────┘",
]);
terminal.backend().assert_buffer(&expected);
// render with 1 item => offset will be at 1
state.select(Some(1));
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![Row::new(vec!["Row31", "Row32", "Row33"])])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1);
f.render_stateful_widget(table, size, &mut state);
})
.unwrap();
let expected = Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row31 Row32 Row33 │",
"│ │",
"│ │",
"│ │",
"└────────────────────────────┘",
]);
terminal.backend().assert_buffer(&expected);
}

48
tests/widgets_tabs.rs Normal file
View File

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