Compare commits

...

49 Commits

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

View File

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

View File

@@ -2,6 +2,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

View File

@@ -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"]

View File

@@ -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

View File

@@ -4,7 +4,7 @@
[![Crate Status](https://img.shields.io/crates/v/tui.svg)](https://crates.io/crates/tui)
[![Docs Status](https://docs.rs/tui/badge.svg)](https://docs.rs/crate/tui/)
<img src="./docs/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
user interfaces and dashboards. It is heavily inspired by the `Javascript`
@@ -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

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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
View 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(())
}

View File

@@ -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(())
}

View File

@@ -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]);
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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();
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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
View File

@@ -0,0 +1,83 @@
use std::io;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use termion::event::Key;
use termion::input::TermRead;
pub enum Event<I> {
Input(I),
Tick,
}
/// 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()
}
}

View File

@@ -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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

128
src/backend/crossterm.rs Normal file
View 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,
}
}
}

View File

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

View File

@@ -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)),
}
}

View File

@@ -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
View 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(())
}
}

View File

@@ -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());

View File

@@ -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
}
}

View File

@@ -1,7 +1,7 @@
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
//! terminal users interfaces and dashboards.
//!
//! ![](https://raw.githubusercontent.com/fdehau/tui-rs/master/docs/demo.gif)
//! ![](https://raw.githubusercontent.com/fdehau/tui-rs/master/assets/demo.gif)
//!
//! # Get started
//!
@@ -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};

View File

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

View File

@@ -1,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()
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,
}
}
}

View File

@@ -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,
}

View File

@@ -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

View File

@@ -2,12 +2,12 @@ use std::cmp::max;
use unicode_width::UnicodeWidthStr;
use widgets::{Block, Borders, Widget};
use widgets::canvas::{Canvas, Points};
use buffer::Buffer;
use layout::Rect;
use style::Style;
use symbols;
use 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,
);
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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
View 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());
}