Compare commits

...

82 Commits

Author SHA1 Message Date
Florian Dehau
f5d7f70472 Release v0.4.0 2019-02-03 23:03:48 +01:00
Florian Dehau
0168442c22 chore: remove typos 2019-02-03 22:42:09 +01:00
Florian Dehau
22579b77cc chore(Makefile): make run-examples compile the examples in release mode 2019-02-03 22:42:09 +01:00
Florian Dehau
09c09d2fd1 fix(examples): remove logging in layout example 2019-02-03 22:42:09 +01:00
Florian Dehau
b669cf9ce7 style: fix clippy warnings 2019-02-03 22:42:09 +01:00
Florian Dehau
5bc617a9a6 chore(Makefile): build and test using all features 2019-02-03 22:42:09 +01:00
Florian Dehau
a75b811061 chore: bump itertools to 0.8 2019-02-03 22:42:09 +01:00
Florian Dehau
ec6b46324e feat(examples): add cmd line args to the demo 2019-02-03 22:42:09 +01:00
Florian Dehau
97f764b45d feat: handle crossterm errors 2019-02-03 20:02:36 +01:00
Florian Dehau
7f31a55506 chore: show appveyor build status 2019-02-03 19:00:49 +01:00
Florian Dehau
2286d097dc chore(ci): add appveyor config 2019-02-03 18:57:42 +01:00
Florian Dehau
52a40ec99a fix: remove undefined crossterm attributes in windows builds 2019-01-23 07:28:40 +01:00
Sven-Hendrik Haase
a78fa73b34 Add new shape: Rectangle 2019-01-15 15:47:05 +00:00
Sven-Hendrik Haase
d7e4a252fb Mention crossterm in README 2019-01-15 15:46:48 +00:00
Jens Krause
1c0b0abf61 Use UnicodeWidthStr::width()
to get width of `divider`.

Also use `set_string` instead of `set_symbol`. The latter cuts content of a multi-char divider.
2019-01-13 17:21:03 +00:00
Jens Krause
f7c6620e25 Fix documented example to fix doc-tests on CI 2019-01-13 17:21:03 +00:00
Jens Krause
16372f7847 Don't show divider after last tab 2019-01-13 17:21:03 +00:00
Jens Krause
72c2eb7182 Add divider to Tabs
to change appearance of tab dividers.
2019-01-13 17:21:03 +00:00
Sven-Hendrik Haase
144bfb71cf Upgrade to 2018 edition 2019-01-13 14:35:51 +00:00
Karoline Pauls
3fd9e23851 Buffer: correct diffing of buffers with multi-width characters
Resolves #104
2019-01-13 13:46:39 +00:00
Karoline Pauls
10642d0e04 Paragraph: word wrapping 2018-12-22 15:38:51 +01:00
Karoline Pauls
063ab2f87d Improve the Paragraph example 2018-12-22 15:38:51 +01:00
Karoline Pauls
1802cf8dbc Improve wrapping of double-width characters 2018-12-22 15:38:51 +01:00
Karoline Pauls
090975481b Update tests and docs to take size from the Frame 2018-12-07 21:32:00 +01:00
Karoline Pauls
228816f5f8 Frame: provide consistent size for rendering 2018-12-07 21:32:00 +01:00
Karoline Pauls
8522e028f1 Run cargo fmt with the new Rust stable toolchain (1.31.0) 2018-12-07 19:54:13 +01:00
Ash
a2776dfc86 Make sure we always emit a cursor goto for the first update.
Otherwise, if the first update is to (1, 0) then no goto occurs.
2018-12-07 19:52:22 +01:00
Karoline Pauls
cc95c8cfb0 Gauge: use f64 internally and allow to set any f64 between 0 and 1 2018-12-05 21:20:12 +01:00
Karoline Pauls
89dac9d2a6 buffer: add quotes to fmt::Debug for better testing experience 2018-12-05 21:20:12 +01:00
Karoline Pauls
8cdfc883b9 Feature: Autoresize
It basically never makes sense to render without syncing the size.

Without resizing, if shrinking, we get artefacts. If growing, we may get
panics (before this change the Rustbox sample (the only one which didn't
handle resizing on its own) panicked because the widget would get an
updated size, while the terminal would not).
2018-12-04 08:39:32 +01:00
Florian Dehau
b3689eceb7 feat: update outdated dependencies 2018-11-25 21:49:37 +01:00
Karoline Pauls
5cee2afc6d Limit Rect size to prevent u16 overflow 2018-11-25 19:59:12 +01:00
Karoline Pauls
50fef0fb26 Fix rustbox example 2018-11-25 19:59:12 +01:00
Florian Dehau
4c46ef69e9 Release v0.3.0 2018-11-04 20:25:07 +01:00
Florian Dehau
22e8fade7e feat: add experimental test backend 2018-11-04 20:16:10 +01:00
Florian Dehau
37aa06f508 style(examples): rustfmt 2018-11-04 19:04:51 +01:00
Florian Dehau
f6d2f8f929 feat(examples): use generic backend in draw functions 2018-11-04 18:49:30 +01:00
Florian Dehau
32947669d5 feat(examples): show how to move the cursor 2018-11-04 18:32:31 +01:00
Florian Dehau
fdf3015ad0 feat(terminal): log error if failed to show cursor on drop 2018-10-14 17:00:13 +02:00
Karoline Pauls
03bfcde147 [widgets][paragraph]: Truncate long lines when wrap is false 2018-10-14 16:11:28 +02:00
Florian Dehau
56fc43400a Release v0.3.0-beta.3 2018-09-24 08:09:00 +02:00
Florian Dehau
7b4d35d224 feat: restore the cursor state on terminal drop 2018-09-24 08:03:52 +02:00
Florian Dehau
a99fc115f8 Release v0.3.0-beta.2 2018-09-23 21:16:32 +02:00
Florian Dehau
d8e5f57d53 style: fmt 2018-09-23 21:00:36 +02:00
Florian Dehau
aa85e597d9 fix(crossterm): fix goto coordinates 2018-09-23 21:00:18 +02:00
Florian Dehau
08ab92da80 refactor: clean examples
* Introduce a common event handler in order to focus on the drawing part
* Remove deprecated custom termion backends
2018-09-23 20:59:51 +02:00
Florian Dehau
5d52fd2486 refactor: remove custom termion backends 2018-09-23 20:55:50 +02:00
Florian Dehau
4ae9850e13 fix: replace links to assets 2018-09-09 08:55:51 +02:00
Florian Dehau
e14190ae4b fix: update crossterm example 2018-09-09 08:54:12 +02:00
Florian Dehau
ce445a8096 chore: remove scripts 2018-09-09 08:53:37 +02:00
Florian Dehau
dd71d6471c Release v0.3.0-beta.1 2018-09-08 09:23:22 +02:00
Antoine Büsch
f795173886 Unify Item and Text 2018-09-08 08:41:57 +02:00
Antoine Büsch
e42ab1fed8 Move Text to widgets/mod.rs 2018-09-08 08:41:57 +02:00
Antoine Büsch
0544c023f5 Rename Text::{Data -> Raw, StyledData -> Styled} 2018-09-08 08:41:57 +02:00
Antoine Büsch
ff47f9480b Introduce builder methods for Text to make it more ergonomic 2018-09-08 08:41:57 +02:00
Antoine Büsch
70561b7c54 Fix examples and doctests 2018-09-08 08:41:57 +02:00
Antoine Büsch
559c9c75f3 Make Text accept both borrowed and owned strings 2018-09-08 08:41:57 +02:00
Florian Dehau
6c69160d6b feat: remove unecessary borrows of Style 2018-09-07 22:24:52 +02:00
Florian Dehau
d0cee47e22 Release v0.3.0-beta.0 2018-09-04 22:52:18 +02:00
Florian Dehau
ccebb56a83 chore(Cargo): update dependencies 2018-09-04 22:23:44 +02:00
Florian Dehau
cf169d1582 style: run rustfmt and clippy 2018-09-04 22:23:44 +02:00
Florian Dehau
bcd1e30376 refactor: update List select behavior
* allow a selectable list to have no selected item
* show highlight_symbol only when something is selected
2018-09-04 22:23:44 +02:00
Florian Dehau
40bad7a718 feat: add initial support for crossterm 2018-09-04 22:23:44 +02:00
Florian Dehau
3d63f9607f doc: update main documentation 2018-09-04 22:23:44 +02:00
Florian Dehau
13e194cd26 refactor: update widgets
* all widgets use the consumable builder pattern
* `draw` on terminal expect a closure that take a frame as only arg
2018-09-04 22:23:44 +02:00
Florian Dehau
d6016788ef refactor: clippy + rustfmt 2018-09-04 22:23:44 +02:00
Florian Dehau
ad602a54bf refactor(widgets): replace text rendering in Paragraph
* remove custom markup language
* add Text container for both raw and styled strings
2018-09-04 22:23:44 +02:00
Florian Dehau
7181970a32 feat: split layout from rendering
* remove layout logic from Terminal
* replace Group with Layout
* add Frame intermediate object
2018-09-04 22:23:44 +02:00
Jeremy Day
cfc90ab7f6 fix(widgets): Prevent chart legend from rendering when no dataset has a name 2018-08-24 06:27:16 +02:00
Florian Dehau
05c96eaa28 Release v0.2.3 2018-06-09 11:49:44 +02:00
Florian Dehau
9a9f49f467 fix(backend): Add missing color pattern 2018-06-09 11:49:44 +02:00
Florian Dehau
c552ae98b4 chore(README): Add link to third-party widgets and other crates 2018-06-09 11:32:49 +02:00
Florian Dehau
df7493fd33 style: Run rustfmt 2018-06-09 11:26:59 +02:00
Florian Dehau
5de571fb03 feat(widgets): Add start_corner option to List 2018-06-09 11:26:59 +02:00
Florian Dehau
62df7badf3 feat(layout): Add Corner enum 2018-06-09 11:26:59 +02:00
Robin Nehls
597e219257 [examples] update paragraph example to show text alignment 2018-05-25 21:09:27 +02:00
Robin Nehls
3f8a9079ee [widgets] implement text alignment for paragraph widget 2018-05-25 21:09:27 +02:00
Robin Nehls
5981767543 [style] add enum for text alignment 2018-05-25 21:09:27 +02:00
Florian Dehau
36146d970a [style] rustfmt 2018-05-25 07:57:00 +02:00
Florian Dehau
464ba4f334 travis: check style on stable only 2018-05-06 15:54:47 +02:00
Florian Dehau
36a5eb2110 Format code 2018-05-06 15:54:47 +02:00
Florian Dehau
55840210c7 Simplify travis configuration 2018-05-06 15:54:47 +02:00
61 changed files with 8808 additions and 7673 deletions

View File

@@ -5,18 +5,17 @@ rust:
- beta
- nightly
env:
- NO_RUSTUP=1
cache: cargo
matrix:
fast_finish: true
allow_failures:
- rust: nightly
- rust: beta
before_script:
- ./scripts/travis/before_script.sh
- rustup component add rustfmt-preview
script:
- ./scripts/travis/script.sh
- if [ "$TRAVIS_RUST_VERSION" == "stable" ]; then make fmt; fi
- make build
- make test

View File

@@ -2,6 +2,105 @@
## To be released
## v0.4.0 - 2019-02-03
### Added
* Add a new canvas shape: `Rectangle`.
* Official support of `Crossterm` backend.
* Make it possible to choose the divider between `Tabs`.
* Add word wrapping on Paragraph.
* The gauge widget accepts a ratio (f64 between 0 and 1) in addition of a
percentage.
### Changed
* Upgrade to Rust 2018 edition.
### Fixed
* Fix rendering of double-width characters.
* Fix race condition on the size of the terminal and expose a size that is
safe to use when drawing through `Frame::size`.
* Prevent unsigned int overflow on large screens.
## v0.3.0 - 2018-11-04
### Added
* Add experimental test backend
## v0.3.0-beta.3 - 2018-09-24
### Changed
* `show_cursor` is called when `Terminal` is dropped if the cursor is hidden.
## v0.3.0-beta.2 - 2018-09-23
### Changed
* Remove custom `termion` backends. This is motivated by the fact that
`termion` structs are meant to be combined/wrapped to provide additional
functionalities to the terminal (e.g AlternateScreen, Mouse support, ...).
Thus providing exclusive types do not make a lot of sense and give a false
hint that additional features cannot be used together. The recommended
approach is now to create your own version of `stdout`:
```rust
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
```
and then to create the corresponding `termion` backend:
```rust
let backend = TermionBackend::new(stdout);
```
The resulting code is more verbose but it works with all combinations of
additional `termion` features.
## v0.3.0-beta.1 - 2018-09-08
### Changed
* Replace `Item` by a generic and flexible `Text` that can be used in both
`Paragraph` and `List` widgets.
* Remove unecessary borrows on `Style`.
## v0.3.0-beta.0 - 2018-09-04
### Added
* Add a basic `Crossterm` backend
### Changed
* Remove `Group` and introduce `Layout` in its place
- `Terminal` is no longer required to compute a layout
- `Size` has been renamed `Constraint`
* Widgets are rendered on a `Frame` instead of a `Terminal` in order to
avoid mixing `draw` and `render` calls
* `draw` on `Terminal` expects a closure where the UI is built by rendering
widgets on the given `Frame`
* Update `Widget` trait
- `draw` takes area by value
- `render` takes a `Frame` instead of a `Terminal`
* All widgets use the consumable builder pattern
* `SelectableList` can have no selected item and the highlight symbol is hidden
in this case
* Remove markup langage inside `Paragraph`. `Paragraph` now expects an iterator
of `Text` items
## v0.2.3 - 2018-06-09
### Added
* Add `start_corner` option for `List`
* Add more text aligment options for `Paragraph`
## v0.2.2 - 2018-05-06
### Added

View File

@@ -1,6 +1,6 @@
[package]
name = "tui"
version = "0.2.2"
version = "0.4.0"
authors = ["Florian Dehau <work@fdehau.com>"]
description = """
A library to build rich terminal user interfaces or dashboards
@@ -8,62 +8,34 @@ A library to build rich terminal user interfaces or dashboards
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/fdehau/tui-rs"
license = "MIT"
exclude = ["docs/*", ".travis.yml"]
exclude = ["assets/*", ".travis.yml"]
autoexamples = true
edition = "2018"
[badges]
travis-ci = { repository = "fdehau/tui-rs" }
appveyor = { repository = "fdehau/tui-rs" }
[features]
default = ["termion"]
[dependencies]
bitflags = "1.0.1"
cassowary = "0.3.0"
log = "0.4.1"
unicode-segmentation = "1.2.0"
unicode-width = "0.1.4"
termion = { version = "1.5.1", optional = true }
rustbox = { version = "0.11.0", optional = true }
bitflags = "1.0"
cassowary = "0.3"
itertools = "0.8"
log = "0.4"
either = "1.5"
unicode-segmentation = "1.2"
unicode-width = "0.1"
termion = { version = "1.5", optional = true }
rustbox = { version = "0.11", optional = true }
crossterm = { version = "0.6", optional = true }
[dev-dependencies]
stderrlog = "0.3.0"
rand = "0.4.2"
[[example]]
name = "barchart"
path = "examples/barchart.rs"
[[example]]
name = "block"
path = "examples/block.rs"
[[example]]
name = "canvas"
path = "examples/canvas.rs"
[[example]]
name = "chart"
path = "examples/chart.rs"
[[example]]
name = "custom_widget"
path = "examples/custom_widget.rs"
[[example]]
name = "demo"
path = "examples/demo.rs"
[[example]]
name = "gauge"
path = "examples/gauge.rs"
[[example]]
name = "list"
path = "examples/list.rs"
[[example]]
name = "paragraph"
path = "examples/paragraph.rs"
stderrlog = "0.4"
rand = "0.6"
failure = "0.1"
structopt = "0.2"
[[example]]
name = "rustbox"
@@ -71,21 +43,6 @@ path = "examples/rustbox.rs"
required-features = ["rustbox"]
[[example]]
name = "sparkline"
path = "examples/sparkline.rs"
[[example]]
name = "table"
path = "examples/table.rs"
[[example]]
name = "tabs"
path = "examples/tabs.rs"
[[example]]
name = "user_input"
path = "examples/user_input.rs"
[[example]]
name = "layout"
path = "examples/layout.rs"
name = "crossterm"
path = "examples/crossterm.rs"
required-features = ["crossterm"]

View File

@@ -1,5 +1,4 @@
# Makefile for the tui-rs project (https://github.com/fdehau/tui-rs)
SHELL=/bin/bash
# ================================ Cargo ======================================
@@ -21,67 +20,64 @@ 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}'
# ================================ Tools ======================================
install-tools: install-rustfmt install-clippy ## Install tools dependencies
INSTALL_RUSTFMT = ./scripts/tools/install.sh --name=rustfmt-nightly
ifndef CI
INSTALL_RUSTFMT += --channel=nightly
endif
install-rustfmt: ## Intall rustfmt
$(INSTALL_RUSTFMT)
INSTALL_CLIPPY = ./scripts/tools/install.sh --name=clippy
ifndef CI
INSTALL_CLIPPY += --channel=nightly
endif
install-clippy: ## Intall rustfmt
$(INSTALL_CLIPPY)
# =============================== 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 =======================================
RUSTFMT_WRITEMODE ?= 'diff'
.PHONY: lint
lint: fmt clippy ## Lint project files
.PHONY: fmt
fmt: ## Check the format of the source code
cargo fmt -- --write-mode=$(RUSTFMT_WRITEMODE)
cargo fmt --all -- --check
clippy: RUST_CHANNEL = nightly
.PHONY: clippy
clippy: ## Check the style of the source code and catch common errors
$(CARGO) clippy --features="termion rustbox"
$(CARGO) clippy --all-features
# ================================ Test =======================================
.PHONY: test
test: ## Run the tests
$(CARGO) test
$(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
@@ -90,22 +86,28 @@ doc: ## Build the documentation (available at ./target/doc)
# 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 test ## Run build and tests for stable
.PHONY: beta
beta: RUST_CHANNEL = beta
beta: build test ## Run build and tests for beta
.PHONY: nightly
nightly: RUST_CHANNEL = nightly
nightly: install-tools build lint test ## Run build, lint and tests for nightly
nightly: build lint test ## Run build, lint and tests for nightly

View File

@@ -1,23 +1,25 @@
# tui-rs
[![Build Status](https://travis-ci.org/fdehau/tui-rs.svg?branch=master)](https://travis-ci.org/fdehau/tui-rs)
[![Build status](https://ci.appveyor.com/api/projects/status/t724mb1q31xpyxy5/branch/master?svg=true)](https://ci.appveyor.com/project/fdehau/tui-rs/branch/master)
[![Crate Status](https://img.shields.io/crates/v/tui.svg)](https://crates.io/crates/tui)
[![Docs Status](https://docs.rs/tui/badge.svg)](https://docs.rs/crate/tui/)
<img src="./docs/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
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 two different backends to draw to the terminal. You
The library itself supports three different backends to draw to the terminal. You
can either choose from:
- [termion](https://github.com/ticki/termion)
- [rustbox](https://github.com/gchp/rustbox)
- [crossterm](https://github.com/TimonPost/crossterm)
However, some features may only be available in one of the two.
However, some features may only be available in one of the three.
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,6 +34,10 @@ you may rely on the previously cited libraries to achieve such features.
### [Documentation](https://docs.rs/tui)
### Demo
The [source code](examples/demo.rs) of the demo gif.
### Widgets
The library comes with the following list of widgets:
@@ -50,14 +56,15 @@ The library comes with the following list of widgets:
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`.
### Demo
### Third-party widgets
The [source code](examples/demo.rs) of the demo gif.
* [tui-logger](https://github.com/gin66/tui-logger)
### Alternatives
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an
alternative solution to build text user interfaces in Rust.
## License
[MIT](LICENSE)
## Author
Florian Dehau

19
appveyor.yml Normal file
View File

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

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -1,29 +1,27 @@
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use std::io;
use std::thread;
use std::time;
use std::sync::mpsc;
use termion::event;
use termion::input::TermRead;
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{BarChart, Block, Borders, Widget};
use tui::layout::{Direction, Group, Rect, Size};
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{BarChart, Block, Borders, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
struct App<'a> {
size: Rect,
data: Vec<(&'a str, u64)>,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
size: Rect::default(),
data: vec![
("B1", 9),
("B2", 12),
@@ -53,112 +51,77 @@ impl<'a> App<'a> {
}
}
fn advance(&mut self) {
fn update(&mut self) {
let value = self.data.pop().unwrap();
self.data.insert(0, value);
}
}
enum Event {
Input(event::Key),
Tick,
}
fn main() {
fn main() -> Result<(), failure::Error> {
// Terminal initialization
let backend = MouseBackend::new().unwrap();
let mut terminal = Terminal::new(backend).unwrap();
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()?;
// Channels
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
let clock_tx = tx.clone();
// Input
thread::spawn(move || {
let stdin = io::stdin();
for c in stdin.keys() {
let evt = c.unwrap();
input_tx.send(Event::Input(evt)).unwrap();
if evt == event::Key::Char('q') {
break;
}
}
});
// Tick
thread::spawn(move || loop {
clock_tx.send(Event::Tick).unwrap();
thread::sleep(time::Duration::from_millis(500));
});
// Setup event handlers
let events = Events::new();
// App
let mut app = App::new();
// First draw call
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
app.size = terminal.size().unwrap();
draw(&mut terminal, &app);
loop {
let size = terminal.size().unwrap();
if app.size != size {
terminal.resize(size).unwrap();
app.size = size;
}
let evt = rx.recv().unwrap();
match evt {
Event::Input(input) => if input == event::Key::Char('q') {
break;
},
Event::Tick => {
app.advance();
}
}
draw(&mut terminal, &app);
}
terminal.show_cursor().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
Group::default()
.direction(Direction::Vertical)
.margin(2)
.sizes(&[Size::Percent(50), Size::Percent(50)])
.render(t, &app.size, |t, chunks| {
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());
BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
.bar_width(9)
.style(Style::default().fg(Color::Yellow))
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow))
.render(t, &chunks[0]);
Group::default()
.direction(Direction::Horizontal)
.sizes(&[Size::Percent(50), Size::Percent(50)])
.render(t, &chunks[1], |t, chunks| {
BarChart::default()
.block(Block::default().title("Data2").borders(Borders::ALL))
.data(&app.data)
.bar_width(5)
.bar_gap(3)
.style(Style::default().fg(Color::Green))
.value_style(Style::default().bg(Color::Green).modifier(Modifier::Bold))
.render(t, &chunks[0]);
BarChart::default()
.block(Block::default().title("Data3").borders(Borders::ALL))
.data(&app.data)
.style(Style::default().fg(Color::Red))
.bar_width(7)
.bar_gap(0)
.value_style(Style::default().bg(Color::Red))
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::Italic))
.render(t, &chunks[1]);
})
});
.render(&mut f, chunks[0]);
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
BarChart::default()
.block(Block::default().title("Data2").borders(Borders::ALL))
.data(&app.data)
.bar_width(5)
.bar_gap(3)
.style(Style::default().fg(Color::Green))
.value_style(Style::default().bg(Color::Green).modifier(Modifier::Bold))
.render(&mut f, chunks[0]);
BarChart::default()
.block(Block::default().title("Data3").borders(Borders::ALL))
.data(&app.data)
.style(Style::default().fg(Color::Red))
.bar_width(7)
.bar_gap(0)
.value_style(Style::default().bg(Color::Red))
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::Italic))
.render(&mut f, chunks[1]);
}
})?;
t.draw().unwrap();
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
}
}
Event::Tick => {
app.update();
}
}
}
Ok(())
}

View File

@@ -1,83 +1,88 @@
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use std::io;
use termion::event;
use termion::input::TermRead;
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{Block, Borders, Widget};
use tui::layout::{Direction, Group, Rect, Size};
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Widget};
use tui::Terminal;
fn main() {
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
let stdin = io::stdin();
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
use crate::util::event::{Event, Events};
let mut term_size = terminal.size().unwrap();
draw(&mut terminal, &term_size);
for c in stdin.keys() {
let size = terminal.size().unwrap();
if term_size != size {
terminal.resize(size).unwrap();
term_size = size;
}
draw(&mut terminal, &term_size);
let evt = c.unwrap();
if evt == event::Key::Char('q') {
break;
fn main() -> Result<(), failure::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()?;
// Setup event handlers
let events = Events::new();
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();
Block::default().borders(Borders::ALL).render(&mut f, 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]);
Block::default()
.title("With background")
.title_style(Style::default().fg(Color::Yellow))
.style(Style::default().bg(Color::Green))
.render(&mut f, chunks[0]);
Block::default()
.title("Styled title")
.title_style(
Style::default()
.fg(Color::White)
.bg(Color::Red)
.modifier(Modifier::Bold),
)
.render(&mut f, chunks[1]);
}
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
Block::default()
.title("With borders")
.borders(Borders::ALL)
.render(&mut f, chunks[0]);
Block::default()
.title("With styled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.render(&mut f, chunks[1]);
}
})?;
match events.next()? {
Event::Input(key) => {
if key == Key::Char('q') {
break;
}
}
_ => {}
}
}
terminal.show_cursor().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, size: &Rect) {
// 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
Block::default().borders(Borders::ALL).render(t, size);
Group::default()
.direction(Direction::Vertical)
.margin(4)
.sizes(&[Size::Percent(50), Size::Percent(50)])
.render(t, size, |t, chunks| {
Group::default()
.direction(Direction::Horizontal)
.sizes(&[Size::Percent(50), Size::Percent(50)])
.render(t, &chunks[0], |t, chunks| {
Block::default()
.title("With background")
.title_style(Style::default().fg(Color::Yellow))
.style(Style::default().bg(Color::Green))
.render(t, &chunks[0]);
Block::default()
.title("Styled title")
.title_style(
Style::default()
.fg(Color::White)
.bg(Color::Red)
.modifier(Modifier::Bold),
)
.render(t, &chunks[1]);
});
Group::default()
.direction(Direction::Horizontal)
.sizes(&[Size::Percent(50), Size::Percent(50)])
.render(t, &chunks[1], |t, chunks| {
Block::default()
.title("With borders")
.borders(Borders::ALL)
.render(t, &chunks[0]);
Block::default()
.title("With styled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.render(t, &chunks[1]);
});
});
t.draw().unwrap();
Ok(())
}

View File

@@ -1,23 +1,23 @@
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use std::io;
use std::thread;
use std::time;
use std::sync::mpsc;
use std::time::Duration;
use termion::event;
use termion::input::TermRead;
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{Block, Borders, Widget};
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution};
use tui::layout::{Direction, Group, Rect, Size};
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout, Rect};
use tui::style::Color;
use tui::widgets::canvas::{Canvas, Map, MapResolution, Rectangle};
use tui::widgets::{Block, Borders, Widget};
use tui::Terminal;
use crate::util::event::{Config, Event, Events};
struct App {
size: Rect,
x: f64,
y: f64,
ball: Rect,
@@ -31,7 +31,6 @@ struct App {
impl App {
fn new() -> App {
App {
size: Default::default(),
x: 0.0,
y: 0.0,
ball: Rect::new(10, 30, 10, 10),
@@ -43,7 +42,7 @@ impl App {
}
}
fn advance(&mut self) {
fn update(&mut self) {
if self.ball.left() < self.playground.left() || self.ball.right() > self.playground.right()
{
self.dir_x = !self.dir_x;
@@ -67,91 +66,31 @@ impl App {
}
}
enum Event {
Input(event::Key),
Tick,
}
fn main() {
fn main() -> Result<(), failure::Error> {
// Terminal initialization
let backend = MouseBackend::new().unwrap();
let mut terminal = Terminal::new(backend).unwrap();
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()?;
// Channels
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
let clock_tx = tx.clone();
// Input
thread::spawn(move || {
let stdin = io::stdin();
for c in stdin.keys() {
let evt = c.unwrap();
input_tx.send(Event::Input(evt)).unwrap();
if evt == event::Key::Char('q') {
break;
}
}
});
// Tick
thread::spawn(move || loop {
clock_tx.send(Event::Tick).unwrap();
thread::sleep(time::Duration::from_millis(500));
});
// Setup event handlers
let config = Config {
tick_rate: Duration::from_millis(100),
..Default::default()
};
let events = Events::with_config(config);
// App
let mut app = App::new();
// First draw call
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
app.size = terminal.size().unwrap();
draw(&mut terminal, &app);
loop {
let size = terminal.size().unwrap();
if size != app.size {
terminal.resize(size).unwrap();
app.size = size;
}
let evt = rx.recv().unwrap();
match evt {
Event::Input(input) => match input {
event::Key::Char('q') => {
break;
}
event::Key::Down => {
app.y += 1.0;
}
event::Key::Up => {
app.y -= 1.0;
}
event::Key::Right => {
app.x += 1.0;
}
event::Key::Left => {
app.x -= 1.0;
}
_ => {}
},
Event::Tick => {
app.advance();
}
}
draw(&mut terminal, &app);
}
terminal.show_cursor().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
Group::default()
.direction(Direction::Horizontal)
.sizes(&[Size::Percent(50), Size::Percent(50)])
.render(t, &app.size, |t, chunks| {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.paint(|ctx| {
@@ -163,43 +102,45 @@ fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
.render(t, &chunks[0]);
.render(&mut f, chunks[0]);
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("List"))
.block(Block::default().borders(Borders::ALL).title("Pong"))
.paint(|ctx| {
ctx.draw(&Line {
x1: f64::from(app.ball.left()),
y1: f64::from(app.ball.top()),
x2: f64::from(app.ball.right()),
y2: f64::from(app.ball.top()),
color: Color::Yellow,
});
ctx.draw(&Line {
x1: f64::from(app.ball.right()),
y1: f64::from(app.ball.top()),
x2: f64::from(app.ball.right()),
y2: f64::from(app.ball.bottom()),
color: Color::Yellow,
});
ctx.draw(&Line {
x1: f64::from(app.ball.right()),
y1: f64::from(app.ball.bottom()),
x2: f64::from(app.ball.left()),
y2: f64::from(app.ball.bottom()),
color: Color::Yellow,
});
ctx.draw(&Line {
x1: f64::from(app.ball.left()),
y1: f64::from(app.ball.bottom()),
x2: f64::from(app.ball.left()),
y2: f64::from(app.ball.top()),
ctx.draw(&Rectangle {
rect: app.ball,
color: Color::Yellow,
});
})
.x_bounds([10.0, 110.0])
.y_bounds([10.0, 110.0])
.render(t, &chunks[1]);
});
.render(&mut f, chunks[1]);
})?;
t.draw().unwrap();
match events.next()? {
Event::Input(input) => match input {
Key::Char('q') => {
break;
}
Key::Down => {
app.y += 1.0;
}
Key::Up => {
app.y -= 1.0;
}
Key::Right => {
app.x += 1.0;
}
Key::Left => {
app.x -= 1.0;
}
_ => {}
},
Event::Tick => {
app.update();
}
}
}
Ok(())
}

View File

@@ -1,25 +1,21 @@
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use util::*;
use std::io;
use std::thread;
use std::time;
use std::sync::mpsc;
use termion::event;
use termion::input::TermRead;
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Widget};
use tui::layout::Rect;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use crate::util::SinSignal;
struct App {
size: Rect,
signal1: SinSignal,
data1: Vec<(f64, f64)>,
signal2: SinSignal,
@@ -34,16 +30,15 @@ impl App {
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
App {
size: Rect::default(),
signal1: signal1,
data1: data1,
signal2: signal2,
data2: data2,
signal1,
data1,
signal2,
data2,
window: [0.0, 20.0],
}
}
fn advance(&mut self) {
fn update(&mut self) {
for _ in 0..5 {
self.data1.remove(0);
}
@@ -57,111 +52,76 @@ impl App {
}
}
enum Event {
Input(event::Key),
Tick,
}
fn main() {
fn main() -> Result<(), failure::Error> {
// Terminal initialization
let backend = MouseBackend::new().unwrap();
let mut terminal = Terminal::new(backend).unwrap();
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()?;
// Channels
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
let clock_tx = tx.clone();
// Input
thread::spawn(move || {
let stdin = io::stdin();
for c in stdin.keys() {
let evt = c.unwrap();
input_tx.send(Event::Input(evt)).unwrap();
if evt == event::Key::Char('q') {
break;
}
}
});
// Tick
thread::spawn(move || loop {
clock_tx.send(Event::Tick).unwrap();
thread::sleep(time::Duration::from_millis(500));
});
let events = Events::new();
// App
let mut app = App::new();
// First draw call
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
app.size = terminal.size().unwrap();
draw(&mut terminal, &app);
loop {
let size = terminal.size().unwrap();
if app.size != size {
terminal.resize(size).unwrap();
app.size = size;
}
terminal.draw(|mut f| {
let size = f.size();
Chart::default()
.block(
Block::default()
.title("Chart")
.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(&[
&format!("{}", app.window[0]),
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
&format!("{}", app.window[1]),
]),
)
.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(&[
Dataset::default()
.name("data2")
.marker(Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data1),
Dataset::default()
.name("data3")
.marker(Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
])
.render(&mut f, size);
})?;
let evt = rx.recv().unwrap();
match evt {
Event::Input(input) => if input == event::Key::Char('q') {
break;
},
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
}
}
Event::Tick => {
app.advance();
app.update();
}
}
draw(&mut terminal, &app);
}
terminal.show_cursor().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
Chart::default()
.block(
Block::default()
.title("Chart")
.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(&[
&format!("{}", app.window[0]),
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
&format!("{}", app.window[1]),
]),
)
.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(&[
Dataset::default()
.name("data2")
.marker(Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data1),
Dataset::default()
.name("data3")
.marker(Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
])
.render(t, &app.size);
t.draw().unwrap();
Ok(())
}

38
examples/crossterm.rs Normal file
View File

@@ -0,0 +1,38 @@
use tui::backend::CrosstermBackend;
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
use tui::Terminal;
fn main() -> Result<(), failure::Error> {
let mut terminal = Terminal::new(CrosstermBackend::new())?;
terminal.clear()?;
terminal.hide_cursor()?;
loop {
terminal.draw(|mut f| {
let size = f.size();
let text = [
Text::raw("It "),
Text::styled("works", Style::default().fg(Color::Yellow)),
];
Paragraph::new(text.iter())
.block(
Block::default()
.title("Crossterm Backend")
.title_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta)),
)
.render(&mut f, size);
})?;
let input = crossterm::input();
match input.read_char()? {
'q' => {
break;
}
_ => {}
};
}
Ok(())
}

View File

@@ -1,11 +1,20 @@
extern crate tui;
#[allow(dead_code)]
mod util;
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::Widget;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::style::Style;
use tui::widgets::Widget;
use tui::Terminal;
use crate::util::event::{Event, Events};
struct Label<'a> {
text: &'a str,
@@ -18,8 +27,8 @@ impl<'a> Default for Label<'a> {
}
impl<'a> Widget for Label<'a> {
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
buf.set_string(area.left(), area.top(), self.text, &Style::default());
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.left(), area.top(), self.text, Style::default());
}
}
@@ -30,10 +39,31 @@ impl<'a> Label<'a> {
}
}
fn main() {
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
let size = terminal.size().unwrap();
terminal.clear().unwrap();
Label::default().text("Test").render(&mut terminal, &size);
terminal.draw().unwrap();
fn main() -> Result<(), failure::Error> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let events = Events::new();
loop {
terminal.draw(|mut f| {
let size = f.size();
Label::default().text("Test").render(&mut f, size);
})?;
match events.next()? {
Event::Input(key) => {
if key == Key::Char('q') {
break;
}
}
_ => {}
}
}
Ok(())
}

View File

@@ -1,28 +1,26 @@
#[macro_use]
extern crate log;
extern crate stderrlog;
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use std::io;
use std::thread;
use std::time;
use std::sync::mpsc;
use std::time::Duration;
use termion::event;
use termion::input::TermRead;
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, Item, List, Marker,
Paragraph, Row, SelectableList, Sparkline, Table, Tabs, Widget};
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution};
use tui::layout::{Direction, Group, Rect, Size};
use structopt::StructOpt;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::{Backend, TermionBackend};
use tui::layout::{Constraint, Direction, Layout, Rect};
use tui::style::{Color, Modifier, Style};
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle};
use tui::widgets::{
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Marker, Paragraph, Row,
SelectableList, Sparkline, Table, Tabs, Text, Widget,
};
use tui::{Frame, Terminal};
use util::*;
use crate::util::event::{Config, Event, Events};
use crate::util::{RandomSignal, SinSignal, TabsState};
struct Server<'a> {
name: &'a str,
@@ -32,11 +30,10 @@ struct Server<'a> {
}
struct App<'a> {
size: Rect,
items: Vec<&'a str>,
events: Vec<(&'a str, &'a str)>,
selected: usize,
tabs: MyTabs<'a>,
tabs: TabsState<'a>,
show_chart: bool,
progress: u16,
data: Vec<u64>,
@@ -49,25 +46,36 @@ struct App<'a> {
servers: Vec<Server<'a>>,
}
enum Event {
Input(event::Key),
Tick,
#[derive(Debug, StructOpt)]
struct Cli {
#[structopt(long = "tick-rate", default_value = "250")]
tick_rate: u64,
#[structopt(long = "log")]
log: bool,
}
fn main() {
stderrlog::new()
.module(module_path!())
.verbosity(4)
.init()
.unwrap();
info!("Start");
fn main() -> Result<(), failure::Error> {
let cli = Cli::from_args();
stderrlog::new().quiet(!cli.log).verbosity(4).init()?;
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::with_config(Config {
tick_rate: Duration::from_millis(cli.tick_rate),
..Config::default()
});
let mut rand_signal = RandomSignal::new(0, 100);
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
let mut sin_signal2 = SinSignal::new(0.1, 2.0, 10.0);
let mut app = App {
size: Rect::default(),
items: vec![
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9",
"Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17",
@@ -102,15 +110,12 @@ fn main() {
("Event26", "INFO"),
],
selected: 0,
tabs: MyTabs {
titles: vec!["Tab0", "Tab1"],
selection: 0,
},
tabs: TabsState::new(vec!["Tab0", "Tab1"]),
show_chart: true,
progress: 0,
data: rand_signal.clone().take(300).collect(),
data2: sin_signal.clone().take(100).collect(),
data3: sin_signal2.clone().take(200).collect(),
data: rand_signal.by_ref().take(300).collect(),
data2: sin_signal.by_ref().take(100).collect(),
data3: sin_signal2.by_ref().take(200).collect(),
data4: vec![
("B1", 9),
("B2", 12),
@@ -167,68 +172,49 @@ fn main() {
},
],
};
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
for _ in 0..100 {
sin_signal.next();
}
for _ in 0..200 {
sin_signal2.next();
}
thread::spawn(move || {
let stdin = io::stdin();
for c in stdin.keys() {
let evt = c.unwrap();
input_tx.send(Event::Input(evt)).unwrap();
if evt == event::Key::Char('q') {
break;
}
}
});
thread::spawn(move || {
let tx = tx.clone();
loop {
tx.send(Event::Tick).unwrap();
thread::sleep(time::Duration::from_millis(200));
}
});
let backend = MouseBackend::new().unwrap();
let mut terminal = Terminal::new(backend).unwrap();
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
loop {
let size = terminal.size().unwrap();
if size != app.size {
terminal.resize(size).unwrap();
app.size = size;
}
draw(&mut terminal, &app).unwrap();
let evt = rx.recv().unwrap();
match evt {
// Draw UI
terminal.draw(|mut f| {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size());
Tabs::default()
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.titles(&app.tabs.titles)
.style(Style::default().fg(Color::Green))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index)
.render(&mut f, chunks[0]);
match app.tabs.index {
0 => draw_first_tab(&mut f, &app, chunks[1]),
1 => draw_second_tab(&mut f, &app, chunks[1]),
_ => {}
};
})?;
match events.next()? {
Event::Input(input) => match input {
event::Key::Char('q') => {
Key::Char('q') => {
break;
}
event::Key::Up => {
Key::Up => {
if app.selected > 0 {
app.selected -= 1
};
}
event::Key::Down => if app.selected < app.items.len() - 1 {
app.selected += 1;
},
event::Key::Left => {
Key::Down => {
if app.selected < app.items.len() - 1 {
app.selected += 1;
}
}
Key::Left => {
app.tabs.previous();
}
event::Key::Right => {
Key::Right => {
app.tabs.next();
}
event::Key::Char('t') => {
Key::Char('t') => {
app.show_chart = !app.show_chart;
}
_ => {}
@@ -261,184 +247,187 @@ fn main() {
}
}
}
terminal.show_cursor().unwrap();
terminal.clear().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
Group::default()
.direction(Direction::Vertical)
.sizes(&[Size::Fixed(3), Size::Min(0)])
.render(t, &app.size, |t, chunks| {
Tabs::default()
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.titles(&app.tabs.titles)
.style(Style::default().fg(Color::Green))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.selection)
.render(t, &chunks[0]);
match app.tabs.selection {
0 => {
draw_first_tab(t, app, &chunks[1]);
}
1 => {
draw_second_tab(t, app, &chunks[1]);
}
_ => {}
};
});
try!(t.draw());
Ok(())
}
fn draw_first_tab(t: &mut Terminal<MouseBackend>, app: &App, area: &Rect) {
Group::default()
.direction(Direction::Vertical)
.sizes(&[Size::Fixed(7), Size::Min(7), Size::Fixed(7)])
.render(t, area, |t, chunks| {
draw_gauges(t, app, &chunks[0]);
draw_charts(t, app, &chunks[1]);
draw_text(t, &chunks[2]);
});
fn draw_first_tab<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.constraints(
[
Constraint::Length(7),
Constraint::Min(7),
Constraint::Length(7),
]
.as_ref(),
)
.split(area);
draw_gauges(f, app, chunks[0]);
draw_charts(f, app, chunks[1]);
draw_text(f, chunks[2]);
}
fn draw_gauges(t: &mut Terminal<MouseBackend>, app: &App, area: &Rect) {
fn draw_gauges<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.constraints([Constraint::Length(2), Constraint::Length(3)].as_ref())
.margin(1)
.split(area);
Block::default()
.borders(Borders::ALL)
.title("Graphs")
.render(t, area);
Group::default()
.direction(Direction::Vertical)
.margin(1)
.sizes(&[Size::Fixed(2), Size::Fixed(3)])
.render(t, area, |t, chunks| {
Gauge::default()
.block(Block::default().title("Gauge:"))
.style(
Style::default()
.fg(Color::Magenta)
.bg(Color::Black)
.modifier(Modifier::Italic),
)
.label(&format!("{} / 100", app.progress))
.percent(app.progress)
.render(t, &chunks[0]);
Sparkline::default()
.block(Block::default().title("Sparkline:"))
.style(Style::default().fg(Color::Green))
.data(&app.data)
.render(t, &chunks[1]);
});
.render(f, area);
Gauge::default()
.block(Block::default().title("Gauge:"))
.style(
Style::default()
.fg(Color::Magenta)
.bg(Color::Black)
.modifier(Modifier::Italic),
)
.label(&format!("{} / 100", app.progress))
.percent(app.progress)
.render(f, chunks[0]);
Sparkline::default()
.block(Block::default().title("Sparkline:"))
.style(Style::default().fg(Color::Green))
.data(&app.data)
.render(f, chunks[1]);
}
fn draw_charts(t: &mut Terminal<MouseBackend>, app: &App, area: &Rect) {
let sizes = if app.show_chart {
vec![Size::Percent(50), Size::Percent(50)]
fn draw_charts<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
let constraints = if app.show_chart {
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
} else {
vec![Size::Percent(100)]
vec![Constraint::Percentage(100)]
};
Group::default()
let chunks = Layout::default()
.constraints(constraints)
.direction(Direction::Horizontal)
.sizes(&sizes)
.render(t, area, |t, chunks| {
Group::default()
.direction(Direction::Vertical)
.sizes(&[Size::Percent(50), Size::Percent(50)])
.render(t, &chunks[0], |t, chunks| {
Group::default()
.direction(Direction::Horizontal)
.sizes(&[Size::Percent(50), Size::Percent(50)])
.render(t, &chunks[0], |t, chunks| {
SelectableList::default()
.block(Block::default().borders(Borders::ALL).title("List"))
.items(&app.items)
.select(app.selected)
.highlight_style(
Style::default().fg(Color::Yellow).modifier(Modifier::Bold),
)
.highlight_symbol(">")
.render(t, &chunks[0]);
let info_style = Style::default().fg(Color::White);
let warning_style = Style::default().fg(Color::Yellow);
let error_style = Style::default().fg(Color::Magenta);
let critical_style = Style::default().fg(Color::Red);
let events = app.events.iter().map(|&(evt, level)| {
Item::StyledData(
format!("{}: {}", level, evt),
match level {
"ERROR" => &error_style,
"CRITICAL" => &critical_style,
"WARNING" => &warning_style,
_ => &info_style,
},
)
});
List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.render(t, &chunks[1]);
});
BarChart::default()
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
.data(&app.data4)
.bar_width(3)
.bar_gap(2)
.value_style(
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.modifier(Modifier::Italic),
)
.label_style(Style::default().fg(Color::Yellow))
.style(Style::default().fg(Color::Green))
.render(t, &chunks[1]);
});
if app.show_chart {
Chart::default()
.block(
Block::default()
.title("Chart")
.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(&[
&format!("{}", app.window[0]),
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
&format!("{}", app.window[1]),
]),
)
.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(&[
Dataset::default()
.name("data2")
.marker(Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data2),
Dataset::default()
.name("data3")
.marker(Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data3),
])
.render(t, &chunks[1]);
}
});
.split(area);
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.direction(Direction::Horizontal)
.split(chunks[0]);
SelectableList::default()
.block(Block::default().borders(Borders::ALL).title("List"))
.items(&app.items)
.select(Some(app.selected))
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
.highlight_symbol(">")
.render(f, chunks[0]);
let info_style = Style::default().fg(Color::White);
let warning_style = Style::default().fg(Color::Yellow);
let error_style = Style::default().fg(Color::Magenta);
let critical_style = Style::default().fg(Color::Red);
let events = app.events.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
"ERROR" => error_style,
"CRITICAL" => critical_style,
"WARNING" => warning_style,
_ => info_style,
},
)
});
List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.render(f, chunks[1]);
}
BarChart::default()
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
.data(&app.data4)
.bar_width(3)
.bar_gap(2)
.value_style(
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.modifier(Modifier::Italic),
)
.label_style(Style::default().fg(Color::Yellow))
.style(Style::default().fg(Color::Green))
.render(f, chunks[1]);
}
if app.show_chart {
Chart::default()
.block(
Block::default()
.title("Chart")
.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(&[
&format!("{}", app.window[0]),
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
&format!("{}", app.window[1]),
]),
)
.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(&[
Dataset::default()
.name("data2")
.marker(Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data2),
Dataset::default()
.name("data3")
.marker(Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data3),
])
.render(f, chunks[1]);
}
}
fn draw_text(t: &mut Terminal<MouseBackend>, area: &Rect) {
Paragraph::default()
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::Invert)),
Text::raw(" your "),
Text::styled("text", Style::default().modifier(Modifier::Underline)),
Text::raw(".\nOne more thing is that it should display unicode characters: 10€")
];
Paragraph::new(text.iter())
.block(
Block::default()
.borders(Borders::ALL)
@@ -446,72 +435,72 @@ fn draw_text(t: &mut Terminal<MouseBackend>, area: &Rect) {
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::Bold)),
)
.wrap(true)
.text(
"This is a paragraph with several lines.\nYou can change the color.\nUse \
\\{fg=[color];bg=[color];mod=[modifier] [text]} to highlight the text with a color. \
For example, {fg=red u}{fg=green n}{fg=yellow d}{fg=magenta e}{fg=cyan r} \
{fg=gray t}{fg=light_gray h}{fg=light_red e} {fg=light_green r}{fg=light_yellow a} \
{fg=light_magenta i}{fg=light_cyan n}{fg=white b}{fg=red o}{fg=green w}.\n\
Oh, and if you didn't {mod=italic notice} you can {mod=bold automatically} \
{mod=invert wrap} your {mod=underline text} =).\nOne more thing is that \
it should display unicode characters properly: 日本国, ٩(-̮̮̃-̃)۶ ٩(●̮̮̃•̃)۶ ٩(͡๏̯͡๏)۶ \
٩(-̮̮̃•̃).",
)
.render(t, area);
.render(f, area);
}
fn draw_second_tab(t: &mut Terminal<MouseBackend>, app: &App, area: &Rect) {
Group::default()
fn draw_second_tab<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
.direction(Direction::Horizontal)
.sizes(&[Size::Percent(30), Size::Percent(70)])
.render(t, area, |t, chunks| {
let up_style = Style::default().fg(Color::Green);
let failure_style = Style::default().fg(Color::Red);
Table::new(
["Server", "Location", "Status"].into_iter(),
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)
}),
).block(Block::default().title("Servers").borders(Borders::ALL))
.header_style(Style::default().fg(Color::Yellow))
.widths(&[15, 15, 10])
.render(t, &chunks[0]);
.split(area);
let up_style = Style::default().fg(Color::Green);
let failure_style = Style::default().fg(Color::Red);
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)
});
Table::new(header.into_iter(), rows)
.block(Block::default().title("Servers").borders(Borders::ALL))
.header_style(Style::default().fg(Color::Yellow))
.widths(&[15, 15, 10])
.render(f, chunks[0]);
Canvas::default()
.block(Block::default().title("World").borders(Borders::ALL))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
resolution: MapResolution::High,
Canvas::default()
.block(Block::default().title("World").borders(Borders::ALL))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
resolution: MapResolution::High,
});
ctx.layer();
ctx.draw(&Rectangle {
rect: Rect {
x: 0,
y: 30,
width: 10,
height: 10,
},
color: Color::Yellow,
});
for (i, s1) in app.servers.iter().enumerate() {
for s2 in &app.servers[i + 1..] {
ctx.draw(&Line {
x1: s1.coords.1,
y1: s1.coords.0,
y2: s2.coords.0,
x2: s2.coords.1,
color: Color::Yellow,
});
ctx.layer();
for (i, s1) in app.servers.iter().enumerate() {
for s2 in &app.servers[i + 1..] {
ctx.draw(&Line {
x1: s1.coords.1,
y1: s1.coords.0,
y2: s2.coords.0,
x2: s2.coords.1,
color: Color::Yellow,
});
}
}
for server in &app.servers {
let color = if server.status == "Up" {
Color::Green
} else {
Color::Red
};
ctx.print(server.coords.1, server.coords.0, "X", color);
}
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
.render(t, &chunks[1]);
}
}
for server in &app.servers {
let color = if server.status == "Up" {
Color::Green
} else {
Color::Red
};
ctx.print(server.coords.1, server.coords.0, "X", color);
}
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
.render(f, chunks[1]);
}

View File

@@ -1,40 +1,38 @@
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use std::io;
use std::thread;
use std::time;
use std::sync::mpsc;
use termion::event;
use termion::input::TermRead;
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{Block, Borders, Gauge, Widget};
use tui::layout::{Direction, Group, Rect, Size};
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Gauge, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
struct App {
size: Rect,
progress1: u16,
progress2: u16,
progress3: u16,
progress3: f64,
progress4: u16,
}
impl App {
fn new() -> App {
App {
size: Rect::default(),
progress1: 0,
progress2: 0,
progress3: 0,
progress3: 0.0,
progress4: 0,
}
}
fn advance(&mut self) {
fn update(&mut self) {
self.progress1 += 5;
if self.progress1 > 100 {
self.progress1 = 0;
@@ -43,9 +41,9 @@ impl App {
if self.progress2 > 100 {
self.progress2 = 0;
}
self.progress3 += 1;
if self.progress3 > 100 {
self.progress3 = 0;
self.progress3 += 0.001;
if self.progress3 > 1.0 {
self.progress3 = 0.0;
}
self.progress4 += 3;
if self.progress4 > 100 {
@@ -54,104 +52,70 @@ impl App {
}
}
enum Event {
Input(event::Key),
Tick,
}
fn main() {
fn main() -> Result<(), failure::Error> {
// Terminal initialization
let backend = MouseBackend::new().unwrap();
let mut terminal = Terminal::new(backend).unwrap();
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()?;
// Channels
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
let clock_tx = tx.clone();
let events = Events::new();
// Input
thread::spawn(move || {
let stdin = io::stdin();
for c in stdin.keys() {
let evt = c.unwrap();
input_tx.send(Event::Input(evt)).unwrap();
if evt == event::Key::Char('q') {
break;
}
}
});
// Tick
thread::spawn(move || loop {
clock_tx.send(Event::Tick).unwrap();
thread::sleep(time::Duration::from_millis(500));
});
// App
let mut app = App::new();
// First draw call
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
app.size = terminal.size().unwrap();
draw(&mut terminal, &app);
loop {
let size = terminal.size().unwrap();
if size != app.size {
terminal.resize(size).unwrap();
app.size = size;
}
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 evt = rx.recv().unwrap();
match evt {
Event::Input(input) => if input == event::Key::Char('q') {
break;
},
Event::Tick => {
app.advance();
}
}
draw(&mut terminal, &app);
}
terminal.show_cursor().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
Group::default()
.direction(Direction::Vertical)
.margin(2)
.sizes(&[
Size::Percent(25),
Size::Percent(25),
Size::Percent(25),
Size::Percent(25),
])
.render(t, &app.size, |t, chunks| {
Gauge::default()
.block(Block::default().title("Gauge1").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow))
.percent(app.progress1)
.render(t, &chunks[0]);
.render(&mut f, chunks[0]);
Gauge::default()
.block(Block::default().title("Gauge2").borders(Borders::ALL))
.style(Style::default().fg(Color::Magenta).bg(Color::Green))
.percent(app.progress2)
.label(&format!("{}/100", app.progress2))
.render(t, &chunks[1]);
Gauge::default()
.block(Block::default().title("Gauge2").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow))
.percent(app.progress3)
.render(t, &chunks[2]);
.render(&mut f, chunks[1]);
Gauge::default()
.block(Block::default().title("Gauge3").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow))
.ratio(app.progress3)
.render(&mut f, chunks[2]);
Gauge::default()
.block(Block::default().title("Gauge4").borders(Borders::ALL))
.style(Style::default().fg(Color::Cyan).modifier(Modifier::Italic))
.percent(app.progress4)
.label(&format!("{}/100", app.progress2))
.render(t, &chunks[3]);
});
.render(&mut f, chunks[3]);
})?;
t.draw().unwrap();
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
}
}
Event::Tick => {
app.update();
}
}
}
Ok(())
}

View File

@@ -1,104 +1,63 @@
extern crate log;
extern crate stderrlog;
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use std::io;
use std::thread;
use std::sync::mpsc;
use termion::event;
use termion::input::TermRead;
use tui::Terminal;
use tui::backend::MouseBackend;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::widgets::{Block, Borders, Widget};
use tui::layout::{Direction, Group, Rect, Size};
use tui::Terminal;
struct App {
size: Rect,
}
impl App {
fn new() -> App {
App {
size: Rect::default(),
}
}
}
enum Event {
Input(event::Key),
}
fn main() {
stderrlog::new().verbosity(4).init().unwrap();
use crate::util::event::{Event, Events};
fn main() -> Result<(), failure::Error> {
// Terminal initialization
let backend = MouseBackend::new().unwrap();
let mut terminal = Terminal::new(backend).unwrap();
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()?;
// Channels
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
// Input
thread::spawn(move || {
let stdin = io::stdin();
for c in stdin.keys() {
let evt = c.unwrap();
input_tx.send(Event::Input(evt)).unwrap();
if evt == event::Key::Char('q') {
break;
}
}
});
// App
let mut app = App::new();
// First draw call
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
app.size = terminal.size().unwrap();
draw(&mut terminal, &app);
let events = Events::new();
loop {
let size = terminal.size().unwrap();
if size != app.size {
terminal.resize(size).unwrap();
app.size = size;
}
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());
let evt = rx.recv().unwrap();
match evt {
Event::Input(input) => match input {
event::Key::Char('q') => {
break;
}
_ => {}
},
}
draw(&mut terminal, &app);
}
terminal.show_cursor().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
Group::default()
.direction(Direction::Vertical)
.sizes(&[Size::Percent(10), Size::Percent(80), Size::Percent(10)])
.render(t, &app.size, |t, chunks| {
Block::default()
.title("Block")
.borders(Borders::ALL)
.render(t, &chunks[0]);
.render(&mut f, chunks[0]);
Block::default()
.title("Block 2")
.borders(Borders::ALL)
.render(t, &chunks[2]);
});
.render(&mut f, chunks[2]);
})?;
t.draw().unwrap();
match events.next()? {
Event::Input(input) => {
if let Key::Char('q') = input {
break;
}
}
_ => {}
}
}
Ok(())
}

View File

@@ -1,24 +1,23 @@
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use std::io;
use std::thread;
use std::time;
use std::sync::mpsc;
use termion::event;
use termion::input::TermRead;
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{Block, Borders, Item, List, SelectableList, Widget};
use tui::layout::{Direction, Group, Rect, Size};
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Corner, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, List, SelectableList, Text, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
struct App<'a> {
size: Rect,
items: Vec<&'a str>,
selected: usize,
selected: Option<usize>,
events: Vec<(&'a str, &'a str)>,
info_style: Style,
warning_style: Style,
@@ -29,13 +28,12 @@ struct App<'a> {
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
size: Rect::default(),
items: vec![
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9",
"Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17",
"Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
],
selected: 0,
selected: None,
events: vec![
("Event1", "INFO"),
("Event2", "INFO"),
@@ -77,115 +75,92 @@ impl<'a> App<'a> {
}
}
enum Event {
Input(event::Key),
Tick,
}
fn main() {
fn main() -> Result<(), failure::Error> {
// Terminal initialization
let backend = MouseBackend::new().unwrap();
let mut terminal = Terminal::new(backend).unwrap();
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()?;
// Channels
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
let clock_tx = tx.clone();
// Input
thread::spawn(move || {
let stdin = io::stdin();
for c in stdin.keys() {
let evt = c.unwrap();
input_tx.send(Event::Input(evt)).unwrap();
if evt == event::Key::Char('q') {
break;
}
}
});
// Tick
thread::spawn(move || loop {
clock_tx.send(Event::Tick).unwrap();
thread::sleep(time::Duration::from_millis(500));
});
let events = Events::new();
// App
let mut app = App::new();
// First draw call
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
app.size = terminal.size().unwrap();
draw(&mut terminal, &app);
loop {
let size = terminal.size().unwrap();
if size != app.size {
terminal.resize(size).unwrap();
app.size = size;
}
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let evt = rx.recv().unwrap();
match evt {
Event::Input(input) => match input {
event::Key::Char('q') => {
break;
}
event::Key::Down => {
app.selected += 1;
if app.selected > app.items.len() - 1 {
app.selected = 0;
}
}
event::Key::Up => if app.selected > 0 {
app.selected -= 1;
} else {
app.selected = app.items.len() - 1;
},
_ => {}
},
Event::Tick => {
app.advance();
}
}
draw(&mut terminal, &app);
}
terminal.show_cursor().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
Group::default()
.direction(Direction::Horizontal)
.sizes(&[Size::Percent(50), Size::Percent(50)])
.render(t, &app.size, |t, chunks| {
let style = Style::default().fg(Color::Black).bg(Color::White);
SelectableList::default()
.block(Block::default().borders(Borders::ALL).title("List"))
.items(&app.items)
.select(app.selected)
.style(style)
.highlight_style(style.clone().fg(Color::LightGreen).modifier(Modifier::Bold))
.highlight_style(style.fg(Color::LightGreen).modifier(Modifier::Bold))
.highlight_symbol(">")
.render(t, &chunks[0]);
.render(&mut f, chunks[0]);
{
let events = app.events.iter().map(|&(evt, level)| {
Item::StyledData(
Text::styled(
format!("{}: {}", level, evt),
match level {
"ERROR" => &app.error_style,
"CRITICAL" => &app.critical_style,
"WARNING" => &app.warning_style,
_ => &app.info_style,
"ERROR" => app.error_style,
"CRITICAL" => app.critical_style,
"WARNING" => app.warning_style,
_ => app.info_style,
},
)
});
List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.render(t, &chunks[1]);
.start_corner(Corner::BottomLeft)
.render(&mut f, chunks[1]);
}
});
})?;
t.draw().unwrap();
match events.next()? {
Event::Input(input) => match input {
Key::Char('q') => {
break;
}
Key::Left => {
app.selected = None;
}
Key::Down => {
app.selected = if let Some(selected) = app.selected {
if selected >= app.items.len() - 1 {
Some(0)
} else {
Some(selected + 1)
}
} else {
Some(0)
}
}
Key::Up => {
app.selected = if let Some(selected) = app.selected {
if selected > 0 {
Some(selected - 1)
} else {
Some(app.items.len() - 1)
}
} else {
Some(0)
}
}
_ => {}
},
Event::Tick => {
app.advance();
}
}
}
Ok(())
}

View File

@@ -1,66 +1,110 @@
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use std::io;
use termion::event;
use termion::input::TermRead;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Alignment, Constraint, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{Block, Paragraph, Widget};
use tui::layout::{Direction, Group, Rect, Size};
use tui::style::{Color, Style};
fn main() {
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
let stdin = io::stdin();
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
use crate::util::event::{Event, Events};
let mut term_size = terminal.size().unwrap();
draw(&mut terminal, &term_size);
fn main() -> Result<(), failure::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()?;
for c in stdin.keys() {
let size = terminal.size().unwrap();
if size != term_size {
terminal.resize(size).unwrap();
term_size = size;
}
let events = Events::new();
draw(&mut terminal, &term_size);
let evt = c.unwrap();
if evt == event::Key::Char('q') {
break;
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');
Block::default()
.style(Style::default().bg(Color::White))
.render(&mut f, 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::CrossedOut),
),
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));
Paragraph::new(text.iter())
.block(block.clone().title("Left, no wrap"))
.alignment(Alignment::Left)
.render(&mut f, chunks[0]);
Paragraph::new(text.iter())
.block(block.clone().title("Left, wrap"))
.alignment(Alignment::Left)
.wrap(true)
.render(&mut f, chunks[1]);
Paragraph::new(text.iter())
.block(block.clone().title("Center, wrap"))
.alignment(Alignment::Center)
.wrap(true)
.scroll(scroll)
.render(&mut f, chunks[2]);
Paragraph::new(text.iter())
.block(block.clone().title("Right, wrap"))
.alignment(Alignment::Right)
.wrap(true)
.render(&mut f, chunks[3]);
})?;
scroll += 1;
scroll %= 10;
match events.next()? {
Event::Input(key) => {
if key == Key::Char('q') {
break;
}
}
_ => {}
}
}
terminal.show_cursor().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, size: &Rect) {
Block::default()
.style(Style::default().bg(Color::White))
.render(t, size);
Group::default()
.direction(Direction::Vertical)
.margin(5)
.sizes(&[Size::Percent(100)])
.render(t, size, |t, chunks| {
Group::default()
.direction(Direction::Horizontal)
.sizes(&[Size::Percent(100)])
.render(t, &chunks[0], |t, chunks| {
Paragraph::default()
.text(
"This is a line\n{fg=red This is a line}\n{bg=red This is a \
line}\n{mod=italic This is a line}\n{mod=bold This is a \
line}\n{mod=crossed_out This is a line}\n{mod=invert This is a \
line}\n{mod=underline This is a \
line}\n{bg=green;fg=yellow;mod=italic This is a line}\n",
)
.render(t, &chunks[0]);
});
});
t.draw().unwrap();
Ok(())
}

View File

@@ -1,51 +1,46 @@
extern crate rustbox;
extern crate tui;
use std::error::Error;
use rustbox::Key;
use std::error::Error;
use tui::Terminal;
use tui::backend::RustboxBackend;
use tui::widgets::{Block, Borders, Paragraph, Widget};
use tui::layout::{Direction, Group, Size};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
use tui::Terminal;
fn main() {
fn main() -> Result<(), failure::Error> {
let mut terminal = Terminal::new(RustboxBackend::new().unwrap()).unwrap();
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
draw(&mut terminal);
loop {
draw(&mut terminal)?;
match terminal.backend().rustbox().poll_event(false) {
Ok(rustbox::Event::KeyEvent(key)) => if key == Key::Char('q') {
break;
},
Ok(rustbox::Event::KeyEvent(key)) => {
if key == Key::Char('q') {
break;
}
}
Err(e) => panic!("{}", e.description()),
_ => {}
};
draw(&mut terminal);
}
terminal.show_cursor().unwrap();
terminal.show_cursor()?;
Ok(())
}
fn draw(t: &mut Terminal<RustboxBackend>) {
let size = t.size().unwrap();
Group::default()
.direction(Direction::Vertical)
.sizes(&[Size::Percent(100)])
.render(t, &size, |t, chunks| {
Paragraph::default()
.block(
Block::default()
.title("Rustbox backend")
.title_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta)),
)
.text("It {yellow works}!")
.render(t, &chunks[0]);
});
t.draw().unwrap();
fn draw(t: &mut Terminal<RustboxBackend>) -> Result<(), std::io::Error> {
let text = [
Text::raw("It "),
Text::styled("works", Style::default().fg(Color::Yellow)),
];
t.draw(|mut f| {
let size = f.size();
Paragraph::new(text.iter())
.block(
Block::default()
.title("Rustbox backend")
.title_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta)),
)
.render(&mut f, size)
})
}

View File

@@ -1,25 +1,22 @@
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use util::*;
use std::io;
use std::thread;
use std::time;
use std::sync::mpsc;
use termion::event;
use termion::input::TermRead;
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{Block, Borders, Sparkline, Widget};
use tui::layout::{Direction, Group, Rect, Size};
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, Sparkline, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use crate::util::RandomSignal;
struct App {
size: Rect,
signal: RandomSignal,
data1: Vec<u64>,
data2: Vec<u64>,
@@ -33,15 +30,14 @@ impl App {
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
App {
size: Rect::default(),
signal: signal,
data1: data1,
data2: data2,
data3: data3,
signal,
data1,
data2,
data3,
}
}
fn advance(&mut self) {
fn update(&mut self) {
let value = self.signal.next().unwrap();
self.data1.pop();
self.data1.insert(0, value);
@@ -54,76 +50,36 @@ impl App {
}
}
enum Event {
Input(event::Key),
Tick,
}
fn main() {
fn main() -> Result<(), failure::Error> {
// Terminal initialization
let backend = MouseBackend::new().unwrap();
let mut terminal = Terminal::new(backend).unwrap();
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()?;
// Channels
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
let clock_tx = tx.clone();
// Setup event handlers
let events = Events::new();
// Input
thread::spawn(move || {
let stdin = io::stdin();
for c in stdin.keys() {
let evt = c.unwrap();
input_tx.send(Event::Input(evt)).unwrap();
if evt == event::Key::Char('q') {
break;
}
}
});
// Tick
thread::spawn(move || loop {
clock_tx.send(Event::Tick).unwrap();
thread::sleep(time::Duration::from_millis(500));
});
// App
// Create default app state
let mut app = App::new();
// First draw call
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
app.size = terminal.size().unwrap();
draw(&mut terminal, &app);
loop {
let size = terminal.size().unwrap();
if size != app.size {
terminal.resize(size).unwrap();
app.size = size;
}
let evt = rx.recv().unwrap();
match evt {
Event::Input(input) => if input == event::Key::Char('q') {
break;
},
Event::Tick => {
app.advance();
}
}
draw(&mut terminal, &app);
}
terminal.show_cursor().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
Group::default()
.direction(Direction::Vertical)
.margin(2)
.sizes(&[Size::Fixed(3), Size::Fixed(3), Size::Fixed(7), Size::Min(0)])
.render(t, &app.size, |t, chunks| {
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());
Sparkline::default()
.block(
Block::default()
@@ -132,7 +88,7 @@ fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
)
.data(&app.data1)
.style(Style::default().fg(Color::Yellow))
.render(t, &chunks[0]);
.render(&mut f, chunks[0]);
Sparkline::default()
.block(
Block::default()
@@ -141,7 +97,7 @@ fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
)
.data(&app.data2)
.style(Style::default().bg(Color::Green))
.render(t, &chunks[1]);
.render(&mut f, chunks[1]);
// Multiline
Sparkline::default()
.block(
@@ -151,8 +107,20 @@ fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
)
.data(&app.data3)
.style(Style::default().fg(Color::Red))
.render(t, &chunks[2]);
});
.render(&mut f, chunks[2]);
})?;
t.draw().unwrap();
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
}
}
Event::Tick => {
app.update();
}
}
}
Ok(())
}

View File

@@ -1,19 +1,21 @@
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use std::io;
use termion::event;
use termion::input::TermRead;
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{Block, Borders, Row, Table, Widget};
use tui::layout::{Direction, Group, Rect, Size};
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Row, Table, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
struct App<'a> {
size: Rect,
items: Vec<Vec<&'a str>>,
selected: usize,
}
@@ -21,7 +23,6 @@ struct App<'a> {
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
size: Rect::default(),
items: vec![
vec!["Row12", "Row12", "Row13"],
vec!["Row21", "Row22", "Row23"],
@@ -35,75 +36,67 @@ impl<'a> App<'a> {
}
}
fn main() {
fn main() -> Result<(), failure::Error> {
// Terminal initialization
let backend = MouseBackend::new().unwrap();
let mut terminal = Terminal::new(backend).unwrap();
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();
// App
let mut app = App::new();
// First draw call
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
app.size = terminal.size().unwrap();
draw(&mut terminal, &app);
// Input
let stdin = io::stdin();
for c in stdin.keys() {
let size = terminal.size().unwrap();
if size != app.size {
terminal.resize(size).unwrap();
app.size = size;
}
let evt = c.unwrap();
match evt {
event::Key::Char('q') => {
break;
}
event::Key::Down => {
app.selected += 1;
if app.selected > app.items.len() - 1 {
app.selected = 0;
loop {
terminal.draw(|mut f| {
let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::Bold);
let normal_style = Style::default().fg(Color::White);
let header = ["Header1", "Header2", "Header3"];
let rows = app.items.iter().enumerate().map(|(i, item)| {
if i == app.selected {
Row::StyledData(item.into_iter(), selected_style)
} else {
Row::StyledData(item.into_iter(), normal_style)
}
}
event::Key::Up => if app.selected > 0 {
app.selected -= 1;
} else {
app.selected = app.items.len() - 1;
},
_ => {}
};
draw(&mut terminal, &app);
}
});
terminal.show_cursor().unwrap();
terminal.clear().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::Bold);
let normal_style = Style::default().fg(Color::White);
let header = ["Header1", "Header2", "Header3"];
let rows = app.items.iter().enumerate().map(|(i, item)| {
if i == app.selected {
Row::StyledData(item.into_iter(), &selected_style)
} else {
Row::StyledData(item.into_iter(), &normal_style)
}
});
Group::default()
.direction(Direction::Horizontal)
.sizes(&[Size::Percent(100)])
.margin(5)
.render(t, &app.size, |t, chunks| {
let rects = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.margin(5)
.split(f.size());
Table::new(header.into_iter(), rows)
.block(Block::default().borders(Borders::ALL).title("Table"))
.widths(&[10, 10, 10])
.render(t, &chunks[0]);
});
.render(&mut f, rects[0]);
})?;
t.draw().unwrap();
match events.next()? {
Event::Input(key) => match key {
Key::Char('q') => {
break;
}
Key::Down => {
app.selected += 1;
if app.selected > app.items.len() - 1 {
app.selected = 0;
}
}
Key::Up => {
if app.selected > 0 {
app.selected -= 1;
} else {
app.selected = app.items.len() - 1;
}
}
_ => {}
},
_ => {}
};
}
Ok(())
}

View File

@@ -1,113 +1,93 @@
extern crate termion;
extern crate tui;
#[allow(dead_code)]
mod util;
use util::*;
use std::io;
use termion::event;
use termion::input::TermRead;
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{Block, Borders, Tabs, Widget};
use tui::layout::{Direction, Group, Rect, Size};
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, Tabs, Widget};
use tui::Terminal;
use crate::util::event::{Event, Events};
use crate::util::TabsState;
struct App<'a> {
size: Rect,
tabs: MyTabs<'a>,
tabs: TabsState<'a>,
}
fn main() {
fn main() -> Result<(), failure::Error> {
// Terminal initialization
let backend = MouseBackend::new().unwrap();
let mut terminal = Terminal::new(backend).unwrap();
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();
// App
let mut app = App {
size: Rect::default(),
tabs: MyTabs {
titles: vec!["Tab0", "Tab1", "Tab2", "Tab3"],
selection: 0,
},
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2", "Tab3"]),
};
// First draw call
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
app.size = terminal.size().unwrap();
draw(&mut terminal, &mut app);
// Main loop
let stdin = io::stdin();
for c in stdin.keys() {
let size = terminal.size().unwrap();
if size != app.size {
terminal.resize(size).unwrap();
app.size = size;
}
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);
let evt = c.unwrap();
match evt {
event::Key::Char('q') => {
break;
}
event::Key::Right => app.tabs.next(),
event::Key::Left => app.tabs.previous(),
_ => {}
}
draw(&mut terminal, &mut app);
}
terminal.show_cursor().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, app: &mut App) {
Block::default()
.style(Style::default().bg(Color::White))
.render(t, &app.size);
Group::default()
.direction(Direction::Vertical)
.margin(5)
.sizes(&[Size::Fixed(3), Size::Min(0)])
.render(t, &app.size, |t, chunks| {
Block::default()
.style(Style::default().bg(Color::White))
.render(&mut f, size);
Tabs::default()
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.titles(&app.tabs.titles)
.select(app.tabs.selection)
.select(app.tabs.index)
.style(Style::default().fg(Color::Cyan))
.highlight_style(Style::default().fg(Color::Yellow))
.render(t, &chunks[0]);
match app.tabs.selection {
0 => {
Block::default()
.title("Inner 0")
.borders(Borders::ALL)
.render(t, &chunks[1]);
}
1 => {
Block::default()
.title("Inner 1")
.borders(Borders::ALL)
.render(t, &chunks[1]);
}
2 => {
Block::default()
.title("Inner 2")
.borders(Borders::ALL)
.render(t, &chunks[1]);
}
3 => {
Block::default()
.title("Inner 3")
.borders(Borders::ALL)
.render(t, &chunks[1]);
}
.render(&mut f, chunks[0]);
match app.tabs.index {
0 => Block::default()
.title("Inner 0")
.borders(Borders::ALL)
.render(&mut f, chunks[1]),
1 => Block::default()
.title("Inner 1")
.borders(Borders::ALL)
.render(&mut f, chunks[1]),
2 => Block::default()
.title("Inner 2")
.borders(Borders::ALL)
.render(&mut f, chunks[1]),
3 => Block::default()
.title("Inner 3")
.borders(Borders::ALL)
.render(&mut f, chunks[1]),
_ => {}
}
});
})?;
t.draw().unwrap();
match events.next()? {
Event::Input(input) => match input {
Key::Char('q') => {
break;
}
Key::Right => app.tabs.next(),
Key::Left => app.tabs.previous(),
_ => {}
},
_ => {}
}
}
Ok(())
}

View File

@@ -9,123 +9,105 @@
/// * Pressing Backspace erases a character
/// * Pressing Enter pushes the current input in the history of previous
/// messages
extern crate termion;
extern crate tui;
use std::io;
use std::thread;
use std::sync::mpsc;
#[allow(dead_code)]
mod util;
use termion::event;
use termion::input::TermRead;
use std::io::{self, Write};
use tui::Terminal;
use tui::backend::MouseBackend;
use tui::widgets::{Block, Borders, Item, List, Paragraph, Widget};
use tui::layout::{Direction, Group, Rect, Size};
use termion::cursor::Goto;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget};
use tui::Terminal;
use unicode_width::UnicodeWidthStr;
use crate::util::event::{Event, Events};
/// App holds the state of the application
struct App {
size: Rect,
/// Current value of the input box
input: String,
/// History of recorded messages
messages: Vec<String>,
}
impl App {
fn new() -> App {
impl Default for App {
fn default() -> App {
App {
size: Rect::default(),
input: String::new(),
messages: Vec::new(),
}
}
}
enum Event {
Input(event::Key),
}
fn main() {
fn main() -> Result<(), failure::Error> {
// Terminal initialization
let backend = MouseBackend::new().unwrap();
let mut terminal = Terminal::new(backend).unwrap();
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)?;
// Channels
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
// Setup event handlers
let events = Events::new();
// Input
thread::spawn(move || {
let stdin = io::stdin();
for c in stdin.keys() {
let evt = c.unwrap();
input_tx.send(Event::Input(evt)).unwrap();
if evt == event::Key::Char('q') {
break;
}
}
});
// App
let mut app = App::new();
// First draw call
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
app.size = terminal.size().unwrap();
draw(&mut terminal, &app);
// Create default app state
let mut app = App::default();
loop {
let size = terminal.size().unwrap();
if app.size != size {
terminal.resize(size).unwrap();
app.size = size;
}
// Draw UI
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
.split(f.size());
Paragraph::new([Text::raw(&app.input)].iter())
.style(Style::default().fg(Color::Yellow))
.block(Block::default().borders(Borders::ALL).title("Input"))
.render(&mut f, chunks[0]);
let messages = app
.messages
.iter()
.enumerate()
.map(|(i, m)| Text::raw(format!("{}: {}", i, m)));
List::new(messages)
.block(Block::default().borders(Borders::ALL).title("Messages"))
.render(&mut f, chunks[1]);
})?;
let evt = rx.recv().unwrap();
match evt {
// Put the cursor back inside the input box
write!(
terminal.backend_mut(),
"{}",
Goto(4 + app.input.width() as u16, 4)
)?;
// Handle input
match events.next()? {
Event::Input(input) => match input {
event::Key::Char('q') => {
Key::Char('q') => {
break;
}
event::Key::Char('\n') => {
Key::Char('\n') => {
app.messages.push(app.input.drain(..).collect());
}
event::Key::Char(c) => {
Key::Char(c) => {
app.input.push(c);
}
event::Key::Backspace => {
Key::Backspace => {
app.input.pop();
}
_ => {}
},
_ => {}
}
draw(&mut terminal, &app);
}
terminal.show_cursor().unwrap();
terminal.clear().unwrap();
}
fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
Group::default()
.direction(Direction::Vertical)
.margin(2)
.sizes(&[Size::Fixed(3), Size::Min(1)])
.render(t, &app.size, |t, chunks| {
Paragraph::default()
.style(Style::default().fg(Color::Yellow))
.block(Block::default().borders(Borders::ALL).title("Input"))
.text(&app.input)
.render(t, &chunks[0]);
List::new(
app.messages
.iter()
.enumerate()
.map(|(i, m)| Item::Data(format!("{}: {}", i, m))),
).block(Block::default().borders(Borders::ALL).title("Messages"))
.render(t, &chunks[1]);
});
t.draw().unwrap();
Ok(())
}

83
examples/util/event.rs Normal file
View File

@@ -0,0 +1,83 @@
use std::io;
use std::sync::mpsc;
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<()>,
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 input_handle = {
let tx = tx.clone();
thread::spawn(move || {
let stdin = io::stdin();
for evt in stdin.keys() {
match evt {
Ok(key) => {
if let Err(_) = tx.send(Event::Input(key)) {
return;
}
if key == config.exit_key {
return;
}
}
Err(_) => {}
}
}
})
};
let tick_handle = {
let tx = tx.clone();
thread::spawn(move || {
let tx = tx.clone();
loop {
tx.send(Event::Tick).unwrap();
thread::sleep(config.tick_rate);
}
})
};
Events {
rx,
input_handle,
tick_handle,
}
}
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
self.rx.recv()
}
}

View File

@@ -1,19 +1,18 @@
#![allow(dead_code)]
pub mod event;
extern crate rand;
use self::rand::distributions::{IndependentSample, Range};
use rand::distributions::{Distribution, Uniform};
use rand::rngs::ThreadRng;
#[derive(Clone)]
pub struct RandomSignal {
range: Range<u64>,
rng: rand::ThreadRng,
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> RandomSignal {
RandomSignal {
range: Range::new(lower, upper),
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
}
@@ -22,7 +21,7 @@ impl RandomSignal {
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.range.ind_sample(&mut self.rng))
Some(self.distribution.sample(&mut self.rng))
}
}
@@ -38,9 +37,9 @@ impl SinSignal {
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
SinSignal {
x: 0.0,
interval: interval,
period: period,
scale: scale,
interval,
period,
scale,
}
}
}
@@ -54,21 +53,24 @@ impl Iterator for SinSignal {
}
}
pub struct MyTabs<'a> {
pub struct TabsState<'a> {
pub titles: Vec<&'a str>,
pub selection: usize,
pub index: usize,
}
impl<'a> MyTabs<'a> {
impl<'a> TabsState<'a> {
pub fn new(titles: Vec<&'a str>) -> TabsState {
TabsState { titles, index: 0 }
}
pub fn next(&mut self) {
self.selection = (self.selection + 1) % self.titles.len();
self.index = (self.index + 1) % self.titles.len();
}
pub fn previous(&mut self) {
if self.selection > 0 {
self.selection -= 1;
if self.index > 0 {
self.index -= 1;
} else {
self.selection = self.titles.len() - 1;
self.index = self.titles.len() - 1;
}
}
}

View File

@@ -1,15 +0,0 @@
#!/bin/bash
# Build all examples in examples directory
set -eu -o pipefail
for file in examples/*.rs; do
name=$(basename ${file//.rs/})
echo "[EXAMPLE] $name"
if [[ "$name" == "rustbox" ]]; then
cargo build --features rustbox --example "$name"
else
cargo build --example "$name"
fi
done

View File

@@ -1,14 +0,0 @@
#!/bin/bash
# Run all examples in examples directory
set -eu -o pipefail
for file in examples/*.rs; do
name=$(basename ${file//.rs/})
if [[ "$name" == "rustbox" ]]; then
cargo run --features rustbox --example "$name"
else
cargo run --example "$name"
fi
done

View File

@@ -1,65 +0,0 @@
#!/bin/bash
set -e
USAGE="$0 --name=STRING [--channel=STRING]"
# Default values
CARGO="cargo"
NAME=""
CHANNEL=""
# Parse args
for i in "$@"; do
case $i in
--name=*)
NAME="${i#*=}"
shift
;;
--channel=*)
CHANNEL="${i#*=}"
shift
;;
*)
echo "$USAGE"
exit 1
;;
esac
done
current_version() {
local crate="$1"
$CARGO install --list | \
grep "$crate" | \
head -n 1 | \
cut -d ' ' -f 2 | \
sed 's/v\(.*\):/\1/g'
}
upstream_version() {
local crate="$1"
$CARGO search "$crate" | \
grep "$crate" | \
head -n 1 | \
cut -d' ' -f 3 | \
sed 's/"//g'
}
if [ "$NAME" == "" ]; then
echo "$USAGE"
exit 1
fi
if [ "$CHANNEL" != "" ]; then
CARGO+=" +$CHANNEL"
fi
CURRENT_VERSION=$(current_version "$NAME")
UPSTREAM_VERSION=$(upstream_version "$NAME")
if [[ "$CURRENT_VERSION" != "$UPSTREAM_VERSION" ]]; then
echo "WARN: Latest version of $NAME not installed: $CURRENT_VERSION -> $UPSTREAM_VERSION"
$CARGO install --force "$NAME"
else
echo "INFO: Latest version of $NAME already installed ($CURRENT_VERSION)"
fi

View File

@@ -1,9 +0,0 @@
#!/bin/bash
set -eu
export PATH="$PATH:$HOME/.cargo/bin"
if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
export LD_LIBRARY_PATH="$(rustc +nightly --print sysroot)/lib:${LD_LIBRARY_PATH:-""}"
make install-tools
fi

View File

@@ -1,9 +0,0 @@
#!/bin/bash
set -eu
make build
make test
if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
make lint
fi

159
src/backend/crossterm.rs Normal file
View File

@@ -0,0 +1,159 @@
use std::io;
use crate::backend::Backend;
use crate::buffer::Cell;
use crate::layout::Rect;
use crate::style::{Color, Modifier};
use crossterm::error::ErrorKind;
pub struct CrosstermBackend {
screen: crossterm::Screen,
}
impl Default for CrosstermBackend {
fn default() -> CrosstermBackend {
CrosstermBackend {
screen: crossterm::Screen::default(),
}
}
}
impl CrosstermBackend {
pub fn new() -> CrosstermBackend {
CrosstermBackend::default()
}
pub fn screen(&self) -> &crossterm::Screen {
&self.screen
}
}
// TODO: consider associated Error type on Backend to allow custom error types
// per backend
fn convert_error(error: ErrorKind) -> io::Error {
match error {
ErrorKind::IoError(err) => err,
ErrorKind::FmtError(err) => {
io::Error::new(io::ErrorKind::Other, format!("Invalid formatting: {}", err))
}
ErrorKind::ResizingTerminalFailure(err) => io::Error::new(
io::ErrorKind::Other,
format!("Failed to resize terminal: {}", err),
),
_ => io::Error::new(io::ErrorKind::Other, "Unknown crossterm error"),
}
}
impl Backend for CrosstermBackend {
fn clear(&mut self) -> io::Result<()> {
let terminal = crossterm::terminal();
terminal
.clear(crossterm::ClearType::All)
.map_err(convert_error)?;
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
let cursor = crossterm::cursor();
cursor.hide().map_err(convert_error)?;
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
let cursor = crossterm::cursor();
cursor.show().map_err(convert_error)?;
Ok(())
}
fn size(&self) -> io::Result<Rect> {
let terminal = crossterm::terminal();
let (width, height) = terminal.terminal_size();
Ok(Rect::new(0, 0, width, height))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
let cursor = crossterm::cursor();
let crossterm = crossterm::Crossterm::from_screen(&self.screen);
let mut last_y = 0;
let mut last_x = 0;
let mut first = true;
for (x, y, cell) in content {
if y != last_y || x != last_x + 1 || first {
cursor.goto(x, y).map_err(convert_error)?;
first = false;
}
last_x = x;
last_y = y;
let mut s = crossterm::style(&cell.symbol);
if let Some(color) = cell.style.fg.into() {
s = s.with(color)
}
if let Some(color) = cell.style.bg.into() {
s = s.on(color)
}
if let Some(attr) = cell.style.modifier.into() {
s = s.attr(attr)
}
crossterm.paint(s).map_err(convert_error)?;
}
Ok(())
}
}
impl From<Color> for Option<crossterm::Color> {
fn from(color: Color) -> Option<crossterm::Color> {
match color {
Color::Reset => None,
Color::Black => Some(crossterm::Color::Black),
Color::Red => Some(crossterm::Color::DarkRed),
Color::Green => Some(crossterm::Color::DarkGreen),
Color::Yellow => Some(crossterm::Color::DarkYellow),
Color::Blue => Some(crossterm::Color::DarkBlue),
Color::Magenta => Some(crossterm::Color::DarkMagenta),
Color::Cyan => Some(crossterm::Color::DarkCyan),
Color::Gray => Some(crossterm::Color::Grey),
Color::DarkGray => Some(crossterm::Color::Grey),
Color::LightRed => Some(crossterm::Color::Red),
Color::LightGreen => Some(crossterm::Color::Green),
Color::LightBlue => Some(crossterm::Color::Blue),
Color::LightYellow => Some(crossterm::Color::Yellow),
Color::LightMagenta => Some(crossterm::Color::Magenta),
Color::LightCyan => Some(crossterm::Color::Cyan),
Color::White => Some(crossterm::Color::White),
Color::Rgb(r, g, b) => Some(crossterm::Color::Rgb { r, g, b }),
}
}
}
impl From<Modifier> for Option<crossterm::Attribute> {
#[cfg(unix)]
fn from(modifier: Modifier) -> Option<crossterm::Attribute> {
match modifier {
Modifier::Blink => Some(crossterm::Attribute::SlowBlink),
Modifier::Bold => Some(crossterm::Attribute::Bold),
Modifier::CrossedOut => Some(crossterm::Attribute::CrossedOut),
Modifier::Faint => Some(crossterm::Attribute::Dim),
Modifier::Invert => Some(crossterm::Attribute::Reverse),
Modifier::Italic => Some(crossterm::Attribute::Italic),
Modifier::Underline => Some(crossterm::Attribute::Underlined),
_ => None,
}
}
#[cfg(windows)]
fn from(modifier: Modifier) -> Option<crossterm::Attribute> {
match modifier {
Modifier::Bold => Some(crossterm::Attribute::Bold),
Modifier::Underline => Some(crossterm::Attribute::Underlined),
_ => None,
}
}
}

View File

@@ -1,7 +1,7 @@
use std::io;
use buffer::Cell;
use layout::Rect;
use crate::buffer::Cell;
use crate::layout::Rect;
#[cfg(feature = "rustbox")]
mod rustbox;
@@ -11,7 +11,15 @@ pub use self::rustbox::RustboxBackend;
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termion")]
pub use self::termion::{MouseBackend, RawBackend, TermionBackend, AlternateScreenBackend};
pub use self::termion::TermionBackend;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
mod test;
pub use self::test::TestBackend;
pub trait Backend {
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>

View File

@@ -1,11 +1,10 @@
extern crate rustbox;
use log::debug;
use std::io;
use super::Backend;
use buffer::Cell;
use layout::Rect;
use style::{Color, Modifier};
use crate::buffer::Cell;
use crate::layout::Rect;
use crate::style::{Color, Modifier};
pub struct RustboxBackend {
rustbox: rustbox::RustBox,
@@ -13,8 +12,8 @@ pub struct RustboxBackend {
impl RustboxBackend {
pub fn new() -> Result<RustboxBackend, rustbox::InitError> {
let rustbox = try!(rustbox::RustBox::init(Default::default()));
Ok(RustboxBackend { rustbox: rustbox })
let rustbox = r#try!(rustbox::RustBox::init(Default::default()));
Ok(RustboxBackend { rustbox })
}
pub fn with_rustbox(instance: rustbox::RustBox) -> RustboxBackend {
@@ -57,12 +56,23 @@ impl Backend for RustboxBackend {
Ok(())
}
fn size(&self) -> Result<Rect, io::Error> {
Ok(Rect {
x: 0,
y: 0,
width: self.rustbox.width() as u16,
height: self.rustbox.height() as u16,
})
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();
@@ -85,6 +95,7 @@ impl Into<rustbox::Color> for Color {
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::Rgb(r, g, b) => rustbox::Color::Byte(rgb_to_byte(r, g, b)),
}
}

View File

@@ -1,14 +1,11 @@
extern crate termion;
use log::debug;
use std::io;
use std::io::Write;
use self::termion::raw::IntoRawMode;
use super::Backend;
use buffer::Cell;
use layout::Rect;
use style::{Color, Modifier, Style};
use crate::buffer::Cell;
use crate::layout::Rect;
use crate::style::{Color, Modifier, Style};
pub struct TermionBackend<W>
where
@@ -17,40 +14,11 @@ where
stdout: W,
}
pub type RawBackend = TermionBackend<termion::raw::RawTerminal<io::Stdout>>;
pub type MouseBackend =
TermionBackend<termion::input::MouseTerminal<termion::raw::RawTerminal<io::Stdout>>>;
pub type AlternateScreenBackend =
TermionBackend<termion::screen::AlternateScreen<termion::raw::RawTerminal<io::Stdout>>>;
impl RawBackend {
pub fn new() -> Result<RawBackend, io::Error> {
let raw = io::stdout().into_raw_mode()?;
Ok(TermionBackend::with_stdout(raw))
}
}
impl MouseBackend {
pub fn new() -> Result<MouseBackend, io::Error> {
let raw = io::stdout().into_raw_mode()?;
let mouse = termion::input::MouseTerminal::from(raw);
Ok(TermionBackend::with_stdout(mouse))
}
}
impl AlternateScreenBackend {
pub fn new() -> Result<AlternateScreenBackend, io::Error> {
let raw = io::stdout().into_raw_mode()?;
let screen = termion::screen::AlternateScreen::from(raw);
Ok(TermionBackend::with_stdout(screen))
}
}
impl<W> TermionBackend<W>
where
W: Write,
{
pub fn with_stdout(stdout: W) -> TermionBackend<W> {
pub fn new(stdout: W) -> TermionBackend<W> {
TermionBackend { stdout }
}
}
@@ -101,7 +69,7 @@ where
let mut last_x = 0;
let mut inst = 0;
for (x, y, cell) in content {
if y != last_y || x != last_x + 1 {
if y != last_y || x != last_x + 1 || inst == 0 {
string.push_str(&format!("{}", termion::cursor::Goto(x + 1, y + 1)));
inst += 1;
}
@@ -141,14 +109,9 @@ where
}
/// Return the size of the terminal
fn size(&self) -> Result<Rect, io::Error> {
let terminal = try!(termion::terminal_size());
Ok(Rect {
x: 0,
y: 0,
width: terminal.0,
height: terminal.1,
})
fn size(&self) -> io::Result<Rect> {
let terminal = r#try!(termion::terminal_size());
Ok(Rect::new(0, 0, terminal.0, terminal.1))
}
fn flush(&mut self) -> io::Result<()> {
@@ -157,38 +120,38 @@ where
}
macro_rules! termion_fg {
($color:ident) => (
($color:ident) => {
format!("{}", termion::color::Fg(termion::color::$color))
);
};
}
macro_rules! termion_fg_rgb {
($r:expr, $g:expr, $b:expr) => (
($r:expr, $g:expr, $b:expr) => {
format!("{}", termion::color::Fg(termion::color::Rgb($r, $g, $b)))
);
};
}
macro_rules! termion_bg {
($color:ident) => (
($color:ident) => {
format!("{}", termion::color::Bg(termion::color::$color))
);
};
}
macro_rules! termion_bg_rgb {
($r:expr, $g:expr, $b:expr) => (
($r:expr, $g:expr, $b:expr) => {
format!("{}", termion::color::Bg(termion::color::Rgb($r, $g, $b)))
);
};
}
macro_rules! termion_modifier {
($style:ident) => (
($style:ident) => {
format!("{}", termion::style::$style)
);
};
}
impl Color {
pub fn termion_fg(&self) -> String {
match *self {
pub fn termion_fg(self) -> String {
match self {
Color::Reset => termion_fg!(Reset),
Color::Black => termion_fg!(Black),
Color::Red => termion_fg!(Red),
@@ -209,8 +172,8 @@ impl Color {
Color::Rgb(r, g, b) => termion_fg_rgb!(r, g, b),
}
}
pub fn termion_bg(&self) -> String {
match *self {
pub fn termion_bg(self) -> String {
match self {
Color::Reset => termion_bg!(Reset),
Color::Black => termion_bg!(Black),
Color::Red => termion_bg!(Red),
@@ -234,8 +197,8 @@ impl Color {
}
impl Modifier {
pub fn termion_modifier(&self) -> String {
match *self {
pub fn termion_modifier(self) -> String {
match self {
Modifier::Blink => termion_modifier!(Blink),
Modifier::Bold => termion_modifier!(Bold),
Modifier::CrossedOut => termion_modifier!(CrossedOut),

58
src/backend/test.rs Normal file
View File

@@ -0,0 +1,58 @@
use crate::backend::Backend;
use crate::buffer::{Buffer, Cell};
use crate::layout::Rect;
use std::io;
#[derive(Debug)]
pub struct TestBackend {
width: u16,
buffer: Buffer,
height: u16,
cursor: bool,
}
impl TestBackend {
pub fn new(width: u16, height: u16) -> TestBackend {
TestBackend {
width,
height,
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
cursor: false,
}
}
pub fn buffer(&self) -> &Buffer {
&self.buffer
}
}
impl Backend for TestBackend {
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, c) in content {
let cell = self.buffer.get_mut(x, y);
cell.symbol = c.symbol.clone();
cell.style = c.style;
}
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 clear(&mut self) -> Result<(), io::Error> {
Ok(())
}
fn size(&self) -> Result<Rect, io::Error> {
Ok(Rect::new(0, 0, self.width, self.height))
}
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
}

View File

@@ -1,10 +1,12 @@
use std::cmp::min;
use std::fmt;
use std::usize;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use layout::Rect;
use style::{Color, Modifier, Style};
use crate::layout::Rect;
use crate::style::{Color, Modifier, Style};
/// A buffer cell
#[derive(Debug, Clone, PartialEq)]
@@ -72,7 +74,6 @@ impl Default for Cell {
/// # Examples:
///
/// ```
/// # extern crate tui;
/// use tui::buffer::{Buffer, Cell};
/// use tui::layout::Rect;
/// use tui::style::{Color, Style, Modifier};
@@ -81,7 +82,7 @@ impl Default for Cell {
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf.get_mut(0, 2).set_symbol("x");
/// assert_eq!(buf.get(0, 2).symbol, "x");
/// buf.set_string(3, 0, "string", &Style::default().fg(Color::Red).bg(Color::White));
/// 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 {
@@ -93,7 +94,7 @@ impl Default for Cell {
/// assert_eq!(buf.get(5, 0).symbol, "x");
/// # }
/// ```
#[derive(Debug, Clone)]
#[derive(Clone, PartialEq)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
@@ -111,6 +112,49 @@ impl Default for Buffer {
}
}
impl fmt::Debug for Buffer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Buffer: {:?}", self.area)?;
f.write_str("Content (quoted lines):\n")?;
for cells in self.content.chunks(self.area.width as usize) {
let mut line = String::new();
let mut overwritten = vec![];
let mut skip: usize = 0;
for (x, c) in cells.iter().enumerate() {
if skip == 0 {
line.push_str(&c.symbol);
} else {
overwritten.push((x, &c.symbol))
}
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
}
f.write_fmt(format_args!("{:?},", line))?;
if !overwritten.is_empty() {
f.write_fmt(format_args!(
" Hidden by multi-width symbols: {:?}",
overwritten
))?;
}
f.write_str("\n")?;
}
f.write_str("Style:\n")?;
for cells in self.content.chunks(self.area.width as usize) {
f.write_str("|")?;
for cell in cells {
write!(
f,
"{} {} {}|",
cell.style.fg.code(),
cell.style.bg.code(),
cell.style.modifier.code()
)?;
}
f.write_str("\n")?;
}
Ok(())
}
}
impl Buffer {
/// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer {
@@ -125,10 +169,28 @@ impl Buffer {
for _ in 0..size {
content.push(cell.clone());
}
Buffer {
area: area,
content: content,
Buffer { area, content }
}
/// Returns a Buffer containing the given lines
pub fn with_lines<S>(lines: Vec<S>) -> Buffer
where
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 mut buffer = Buffer::empty(Rect {
x: 0,
y: 0,
width,
height,
});
for (y, line) in lines.iter().enumerate() {
buffer.set_string(0, y as u16, line, Style::default());
}
buffer
}
/// Returns the content of the buffer as a slice
@@ -183,7 +245,9 @@ impl Buffer {
/// ```
pub fn index_of(&self, x: u16, y: u16) -> usize {
debug_assert!(
x >= self.area.left() && x < self.area.right() && y >= self.area.top()
x >= self.area.left()
&& x < self.area.right()
&& y >= self.area.top()
&& y < self.area.bottom(),
"Trying to access position outside the buffer: x={}, y={}, area={:?}",
x,
@@ -234,21 +298,39 @@ impl Buffer {
}
/// Print a string, starting at the position (x, y)
pub fn set_string(&mut self, x: u16, y: u16, string: &str, style: &Style) {
pub fn set_string<S>(&mut self, x: u16, y: u16, string: S, style: Style)
where
S: AsRef<str>,
{
self.set_stringn(x, y, string, usize::MAX, style);
}
/// Print at most the first n characters of a string if enough space is available
/// until the end of the line
pub fn set_stringn(&mut self, x: u16, y: u16, string: &str, limit: usize, style: &Style) {
pub fn set_stringn<S>(&mut self, x: u16, y: u16, string: S, width: usize, style: Style)
where
S: AsRef<str>,
{
let mut index = self.index_of(x, y);
let graphemes = UnicodeSegmentation::graphemes(string, true).collect::<Vec<&str>>();
let max_index = min((self.area.right() - x) as usize, limit);
for s in graphemes.into_iter().take(max_index) {
self.content[index].symbol.clear();
self.content[index].symbol.push_str(s);
self.content[index].style = *style;
index += 1;
let mut x_offset = x as usize;
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
for s in graphemes {
let width = s.width();
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
// change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
if width > max_offset.saturating_sub(x_offset) {
break;
}
self.content[index].set_symbol(s);
self.content[index].set_style(style);
// Reset following cells if multi-width (they would be hidden by the grapheme),
for i in index + 1..index + width {
self.content[i].reset();
}
index += width;
x_offset += width;
}
}
@@ -273,7 +355,7 @@ impl Buffer {
/// Merge an other buffer into this one
pub fn merge(&mut self, other: &Buffer) {
let area = self.area.union(&other.area);
let area = self.area.union(other.area);
let cell: Cell = Default::default();
self.content.resize(area.area() as usize, cell.clone());
@@ -304,12 +386,72 @@ impl Buffer {
}
self.area = area;
}
/// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
/// self to other.
///
/// We're assuming that buffers are well-formed, that is no double-width cell is followed by
/// a non-blank cell.
///
/// # Multi-width characters handling:
///
/// ```text
/// (Index:) `01`
/// Prev: `コ`
/// Next: `aa`
/// Updates: `0: a, 1: a'
/// ```
///
/// ```text
/// (Index:) `01`
/// Prev: `a `
/// Next: `コ`
/// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
/// ```
///
/// ```text
/// (Index:) `012`
/// Prev: `aaa`
/// Next: `aコ`
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
/// ```
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
let previous_buffer = &self.content;
let next_buffer = &other.content;
let width = self.area.width;
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
// Cells invalidated by drawing/replacing preceeding multi-width characters:
let mut invalidated: usize = 0;
// Cells from the current buffer to skip due to preceeding multi-width characters taking their
// place (the skipped cells should be blank anyway):
let mut to_skip: usize = 0;
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
if (current != previous || invalidated > 0) && to_skip == 0 {
let x = i as u16 % width;
let y = i as u16 / width;
updates.push((x, y, &next_buffer[i]));
}
to_skip = current.symbol.width().saturating_sub(1);
let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
}
updates
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cell(s: &str) -> Cell {
let mut cell = Cell::default();
cell.set_symbol(s);
cell
}
#[test]
fn it_translates_to_and_from_coordinates() {
let rect = Rect::new(200, 100, 50, 80);
@@ -343,4 +485,149 @@ mod tests {
// width is 10; zero-indexed means that 10 would be the 11th cell.
buf.index_of(10, 0);
}
#[test]
fn buffer_set_string() {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
// Zero-width
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
assert_eq!(buffer, Buffer::with_lines(vec![" "]));
buffer.set_string(0, 0, "aaa", Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
// Width limit:
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
buffer.set_string(0, 0, "12345", Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// Width truncation:
buffer.set_string(0, 0, "123456", Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
}
#[test]
fn buffer_set_string_double_width() {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
buffer.set_string(0, 0, "コン", Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
// Only 1 space left.
buffer.set_string(0, 0, "コンピ", Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
}
#[test]
fn buffer_with_lines() {
let buffer = Buffer::with_lines(vec![
"┌────────┐",
"│コンピュ│",
"│ーa 上で│",
"└────────┘",
]);
assert_eq!(buffer.area.x, 0);
assert_eq!(buffer.area.y, 0);
assert_eq!(buffer.area.width, 10);
assert_eq!(buffer.area.height, 4);
}
#[test]
fn buffer_diffing_empty_empty() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::empty(area);
let next = Buffer::empty(area);
let diff = prev.diff(&next);
assert_eq!(diff, vec![]);
}
#[test]
fn buffer_diffing_empty_filled() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::empty(area);
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
let diff = prev.diff(&next);
assert_eq!(diff.len(), 40 * 40);
}
#[test]
fn buffer_diffing_filled_filled() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
let diff = prev.diff(&next);
assert_eq!(diff, vec![]);
}
#[test]
fn buffer_diffing_single_width() {
let prev = Buffer::with_lines(vec![
" ",
"┌Title─┐ ",
"│ │ ",
"│ │ ",
"└──────┘ ",
]);
let next = Buffer::with_lines(vec![
" ",
"┌TITLE─┐ ",
"│ │ ",
"│ │ ",
"└──────┘ ",
]);
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![
(2, 1, &cell("I")),
(3, 1, &cell("T")),
(4, 1, &cell("L")),
(5, 1, &cell("E")),
]
);
}
#[test]
#[rustfmt::skip]
fn buffer_diffing_multi_width() {
let prev = Buffer::with_lines(vec![
"┌Title─┐ ",
"└──────┘ ",
]);
let next = Buffer::with_lines(vec![
"┌称号──┐ ",
"└──────┘ ",
]);
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![
(1, 0, &cell("")),
// Skipped "i"
(3, 0, &cell("")),
// Skipped "l"
(5, 0, &cell("")),
]
);
}
#[test]
fn buffer_diffing_multi_width_offset() {
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![
(1, 0, &cell("")),
(2, 0, &cell("")),
(4, 0, &cell("")),
]
);
}
}

View File

@@ -1,212 +1,188 @@
use std::cell::RefCell;
use std::cmp::{max, min};
use std::collections::HashMap;
use cassowary::{Constraint, Expression, Solver, Variable};
use cassowary::WeightedRelation::*;
use cassowary::strength::{REQUIRED, WEAK};
use terminal::Terminal;
use backend::Backend;
use cassowary::WeightedRelation::*;
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
pub enum Corner {
TopLeft,
TopRight,
BottomRight,
BottomLeft,
}
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
pub enum Direction {
Horizontal,
Vertical,
}
/// 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)]
pub struct Rect {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
impl Default for Rect {
fn default() -> Rect {
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}
}
}
impl Rect {
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
Rect {
x: x,
y: y,
width: width,
height: height,
}
}
pub fn area(&self) -> u16 {
self.width * self.height
}
pub fn left(&self) -> u16 {
self.x
}
pub fn right(&self) -> u16 {
self.x + self.width
}
pub fn top(&self) -> u16 {
self.y
}
pub fn bottom(&self) -> u16 {
self.y + self.height
}
pub fn inner(&self, margin: u16) -> Rect {
if self.width < 2 * margin || self.height < 2 * margin {
Rect::default()
} else {
Rect {
x: self.x + margin,
y: self.y + margin,
width: self.width - 2 * margin,
height: self.height - 2 * margin,
}
}
}
pub fn union(&self, other: &Rect) -> Rect {
let x1 = min(self.x, other.x);
let y1 = min(self.y, other.y);
let x2 = max(self.x + self.width, other.x + other.width);
let y2 = max(self.y + self.height, other.y + other.height);
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
}
pub fn intersection(&self, other: &Rect) -> Rect {
let x1 = max(self.x, other.x);
let y1 = max(self.y, other.y);
let x2 = min(self.x + self.width, other.x + other.width);
let y2 = min(self.y + self.height, other.y + other.height);
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
}
pub fn intersects(&self, other: &Rect) -> bool {
self.x < other.x + other.width && self.x + self.width > other.x
&& self.y < other.y + other.height && self.y + self.height > other.y
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Size {
Fixed(u16),
Percent(u16),
pub enum Constraint {
// TODO: enforce range 0 - 100
Percentage(u16),
Length(u16),
Max(u16),
Min(u16),
}
/// 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.
///
/// # Examples
/// ```
/// # extern crate tui;
/// # use tui::layout::{Rect, Size, Direction, split};
///
/// # fn main() {
/// let chunks = split(&Rect{x: 2, y: 2, width: 10, height: 10},
/// &Direction::Vertical,
/// 0,
/// &[Size::Fixed(5), Size::Min(0)]);
/// assert_eq!(chunks, vec![Rect{x:2, y: 2, width: 10, height: 5},
/// Rect{x: 2, y: 7, width: 10, height: 5}])
/// # }
///
/// ```
pub fn split(area: &Rect, dir: &Direction, margin: u16, sizes: &[Size]) -> Vec<Rect> {
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alignment {
Left,
Center,
Right,
}
// TODO: enforce constraints size once const generics has landed
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Layout {
direction: Direction,
margin: u16,
constraints: Vec<Constraint>,
}
thread_local! {
static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
}
impl Default for Layout {
fn default() -> Layout {
Layout {
direction: Direction::Vertical,
margin: 0,
constraints: Vec::new(),
}
}
}
impl Layout {
pub fn constraints<C>(mut self, constraints: C) -> Layout
where
C: Into<Vec<Constraint>>,
{
self.constraints = constraints.into();
self
}
pub fn margin(mut self, margin: u16) -> Layout {
self.margin = margin;
self
}
pub fn direction(mut self, direction: Direction) -> Layout {
self.direction = direction;
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.
///
/// # Examples
/// ```
/// # use tui::layout::{Rect, Constraint, Direction, Layout};
///
/// # fn main() {
/// let chunks = Layout::default()
/// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
/// .split(Rect{x: 2, y: 2, width: 10, height: 10});
/// assert_eq!(chunks, vec![Rect{x:2, y: 2, width: 10, height: 5},
/// Rect{x: 2, y: 7, width: 10, height: 5}])
/// # }
///
/// ```
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))
.clone()
})
}
}
fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
let mut solver = Solver::new();
let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
let elements = sizes
let elements = layout
.constraints
.iter()
.map(|_| Element::new())
.collect::<Vec<Element>>();
let mut results = sizes.iter().map(|_| Rect::default()).collect::<Vec<Rect>>();
let dest_area = area.inner(margin);
let mut results = layout
.constraints
.iter()
.map(|_| Rect::default())
.collect::<Vec<Rect>>();
let dest_area = area.inner(layout.margin);
for (i, e) in elements.iter().enumerate() {
vars.insert(e.x, (i, 0));
vars.insert(e.y, (i, 1));
vars.insert(e.width, (i, 2));
vars.insert(e.height, (i, 3));
}
let mut constraints: Vec<Constraint> = Vec::with_capacity(elements.len() * 4 + sizes.len() * 6);
let mut ccs: Vec<CassowaryConstraint> =
Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6);
for elt in &elements {
constraints.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
constraints.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
constraints.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
constraints.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom()));
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()));
ccs.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom()));
}
if let Some(first) = elements.first() {
constraints.push(match *dir {
ccs.push(match layout.direction {
Direction::Horizontal => first.left() | EQ(REQUIRED) | f64::from(dest_area.left()),
Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
});
}
if let Some(last) = elements.last() {
constraints.push(match *dir {
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 *dir {
match layout.direction {
Direction::Horizontal => {
for pair in elements.windows(2) {
constraints.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
}
for (i, size) in sizes.iter().enumerate() {
constraints.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
constraints.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
constraints.push(match *size {
Size::Fixed(v) => elements[i].width | EQ(WEAK) | f64::from(v),
Size::Percent(v) => {
for (i, size) in layout.constraints.iter().enumerate() {
ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
ccs.push(match *size {
Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
}
Size::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
Size::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
});
}
}
Direction::Vertical => {
for pair in elements.windows(2) {
constraints.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
}
for (i, size) in sizes.iter().enumerate() {
constraints.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
constraints.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
constraints.push(match *size {
Size::Fixed(v) => elements[i].height | EQ(WEAK) | f64::from(v),
Size::Percent(v) => {
for (i, size) in layout.constraints.iter().enumerate() {
ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
ccs.push(match *size {
Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
}
Size::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
Size::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
});
}
}
}
solver.add_constraints(&constraints).unwrap();
solver.add_constraints(&ccs).unwrap();
for &(var, value) in solver.fetch_changes() {
let (index, attr) = vars[&var];
let value = if value.is_sign_negative() {
@@ -233,7 +209,7 @@ pub fn split(area: &Rect, dir: &Direction, margin: u16, sizes: &[Size]) -> Vec<R
// Fix imprecision by extending the last item a bit if necessary
if let Some(last) = results.last_mut() {
match *dir {
match layout.direction {
Direction::Vertical => {
last.height = dest_area.bottom() - last.y;
}
@@ -280,58 +256,157 @@ impl Element {
}
}
/// Describes a layout and may be used to group widgets in a specific area of the terminal
///
/// # Examples
///
/// ```
/// # extern crate tui;
/// use tui::layout::{Group, Direction, Size};
/// # fn main() {
/// Group::default()
/// .direction(Direction::Vertical)
/// .margin(0)
/// .sizes(&[Size::Percent(50), Size::Percent(50)]);
/// # }
/// ```
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
pub struct Group {
pub direction: Direction,
pub margin: u16,
pub sizes: Vec<Size>,
/// 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)]
pub struct Rect {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
impl Default for Group {
fn default() -> Group {
Group {
direction: Direction::Horizontal,
margin: 0,
sizes: Vec::new(),
impl Default for Rect {
fn default() -> Rect {
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}
}
}
impl Group {
pub fn direction(&mut self, direction: Direction) -> &mut Group {
self.direction = direction;
self
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.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
let max_area = u16::max_value();
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
let aspect_ratio = f64::from(width) / f64::from(height);
let max_area_f = f64::from(max_area);
let height_f = (max_area_f / aspect_ratio).sqrt();
let width_f = height_f * aspect_ratio;
(width_f as u16, height_f as u16)
} else {
(width, height)
};
Rect {
x,
y,
width: clipped_width,
height: clipped_height,
}
}
pub fn margin(&mut self, margin: u16) -> &mut Group {
self.margin = margin;
self
pub fn area(self) -> u16 {
self.width * self.height
}
pub fn sizes(&mut self, sizes: &[Size]) -> &mut Group {
self.sizes = Vec::from(sizes);
self
pub fn left(self) -> u16 {
self.x
}
pub fn render<F, B>(&self, t: &mut Terminal<B>, area: &Rect, f: F)
where
B: Backend,
F: FnOnce(&mut Terminal<B>, &[Rect]),
{
let chunks = t.compute_layout(self, area);
f(t, &chunks);
pub fn right(self) -> u16 {
self.x + self.width
}
pub fn top(self) -> u16 {
self.y
}
pub fn bottom(self) -> u16 {
self.y + self.height
}
pub fn inner(self, margin: u16) -> Rect {
if self.width < 2 * margin || self.height < 2 * margin {
Rect::default()
} else {
Rect {
x: self.x + margin,
y: self.y + margin,
width: self.width - 2 * margin,
height: self.height - 2 * margin,
}
}
}
pub fn union(self, other: Rect) -> Rect {
let x1 = min(self.x, other.x);
let y1 = min(self.y, other.y);
let x2 = max(self.x + self.width, other.x + other.width);
let y2 = max(self.y + self.height, other.y + other.height);
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
}
pub fn intersection(self, other: Rect) -> Rect {
let x1 = max(self.x, other.x);
let y1 = max(self.y, other.y);
let x2 = min(self.x + self.width, other.x + other.width);
let y2 = min(self.y + self.height, other.y + other.height);
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
}
pub fn intersects(self, other: Rect) -> bool {
self.x < other.x + other.width
&& self.x + self.width > other.x
&& self.y < other.y + other.height
&& self.y + self.height > other.y
}
}
#[test]
fn test_rect_size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
)
}
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
}
#[test]
fn test_rect_size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
}
}
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
}

View File

@@ -1,7 +1,7 @@
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
//! terminal users interfaces and dashboards.
//!
//! ![](https://raw.githubusercontent.com/fdehau/tui-rs/master/docs/demo.gif)
//! ![](https://raw.githubusercontent.com/fdehau/tui-rs/master/assets/demo.gif)
//!
//! # Get started
//!
@@ -13,14 +13,16 @@
//! backend is available.
//!
//! ```rust,no_run
//! extern crate tui;
//!
//! use std::io;
//! use tui::Terminal;
//! use tui::backend::RawBackend;
//! use tui::backend::TermionBackend;
//! use termion::raw::IntoRawMode;
//!
//! fn main() {
//! let backend = RawBackend::new().unwrap();
//! let mut terminal = Terminal::new(backend).unwrap();
//! fn main() -> Result<(), io::Error> {
//! let stdout = io::stdout().into_raw_mode()?;
//! let backend = TermionBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! Ok(())
//! }
//! ```
//!
@@ -29,7 +31,7 @@
//!
//! ```toml
//! [dependencies.tui]
//! version = "0.2.0"
//! version = "0.3.0"
//! default-features = false
//! features = ['rustbox']
//! ```
@@ -37,14 +39,14 @@
//! and then create the terminal in a similar way:
//!
//! ```rust,ignore
//! extern crate tui;
//!
//! use tui::Terminal;
//! use tui::backend::RustboxBackend;
//!
//! fn main() {
//! let backend = RustboxBackend::new().unwrap();
//! let mut terminal = Terminal::new(backend).unwrap();
//! fn main() -> Result<(), io::Error> {
//! let backend = RustboxBackend::new()?;
//! let mut terminal = Terminal::new(backend);
//! Ok(())
//! }
//! ```
//!
@@ -56,117 +58,88 @@
//!
//! Each widget follows a builder pattern API providing a default configuration
//! along with methods to customize them. The widget is then registered using
//! its `render` method that take a `Terminal` instance and an area to draw
//! its `render` method that take a `Frame` instance and an area to draw
//! to.
//!
//! The following example renders a block of the size of the terminal:
//!
//! ```rust,no_run
//! extern crate tui;
//!
//! use std::io;
//!
//! use termion::raw::IntoRawMode;
//! use tui::Terminal;
//! use tui::backend::RawBackend;
//! use tui::backend::TermionBackend;
//! use tui::widgets::{Widget, Block, Borders};
//! use tui::layout::{Group, Size, Direction};
//! use tui::layout::{Layout, Constraint, Direction};
//!
//! fn main() {
//! let mut terminal = init().expect("Failed initialization");
//! draw(&mut terminal).expect("Failed to draw");
//! }
//!
//! fn init() -> Result<Terminal<RawBackend>, io::Error> {
//! let backend = RawBackend::new()?;
//! Terminal::new(backend)
//! }
//!
//! fn draw(t: &mut Terminal<RawBackend>) -> Result<(), io::Error> {
//!
//! let size = t.size()?;
//!
//! Block::default()
//! .title("Block")
//! .borders(Borders::ALL)
//! .render(t, &size);
//!
//! t.draw()
//! 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 size = f.size();
//! Block::default()
//! .title("Block")
//! .borders(Borders::ALL)
//! .render(&mut f, size);
//! })
//! }
//! ```
//!
//! ## Layout
//!
//! The library comes with a basic yet useful layout management object called
//! `Group`. As you may see below and in the examples, the library makes heavy
//! use of the builder pattern to provide full customization. And the `Group`
//! object is no exception:
//! The library comes with a basic yet useful layout management object called `Layout`. As you may
//! see below and in the examples, the library makes heavy use of the builder pattern to provide
//! full customization. And `Layout` is no exception:
//!
//! ```rust,no_run
//! extern crate tui;
//!
//! use std::io;
//!
//! use termion::raw::IntoRawMode;
//! use tui::Terminal;
//! use tui::backend::RawBackend;
//! use tui::backend::TermionBackend;
//! use tui::widgets::{Widget, Block, Borders};
//! use tui::layout::{Group, Size, Direction};
//! use tui::layout::{Layout, Constraint, Direction};
//!
//! fn main() {
//! let mut terminal = init().expect("Failed initialization");
//! draw(&mut terminal).expect("Failed to draw");
//! }
//!
//! fn init() -> Result<Terminal<RawBackend>, io::Error> {
//! let backend = RawBackend::new()?;
//! Terminal::new(backend)
//! }
//!
//! fn draw(t: &mut Terminal<RawBackend>) -> Result<(), io::Error> {
//!
//! let size = t.size()?;
//!
//! Group::default()
//! .direction(Direction::Vertical)
//! .margin(1)
//! .sizes(&[Size::Percent(10), Size::Percent(80), Size::Percent(10)])
//! .render(t, &size, |t, chunks| {
//! Block::default()
//! .title("Block")
//! .borders(Borders::ALL)
//! .render(t, &chunks[0]);
//! Block::default()
//! .title("Block 2")
//! .borders(Borders::ALL)
//! .render(t, &chunks[2]);
//! });
//!
//! t.draw()
//! 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());
//! Block::default()
//! .title("Block")
//! .borders(Borders::ALL)
//! .render(&mut f, chunks[0]);
//! Block::default()
//! .title("Block 2")
//! .borders(Borders::ALL)
//! .render(&mut f, chunks[2]);
//! })
//! }
//! ```
//!
//! This let you describe responsive terminal UI by nesting groups. You should note
//! This let you describe responsive terminal UI by nesting layouts. You should note
//! that by default the computed layout tries to fill the available space
//! completely. So if for any reason you might need a blank space somewhere, try to
//! pass an additional size to the group and don't use the corresponding area inside
//! the render method.
//!
//! Once you have finished to describe the UI, you just need to call `draw`
//! on `Terminal` to actually flush to the terminal.
//! pass an additional constraint and don't use the corresponding area.
#[macro_use]
extern crate bitflags;
extern crate cassowary;
#[macro_use]
extern crate log;
extern crate unicode_segmentation;
extern crate unicode_width;
pub mod buffer;
pub mod symbols;
pub mod backend;
pub mod buffer;
pub mod layout;
pub mod style;
pub mod symbols;
pub mod terminal;
pub mod widgets;
pub mod style;
pub mod layout;
pub use self::terminal::Terminal;
pub use self::terminal::{Frame, Terminal};

View File

@@ -20,6 +20,33 @@ pub enum Color {
Rgb(u8, u8, u8),
}
impl Color {
/// Returns a short code associated with the color, used for debug purpose
/// only
pub(crate) fn code(&self) -> &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 => "g",
Color::DarkGray => "G",
Color::LightRed => "R",
Color::LightGreen => "G",
Color::LightYellow => "Y",
Color::LightBlue => "B",
Color::LightMagenta => "M",
Color::LightCyan => "C",
Color::White => "w",
Color::Rgb(_, _, _) => "o",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Modifier {
Blink,
@@ -40,6 +67,31 @@ pub enum Modifier {
Underline,
}
impl Modifier {
/// Returns a short code associated with the color, used for debug purpose
/// only
pub(crate) fn code(&self) -> &str {
match self {
Modifier::Blink => "bl",
Modifier::Bold => "bo",
Modifier::CrossedOut => "cr",
Modifier::Faint => "fa",
Modifier::Framed => "fr",
Modifier::Invert => "in",
Modifier::Italic => "it",
Modifier::NoBlink => "BL",
Modifier::NoBold => "BO",
Modifier::NoCrossedOut => "CR",
Modifier::NoFaint => "FA",
Modifier::NoInvert => "IN",
Modifier::NoItalic => "IT",
Modifier::NoUnderline => "UN",
Modifier::Reset => "re",
Modifier::Underline => "un",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
pub fg: Color,

View File

@@ -1,17 +1,10 @@
use log::error;
use std::io;
use std::collections::HashMap;
use backend::Backend;
use buffer::Buffer;
use layout::{split, Group, Rect};
use widgets::Widget;
/// Holds a computed layout and keeps track of its use between successive draw calls
#[derive(Debug)]
pub struct LayoutEntry {
chunks: Vec<Rect>,
hot: bool,
}
use crate::backend::Backend;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::widgets::Widget;
/// Interface to the terminal backed by Termion
#[derive(Debug)]
@@ -20,13 +13,55 @@ where
B: Backend,
{
backend: B,
/// Cache to prevent the layout to be computed at each draw call
layout_cache: HashMap<(Group, Rect), LayoutEntry>,
/// Holds the results of the current and previous draw calls. The two are compared at the end
/// of each draw pass to output the necessary updates to the terminal
buffers: [Buffer; 2],
/// Index of the current buffer in the previous array
current: usize,
/// Whether the cursor is currently hidden
hidden_cursor: bool,
/// Terminal size used for rendering.
known_size: Rect,
}
/// Represents a consistent terminal interface for rendering.
pub struct Frame<'a, B: 'a>
where
B: Backend,
{
terminal: &'a mut Terminal<B>,
}
impl<'a, B> Frame<'a, B>
where
B: Backend,
{
/// Terminal size, guaranteed not to change when rendering.
pub fn size(&self) -> Rect {
self.terminal.known_size
}
/// Calls the draw method of a given widget on the current buffer
pub fn render<W>(&mut self, widget: &mut W, area: Rect)
where
W: Widget,
{
widget.draw(area, self.terminal.current_buffer_mut());
}
}
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.hidden_cursor {
if let Err(err) = self.show_cursor() {
error!("Failed to show the cursor: {}", err);
}
}
}
}
impl<B> Terminal<B>
@@ -35,16 +70,26 @@ where
{
/// Wrapper around Termion initialization. Each buffer is initialized with a blank string and
/// default colors for the foreground and the background
pub fn new(backend: B) -> Result<Terminal<B>, io::Error> {
let size = try!(backend.size());
pub fn new(backend: B) -> io::Result<Terminal<B>> {
let size = backend.size()?;
Ok(Terminal {
backend: backend,
layout_cache: HashMap::new(),
backend,
buffers: [Buffer::empty(size), Buffer::empty(size)],
current: 0,
hidden_cursor: false,
known_size: size,
})
}
/// 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 }
}
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
pub fn backend(&self) -> &B {
&self.backend
}
@@ -53,86 +98,50 @@ where
&mut self.backend
}
/// Check if we have already computed a layout for a given group, otherwise it creates one and
/// add it to the layout cache. Moreover the function marks the queried entries so that we can
/// clean outdated ones at the end of the draw call.
pub fn compute_layout(&mut self, group: &Group, area: &Rect) -> Vec<Rect> {
let entry = self.layout_cache
.entry((group.clone(), *area))
.or_insert_with(|| {
let chunks = split(area, &group.direction, group.margin, &group.sizes);
debug!(
"New layout computed:\n* Group = {:?}\n* Chunks = {:?}",
group, chunks
);
LayoutEntry {
chunks: chunks,
hot: true,
}
});
entry.hot = true;
entry.chunks.clone()
/// Obtains a difference between the previous and the current buffer and passes it to the
/// current backend for drawing.
pub fn flush(&mut self) -> io::Result<()> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
self.backend.draw(updates.into_iter())
}
/// Builds a string representing the minimal escape sequences and characters set necessary to
/// update the UI and writes it to stdout.
pub fn flush(&mut self) -> Result<(), io::Error> {
let width = self.buffers[self.current].area.width;
let content = self.buffers[self.current]
.content
.iter()
.zip(self.buffers[1 - self.current].content.iter())
.enumerate()
.filter_map(|(i, (c, p))| {
if c != p {
let i = i as u16;
let x = i % width;
let y = i / width;
Some((x, y, c))
} else {
None
}
});
self.backend.draw(content)
}
/// Calls the draw method of a given widget on the current buffer
pub fn render<W>(&mut self, widget: &mut W, area: &Rect)
where
W: Widget,
{
widget.draw(area, &mut self.buffers[self.current]);
}
/// Updates the interface so that internal buffers matches the current size of the terminal.
/// This leads to a full redraw of the screen.
pub fn resize(&mut self, area: Rect) -> Result<(), io::Error> {
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
/// be saved so the size can remain consistent when rendering.
/// 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].resize(area);
self.buffers[1 - self.current].reset();
self.layout_cache.clear();
self.buffers[1 - self.current].resize(area);
self.known_size = area;
self.backend.clear()
}
/// Flushes the current internal state and prepares the interface for the next draw call
pub fn draw(&mut self) -> Result<(), io::Error> {
/// 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)?;
}
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<()>
where
F: FnOnce(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());
// Draw to stdout
self.flush()?;
// Clean layout cache
let hot = self.layout_cache
.drain()
.filter(|&(_, ref v)| v.hot)
.collect::<Vec<((Group, Rect), LayoutEntry)>>();
for (key, value) in hot {
self.layout_cache.insert(key, value);
}
for e in self.layout_cache.values_mut() {
e.hot = false;
}
// Swap buffers
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
@@ -142,16 +151,21 @@ where
Ok(())
}
pub fn hide_cursor(&mut self) -> Result<(), io::Error> {
self.backend.hide_cursor()
pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
pub fn show_cursor(&mut self) -> Result<(), io::Error> {
self.backend.show_cursor()
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
pub fn clear(&mut self) -> Result<(), io::Error> {
pub fn clear(&mut self) -> io::Result<()> {
self.backend.clear()
}
pub fn size(&self) -> Result<Rect, io::Error> {
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Rect> {
self.backend.size()
}
}

View File

@@ -2,18 +2,17 @@ use std::cmp::{max, min};
use unicode_width::UnicodeWidthStr;
use widgets::{Block, Widget};
use buffer::Buffer;
use layout::Rect;
use style::Style;
use symbols::bar;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::symbols::bar;
use crate::widgets::{Block, Widget};
/// Display multiple bars in a single widgets
///
/// # Examples
///
/// ```
/// # extern crate tui;
/// # use tui::widgets::{Block, Borders, BarChart};
/// # use tui::style::{Style, Color, Modifier};
/// # fn main() {
@@ -67,7 +66,7 @@ impl<'a> Default for BarChart<'a> {
}
impl<'a> BarChart<'a> {
pub fn data(&'a mut self, data: &'a [(&'a str, u64)]) -> &mut BarChart<'a> {
pub fn data(mut self, data: &'a [(&'a str, u64)]) -> BarChart<'a> {
self.data = data;
self.values = Vec::with_capacity(self.data.len());
for &(_, v) in self.data {
@@ -76,60 +75,62 @@ impl<'a> BarChart<'a> {
self
}
pub fn block(&'a mut self, block: Block<'a>) -> &mut BarChart<'a> {
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
self.block = Some(block);
self
}
pub fn max(&'a mut self, max: u64) -> &mut BarChart<'a> {
pub fn max(mut self, max: u64) -> BarChart<'a> {
self.max = Some(max);
self
}
pub fn bar_width(&'a mut self, width: u16) -> &mut BarChart<'a> {
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
self.bar_width = width;
self
}
pub fn bar_gap(&'a mut self, gap: u16) -> &mut BarChart<'a> {
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
self.bar_gap = gap;
self
}
pub fn value_style(&'a mut self, style: Style) -> &mut BarChart<'a> {
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
self.value_style = style;
self
}
pub fn label_style(&'a mut self, style: Style) -> &mut BarChart<'a> {
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
self.label_style = style;
self
}
pub fn style(&'a mut self, style: Style) -> &mut BarChart<'a> {
pub fn style(mut self, style: Style) -> BarChart<'a> {
self.style = style;
self
}
}
impl<'a> Widget for BarChart<'a> {
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
let chart_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.inner(area)
}
None => *area,
None => area,
};
if chart_area.height < 2 {
return;
}
self.background(&chart_area, buf, self.style.bg);
self.background(chart_area, buf, self.style.bg);
let max = self.max
let max = self
.max
.unwrap_or_else(|| self.data.iter().fold(0, |acc, &(_, v)| max(v, acc)));
let max_index = min(
(chart_area.width / (self.bar_width + self.bar_gap)) as usize,
self.data.len(),
);
let mut data = self.data
let mut data = self
.data
.iter()
.take(max_index)
.map(|&(l, v)| (l, v * u64::from(chart_area.height) * 8 / max))
@@ -152,8 +153,9 @@ impl<'a> Widget for BarChart<'a> {
buf.get_mut(
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap) + x,
chart_area.top() + j,
).set_symbol(symbol)
.set_style(self.style);
)
.set_symbol(symbol)
.set_style(self.style);
}
if d.1 > 8 {
@@ -170,11 +172,12 @@ impl<'a> Widget for BarChart<'a> {
let width = value_label.width() as u16;
if width < self.bar_width {
buf.set_string(
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap)
chart_area.left()
+ i as u16 * (self.bar_width + self.bar_gap)
+ (self.bar_width - width) / 2,
chart_area.bottom() - 2,
value_label,
&self.value_style,
self.value_style,
);
}
}
@@ -183,7 +186,7 @@ impl<'a> Widget for BarChart<'a> {
chart_area.bottom() - 1,
label,
self.bar_width as usize,
&self.label_style,
self.label_style,
);
}
}

View File

@@ -1,8 +1,8 @@
use buffer::Buffer;
use layout::Rect;
use style::Style;
use widgets::{Borders, Widget};
use symbols::line;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::symbols::line;
use crate::widgets::{Borders, Widget};
/// Base widget to be used with all upper level ones. It may be used to display a box border around
/// the widget and/or add a title.
@@ -10,7 +10,6 @@ use symbols::line;
/// # Examples
///
/// ```
/// # extern crate tui;
/// # use tui::widgets::{Block, Borders};
/// # use tui::style::{Style, Color};
/// # fn main() {
@@ -75,11 +74,11 @@ impl<'a> Block<'a> {
}
/// Compute the inner area of a block based on its border visibility rules.
pub fn inner(&self, area: &Rect) -> Rect {
pub fn inner(&self, area: Rect) -> Rect {
if area.width < 2 || area.height < 2 {
return Rect::default();
}
let mut inner = *area;
let mut inner = area;
if self.borders.intersects(Borders::LEFT) {
inner.x += 1;
inner.width -= 1;
@@ -99,7 +98,7 @@ impl<'a> Block<'a> {
}
impl<'a> Widget for Block<'a> {
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
if area.width < 2 || area.height < 2 {
return;
}
@@ -178,7 +177,7 @@ impl<'a> Widget for Block<'a> {
area.top(),
title,
width as usize,
&self.title_style,
self.title_style,
);
}
}

View File

@@ -1,5 +1,5 @@
use super::Shape;
use style::Color;
use crate::style::Color;
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
pub struct Line {
@@ -23,6 +23,7 @@ pub struct LineIterator {
impl Iterator for LineIterator {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.end {
let pos = (
@@ -40,6 +41,7 @@ impl Iterator for LineIterator {
impl<'a> IntoIterator for &'a Line {
type Item = (f64, f64);
type IntoIter = LineIterator;
fn into_iter(self) -> Self::IntoIter {
let dx = self.x1.max(self.x2) - self.x1.min(self.x2);
let dy = self.y1.max(self.y2) - self.y1.min(self.y2);
@@ -49,12 +51,12 @@ impl<'a> IntoIterator for &'a Line {
LineIterator {
x: self.x1,
y: self.y1,
dx: dx,
dy: dy,
dir_x: dir_x,
dir_y: dir_y,
dx,
dy,
dir_x,
dir_y,
current: 0.0,
end: end,
end,
}
}
}
@@ -63,6 +65,7 @@ impl<'a> Shape<'a> for Line {
fn color(&self) -> Color {
self.color
}
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
Box::new(self.into_iter())
}

View File

@@ -1,7 +1,7 @@
use widgets::canvas::Shape;
use widgets::canvas::points::PointsIterator;
use widgets::canvas::world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION};
use style::Color;
use crate::style::Color;
use crate::widgets::canvas::points::PointsIterator;
use crate::widgets::canvas::world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION};
use crate::widgets::canvas::Shape;
#[derive(Clone, Copy)]
pub enum MapResolution {
@@ -10,8 +10,8 @@ pub enum MapResolution {
}
impl MapResolution {
fn data(&self) -> &'static [(f64, f64)] {
match *self {
fn data(self) -> &'static [(f64, f64)] {
match self {
MapResolution::Low => &WORLD_LOW_RESOLUTION,
MapResolution::High => &WORLD_HIGH_RESOLUTION,
}

View File

@@ -1,16 +1,18 @@
mod points;
mod line;
mod map;
mod points;
mod rectangle;
mod world;
pub use self::points::Points;
pub use self::line::Line;
pub use self::map::{Map, MapResolution};
pub use self::points::Points;
pub use self::rectangle::Rectangle;
use style::{Color, Style};
use buffer::Buffer;
use widgets::{Block, Widget};
use layout::Rect;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::{Color, Style};
use crate::widgets::{Block, Widget};
pub const DOTS: [[u16; 2]; 4] = [
[0x0001, 0x0008],
@@ -116,12 +118,7 @@ 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: x,
y: y,
text: text,
color: color,
});
self.labels.push(Label { x, y, text, color });
}
/// Push the last layer if necessary
@@ -137,9 +134,9 @@ impl<'a> Context<'a> {
/// # Examples
///
/// ```
/// # extern crate tui;
/// # use tui::widgets::{Block, Borders};
/// # use tui::widgets::canvas::{Canvas, Shape, Line, Map, MapResolution};
/// # use tui::layout::Rect;
/// # use tui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
/// # use tui::style::Color;
/// # fn main() {
/// Canvas::default()
@@ -147,23 +144,25 @@ impl<'a> Context<'a> {
/// .x_bounds([-180.0, 180.0])
/// .y_bounds([-90.0, 90.0])
/// .paint(|ctx| {
/// ctx.draw(&Map{
/// ctx.draw(&Map {
/// resolution: MapResolution::High,
/// color: Color::White
/// });
/// ctx.layer();
/// ctx.draw(&Line{
/// ctx.draw(&Line {
/// x1: 0.0,
/// y1: 10.0,
/// x2: 10.0,
/// y2: 10.0,
/// color: Color::White,
/// });
/// ctx.draw(&Line{
/// x1: 10.0,
/// y1: 10.0,
/// x2: 20.0,
/// y2: 20.0,
/// ctx.draw(&Rectangle {
/// rect: Rect {
/// x: 10,
/// y: 20,
/// width: 10,
/// height: 10,
/// },
/// color: Color::Red
/// });
/// });
@@ -199,26 +198,26 @@ impl<'a, F> Canvas<'a, F>
where
F: Fn(&mut Context),
{
pub fn block(&mut self, block: Block<'a>) -> &mut Canvas<'a, F> {
pub fn block(mut self, block: Block<'a>) -> Canvas<'a, F> {
self.block = Some(block);
self
}
pub fn x_bounds(&mut self, bounds: [f64; 2]) -> &mut Canvas<'a, F> {
pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
self.x_bounds = bounds;
self
}
pub fn y_bounds(&mut self, bounds: [f64; 2]) -> &mut Canvas<'a, F> {
pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
self.y_bounds = bounds;
self
}
/// Store the closure that will be used to draw to the Canvas
pub fn paint(&mut self, f: F) -> &mut Canvas<'a, F> {
pub fn paint(mut self, f: F) -> Canvas<'a, F> {
self.painter = Some(f);
self
}
pub fn background_color(&'a mut self, color: Color) -> &mut Canvas<'a, F> {
pub fn background_color(mut self, color: Color) -> Canvas<'a, F> {
self.background_color = color;
self
}
@@ -228,13 +227,13 @@ impl<'a, F> Widget for Canvas<'a, F>
where
F: Fn(&mut Context),
{
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
let canvas_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.inner(area)
}
None => *area,
None => area,
};
let width = canvas_area.width as usize;
@@ -277,7 +276,9 @@ where
// Finally draw the labels
let style = Style::default().bg(self.background_color);
for label in ctx.labels.iter().filter(|l| {
!(l.x < self.x_bounds[0] || l.x > self.x_bounds[1] || l.y < self.y_bounds[0]
!(l.x < self.x_bounds[0]
|| l.x > self.x_bounds[1]
|| l.y < self.y_bounds[0]
|| l.y > self.y_bounds[1])
}) {
let dy = ((self.y_bounds[1] - label.y) * f64::from(canvas_area.height - 1)
@@ -288,7 +289,7 @@ where
dx + canvas_area.left(),
dy + canvas_area.top(),
label.text,
&style.fg(label.color),
style.fg(label.color),
);
}
}

View File

@@ -1,7 +1,7 @@
use std::slice;
use super::Shape;
use style::Color;
use crate::style::Color;
/// A shape to draw a group of points with the given color
pub struct Points<'a> {

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,12 @@ use std::cmp::max;
use unicode_width::UnicodeWidthStr;
use widgets::{Block, Borders, Widget};
use widgets::canvas::{Canvas, Points};
use buffer::Buffer;
use layout::Rect;
use style::Style;
use symbols;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::symbols;
use crate::widgets::canvas::{Canvas, Points};
use crate::widgets::{Block, Borders, Widget};
/// An X or Y axis for the chart widget
pub struct Axis<'a, L>
@@ -166,7 +166,6 @@ impl Default for ChartLayout {
/// # Examples
///
/// ```
/// # extern crate tui;
/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, Marker};
/// # use tui::style::{Style, Color};
/// # fn main() {
@@ -195,6 +194,7 @@ impl Default for ChartLayout {
/// .style(Style::default().fg(Color::Magenta))
/// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)])]);
/// # }
/// ```
pub struct Chart<'a, LX, LY>
where
LX: AsRef<str> + 'a,
@@ -233,34 +233,34 @@ where
LX: AsRef<str>,
LY: AsRef<str>,
{
pub fn block(&'a mut self, block: Block<'a>) -> &mut Chart<'a, LX, LY> {
pub fn block(mut self, block: Block<'a>) -> Chart<'a, LX, LY> {
self.block = Some(block);
self
}
pub fn style(&mut self, style: Style) -> &mut Chart<'a, LX, LY> {
pub fn style(mut self, style: Style) -> Chart<'a, LX, LY> {
self.style = style;
self
}
pub fn x_axis(&mut self, axis: Axis<'a, LX>) -> &mut Chart<'a, LX, LY> {
pub fn x_axis(mut self, axis: Axis<'a, LX>) -> Chart<'a, LX, LY> {
self.x_axis = axis;
self
}
pub fn y_axis(&mut self, axis: Axis<'a, LY>) -> &mut Chart<'a, LX, LY> {
pub fn y_axis(mut self, axis: Axis<'a, LY>) -> Chart<'a, LX, LY> {
self.y_axis = axis;
self
}
pub fn datasets(&mut self, datasets: &'a [Dataset<'a>]) -> &mut Chart<'a, LX, LY> {
pub fn datasets(mut self, datasets: &'a [Dataset<'a>]) -> Chart<'a, LX, LY> {
self.datasets = datasets;
self
}
/// Compute the internal layout of the chart given the area. If the area is too small some
/// elements may be automatically hidden
fn layout(&self, area: &Rect) -> ChartLayout {
fn layout(&self, area: Rect) -> ChartLayout {
let mut layout = ChartLayout::default();
if area.height == 0 || area.width == 0 {
return layout;
@@ -279,7 +279,7 @@ where
.fold(0, |acc, l| max(l.as_ref().width(), acc))
as u16;
if let Some(x_labels) = self.x_axis.labels {
if x_labels.len() > 0 {
if !x_labels.is_empty() {
max_width = max(max_width, x_labels[0].as_ref().width() as u16);
}
}
@@ -322,6 +322,7 @@ where
let legend_height = self.datasets.len() as u16 + 2;
if legend_width < layout.graph_area.width / 3
&& legend_height < layout.graph_area.height / 3
&& inner_width > 0
{
layout.legend_area = Some(Rect::new(
layout.graph_area.right() - legend_width,
@@ -340,31 +341,31 @@ where
LX: AsRef<str>,
LY: AsRef<str>,
{
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
let chart_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.inner(area)
}
None => *area,
None => area,
};
let layout = self.layout(&chart_area);
let layout = self.layout(chart_area);
let graph_area = layout.graph_area;
if graph_area.width < 1 || graph_area.height < 1 {
return;
}
self.background(&chart_area, buf, self.style.bg);
self.background(chart_area, buf, self.style.bg);
if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap();
buf.set_string(x, y, title, &self.x_axis.style);
buf.set_string(x, y, title, self.x_axis.style);
}
if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap();
buf.set_string(x, y, title, &self.y_axis.style);
buf.set_string(x, y, title, self.y_axis.style);
}
if let Some(y) = layout.label_x {
@@ -378,7 +379,7 @@ where
- label.as_ref().width() as u16,
y,
label.as_ref(),
&self.x_axis.labels_style,
self.x_axis.labels_style,
);
}
}
@@ -394,7 +395,7 @@ where
x,
graph_area.bottom() - 1 - dy,
label.as_ref(),
&self.y_axis.labels_style,
self.y_axis.labels_style,
);
}
}
@@ -426,23 +427,26 @@ where
for dataset in self.datasets {
match dataset.marker {
Marker::Dot => for &(x, y) in dataset.data.iter().filter(|&&(x, y)| {
!(x < self.x_axis.bounds[0] || x > self.x_axis.bounds[1]
|| y < self.y_axis.bounds[0]
|| y > self.y_axis.bounds[1])
}) {
let dy = ((self.y_axis.bounds[1] - y) * f64::from(graph_area.height - 1)
/ (self.y_axis.bounds[1] - self.y_axis.bounds[0]))
as u16;
let dx = ((x - self.x_axis.bounds[0]) * f64::from(graph_area.width - 1)
/ (self.x_axis.bounds[1] - self.x_axis.bounds[0]))
as u16;
Marker::Dot => {
for &(x, y) in dataset.data.iter().filter(|&&(x, y)| {
!(x < self.x_axis.bounds[0]
|| x > self.x_axis.bounds[1]
|| y < self.y_axis.bounds[0]
|| y > self.y_axis.bounds[1])
}) {
let dy = ((self.y_axis.bounds[1] - y) * f64::from(graph_area.height - 1)
/ (self.y_axis.bounds[1] - self.y_axis.bounds[0]))
as u16;
let dx = ((x - self.x_axis.bounds[0]) * f64::from(graph_area.width - 1)
/ (self.x_axis.bounds[1] - self.x_axis.bounds[0]))
as u16;
buf.get_mut(graph_area.left() + dx, graph_area.top() + dy)
.set_symbol(symbols::DOT)
.set_fg(dataset.style.fg)
.set_bg(dataset.style.bg);
},
buf.get_mut(graph_area.left() + dx, graph_area.top() + dy)
.set_symbol(symbols::DOT)
.set_fg(dataset.style.fg)
.set_bg(dataset.style.bg);
}
}
Marker::Braille => {
Canvas::default()
.background_color(self.style.bg)
@@ -454,7 +458,7 @@ where
color: dataset.style.fg,
});
})
.draw(&graph_area, buf);
.draw(graph_area, buf);
}
}
}
@@ -462,13 +466,13 @@ where
if let Some(legend_area) = layout.legend_area {
Block::default()
.borders(Borders::ALL)
.draw(&legend_area, buf);
.draw(legend_area, buf);
for (i, dataset) in self.datasets.iter().enumerate() {
buf.set_string(
legend_area.x + 1,
legend_area.y + 1 + i as u16,
dataset.name,
&dataset.style,
dataset.style,
);
}
}

View File

@@ -1,16 +1,15 @@
use unicode_width::UnicodeWidthStr;
use widgets::{Block, Widget};
use buffer::Buffer;
use style::{Color, Style};
use layout::Rect;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::{Color, Style};
use crate::widgets::{Block, Widget};
/// A widget to display a task progress.
///
/// # Examples:
///
/// ```
/// # extern crate tui;
/// # use tui::widgets::{Widget, Gauge, Block, Borders};
/// # use tui::style::{Style, Color, Modifier};
/// # fn main() {
@@ -22,7 +21,7 @@ use layout::Rect;
/// ```
pub struct Gauge<'a> {
block: Option<Block<'a>>,
percent: u16,
ratio: f64,
label: Option<&'a str>,
style: Style,
}
@@ -31,7 +30,7 @@ impl<'a> Default for Gauge<'a> {
fn default() -> Gauge<'a> {
Gauge {
block: None,
percent: 0,
ratio: 0.0,
label: None,
style: Default::default(),
}
@@ -39,46 +38,60 @@ impl<'a> Default for Gauge<'a> {
}
impl<'a> Gauge<'a> {
pub fn block(&mut self, block: Block<'a>) -> &mut Gauge<'a> {
pub fn block(mut self, block: Block<'a>) -> Gauge<'a> {
self.block = Some(block);
self
}
pub fn percent(&mut self, percent: u16) -> &mut Gauge<'a> {
self.percent = percent;
pub fn percent(mut self, percent: u16) -> Gauge<'a> {
assert!(
percent <= 100,
"Percentage should be between 0 and 100 inclusively."
);
self.ratio = f64::from(percent) / 100.0;
self
}
pub fn label(&mut self, string: &'a str) -> &mut 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,
"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);
self
}
pub fn style(&mut self, style: Style) -> &mut Gauge<'a> {
pub fn style(mut self, style: Style) -> Gauge<'a> {
self.style = style;
self
}
}
impl<'a> Widget for Gauge<'a> {
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
let gauge_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.inner(area)
}
None => *area,
None => area,
};
if gauge_area.height < 1 {
return;
}
if self.style.bg != Color::Reset {
self.background(&gauge_area, buf, self.style.bg);
self.background(gauge_area, buf, self.style.bg);
}
let center = gauge_area.height / 2 + gauge_area.top();
let width = (gauge_area.width * self.percent) / 100;
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
@@ -88,11 +101,11 @@ impl<'a> Widget for Gauge<'a> {
if y == center {
// Label
let precent_label = format!("{}%", self.percent);
let precent_label = format!("{}%", (self.ratio * 100.0).round());
let label = self.label.unwrap_or(&precent_label);
let label_width = label.width() as u16;
let middle = (gauge_area.width - label_width) / 2 + gauge_area.left();
buf.set_string(middle, y, label, &self.style);
buf.set_string(middle, y, label, self.style);
}
// Fix colors
@@ -104,3 +117,26 @@ impl<'a> Widget for Gauge<'a> {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn gauge_invalid_percentage() {
Gauge::default().percent(110);
}
#[test]
#[should_panic]
fn gauge_invalid_ratio_upper_bound() {
Gauge::default().ratio(1.1);
}
#[test]
#[should_panic]
fn gauge_invalid_ratio_lower_bound() {
Gauge::default().ratio(-0.5);
}
}

View File

@@ -1,115 +1,111 @@
use std::iter;
use std::fmt::Display;
use std::iter::Iterator;
use unicode_width::UnicodeWidthStr;
use buffer::Buffer;
use widgets::{Block, Widget};
use layout::Rect;
use style::Style;
use crate::buffer::Buffer;
use crate::layout::{Corner, Rect};
use crate::style::Style;
use crate::widgets::{Block, Text, Widget};
pub enum Item<'i, D: 'i> {
Data(D),
StyledData(D, &'i Style),
}
pub struct List<'b, 'i, L, D: 'i>
pub struct List<'b, L>
where
L: Iterator<Item = Item<'i, D>>,
L: Iterator<Item = Text<'b>>,
{
block: Option<Block<'b>>,
items: L,
style: Style,
start_corner: Corner,
}
impl<'b, 'i, L, D> Default for List<'b, 'i, L, D>
impl<'b, L> Default for List<'b, L>
where
L: Iterator<Item = Item<'i, D>> + Default,
L: Iterator<Item = Text<'b>> + Default,
{
fn default() -> List<'b, 'i, L, D> {
fn default() -> List<'b, L> {
List {
block: None,
items: L::default(),
style: Default::default(),
start_corner: Corner::TopLeft,
}
}
}
impl<'b, 'i, L, D> List<'b, 'i, L, D>
impl<'b, L> List<'b, L>
where
L: Iterator<Item = Item<'i, D>>,
L: Iterator<Item = Text<'b>>,
{
pub fn new(items: L) -> List<'b, 'i, L, D> {
pub fn new(items: L) -> List<'b, L> {
List {
block: None,
items: items,
items,
style: Default::default(),
start_corner: Corner::TopLeft,
}
}
pub fn block(&'b mut self, block: Block<'b>) -> &mut List<'b, 'i, L, D> {
pub fn block(mut self, block: Block<'b>) -> List<'b, L> {
self.block = Some(block);
self
}
pub fn items<I>(&'b mut self, items: I) -> &mut List<'b, 'i, L, D>
pub fn items<I>(mut self, items: I) -> List<'b, L>
where
I: IntoIterator<Item = Item<'i, D>, IntoIter = L>,
I: IntoIterator<Item = Text<'b>, IntoIter = L>,
{
self.items = items.into_iter();
self
}
pub fn style(&'b mut self, style: Style) -> &mut List<'b, 'i, L, D> {
pub fn style(mut self, style: Style) -> List<'b, L> {
self.style = style;
self
}
pub fn start_corner(mut self, corner: Corner) -> List<'b, L> {
self.start_corner = corner;
self
}
}
impl<'b, 'i, L, D> Widget for List<'b, 'i, L, D>
impl<'b, L> Widget for List<'b, L>
where
L: Iterator<Item = Item<'i, D>>,
D: Display,
L: Iterator<Item = Text<'b>>,
{
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
let list_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.inner(area)
}
None => *area,
None => area,
};
if list_area.width < 1 || list_area.height < 1 {
return;
}
self.background(&list_area, buf, self.style.bg);
self.background(list_area, buf, self.style.bg);
for (i, item) in self.items
for (i, item) in self
.items
.by_ref()
.enumerate()
.take(list_area.height as usize)
{
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),
};
match item {
Item::Data(ref v) => {
buf.set_stringn(
list_area.left(),
list_area.top() + i as u16,
&format!("{}", v),
list_area.width as usize,
&Style::default(),
);
Text::Raw(ref v) => {
buf.set_stringn(x, y, v, list_area.width as usize, Style::default());
}
Item::StyledData(ref v, s) => {
buf.set_stringn(
list_area.left(),
list_area.top() + i as u16,
&format!("{}", v),
list_area.width as usize,
s,
);
Text::Styled(ref v, s) => {
buf.set_stringn(x, y, v, list_area.width as usize, s);
}
};
}
@@ -121,14 +117,13 @@ where
/// # Examples
///
/// ```
/// # extern crate tui;
/// # use tui::widgets::{Block, Borders, SelectableList};
/// # use tui::style::{Style, Color, Modifier};
/// # fn main() {
/// SelectableList::default()
/// .block(Block::default().title("SelectableList").borders(Borders::ALL))
/// .items(&["Item 1", "Item 2", "Item 3"])
/// .select(1)
/// .select(Some(1))
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().modifier(Modifier::Italic))
/// .highlight_symbol(">>");
@@ -162,12 +157,12 @@ impl<'b> Default for SelectableList<'b> {
}
impl<'b> SelectableList<'b> {
pub fn block(&'b mut self, block: Block<'b>) -> &mut SelectableList<'b> {
pub fn block(mut self, block: Block<'b>) -> SelectableList<'b> {
self.block = Some(block);
self
}
pub fn items<I>(&'b mut self, items: &'b [I]) -> &mut SelectableList<'b>
pub fn items<I>(mut self, items: &'b [I]) -> SelectableList<'b>
where
I: AsRef<str> + 'b,
{
@@ -175,61 +170,70 @@ impl<'b> SelectableList<'b> {
self
}
pub fn style(&'b mut self, style: Style) -> &mut SelectableList<'b> {
pub fn style(mut self, style: Style) -> SelectableList<'b> {
self.style = style;
self
}
pub fn highlight_symbol(&'b mut self, highlight_symbol: &'b str) -> &mut SelectableList<'b> {
pub fn highlight_symbol(mut self, highlight_symbol: &'b str) -> SelectableList<'b> {
self.highlight_symbol = Some(highlight_symbol);
self
}
pub fn highlight_style(&'b mut self, highlight_style: Style) -> &mut SelectableList<'b> {
pub fn highlight_style(mut self, highlight_style: Style) -> SelectableList<'b> {
self.highlight_style = highlight_style;
self
}
pub fn select(&'b mut self, index: usize) -> &'b mut SelectableList<'b> {
self.selected = Some(index);
pub fn select(mut self, index: Option<usize>) -> SelectableList<'b> {
self.selected = index;
self
}
}
impl<'b> Widget for SelectableList<'b> {
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
let list_area = match self.block {
Some(ref mut b) => b.inner(area),
None => *area,
None => area,
};
let list_height = list_area.height as usize;
// Use highlight_style only if something is selected
let (selected, highlight_style) = match self.selected {
Some(i) => (i, &self.highlight_style),
None => (0, &self.style),
Some(i) => (Some(i), self.highlight_style),
None => (None, self.style),
};
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
// Make sure the list show the selected item
let offset = if selected >= list_height {
selected - list_height + 1
let offset = if let Some(selected) = selected {
if selected >= list_height {
selected - list_height + 1
} else {
0
}
} else {
0
};
// Render items
let items = self.items
let items = self
.items
.iter()
.enumerate()
.map(|(i, item)| {
if i == selected {
Item::StyledData(format!("{} {}", highlight_symbol, item), highlight_style)
.map(|(i, &item)| {
if let Some(s) = selected {
if i == s {
Text::styled(format!("{} {}", highlight_symbol, item), highlight_style)
} else {
Text::styled(format!("{} {}", blank_symbol, item), self.style)
}
} else {
Item::StyledData(format!("{} {}", blank_symbol, item), &self.style)
Text::styled(item, self.style)
}
})
.skip(offset as usize);

View File

@@ -1,29 +1,33 @@
mod block;
mod paragraph;
mod list;
mod gauge;
mod sparkline;
mod chart;
use bitflags::bitflags;
use std::borrow::Cow;
mod barchart;
mod tabs;
mod table;
mod block;
pub mod canvas;
mod chart;
mod gauge;
mod list;
mod paragraph;
mod reflow;
mod sparkline;
mod table;
mod tabs;
pub use self::block::Block;
pub use self::paragraph::Paragraph;
pub use self::list::{Item, List, SelectableList};
pub use self::gauge::Gauge;
pub use self::sparkline::Sparkline;
pub use self::chart::{Axis, Chart, Dataset, Marker};
pub use self::barchart::BarChart;
pub use self::tabs::Tabs;
pub use self::block::Block;
pub use self::chart::{Axis, Chart, Dataset, Marker};
pub use self::gauge::Gauge;
pub use self::list::{List, SelectableList};
pub use self::paragraph::Paragraph;
pub use self::sparkline::Sparkline;
pub use self::table::{Row, Table};
pub use self::tabs::Tabs;
use buffer::Buffer;
use layout::Rect;
use terminal::Terminal;
use backend::Backend;
use style::Color;
use crate::backend::Backend;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::{Color, Style};
use crate::terminal::Frame;
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
bitflags! {
@@ -43,13 +47,28 @@ bitflags! {
}
}
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.
fn draw(&mut self, area: &Rect, buf: &mut Buffer);
fn draw(&mut self, area: Rect, buf: &mut Buffer);
/// Helper method to quickly set the background of all cells inside the specified area.
fn background(&self, area: &Rect, buf: &mut Buffer, color: Color) {
fn background(&self, area: Rect, buf: &mut Buffer, color: Color) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
buf.get_mut(x, y).set_bg(color);
@@ -57,11 +76,11 @@ pub trait Widget {
}
}
/// Helper method that can be chained with a widget's builder methods to render it.
fn render<B>(&mut self, t: &mut Terminal<B>, area: &Rect)
fn render<B>(&mut self, f: &mut Frame<B>, area: Rect)
where
Self: Sized,
B: Backend,
{
t.render(self, area);
f.render(self, area);
}
}

View File

@@ -1,29 +1,45 @@
use either::Either;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use widgets::{Block, Widget};
use buffer::Buffer;
use layout::Rect;
use style::{Color, Modifier, Style};
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};
/// A widget to display some text. You can specify colors using commands embedded in
/// the text such as "{[color] [text]}".
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),
Alignment::Right => text_area_width.saturating_sub(line_width),
Alignment::Left => 0,
}
}
/// A widget to display some text.
///
/// # Examples
///
/// ```
/// # extern crate tui;
/// # use tui::widgets::{Block, Borders, Paragraph};
/// # use tui::widgets::{Block, Borders, Paragraph, Text};
/// # use tui::style::{Style, Color};
/// # use tui::layout::{Alignment};
/// # fn main() {
/// Paragraph::default()
/// let text = [
/// Text::raw("First line\n"),
/// Text::styled("Second line\n", Style::default().fg(Color::Red))
/// ];
/// Paragraph::new(text.iter())
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White).bg(Color::Black))
/// .wrap(true)
/// .text("First line\nSecond line\n{red Colored text}.");
/// .alignment(Alignment::Center)
/// .wrap(true);
/// # }
/// ```
pub struct Paragraph<'a> {
pub struct Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
/// A block to wrap the widget in
block: Option<Block<'a>>,
/// Widget style
@@ -31,252 +47,113 @@ pub struct Paragraph<'a> {
/// Wrap the text or not
wrapping: bool,
/// The text to display
text: &'a str,
text: T,
/// Should we parse the text for embedded commands
raw: bool,
/// Scroll
scroll: u16,
/// Aligenment of the text
alignment: Alignment,
}
impl<'a> Default for Paragraph<'a> {
fn default() -> Paragraph<'a> {
impl<'a, 't, T> Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
pub fn new(text: T) -> Paragraph<'a, 't, T> {
Paragraph {
block: None,
style: Default::default(),
wrapping: false,
raw: false,
text: "",
text,
scroll: 0,
alignment: Alignment::Left,
}
}
}
impl<'a> Paragraph<'a> {
pub fn block(&'a mut self, block: Block<'a>) -> &mut Paragraph<'a> {
pub fn block(mut self, block: Block<'a>) -> Paragraph<'a, 't, T> {
self.block = Some(block);
self
}
pub fn text(&mut self, text: &'a str) -> &mut Paragraph<'a> {
self.text = text;
self
}
pub fn style(&mut self, style: Style) -> &mut Paragraph<'a> {
pub fn style(mut self, style: Style) -> Paragraph<'a, 't, T> {
self.style = style;
self
}
pub fn wrap(&mut self, flag: bool) -> &mut Paragraph<'a> {
pub fn wrap(mut self, flag: bool) -> Paragraph<'a, 't, T> {
self.wrapping = flag;
self
}
pub fn raw(&mut self, flag: bool) -> &mut Paragraph<'a> {
pub fn raw(mut self, flag: bool) -> Paragraph<'a, 't, T> {
self.raw = flag;
self
}
pub fn scroll(&mut self, offset: u16) -> &mut Paragraph<'a> {
pub fn scroll(mut self, offset: u16) -> Paragraph<'a, 't, T> {
self.scroll = offset;
self
}
pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a, 't, T> {
self.alignment = alignment;
self
}
}
struct Parser<'a, T>
impl<'a, 't, 'b, T> Widget for Paragraph<'a, 't, T>
where
T: Iterator<Item = &'a str>,
T: Iterator<Item = &'t Text<'t>>,
{
text: T,
mark: bool,
cmd_string: String,
style: Style,
base_style: Style,
escaping: bool,
styling: bool,
}
impl<'a, T> Parser<'a, T>
where
T: Iterator<Item = &'a str>,
{
fn new(text: T, base_style: Style) -> Parser<'a, T> {
Parser {
text: text,
mark: false,
cmd_string: String::from(""),
style: base_style,
base_style: base_style,
escaping: false,
styling: false,
}
}
fn update_style(&mut self) {
for cmd in self.cmd_string.split(';') {
let args = cmd.split('=').collect::<Vec<&str>>();
if let Some(first) = args.get(0) {
match *first {
"fg" => if let Some(snd) = args.get(1) {
self.style.fg = Parser::<T>::str_to_color(snd);
},
"bg" => if let Some(snd) = args.get(1) {
self.style.bg = Parser::<T>::str_to_color(snd);
},
"mod" => if let Some(snd) = args.get(1) {
self.style.modifier = Parser::<T>::str_to_modifier(snd);
},
_ => {}
}
}
}
}
fn str_to_color(string: &str) -> Color {
match string {
"black" => Color::Black,
"red" => Color::Red,
"green" => Color::Green,
"yellow" => Color::Yellow,
"blue" => Color::Blue,
"magenta" => Color::Magenta,
"cyan" => Color::Cyan,
"gray" => Color::Gray,
"dark_gray" => Color::DarkGray,
"light_red" => Color::LightRed,
"light_green" => Color::LightGreen,
"light_blue" => Color::LightBlue,
"light_yellow" => Color::LightYellow,
"light_magenta" => Color::LightMagenta,
"light_cyan" => Color::LightCyan,
"white" => Color::White,
_ => Color::Reset,
}
}
fn str_to_modifier(string: &str) -> Modifier {
match string {
"bold" => Modifier::Bold,
"italic" => Modifier::Italic,
"underline" => Modifier::Underline,
"invert" => Modifier::Invert,
"crossed_out" => Modifier::CrossedOut,
_ => Modifier::Reset,
}
}
fn reset(&mut self) {
self.styling = false;
self.mark = false;
self.style = self.base_style;
self.cmd_string.clear();
}
}
impl<'a, T> Iterator for Parser<'a, T>
where
T: Iterator<Item = &'a str>,
{
type Item = (&'a str, Style);
fn next(&mut self) -> Option<Self::Item> {
match self.text.next() {
Some(s) => if s == "\\" {
if self.escaping {
Some((s, self.style))
} else {
self.escaping = true;
self.next()
}
} else if s == "{" {
if self.escaping {
self.escaping = false;
Some((s, self.style))
} else if self.mark {
Some((s, self.style))
} else {
self.style = self.base_style;
self.mark = true;
self.next()
}
} else if s == "}" && self.mark {
self.reset();
self.next()
} else if s == " " && self.mark {
if self.styling {
Some((s, self.style))
} else {
self.styling = true;
self.update_style();
self.next()
}
} else if self.mark && !self.styling {
self.cmd_string.push_str(s);
self.next()
} else {
Some((s, self.style))
},
None => None,
}
}
}
impl<'a> Widget for Paragraph<'a> {
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
let text_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.inner(area)
}
None => *area,
None => area,
};
if text_area.height < 1 {
return;
}
self.background(&text_area, buf, self.style.bg);
self.background(text_area, buf, self.style.bg);
let mut x = 0;
let mut y = 0;
let graphemes = UnicodeSegmentation::graphemes(self.text, true);
let styled: Box<Iterator<Item = (&str, Style)>> = if self.raw {
Box::new(graphemes.map(|g| (g, self.style)))
} else {
Box::new(Parser::new(graphemes, self.style))
};
let mut remove_leading_whitespaces = false;
for (string, style) in styled {
if string == "\n" {
x = 0;
y += 1;
continue;
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)))
}
if x >= text_area.width {
if self.wrapping {
x = 0;
y += 1;
remove_leading_whitespaces = true
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 line_composer: Box<dyn LineComposer> = if self.wrapping {
Box::new(WordWrapper::new(&mut styled, text_area.width))
} else {
Box::new(LineTruncator::new(&mut styled, text_area.width))
};
let mut y = 0;
while let Some((current_line, current_line_width)) = line_composer.next_line() {
if y >= self.scroll {
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
for Styled(symbol, style) in current_line {
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll)
.set_symbol(symbol)
.set_style(*style);
x += symbol.width() as u16;
}
}
if remove_leading_whitespaces && string == " " {
continue;
}
remove_leading_whitespaces = false;
if y > text_area.height + self.scroll - 1 {
y += 1;
if y >= text_area.height + self.scroll {
break;
}
if y < self.scroll {
continue;
}
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll)
.set_symbol(string)
.set_style(style);
x += string.width() as u16;
}
}
}

446
src/widgets/reflow.rs Normal file
View File

@@ -0,0 +1,446 @@
use crate::style::Style;
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)>;
}
/// A state machine that wraps lines on word boundaries.
pub struct WordWrapper<'a, 'b> {
symbols: &'b mut Iterator<Item = Styled<'a>>,
max_line_width: u16,
current_line: Vec<Styled<'a>>,
next_line: Vec<Styled<'a>>,
}
impl<'a, 'b> WordWrapper<'a, 'b> {
pub fn new(
symbols: &'b mut Iterator<Item = Styled<'a>>,
max_line_width: u16,
) -> WordWrapper<'a, 'b> {
WordWrapper {
symbols,
max_line_width,
current_line: vec![],
next_line: vec![],
}
}
}
impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> {
if self.max_line_width == 0 {
return None;
}
std::mem::swap(&mut self.current_line, &mut self.next_line);
self.next_line.truncate(0);
let mut current_line_width = self
.current_line
.iter()
.map(|Styled(c, _)| c.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 {
symbols_exhausted = false;
let symbol_whitespace = symbol.chars().all(&char::is_whitespace);
// Ignore characters wider that the total max width.
if symbol.width() as u16 > self.max_line_width
// Skip leading whitespace.
|| symbol_whitespace && symbol != "\n" && current_line_width == 0
{
continue;
}
// Break on newline and discard it.
if symbol == "\n" {
if prev_whitespace {
current_line_width = width_to_last_word_end;
self.current_line.truncate(symbols_to_last_word_end);
}
break;
}
// Mark the previous symbol as word end.
if symbol_whitespace && !prev_whitespace && symbol != NBSP {
symbols_to_last_word_end = self.current_line.len();
width_to_last_word_end = current_line_width;
}
self.current_line.push(Styled(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)
};
// 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))
{
self.next_line
.extend_from_slice(&remainder[remainder_nonwhite..]);
}
}
self.current_line.truncate(truncate_at);
current_line_width = truncated_width;
break;
}
prev_whitespace = symbol_whitespace;
}
// Even if the iterator is exhausted, pass the previous remainder.
if symbols_exhausted && self.current_line.is_empty() {
None
} else {
Some((&self.current_line[..], current_line_width))
}
}
}
/// A state machine that truncates overhanging lines.
pub struct LineTruncator<'a, 'b> {
symbols: &'b mut Iterator<Item = Styled<'a>>,
max_line_width: u16,
current_line: Vec<Styled<'a>>,
}
impl<'a, 'b> LineTruncator<'a, 'b> {
pub fn new(
symbols: &'b mut Iterator<Item = Styled<'a>>,
max_line_width: u16,
) -> LineTruncator<'a, 'b> {
LineTruncator {
symbols,
max_line_width,
current_line: vec![],
}
}
}
impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> {
if self.max_line_width == 0 {
return None;
}
self.current_line.truncate(0);
let mut current_line_width = 0;
let mut skip_rest = false;
let mut symbols_exhausted = true;
for Styled(symbol, style) in &mut self.symbols {
symbols_exhausted = false;
// Ignore characters wider that the total max width.
if symbol.width() as u16 > self.max_line_width {
continue;
}
// Break on newline and discard it.
if symbol == "\n" {
break;
}
if current_line_width + symbol.width() as u16 > self.max_line_width {
// Exhaust the remainder of the line.
skip_rest = true;
break;
}
current_line_width += symbol.width() as u16;
self.current_line.push(Styled(symbol, style));
}
if skip_rest {
for Styled(symbol, _) in &mut self.symbols {
if symbol == "\n" {
break;
}
}
}
if symbols_exhausted && self.current_line.is_empty() {
None
} else {
Some((&self.current_line[..], current_line_width))
}
}
}
#[cfg(test)]
mod test {
use super::*;
use unicode_segmentation::UnicodeSegmentation;
enum Composer {
WordWrapper,
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 composer: Box<dyn LineComposer> = match which {
Composer::WordWrapper => Box::new(WordWrapper::new(&mut styled, text_area_width)),
Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
};
let mut lines = vec![];
let mut widths = vec![];
while let Some((styled, width)) = composer.next_line() {
let line = styled
.iter()
.map(|Styled(g, _style)| *g)
.collect::<String>();
assert!(width <= text_area_width);
lines.push(line);
widths.push(width);
}
(lines, widths)
}
#[test]
fn line_composer_one_line() {
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 (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
let expected = vec![text];
assert_eq!(word_wrapper, expected);
assert_eq!(line_truncator, expected);
}
}
#[test]
fn line_composer_short_lines() {
let width = 20;
let text =
"abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
let wrapped: Vec<&str> = text.split('\n').collect();
assert_eq!(word_wrapper, wrapped);
assert_eq!(line_truncator, wrapped);
}
#[test]
fn line_composer_long_word() {
let width = 20;
let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width as u16);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
let wrapped = vec![
&text[..width],
&text[width..width * 2],
&text[width * 2..width * 3],
&text[width * 3..],
];
assert_eq!(
word_wrapper, wrapped,
"WordWrapper should deect the line cannot be broken on word boundary and \
break it at line width limit."
);
assert_eq!(line_truncator, vec![&text[..width]]);
}
#[test]
fn line_composer_long_sentence() {
let width = 20;
let text =
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
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 (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
let word_wrapped = vec![
"abcd efghij",
"klmnopabcd efgh",
"ijklmnopabcdefg",
"hijkl mnopab c d e f",
"g h i j k l m n o",
];
assert_eq!(word_wrapper_single_space, word_wrapped);
assert_eq!(word_wrapper_multi_space, word_wrapped);
assert_eq!(line_truncator, vec![&text[..width]]);
}
#[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 (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
let expected: Vec<&str> = Vec::new();
assert_eq!(word_wrapper, expected);
assert_eq!(line_truncator, expected);
}
#[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 (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
.filter(|g| g.chars().any(|c| !c.is_whitespace()))
.collect();
assert_eq!(word_wrapper, expected);
assert_eq!(line_truncator, vec!["a"]);
}
#[test]
fn line_composer_max_line_width_of_1_double_width_characters() {
let width = 1;
let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
両端点では、";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, 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"]);
}
/// Tests WordWrapper with words some of which exceed line length and some not.
#[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);
assert_eq!(
word_wrapper,
vec![
"abcd efghij",
"klmnopabcdefghijklmn",
"opabcdefghijkl",
"mnopab cdefghi j",
"klmno",
]
)
}
#[test]
fn line_composer_double_width_chars() {
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);
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
let wrapped = vec![
"コンピュータ上で文字",
"を扱う場合、典型的に",
"は文字による通信を行",
"う場合にその両端点で",
"は、",
];
assert_eq!(word_wrapper, wrapped);
assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
}
#[test]
fn line_composer_leading_whitespace_removal() {
let width = 20;
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
}
/// Tests truncation of leading whitespace.
#[test]
fn line_composer_lots_of_spaces() {
let width = 20;
let text = " ";
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(word_wrapper, vec![""]);
assert_eq!(line_truncator, vec![" "]);
}
/// Tests an input starting with a letter, folowed by spaces - some of the behaviour is
/// incidental.
#[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 (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
// discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
// that much.
assert_eq!(word_wrapper, vec!["a", ""]);
assert_eq!(line_truncator, vec!["a "]);
}
#[test]
fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
let width = 20;
// Japanese seems not to use spaces but we should break on spaces anyway... We're using it
// to test double-width chars.
// You are more than welcome to add word boundary detection based of alterations of
// 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);
assert_eq!(
word_wrapper,
vec![
"コンピュ",
"ータ上で文字を扱う場",
"合、 典型的には文",
"字による 通信を行",
"う場合にその両端点で",
"は、",
]
);
// Odd-sized lines have a space in them.
assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]);
}
/// Ensure words separated by nbsp are wrapped as if they were a single one.
#[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);
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);
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
}
}

View File

@@ -1,17 +1,16 @@
use std::cmp::min;
use layout::Rect;
use buffer::Buffer;
use widgets::{Block, Widget};
use style::Style;
use symbols::bar;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::symbols::bar;
use crate::widgets::{Block, Widget};
/// Widget to render a sparkline over one or more lines.
///
/// # Examples
///
/// ```
/// # extern crate tui;
/// # use tui::widgets::{Block, Borders, Sparkline};
/// # use tui::style::{Style, Color};
/// # fn main() {
@@ -46,35 +45,35 @@ impl<'a> Default for Sparkline<'a> {
}
impl<'a> Sparkline<'a> {
pub fn block(&mut self, block: Block<'a>) -> &mut Sparkline<'a> {
pub fn block(mut self, block: Block<'a>) -> Sparkline<'a> {
self.block = Some(block);
self
}
pub fn style(&mut self, style: Style) -> &mut Sparkline<'a> {
pub fn style(mut self, style: Style) -> Sparkline<'a> {
self.style = style;
self
}
pub fn data(&mut self, data: &'a [u64]) -> &mut Sparkline<'a> {
pub fn data(mut self, data: &'a [u64]) -> Sparkline<'a> {
self.data = data;
self
}
pub fn max(&mut self, max: u64) -> &mut Sparkline<'a> {
pub fn max(mut self, max: u64) -> Sparkline<'a> {
self.max = Some(max);
self
}
}
impl<'a> Widget for Sparkline<'a> {
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
let spark_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.inner(area)
}
None => *area,
None => area,
};
if spark_area.height < 1 {
@@ -86,7 +85,8 @@ impl<'a> Widget for Sparkline<'a> {
None => *self.data.iter().max().unwrap_or(&1u64),
};
let max_index = min(spark_area.width as usize, self.data.len());
let mut data = self.data
let mut data = self
.data
.iter()
.take(max_index)
.map(|e| e * u64::from(spark_area.height) * 8 / max)

View File

@@ -1,19 +1,19 @@
use std::fmt::Display;
use std::iter::Iterator;
use buffer::Buffer;
use widgets::{Block, Widget};
use layout::Rect;
use style::Style;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::widgets::{Block, Widget};
/// Holds data to be displayed in a Table widget
pub enum Row<'i, D, I>
pub enum Row<D, I>
where
D: Iterator<Item = I>,
I: Display,
{
Data(D),
StyledData(D, &'i Style),
StyledData(D, Style),
}
/// A widget to display data in formatted columns
@@ -28,9 +28,9 @@ where
/// 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::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()
/// )
@@ -41,13 +41,13 @@ where
/// .column_spacing(1);
/// # }
/// ```
pub struct Table<'a, 'i, T, H, I, D, R>
pub struct Table<'a, T, H, I, D, R>
where
T: Display,
H: Iterator<Item = T>,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<'i, D, I>>,
R: Iterator<Item = Row<D, I>>,
{
/// A block to wrap the widget in
block: Option<Block<'a>>,
@@ -66,15 +66,15 @@ where
rows: R,
}
impl<'a, 'i, T, H, I, D, R> Default for Table<'a, 'i, T, H, I, D, R>
impl<'a, T, H, I, D, R> Default for Table<'a, T, H, I, D, R>
where
T: Display,
H: Iterator<Item = T> + Default,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<'i, D, I>> + Default,
R: Iterator<Item = Row<D, I>> + Default,
{
fn default() -> Table<'a, 'i, T, H, I, D, R> {
fn default() -> Table<'a, T, H, I, D, R> {
Table {
block: None,
style: Style::default(),
@@ -87,31 +87,31 @@ where
}
}
impl<'a, 'i, T, H, I, D, R> Table<'a, 'i, T, H, I, D, R>
impl<'a, T, H, I, D, R> Table<'a, T, H, I, D, R>
where
T: Display,
H: Iterator<Item = T>,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<'i, D, I>>,
R: Iterator<Item = Row<D, I>>,
{
pub fn new(header: H, rows: R) -> Table<'a, 'i, T, H, I, D, R> {
pub fn new(header: H, rows: R) -> Table<'a, T, H, I, D, R> {
Table {
block: None,
style: Style::default(),
header: header,
header,
header_style: Style::default(),
widths: &[],
rows: rows,
rows,
column_spacing: 1,
}
}
pub fn block(&'a mut self, block: Block<'a>) -> &mut Table<'a, 'i, T, H, I, D, R> {
pub fn block(mut self, block: Block<'a>) -> Table<'a, T, H, I, D, R> {
self.block = Some(block);
self
}
pub fn header<II>(&mut self, header: II) -> &mut Table<'a, 'i, T, H, I, D, R>
pub fn header<II>(mut self, header: II) -> Table<'a, T, H, I, D, R>
where
II: IntoIterator<Item = T, IntoIter = H>,
{
@@ -119,55 +119,55 @@ where
self
}
pub fn header_style(&mut self, style: Style) -> &mut Table<'a, 'i, T, H, I, D, R> {
pub fn header_style(mut self, style: Style) -> Table<'a, T, H, I, D, R> {
self.header_style = style;
self
}
pub fn widths(&mut self, widths: &'a [u16]) -> &mut Table<'a, 'i, T, H, I, D, R> {
pub fn widths(mut self, widths: &'a [u16]) -> Table<'a, T, H, I, D, R> {
self.widths = widths;
self
}
pub fn rows<II>(&mut self, rows: II) -> &mut Table<'a, 'i, T, H, I, D, R>
pub fn rows<II>(mut self, rows: II) -> Table<'a, T, H, I, D, R>
where
II: IntoIterator<Item = Row<'i, D, I>, IntoIter = R>,
II: IntoIterator<Item = Row<D, I>, IntoIter = R>,
{
self.rows = rows.into_iter();
self
}
pub fn style(&mut self, style: Style) -> &mut Table<'a, 'i, T, H, I, D, R> {
pub fn style(mut self, style: Style) -> Table<'a, T, H, I, D, R> {
self.style = style;
self
}
pub fn column_spacing(&mut self, spacing: u16) -> &mut Table<'a, 'i, T, H, I, D, R> {
pub fn column_spacing(mut self, spacing: u16) -> Table<'a, T, H, I, D, R> {
self.column_spacing = spacing;
self
}
}
impl<'a, 'i, T, H, I, D, R> Widget for Table<'a, 'i, T, H, I, D, R>
impl<'a, T, H, I, D, R> Widget for Table<'a, T, H, I, D, R>
where
T: Display,
H: Iterator<Item = T>,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<'i, D, I>>,
R: Iterator<Item = Row<D, I>>,
{
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
// Render block if necessary and get the drawing area
let table_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.inner(area)
}
None => *area,
None => area,
};
// Set the background
self.background(&table_area, buf, self.style.bg);
self.background(table_area, buf, self.style.bg);
// Save widths of the columns that will fit in the given area
let mut x = 0;
@@ -185,7 +185,7 @@ where
if y < table_area.bottom() {
x = table_area.left();
for (w, t) in widths.iter().zip(self.header.by_ref()) {
buf.set_string(x, y, &format!("{}", t), &self.header_style);
buf.set_string(x, y, format!("{}", t), self.header_style);
x += *w + self.column_spacing;
}
}
@@ -197,12 +197,12 @@ where
let remaining = (table_area.bottom() - y) as usize;
for (i, row) in self.rows.by_ref().take(remaining).enumerate() {
let (data, style) = match row {
Row::Data(d) => (d, &default_style),
Row::Data(d) => (d, default_style),
Row::StyledData(d, s) => (d, s),
};
x = table_area.left();
for (w, elt) in widths.iter().zip(data) {
buf.set_stringn(x, y + i as u16, &format!("{}", elt), *w as usize, style);
buf.set_stringn(x, y + i as u16, format!("{}", elt), *w as usize, style);
x += *w + self.column_spacing;
}
}

View File

@@ -1,25 +1,26 @@
use unicode_width::UnicodeWidthStr;
use widgets::{Block, Widget};
use buffer::Buffer;
use layout::Rect;
use style::Style;
use symbols::line;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::symbols::line;
use crate::widgets::{Block, Widget};
/// A widget to display available tabs in a multiple panels context.
///
/// # Examples
///
/// ```
/// # extern crate tui;
/// # use tui::widgets::{Block, Borders, Tabs};
/// # use tui::style::{Style, Color};
/// # use tui::symbols::{DOT};
/// # fn main() {
/// Tabs::default()
/// .block(Block::default().title("Tabs").borders(Borders::ALL))
/// .titles(&["Tab1", "Tab2", "Tab3", "Tab4"])
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().fg(Color::Yellow));
/// .highlight_style(Style::default().fg(Color::Yellow))
/// .divider(DOT);
/// # }
/// ```
pub struct Tabs<'a, T>
@@ -36,6 +37,8 @@ where
style: Style,
/// The style used to display the selected item
highlight_style: Style,
/// Tab divider
divider: &'a str,
}
impl<'a, T> Default for Tabs<'a, T>
@@ -49,6 +52,7 @@ where
selected: 0,
style: Default::default(),
highlight_style: Default::default(),
divider: line::VERTICAL,
}
}
}
@@ -57,57 +61,65 @@ impl<'a, T> Tabs<'a, T>
where
T: AsRef<str>,
{
pub fn block(&mut self, block: Block<'a>) -> &mut Tabs<'a, T> {
pub fn block(mut self, block: Block<'a>) -> Tabs<'a, T> {
self.block = Some(block);
self
}
pub fn titles(&mut self, titles: &'a [T]) -> &mut Tabs<'a, T> {
pub fn titles(mut self, titles: &'a [T]) -> Tabs<'a, T> {
self.titles = titles;
self
}
pub fn select(&mut self, selected: usize) -> &mut Tabs<'a, T> {
pub fn select(mut self, selected: usize) -> Tabs<'a, T> {
self.selected = selected;
self
}
pub fn style(&mut self, style: Style) -> &mut Tabs<'a, T> {
pub fn style(mut self, style: Style) -> Tabs<'a, T> {
self.style = style;
self
}
pub fn highlight_style(&mut self, style: Style) -> &mut Tabs<'a, T> {
pub fn highlight_style(mut self, style: Style) -> Tabs<'a, T> {
self.highlight_style = style;
self
}
pub fn divider(mut self, divider: &'a str) -> Tabs<'a, T> {
self.divider = divider;
self
}
}
impl<'a, T> Widget for Tabs<'a, T>
where
T: AsRef<str>,
{
fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
let tabs_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.inner(area)
}
None => *area,
None => area,
};
if tabs_area.height < 1 {
return;
}
self.background(&tabs_area, buf, self.style.bg);
self.background(tabs_area, buf, self.style.bg);
let mut x = tabs_area.left();
for (title, style) in self.titles.iter().enumerate().map(|(i, t)| {
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)
(t, self.highlight_style, lt)
} else {
(t, &self.style)
(t, self.style, lt)
}
}) {
x += 1;
@@ -116,14 +128,11 @@ where
} 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() {
if x >= tabs_area.right() || last_title {
break;
} else {
buf.get_mut(x, tabs_area.top())
.set_symbol(line::VERTICAL)
.set_fg(self.style.fg)
.set_bg(self.style.bg);
x += 1;
buf.set_string(x, tabs_area.top(), self.divider, style);
x += divider_width;
}
}
}

45
tests/block.rs Normal file
View File

@@ -0,0 +1,45 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, Widget};
use tui::Terminal;
#[test]
fn it_draws_a_block() {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
Block::default()
.title("Title")
.borders(Borders::ALL)
.title_style(Style::default().fg(Color::LightBlue))
.render(
&mut f,
Rect {
x: 0,
y: 0,
width: 8,
height: 8,
},
);
})
.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());
}

42
tests/gauge.rs Normal file
View File

@@ -0,0 +1,42 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::{Constraint, Direction, Layout};
use tui::widgets::{Block, Borders, Gauge, Widget};
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());
Gauge::default()
.block(Block::default().title("Percentage").borders(Borders::ALL))
.percent(43)
.render(&mut f, chunks[0]);
Gauge::default()
.block(Block::default().title("Ratio").borders(Borders::ALL))
.ratio(0.2113139343131)
.render(&mut f, chunks[1]);
})
.unwrap();
let expected = Buffer::with_lines(vec![
" ",
" ",
" ┌Percentage────────────────────────┐ ",
" │ 43% │ ",
" └──────────────────────────────────┘ ",
" ┌Ratio─────────────────────────────┐ ",
" │ 21% │ ",
" └──────────────────────────────────┘ ",
" ",
" ",
]);
assert_eq!(&expected, terminal.backend().buffer());
}

140
tests/paragraph.rs Normal file
View File

@@ -0,0 +1,140 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::Alignment;
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
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)];
Paragraph::new(text.iter())
.block(Block::default().borders(Borders::ALL))
.alignment(alignment)
.wrap(true)
.render(&mut f, 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)];
Paragraph::new(text.iter())
.block(Block::default().borders(Borders::ALL))
.wrap(true)
.render(&mut f, 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)];
Paragraph::new(text.iter())
.block(Block::default().borders(Borders::ALL))
.wrap(true)
.render(&mut f, 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());
}

11
tests/size.rs Normal file
View File

@@ -0,0 +1,11 @@
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);
}