Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c46ef69e9 | ||
|
|
22e8fade7e | ||
|
|
37aa06f508 | ||
|
|
f6d2f8f929 | ||
|
|
32947669d5 | ||
|
|
fdf3015ad0 | ||
|
|
03bfcde147 | ||
|
|
56fc43400a | ||
|
|
7b4d35d224 | ||
|
|
a99fc115f8 | ||
|
|
d8e5f57d53 | ||
|
|
aa85e597d9 | ||
|
|
08ab92da80 | ||
|
|
5d52fd2486 | ||
|
|
4ae9850e13 | ||
|
|
e14190ae4b | ||
|
|
ce445a8096 | ||
|
|
dd71d6471c | ||
|
|
f795173886 | ||
|
|
e42ab1fed8 | ||
|
|
0544c023f5 | ||
|
|
ff47f9480b | ||
|
|
70561b7c54 | ||
|
|
559c9c75f3 | ||
|
|
6c69160d6b | ||
|
|
d0cee47e22 | ||
|
|
ccebb56a83 | ||
|
|
cf169d1582 | ||
|
|
bcd1e30376 | ||
|
|
40bad7a718 | ||
|
|
3d63f9607f | ||
|
|
13e194cd26 | ||
|
|
d6016788ef | ||
|
|
ad602a54bf | ||
|
|
7181970a32 | ||
|
|
cfc90ab7f6 | ||
|
|
05c96eaa28 | ||
|
|
9a9f49f467 | ||
|
|
c552ae98b4 | ||
|
|
df7493fd33 | ||
|
|
5de571fb03 | ||
|
|
62df7badf3 | ||
|
|
597e219257 | ||
|
|
3f8a9079ee | ||
|
|
5981767543 | ||
|
|
36146d970a | ||
|
|
464ba4f334 | ||
|
|
36a5eb2110 | ||
|
|
55840210c7 |
11
.travis.yml
11
.travis.yml
@@ -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
|
||||
|
||||
77
CHANGELOG.md
77
CHANGELOG.md
@@ -2,6 +2,83 @@
|
||||
|
||||
## To be released
|
||||
|
||||
## 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
|
||||
|
||||
84
Cargo.toml
84
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tui"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
authors = ["Florian Dehau <work@fdehau.com>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
@@ -8,7 +8,8 @@ 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
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "fdehau/tui-rs" }
|
||||
@@ -17,53 +18,21 @@ travis-ci = { repository = "fdehau/tui-rs" }
|
||||
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.7"
|
||||
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.4", 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.4"
|
||||
failure = "0.1"
|
||||
|
||||
[[example]]
|
||||
name = "rustbox"
|
||||
@@ -71,21 +40,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"]
|
||||
|
||||
58
Makefile
58
Makefile
@@ -21,53 +21,37 @@ 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
|
||||
|
||||
.PHONY: clippy
|
||||
clippy: RUST_CHANNEL = nightly
|
||||
clippy: ## Check the style of the source code and catch common errors
|
||||
$(CARGO) clippy --features="termion rustbox"
|
||||
@@ -75,13 +59,27 @@ clippy: ## Check the style of the source code and catch common errors
|
||||
|
||||
# ================================ Test =======================================
|
||||
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run the tests
|
||||
$(CARGO) test
|
||||
$(CARGO) test --features=termion,crossterm
|
||||
|
||||
# =============================== Examples ====================================
|
||||
|
||||
.PHONY: build-examples
|
||||
build-examples: ## Build all examples
|
||||
@$(CARGO) build --examples --features=termion,crossterm
|
||||
|
||||
.PHONY: run-examples
|
||||
run-examples: ## Run all examples
|
||||
@for file in examples/*.rs; do \
|
||||
name=$$(basename $${file//.rs/}); \
|
||||
$(CARGO) run --features=termion,crossterm --example $$name; \
|
||||
done;
|
||||
|
||||
# ================================ Doc ========================================
|
||||
|
||||
|
||||
.PHONY: doc
|
||||
doc: ## Build the documentation (available at ./target/doc)
|
||||
$(CARGO) doc
|
||||
|
||||
@@ -90,22 +88,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
|
||||
|
||||
19
README.md
19
README.md
@@ -4,7 +4,7 @@
|
||||
[](https://crates.io/crates/tui)
|
||||
[](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`
|
||||
@@ -32,6 +32,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 +54,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
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
@@ -1,19 +1,23 @@
|
||||
extern crate failure;
|
||||
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, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{BarChart, Block, Borders, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use util::event::{Event, Events};
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
@@ -53,112 +57,80 @@ 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();
|
||||
let size = terminal.size()?;
|
||||
if app.size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
terminal.resize(size)?;
|
||||
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(app.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(())
|
||||
}
|
||||
|
||||
@@ -1,83 +1,109 @@
|
||||
extern crate failure;
|
||||
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, Rect};
|
||||
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 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;
|
||||
struct App {
|
||||
size: Rect,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> App {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
}
|
||||
}
|
||||
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]);
|
||||
});
|
||||
});
|
||||
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()?;
|
||||
|
||||
t.draw().unwrap();
|
||||
// Create default app state
|
||||
let mut app = App::default();
|
||||
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
|
||||
loop {
|
||||
let size = terminal.size()?;
|
||||
if app.size != size {
|
||||
terminal.resize(size)?;
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
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
|
||||
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(app.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;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
extern crate failure;
|
||||
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, Line, Map, MapResolution};
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use util::event::{Config, Event, Events};
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
@@ -43,7 +48,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 +72,37 @@ 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();
|
||||
let size = terminal.size()?;
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
terminal.resize(size)?;
|
||||
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(app.size);
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.paint(|ctx| {
|
||||
@@ -160,12 +111,11 @@ fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(app.x, -app.y, "You are here", Color::Yellow);
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
}).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()),
|
||||
@@ -195,11 +145,36 @@ fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
|
||||
y2: f64::from(app.ball.top()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
}).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(())
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
extern crate failure;
|
||||
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 termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use util::event::{Event, Events};
|
||||
use util::SinSignal;
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
@@ -35,15 +37,15 @@ impl App {
|
||||
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 +59,75 @@ 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();
|
||||
let size = terminal.size()?;
|
||||
if app.size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
terminal.resize(size)?;
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
terminal.draw(|mut f| {
|
||||
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, app.size);
|
||||
})?;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
59
examples/crossterm.rs
Normal file
59
examples/crossterm.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
extern crate crossterm;
|
||||
extern crate failure;
|
||||
extern crate tui;
|
||||
|
||||
use tui::backend::CrosstermBackend;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> App {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new())?;
|
||||
terminal.clear()?;
|
||||
terminal.hide_cursor()?;
|
||||
let mut app = App::default();
|
||||
loop {
|
||||
let size = terminal.size()?;
|
||||
if app.size != size {
|
||||
terminal.resize(size)?;
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
terminal.draw(|mut f| {
|
||||
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(terminal.backend().screen());
|
||||
match input.read_char()? {
|
||||
'q' => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,11 +1,36 @@
|
||||
extern crate failure;
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::widgets::Widget;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::Style;
|
||||
use tui::widgets::Widget;
|
||||
use tui::Terminal;
|
||||
|
||||
use util::event::{Event, Events};
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> App {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Label<'a> {
|
||||
text: &'a str,
|
||||
@@ -18,8 +43,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 +55,36 @@ 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();
|
||||
|
||||
let mut app = App::default();
|
||||
|
||||
loop {
|
||||
let size = terminal.size()?;
|
||||
if app.size != size {
|
||||
terminal.resize(size)?;
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
terminal.draw(|mut f| {
|
||||
Label::default().text("Test").render(&mut f, app.size);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(key) => if key == Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
577
examples/demo.rs
577
examples/demo.rs
@@ -1,28 +1,30 @@
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
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 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 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};
|
||||
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 util::event::{Event, Events};
|
||||
use util::{RandomSignal, SinSignal, TabsState};
|
||||
|
||||
struct Server<'a> {
|
||||
name: &'a str,
|
||||
@@ -36,7 +38,7 @@ struct App<'a> {
|
||||
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,18 +51,20 @@ struct App<'a> {
|
||||
servers: Vec<Server<'a>>,
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
stderrlog::new()
|
||||
.module(module_path!())
|
||||
.verbosity(4)
|
||||
.init()
|
||||
.unwrap();
|
||||
info!("Start");
|
||||
.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::new();
|
||||
|
||||
let mut rand_signal = RandomSignal::new(0, 100);
|
||||
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
|
||||
@@ -102,15 +106,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 +168,53 @@ 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();
|
||||
let size = terminal.size()?;
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
terminal.resize(size)?;
|
||||
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(app.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 {
|
||||
Key::Down => if app.selected < app.items.len() - 1 {
|
||||
app.selected += 1;
|
||||
},
|
||||
event::Key::Left => {
|
||||
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,257 +247,242 @@ 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)
|
||||
.title("Footer")
|
||||
.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);
|
||||
).wrap(true)
|
||||
.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();
|
||||
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]);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
extern crate failure;
|
||||
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, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Gauge, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use util::event::{Event, Events};
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
@@ -34,7 +38,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn update(&mut self) {
|
||||
self.progress1 += 5;
|
||||
if self.progress1 > 100 {
|
||||
self.progress1 = 0;
|
||||
@@ -54,104 +58,72 @@ 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();
|
||||
let size = terminal.size()?;
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
terminal.resize(size)?;
|
||||
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(25),
|
||||
Size::Percent(25),
|
||||
Size::Percent(25),
|
||||
Size::Percent(25),
|
||||
])
|
||||
.render(t, &app.size, |t, chunks| {
|
||||
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(app.size);
|
||||
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]);
|
||||
.render(&mut f, 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[2]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge3").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(())
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
extern crate failure;
|
||||
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, Rect};
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
use tui::layout::{Direction, Group, Rect, Size};
|
||||
use tui::Terminal;
|
||||
|
||||
use util::event::{Event, Events};
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
@@ -27,78 +32,58 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
stderrlog::new().verbosity(4).init().unwrap();
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
stderrlog::new().verbosity(4).init()?;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
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();
|
||||
let size = terminal.size()?;
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
terminal.resize(size)?;
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
draw(&mut terminal, &app);
|
||||
}
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
]
|
||||
.as_ref(),
|
||||
).split(app.size);
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
173
examples/list.rs
173
examples/list.rs
@@ -1,24 +1,28 @@
|
||||
extern crate failure;
|
||||
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, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, List, SelectableList, Text, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use 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,
|
||||
@@ -35,7 +39,7 @@ impl<'a> App<'a> {
|
||||
"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 +81,98 @@ 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();
|
||||
let size = terminal.size()?;
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
terminal.resize(size)?;
|
||||
app.size = 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.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(app.size);
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -1,66 +1,111 @@
|
||||
extern crate failure;
|
||||
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, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, 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 util::event::{Event, Events};
|
||||
|
||||
let mut term_size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &term_size);
|
||||
struct App {
|
||||
size: Rect,
|
||||
}
|
||||
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != term_size {
|
||||
terminal.resize(size).unwrap();
|
||||
term_size = size;
|
||||
}
|
||||
|
||||
draw(&mut terminal, &term_size);
|
||||
let evt = c.unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
impl Default for App {
|
||||
fn default() -> App {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
}
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, size: &Rect) {
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::White))
|
||||
.render(t, 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()?;
|
||||
|
||||
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]);
|
||||
});
|
||||
});
|
||||
let events = Events::new();
|
||||
|
||||
t.draw().unwrap();
|
||||
let mut app = App::default();
|
||||
|
||||
loop {
|
||||
let size = terminal.size()?;
|
||||
if size != app.size {
|
||||
terminal.resize(size)?;
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let mut long_line: String = std::iter::repeat('X').take(size.width.into()).collect();
|
||||
long_line.insert_str(0, "Very long line: ");
|
||||
long_line.push('\n');
|
||||
|
||||
terminal.draw(|mut f| {
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::White))
|
||||
.render(&mut f, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
]
|
||||
.as_ref(),
|
||||
).split(size);
|
||||
|
||||
let text = [
|
||||
Text::raw("This a line\n"),
|
||||
Text::styled("This a line\n", Style::default().fg(Color::Red)),
|
||||
Text::styled("This a line\n", Style::default().bg(Color::Blue)),
|
||||
Text::styled(
|
||||
"This a longer line\n",
|
||||
Style::default().modifier(Modifier::CrossedOut),
|
||||
),
|
||||
Text::raw(&long_line),
|
||||
Text::styled(
|
||||
"This a line\n",
|
||||
Style::default().fg(Color::Green).modifier(Modifier::Italic),
|
||||
),
|
||||
];
|
||||
|
||||
Paragraph::new(text.iter())
|
||||
.alignment(Alignment::Left)
|
||||
.render(&mut f, chunks[0]);
|
||||
Paragraph::new(text.iter())
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(true)
|
||||
.render(&mut f, chunks[1]);
|
||||
Paragraph::new(text.iter())
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(true)
|
||||
.render(&mut f, chunks[2]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(key) => if key == Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
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::layout::{Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Paragraph, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(RustboxBackend::new().unwrap()).unwrap();
|
||||
@@ -30,22 +30,18 @@ fn main() {
|
||||
|
||||
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]);
|
||||
});
|
||||
{
|
||||
let mut f = t.get_frame();
|
||||
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(&mut f, &size);
|
||||
}
|
||||
|
||||
t.draw().unwrap();
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
extern crate failure;
|
||||
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, Rect};
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, Sparkline, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use util::event::{Event, Events};
|
||||
use util::RandomSignal;
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
@@ -34,14 +36,14 @@ impl App {
|
||||
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,105 +56,77 @@ 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();
|
||||
let size = terminal.size()?;
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
terminal.resize(size)?;
|
||||
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(app.size);
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data1)
|
||||
).data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.render(t, &chunks[0]);
|
||||
.render(&mut f, chunks[0]);
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data2)
|
||||
).data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green))
|
||||
.render(t, &chunks[1]);
|
||||
.render(&mut f, chunks[1]);
|
||||
// Multiline
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data3)
|
||||
).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(())
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
extern crate failure;
|
||||
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, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Row, Table, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use util::event::{Event, Events};
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
@@ -35,75 +42,71 @@ 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();
|
||||
loop {
|
||||
let size = terminal.size()?;
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
terminal.resize(size)?;
|
||||
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;
|
||||
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(app.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(())
|
||||
}
|
||||
|
||||
147
examples/tabs.rs
147
examples/tabs.rs
@@ -1,113 +1,104 @@
|
||||
extern crate failure;
|
||||
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, Rect};
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, Tabs, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use util::event::{Event, Events};
|
||||
use 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();
|
||||
loop {
|
||||
let size = terminal.size()?;
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
terminal.resize(size)?;
|
||||
app.size = 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.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(app.size);
|
||||
|
||||
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, app.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(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
extern crate failure;
|
||||
/// A simple example demonstrating how to handle user input. This is
|
||||
/// a bit out of the scope of the library as it does not provide any
|
||||
/// input handling out of the box. However, it may helps some to get
|
||||
@@ -11,28 +12,39 @@
|
||||
/// messages
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
extern crate unicode_width;
|
||||
|
||||
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, Rect};
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget};
|
||||
use tui::Terminal;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use util::event::{Event, Events};
|
||||
|
||||
/// App holds the state of the application
|
||||
struct App {
|
||||
/// Current size of the terminal
|
||||
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(),
|
||||
@@ -41,91 +53,75 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
// Handle resize
|
||||
let size = terminal.size()?;
|
||||
if app.size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
terminal.resize(size)?;
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
// 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(app.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]);
|
||||
})?;
|
||||
|
||||
// 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
83
examples/util/event.rs
Normal 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,
|
||||
}
|
||||
|
||||
/// An 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()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
extern crate rand;
|
||||
|
||||
pub mod event;
|
||||
|
||||
use self::rand::distributions::{IndependentSample, Range};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -38,9 +38,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 +54,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
make build
|
||||
make test
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
|
||||
make lint
|
||||
fi
|
||||
128
src/backend/crossterm.rs
Normal file
128
src/backend/crossterm.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
extern crate crossterm;
|
||||
|
||||
use std::io;
|
||||
|
||||
use backend::Backend;
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier};
|
||||
|
||||
pub struct CrosstermBackend {
|
||||
screen: crossterm::Screen,
|
||||
}
|
||||
|
||||
impl CrosstermBackend {
|
||||
pub fn new() -> CrosstermBackend {
|
||||
CrosstermBackend {
|
||||
screen: crossterm::Screen::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn screen(&self) -> &crossterm::Screen {
|
||||
&self.screen
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for CrosstermBackend {
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
let terminal = crossterm::terminal::terminal(&self.screen);
|
||||
terminal.clear(crossterm::terminal::ClearType::All);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
let cursor = crossterm::cursor(&self.screen);
|
||||
cursor.hide();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
let cursor = crossterm::cursor(&self.screen);
|
||||
cursor.show();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let terminal = crossterm::terminal::terminal(&self.screen);
|
||||
let (width, height) = terminal.terminal_size();
|
||||
Ok(Rect {
|
||||
x: 0,
|
||||
y: 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(&self.screen);
|
||||
let mut last_y = 0;
|
||||
let mut last_x = 0;
|
||||
for (x, y, cell) in content {
|
||||
if y != last_y || x != last_x + 1 {
|
||||
cursor.goto(x, y);
|
||||
}
|
||||
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)
|
||||
}
|
||||
s.paint(&self.screen);
|
||||
}
|
||||
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> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -85,6 +85,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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ extern crate termion;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
|
||||
use self::termion::raw::IntoRawMode;
|
||||
|
||||
use super::Backend;
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
@@ -17,40 +15,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 }
|
||||
}
|
||||
}
|
||||
@@ -141,7 +110,7 @@ where
|
||||
}
|
||||
|
||||
/// Return the size of the terminal
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let terminal = try!(termion::terminal_size());
|
||||
Ok(Rect {
|
||||
x: 0,
|
||||
@@ -157,38 +126,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 +178,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 +203,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),
|
||||
|
||||
68
src/backend/test.rs
Normal file
68
src/backend/test.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
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 {
|
||||
x: 0,
|
||||
y: 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 {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
})
|
||||
}
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
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};
|
||||
@@ -81,7 +83,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 +95,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 +113,34 @@ 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\n")?;
|
||||
for cells in self.content.chunks(self.area.width as usize) {
|
||||
for cell in cells {
|
||||
f.write_str(&cell.symbol)?;
|
||||
}
|
||||
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 +155,30 @@ 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,
|
||||
});
|
||||
let mut y = 0;
|
||||
for line in &lines {
|
||||
buffer.set_string(0, y, line, Style::default());
|
||||
y += 1;
|
||||
}
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Returns the content of the buffer as a slice
|
||||
@@ -183,7 +233,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,20 +286,26 @@ 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, limit: 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 graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||
let max_index = min((self.area.right() - x) as usize, limit);
|
||||
for s in graphemes.into_iter().take(max_index) {
|
||||
for s in graphemes.take(max_index) {
|
||||
self.content[index].symbol.clear();
|
||||
self.content[index].symbol.push_str(s);
|
||||
self.content[index].style = *style;
|
||||
self.content[index].style = style;
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
@@ -273,7 +331,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());
|
||||
|
||||
|
||||
411
src/layout.rs
411
src/layout.rs
@@ -1,212 +1,189 @@
|
||||
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
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # 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 +210,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 +257,100 @@ 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 {
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
154
src/lib.rs
154
src/lib.rs
@@ -1,7 +1,7 @@
|
||||
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
|
||||
//! terminal users interfaces and dashboards.
|
||||
//!
|
||||
//! 
|
||||
//! 
|
||||
//!
|
||||
//! # Get started
|
||||
//!
|
||||
@@ -14,13 +14,18 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! extern crate tui;
|
||||
//! extern crate termion;
|
||||
//!
|
||||
//! 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 +34,7 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies.tui]
|
||||
//! version = "0.2.0"
|
||||
//! version = "0.3.0"
|
||||
//! default-features = false
|
||||
//! features = ['rustbox']
|
||||
//! ```
|
||||
@@ -42,9 +47,10 @@
|
||||
//! 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 +62,103 @@
|
||||
//!
|
||||
//! 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;
|
||||
//! extern crate termion;
|
||||
//!
|
||||
//! 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)?;
|
||||
//! let size = terminal.size()?;
|
||||
//! terminal.draw(|mut f| {
|
||||
//! 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;
|
||||
//! extern crate termion;
|
||||
//!
|
||||
//! 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)?;
|
||||
//! let size = terminal.size()?;
|
||||
//! 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(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;
|
||||
extern crate either;
|
||||
extern crate itertools;
|
||||
#[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};
|
||||
|
||||
52
src/style.rs
52
src/style.rs
@@ -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,
|
||||
|
||||
137
src/terminal.rs
137
src/terminal.rs
@@ -1,18 +1,10 @@
|
||||
use std::io;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use backend::Backend;
|
||||
use buffer::Buffer;
|
||||
use layout::{split, Group, Rect};
|
||||
use layout::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,
|
||||
}
|
||||
|
||||
/// Interface to the terminal backed by Termion
|
||||
#[derive(Debug)]
|
||||
pub struct Terminal<B>
|
||||
@@ -20,13 +12,47 @@ 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,
|
||||
}
|
||||
|
||||
pub struct Frame<'a, B: 'a>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
terminal: &'a mut Terminal<B>,
|
||||
}
|
||||
|
||||
impl<'a, B> Frame<'a, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// 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 +61,24 @@ 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,
|
||||
})
|
||||
}
|
||||
|
||||
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,30 +87,9 @@ 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()
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let width = self.buffers[self.current].area.width;
|
||||
let content = self.buffers[self.current]
|
||||
.content
|
||||
@@ -96,43 +109,25 @@ where
|
||||
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> {
|
||||
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.backend.clear()
|
||||
}
|
||||
|
||||
/// Flushes the current internal state and prepares the interface for the next draw call
|
||||
pub fn draw(&mut self) -> Result<(), io::Error> {
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(Frame<B>),
|
||||
{
|
||||
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 +137,20 @@ 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> {
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ 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 widgets::{Block, Widget};
|
||||
|
||||
/// Display multiple bars in a single widgets
|
||||
///
|
||||
@@ -67,7 +67,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,45 +76,45 @@ 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 {
|
||||
@@ -123,13 +123,15 @@ impl<'a> Widget for BarChart<'a> {
|
||||
|
||||
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))
|
||||
@@ -153,7 +155,7 @@ impl<'a> Widget for BarChart<'a> {
|
||||
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_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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use widgets::{Borders, Widget};
|
||||
use symbols::line;
|
||||
use 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.
|
||||
@@ -75,11 +75,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,12 +99,12 @@ 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;
|
||||
}
|
||||
|
||||
self.background(area, buf, self.style.bg);
|
||||
self.background(&area, buf, self.style.bg);
|
||||
|
||||
// Sides
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
@@ -178,7 +178,7 @@ impl<'a> Widget for Block<'a> {
|
||||
area.top(),
|
||||
title,
|
||||
width as usize,
|
||||
&self.title_style,
|
||||
self.title_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,12 +49,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use widgets::canvas::Shape;
|
||||
use style::Color;
|
||||
use widgets::canvas::points::PointsIterator;
|
||||
use widgets::canvas::world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION};
|
||||
use style::Color;
|
||||
use 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,
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
mod points;
|
||||
mod line;
|
||||
mod map;
|
||||
mod points;
|
||||
mod world;
|
||||
|
||||
pub use self::points::Points;
|
||||
pub use self::line::Line;
|
||||
pub use self::map::{Map, MapResolution};
|
||||
pub use self::points::Points;
|
||||
|
||||
use style::{Color, Style};
|
||||
use buffer::Buffer;
|
||||
use widgets::{Block, Widget};
|
||||
use layout::Rect;
|
||||
use style::{Color, Style};
|
||||
use widgets::{Block, Widget};
|
||||
|
||||
pub const DOTS: [[u16; 2]; 4] = [
|
||||
[0x0001, 0x0008],
|
||||
@@ -116,12 +116,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
|
||||
@@ -199,26 +194,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 +223,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 +272,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 +285,7 @@ where
|
||||
dx + canvas_area.left(),
|
||||
dy + canvas_area.top(),
|
||||
label.text,
|
||||
&style.fg(label.color),
|
||||
style.fg(label.color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 widgets::canvas::{Canvas, Points};
|
||||
use widgets::{Block, Borders, Widget};
|
||||
|
||||
/// An X or Y axis for the chart widget
|
||||
pub struct Axis<'a, L>
|
||||
@@ -195,6 +195,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 +234,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 +280,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 +323,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,16 +342,16 @@ 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;
|
||||
@@ -359,12 +361,12 @@ where
|
||||
|
||||
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 +380,7 @@ where
|
||||
- label.as_ref().width() as u16,
|
||||
y,
|
||||
label.as_ref(),
|
||||
&self.x_axis.labels_style,
|
||||
self.x_axis.labels_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -394,7 +396,7 @@ where
|
||||
x,
|
||||
graph_area.bottom() - 1 - dy,
|
||||
label.as_ref(),
|
||||
&self.y_axis.labels_style,
|
||||
self.y_axis.labels_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -427,7 +429,8 @@ 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]
|
||||
!(x < self.x_axis.bounds[0]
|
||||
|| x > self.x_axis.bounds[1]
|
||||
|| y < self.y_axis.bounds[0]
|
||||
|| y > self.y_axis.bounds[1])
|
||||
}) {
|
||||
@@ -453,8 +456,7 @@ where
|
||||
coords: dataset.data,
|
||||
color: dataset.style.fg,
|
||||
});
|
||||
})
|
||||
.draw(&graph_area, buf);
|
||||
}).draw(graph_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -462,13 +464,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use widgets::{Block, Widget};
|
||||
use buffer::Buffer;
|
||||
use style::{Color, Style};
|
||||
use layout::Rect;
|
||||
use style::{Color, Style};
|
||||
use widgets::{Block, Widget};
|
||||
|
||||
/// A widget to display a task progress.
|
||||
///
|
||||
@@ -39,35 +39,35 @@ 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> {
|
||||
pub fn percent(mut self, percent: u16) -> Gauge<'a> {
|
||||
self.percent = percent;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label(&mut self, string: &'a str) -> &mut Gauge<'a> {
|
||||
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;
|
||||
@@ -92,7 +92,7 @@ impl<'a> Widget for Gauge<'a> {
|
||||
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
|
||||
|
||||
@@ -1,84 +1,85 @@
|
||||
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 layout::{Corner, Rect};
|
||||
use style::Style;
|
||||
use 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 {
|
||||
@@ -87,29 +88,24 @@ where
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -128,7 +124,7 @@ where
|
||||
/// 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 +158,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,64 +171,72 @@ 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);
|
||||
}).skip(offset as usize);
|
||||
List::new(items)
|
||||
.block(self.block.unwrap_or_default())
|
||||
.style(self.style)
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
mod block;
|
||||
mod paragraph;
|
||||
mod list;
|
||||
mod gauge;
|
||||
mod sparkline;
|
||||
mod chart;
|
||||
use std::borrow::Cow;
|
||||
|
||||
mod barchart;
|
||||
mod tabs;
|
||||
mod table;
|
||||
mod block;
|
||||
pub mod canvas;
|
||||
mod chart;
|
||||
mod gauge;
|
||||
mod list;
|
||||
mod paragraph;
|
||||
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 backend::Backend;
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use terminal::Terminal;
|
||||
use backend::Backend;
|
||||
use style::Color;
|
||||
use style::{Color, Style};
|
||||
use terminal::Frame;
|
||||
|
||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
||||
bitflags! {
|
||||
@@ -43,11 +45,26 @@ 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) {
|
||||
for y in area.top()..area.bottom() {
|
||||
@@ -57,11 +74,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
use either::Either;
|
||||
use itertools::{multipeek, MultiPeek};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use widgets::{Block, Widget};
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier, Style};
|
||||
use layout::{Alignment, Rect};
|
||||
use style::Style;
|
||||
use widgets::{Block, Text, Widget};
|
||||
|
||||
/// A widget to display some text. You can specify colors using commands embedded in
|
||||
/// the text such as "{[color] [text]}".
|
||||
/// 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,203 +40,73 @@ 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, 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 {
|
||||
@@ -236,25 +115,71 @@ impl<'a> Widget for Paragraph<'a> {
|
||||
|
||||
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 style = self.style;
|
||||
let 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| (g, style)))
|
||||
}
|
||||
Text::Styled(ref d, s) => {
|
||||
let data: &'t str = d; // coerce to &str
|
||||
Either::Right(UnicodeSegmentation::graphemes(data, true).map(move |g| (g, s)))
|
||||
}
|
||||
});
|
||||
let mut styled = multipeek(styled);
|
||||
|
||||
fn get_cur_line_len<'a, I: Iterator<Item = (&'a str, Style)>>(
|
||||
styled: &mut MultiPeek<I>,
|
||||
) -> u16 {
|
||||
let mut line_len = 0;
|
||||
while match &styled.peek() {
|
||||
Some(&(x, _)) => x != "\n",
|
||||
None => false,
|
||||
} {
|
||||
line_len += 1;
|
||||
}
|
||||
line_len
|
||||
};
|
||||
|
||||
let mut x = match self.alignment {
|
||||
Alignment::Center => {
|
||||
(text_area.width / 2).saturating_sub(get_cur_line_len(&mut styled) / 2)
|
||||
}
|
||||
Alignment::Right => (text_area.width).saturating_sub(get_cur_line_len(&mut styled)),
|
||||
Alignment::Left => 0,
|
||||
};
|
||||
let mut y = 0;
|
||||
|
||||
let mut remove_leading_whitespaces = false;
|
||||
for (string, style) in styled {
|
||||
while let Some((string, style)) = styled.next() {
|
||||
if string == "\n" {
|
||||
x = 0;
|
||||
x = match self.alignment {
|
||||
Alignment::Center => {
|
||||
(text_area.width / 2).saturating_sub(get_cur_line_len(&mut styled) / 2)
|
||||
}
|
||||
|
||||
Alignment::Right => {
|
||||
(text_area.width).saturating_sub(get_cur_line_len(&mut styled))
|
||||
}
|
||||
Alignment::Left => 0,
|
||||
};
|
||||
y += 1;
|
||||
continue;
|
||||
}
|
||||
if x >= text_area.width {
|
||||
if self.wrapping {
|
||||
x = 0;
|
||||
if !self.wrapping {
|
||||
continue; // Truncate the remainder of the line.
|
||||
} else {
|
||||
x = match self.alignment {
|
||||
Alignment::Center => {
|
||||
(text_area.width / 2).saturating_sub(get_cur_line_len(&mut styled) / 2)
|
||||
}
|
||||
|
||||
Alignment::Right => {
|
||||
(text_area.width).saturating_sub(get_cur_line_len(&mut styled) + 1)
|
||||
}
|
||||
Alignment::Left => 0,
|
||||
};
|
||||
y += 1;
|
||||
remove_leading_whitespaces = true
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::cmp::min;
|
||||
|
||||
use layout::Rect;
|
||||
use buffer::Buffer;
|
||||
use widgets::{Block, Widget};
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use symbols::bar;
|
||||
use widgets::{Block, Widget};
|
||||
|
||||
/// Widget to render a sparkline over one or more lines.
|
||||
///
|
||||
@@ -46,35 +46,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 +86,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)
|
||||
|
||||
@@ -2,18 +2,18 @@ use std::fmt::Display;
|
||||
use std::iter::Iterator;
|
||||
|
||||
use buffer::Buffer;
|
||||
use widgets::{Block, Widget};
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use 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,51 +119,51 @@ 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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use widgets::{Block, Widget};
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use symbols::line;
|
||||
use widgets::{Block, Widget};
|
||||
|
||||
/// A widget to display available tabs in a multiple panels context.
|
||||
///
|
||||
@@ -57,27 +57,27 @@ 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
|
||||
}
|
||||
@@ -87,13 +87,13 @@ 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 {
|
||||
@@ -105,9 +105,9 @@ where
|
||||
let mut x = tabs_area.left();
|
||||
for (title, style) in self.titles.iter().enumerate().map(|(i, t)| {
|
||||
if i == self.selected {
|
||||
(t, &self.highlight_style)
|
||||
(t, self.highlight_style)
|
||||
} else {
|
||||
(t, &self.style)
|
||||
(t, self.style)
|
||||
}
|
||||
}) {
|
||||
x += 1;
|
||||
|
||||
47
tests/block.rs
Normal file
47
tests/block.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
extern crate tui;
|
||||
extern crate unicode_width;
|
||||
|
||||
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());
|
||||
}
|
||||
Reference in New Issue
Block a user