Compare commits
42 Commits
v0.11.0
...
467/make_r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9226715101 | ||
|
|
4e76bfa2ca | ||
|
|
8832281dcf | ||
|
|
853f3d9200 | ||
|
|
67e996c5f4 | ||
|
|
f09863faa0 | ||
|
|
eb1e3be722 | ||
|
|
4ec902b96f | ||
|
|
74243394d9 | ||
|
|
e7f263efa7 | ||
|
|
0991145c58 | ||
|
|
01d2a8588a | ||
|
|
45431a2649 | ||
|
|
0b78fb9201 | ||
|
|
9cdff275cb | ||
|
|
77c6e106e4 | ||
|
|
efdd6bfb19 | ||
|
|
117098d2d2 | ||
|
|
f933d892aa | ||
|
|
5ea54792c0 | ||
|
|
23a9280db7 | ||
|
|
79e27b1778 | ||
|
|
0a05579a1c | ||
|
|
0030eb4a13 | ||
|
|
5bf40343eb | ||
|
|
1e35f983c4 | ||
|
|
a15ac8870b | ||
|
|
8a27036a54 | ||
|
|
8543523f18 | ||
|
|
5a9b59866b | ||
|
|
dc76956215 | ||
|
|
98fb5e4bbd | ||
|
|
25ff2e5e61 | ||
|
|
5050f1ce1c | ||
|
|
51b691e7ac | ||
|
|
c4cd0a5f31 | ||
|
|
41142732ec | ||
|
|
62495c3bd1 | ||
|
|
d00184a7c3 | ||
|
|
ce32d5537d | ||
|
|
25921fa91a | ||
|
|
932a496c3c |
100
.github/workflows/ci.yml
vendored
100
.github/workflows/ci.yml
vendored
@@ -1,7 +1,16 @@
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
name: CI
|
||||
|
||||
env:
|
||||
CI_CARGO_MAKE_VERSION: 0.32.9
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux
|
||||
@@ -12,48 +21,33 @@ jobs:
|
||||
steps:
|
||||
- name: "Install dependencies"
|
||||
run: sudo apt-get install libncurses5-dev
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
with:
|
||||
rust-version: ${{ matrix.rust }}
|
||||
components: rustfmt,clippy
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: "Get cargo bin directory"
|
||||
id: cargo-bin-dir
|
||||
run: echo "::set-output name=dir::$HOME/.cargo/bin"
|
||||
- name: "Cache cargo make"
|
||||
id: cache-cargo-make
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
profile: default
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
path: ${{ steps.cargo-bin-dir.outputs.dir }}/cargo-make
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
run: cargo make fmt
|
||||
- name: "Check"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --examples
|
||||
- name: "Check (crossterm)"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --no-default-features --features=crossterm --example crossterm_demo
|
||||
- name: "Check (rustbox)"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --no-default-features --features=rustbox --example rustbox_demo
|
||||
- name: "Check (curses)"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --no-default-features --features=curses --example curses_demo
|
||||
run: cargo make check
|
||||
- name: "Test"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
run: cargo make test
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
- name: "Clippy"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-targets --all-features -- -D warnings
|
||||
run: cargo make clippy
|
||||
windows:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
@@ -62,20 +56,30 @@ jobs:
|
||||
rust: ["1.44.0", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
with:
|
||||
profile: default
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
- name: "Check (crossterm)"
|
||||
uses: actions-rs/cargo@v1
|
||||
rust-version: ${{ matrix.rust }}
|
||||
components: rustfmt,clippy
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Get cargo bin directory"
|
||||
id: cargo-bin-dir
|
||||
run: echo "::set-output name=dir::$HOME\.cargo\bin"
|
||||
- name: "Cache cargo make"
|
||||
id: cache-cargo-make
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
command: check
|
||||
args: --no-default-features --features=crossterm --example crossterm_demo
|
||||
- name: "Test (crossterm)"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --no-default-features --features=crossterm --tests --examples
|
||||
path: ${{ steps.cargo-bin-dir.outputs.dir }}\cargo-make.exe
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format"
|
||||
run: cargo make fmt
|
||||
- name: "Check"
|
||||
run: cargo make check
|
||||
- name: "Test"
|
||||
run: cargo make test
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
- name: "Clippy"
|
||||
run: cargo make clippy
|
||||
|
||||
98
CHANGELOG.md
98
CHANGELOG.md
@@ -2,9 +2,105 @@
|
||||
|
||||
## To be released
|
||||
|
||||
## v0.14.0 - 2021-01-01
|
||||
|
||||
### Breaking changes
|
||||
|
||||
#### New API for the Table widget
|
||||
|
||||
The `Table` widget got a lot of improvements that should make it easier to work with:
|
||||
* It should not longer panic when rendered on small areas.
|
||||
* `Row`s are now a collection of `Cell`s, themselves wrapping a `Text`. This means you can style
|
||||
the entire `Table`, an entire `Row`, an entire `Cell` and rely on the styling capabilities of
|
||||
`Text` to get full control over the look of your `Table`.
|
||||
* `Row`s can have multiple lines.
|
||||
* The header is now optional and is just another `Row` always visible at the top.
|
||||
* `Row`s can have a bottom margin.
|
||||
* The header alignment is no longer off when an item is selected.
|
||||
|
||||
Taking the example of the code in `examples/demo/ui.rs`, this is what you may have to change:
|
||||
```diff
|
||||
let failure_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
|
||||
- let header = ["Server", "Location", "Status"];
|
||||
let rows = app.servers.iter().map(|s| {
|
||||
let style = if s.status == "Up" {
|
||||
up_style
|
||||
} else {
|
||||
failure_style
|
||||
};
|
||||
- Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
|
||||
+ Row::new(vec![s.name, s.location, s.status]).style(style)
|
||||
});
|
||||
- let table = Table::new(header.iter(), rows)
|
||||
+ let table = Table::new(rows)
|
||||
+ .header(
|
||||
+ Row::new(vec!["Server", "Location", "Status"])
|
||||
+ .style(Style::default().fg(Color::Yellow))
|
||||
+ .bottom_margin(1),
|
||||
+ )
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
- .header_style(Style::default().fg(Color::Yellow))
|
||||
.widths(&[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
```
|
||||
Here, we had to:
|
||||
- Change the way we construct [`Row`](https://docs.rs/tui/*/tui/widgets/struct.Row.html) which is no
|
||||
longer an `enum` but a `struct`. It accepts anything that can be converted to an iterator of things
|
||||
that can be converted to a [`Cell`](https://docs.rs/tui/*/tui/widgets/struct.Cell.html)
|
||||
- The header is no longer a required parameter so we use
|
||||
[`Table::header`](https://docs.rs/tui/*/tui/widgets/struct.Table.html#method.header) to set it.
|
||||
`Table::header_style` has been removed since the style can be directly set using
|
||||
[`Row::style`](https://docs.rs/tui/*/tui/widgets/struct.Row.html#method.style). In addition, we want
|
||||
to preserve the old margin between the header and the rest of the rows so we add a bottom margin to
|
||||
the header using
|
||||
[`Row::bottom_margin`](https://docs.rs/tui/*/tui/widgets/struct.Row.html#method.bottom_margin).
|
||||
|
||||
You may want to look at the documentation of the different types to get a better understanding:
|
||||
- [`Table`](https://docs.rs/tui/*/tui/widgets/struct.Table.html)
|
||||
- [`Row`](https://docs.rs/tui/*/tui/widgets/struct.Row.html)
|
||||
- [`Cell`](https://docs.rs/tui/*/tui/widgets/struct.Cell.html)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix handling of Non Breaking Space (NBSP) in wrapped text in `Paragraph` widget.
|
||||
|
||||
### Features
|
||||
|
||||
- Add `Style::reset` to create a `Style` resetting all styling properties when applied.
|
||||
- Add an option to render the `Gauge` widget with unicode blocks.
|
||||
- Manage common project tasks with `cargo-make` rather than `make` for easier on-boarding.
|
||||
|
||||
## v0.13.0 - 2020-11-14
|
||||
|
||||
### Features
|
||||
|
||||
* Add `LineGauge` widget which is a more compact variant of the existing `Gauge`.
|
||||
* Bump `crossterm` to 0.18
|
||||
|
||||
### Fixes
|
||||
|
||||
* Take into account the borders of the `Table` widget when the widths of columns is controlled by
|
||||
`Percentage` and `Ratio` constraints.
|
||||
|
||||
## v0.12.0 - 2020-09-27
|
||||
|
||||
### Features
|
||||
|
||||
* Make it easier to work with string with multiple lines in `Text` (#361).
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix a style leak in `Graph` so components drawn on top of the plotted data (i.e legend and axis
|
||||
titles) are not affected by the style of the `Dataset`s (#388).
|
||||
* Make sure `BarChart` shows bars with the max height only when the plotted data is actually equal
|
||||
to the max (#383).
|
||||
|
||||
## v0.11.0 - 2020-09-20
|
||||
|
||||
### Features
|
||||
### Features
|
||||
|
||||
* Add the dot character as a new type of canvas marker (#350).
|
||||
* Support more style modifiers on Windows (#368).
|
||||
|
||||
26
CONTRIBUTING.md
Normal file
26
CONTRIBUTING.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Contributing
|
||||
|
||||
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
|
||||
|
||||
`tui` is an ordinary Rust project where common tasks are managed with [cargo-make]. It wraps common
|
||||
`cargo` commands with sane defaults depending on your platform of choice. Bulding the project should
|
||||
be as easy as running `cargo make`.
|
||||
|
||||
## Continous Integration
|
||||
|
||||
We use Github Actions for the CI where we perform the following checks:
|
||||
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
|
||||
- The tests (docs, lib, tests and examples) should pass.
|
||||
- The code should conform to the default format enforced by `rustfmt`.
|
||||
- The code should not contain common style issues `clippy`.
|
||||
|
||||
You can also check most of those things yourself locally using `cargo make ci` which will offer you
|
||||
a shorter feedback loop.
|
||||
|
||||
## Tests
|
||||
|
||||
The test coverage of the crate is far from being ideal but we already have a fair amount of tests in
|
||||
place. Beside the usal doc and unit tests, one of the most valuable test you can write for `tui` is
|
||||
a test again the `TestBackend` which allows you to assert the content of the output buffer that
|
||||
would have been flushed to the termminal after a given draw call (see `widgets_block_renders` in
|
||||
[tests/widgets_block.rs](./tests/widget_block.rs) for an example).
|
||||
@@ -1,15 +1,16 @@
|
||||
[package]
|
||||
name = "tui"
|
||||
version = "0.11.0"
|
||||
version = "0.14.0"
|
||||
authors = ["Florian Dehau <work@fdehau.com>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
"""
|
||||
documentation = "https://docs.rs/tui/0.10.0/tui/"
|
||||
documentation = "https://docs.rs/tui/0.14.0/tui/"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/fdehau/tui-rs"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["assets/*", ".github"]
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
autoexamples = true
|
||||
edition = "2018"
|
||||
|
||||
@@ -26,7 +27,7 @@ unicode-segmentation = "1.2"
|
||||
unicode-width = "0.1"
|
||||
termion = { version = "1.5", optional = true }
|
||||
rustbox = { version = "0.11", optional = true }
|
||||
crossterm = { version = "0.17", optional = true }
|
||||
crossterm = { version = "0.19", optional = true }
|
||||
easycurses = { version = "0.12.2", optional = true }
|
||||
pancurses = { version = "0.16.1", optional = true, features = ["win32a"] }
|
||||
serde = { version = "1", "optional" = true, features = ["derive"]}
|
||||
|
||||
116
Makefile
116
Makefile
@@ -1,116 +0,0 @@
|
||||
SHELL=/bin/bash
|
||||
|
||||
# ================================ Cargo ======================================
|
||||
|
||||
|
||||
RUST_CHANNEL ?= stable
|
||||
CARGO_FLAGS =
|
||||
RUSTUP_INSTALLED = $(shell command -v rustup 2> /dev/null)
|
||||
TEST_FILTER ?=
|
||||
|
||||
ifndef RUSTUP_INSTALLED
|
||||
CARGO = cargo
|
||||
else
|
||||
ifdef CI
|
||||
CARGO = cargo
|
||||
else
|
||||
CARGO = rustup run $(RUST_CHANNEL) cargo
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
# ================================ Help =======================================
|
||||
|
||||
.PHONY: help
|
||||
help: ## Print all the available commands
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
|
||||
# =============================== Build =======================================
|
||||
|
||||
.PHONY: check
|
||||
check: ## Validate the project code
|
||||
$(CARGO) check
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build the project in debug mode
|
||||
$(CARGO) build $(CARGO_FLAGS)
|
||||
|
||||
.PHONY: release
|
||||
release: CARGO_FLAGS += --release
|
||||
release: build ## Build the project in release mode
|
||||
|
||||
|
||||
# ================================ Lint =======================================
|
||||
|
||||
.PHONY: lint
|
||||
lint: fmt clippy ## Lint project files
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: ## Check the format of the source code
|
||||
cargo fmt --all -- --check
|
||||
|
||||
.PHONY: clippy
|
||||
clippy: ## Check the style of the source code and catch common errors
|
||||
$(CARGO) clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
|
||||
# ================================ Test =======================================
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run the tests
|
||||
$(CARGO) test --all-features $(TEST_FILTER)
|
||||
|
||||
# =============================== Examples ====================================
|
||||
|
||||
.PHONY: build-examples
|
||||
build-examples: ## Build all examples
|
||||
@$(CARGO) build --release --examples --all-features
|
||||
|
||||
.PHONY: run-examples
|
||||
run-examples: build-examples ## Run all examples
|
||||
@for file in examples/*.rs; do \
|
||||
name=$$(basename $${file/.rs/}); \
|
||||
$(CARGO) run --all-features --release --example $$name; \
|
||||
done;
|
||||
|
||||
# ================================ Doc ========================================
|
||||
|
||||
|
||||
.PHONY: doc
|
||||
doc: RUST_CHANNEL = nightly
|
||||
doc: ## Build the documentation (available at ./target/doc)
|
||||
$(CARGO) doc
|
||||
|
||||
|
||||
# ================================= Watch =====================================
|
||||
|
||||
# Requires watchman and watchman-make (https://facebook.github.io/watchman/docs/install.html)
|
||||
|
||||
.PHONY: watch
|
||||
watch: ## Watch file changes and build the project if any
|
||||
watchman-make -p 'src/**/*.rs' -t check build
|
||||
|
||||
.PHONY: watch-test
|
||||
watch-test: ## Watch files changes and run the tests if any
|
||||
watchman-make -p 'src/**/*.rs' 'tests/**/*.rs' 'examples/**/*.rs' -t test
|
||||
|
||||
.PHONY: watch-doc
|
||||
watch-doc: RUST_CHANNEL = nightly
|
||||
watch-doc: ## Watch file changes and rebuild the documentation if any
|
||||
$(CARGO) watch -x doc -x 'test --doc'
|
||||
|
||||
# ================================= Pipelines =================================
|
||||
|
||||
.PHONY: stable
|
||||
stable: RUST_CHANNEL = stable
|
||||
stable: build lint test ## Run build and tests for stable
|
||||
|
||||
.PHONY: beta
|
||||
beta: RUST_CHANNEL = beta
|
||||
beta: build lint test ## Run build and tests for beta
|
||||
|
||||
.PHONY: nightly
|
||||
nightly: RUST_CHANNEL = nightly
|
||||
nightly: build lint test ## Run build, lint and tests for nightly
|
||||
142
Makefile.toml
Normal file
142
Makefile.toml
Normal file
@@ -0,0 +1,142 @@
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[env.TUI_FEATURES]
|
||||
source = "${CARGO_MAKE_RUST_TARGET_OS}"
|
||||
default_value = "unknown"
|
||||
|
||||
[env.TUI_FEATURES.mapping]
|
||||
linux = "serde,crossterm,termion,rustbox,curses"
|
||||
macos = "serde,crossterm,termion,rustbox,curses"
|
||||
windows = "serde,crossterm"
|
||||
|
||||
[tasks.default]
|
||||
dependencies = [
|
||||
"check",
|
||||
]
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = [
|
||||
"fmt",
|
||||
"check",
|
||||
"test",
|
||||
"clippy",
|
||||
]
|
||||
|
||||
|
||||
[tasks.fmt]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"fmt",
|
||||
"--all",
|
||||
"--",
|
||||
"--check",
|
||||
]
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"check",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"build",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
]
|
||||
|
||||
[tasks.clippy]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"clippy",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
linux_alias = "test-unix"
|
||||
mac_alias = "test-unix"
|
||||
windows_alias = "test-windows"
|
||||
|
||||
[tasks.test-unix]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}"
|
||||
]
|
||||
|
||||
# Documentation tests cannot be run on Windows for now
|
||||
[tasks.test-windows]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--lib",
|
||||
"--tests",
|
||||
"--examples",
|
||||
]
|
||||
|
||||
[tasks.run-example]
|
||||
private = true
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME", "TUI_FEATURES"] }
|
||||
command = "cargo"
|
||||
args = [
|
||||
"run",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--release",
|
||||
"--example",
|
||||
"${TUI_EXAMPLE_NAME}"
|
||||
]
|
||||
|
||||
[tasks.run-example-windows]
|
||||
private = true
|
||||
condition = { env = {"TUI_EXAMPLE_NAME" = "crossterm_demo"} }
|
||||
run_task = "run-example"
|
||||
|
||||
[tasks.run-example-router]
|
||||
private = true
|
||||
run_task = [
|
||||
{ name = "run-example-windows", condition = { platforms = ["window"] } },
|
||||
{ name = "run-example" }
|
||||
]
|
||||
|
||||
[tasks.build-examples]
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
command = "cargo"
|
||||
args = [
|
||||
"build",
|
||||
"--examples",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--release"
|
||||
]
|
||||
|
||||
[tasks.run-examples]
|
||||
dependencies = ["build-examples"]
|
||||
script = '''
|
||||
#!@duckscript
|
||||
files = glob_array ./examples/*.rs
|
||||
for file in ${files}
|
||||
name = basename ${file}
|
||||
name = substring ${name} -3
|
||||
set_env TUI_EXAMPLE_NAME ${name}
|
||||
cm_run_task run-example-router
|
||||
end
|
||||
'''
|
||||
@@ -105,6 +105,11 @@ You can run all examples by running `make run-examples`.
|
||||
* [desed](https://github.com/SoptikHa2/desed)
|
||||
* [diskonaut](https://github.com/imsnif/diskonaut)
|
||||
* [tickrs](https://github.com/tarkah/tickrs)
|
||||
* [rusty-krab-manager](https://github.com/aryakaul/rusty-krab-manager)
|
||||
* [termchat](https://github.com/lemunozm/termchat)
|
||||
* [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui)
|
||||
* [gping](https://github.com/orf/gping/)
|
||||
* [Vector](https://vector.dev)
|
||||
|
||||
### Alternatives
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use crossterm::{
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{stdout, Write},
|
||||
io::stdout,
|
||||
sync::mpsc,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
|
||||
@@ -129,7 +129,7 @@ impl<'a> App<'a> {
|
||||
App {
|
||||
title,
|
||||
should_quit: false,
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1"]),
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2"]),
|
||||
show_chart: true,
|
||||
progress: 0.0,
|
||||
sparkline: Signal {
|
||||
|
||||
@@ -7,8 +7,8 @@ use tui::{
|
||||
text::{Span, Spans},
|
||||
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
|
||||
widgets::{
|
||||
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, ListItem, Paragraph, Row,
|
||||
Sparkline, Table, Tabs, Wrap,
|
||||
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
|
||||
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
|
||||
},
|
||||
Frame,
|
||||
};
|
||||
@@ -31,6 +31,7 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
match app.tabs.index {
|
||||
0 => draw_first_tab(f, app, chunks[1]),
|
||||
1 => draw_second_tab(f, app, chunks[1]),
|
||||
2 => draw_third_tab(f, app, chunks[1]),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
@@ -42,8 +43,8 @@ where
|
||||
let chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(7),
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(7),
|
||||
]
|
||||
.as_ref(),
|
||||
@@ -59,7 +60,14 @@ where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(2), Constraint::Length(3)].as_ref())
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.margin(1)
|
||||
.split(area);
|
||||
let block = Block::default().borders(Borders::ALL).title("Graphs");
|
||||
@@ -88,6 +96,17 @@ where
|
||||
symbols::bar::THREE_LEVELS
|
||||
});
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
|
||||
let line_gauge = LineGauge::default()
|
||||
.block(Block::default().title("LineGauge:"))
|
||||
.gauge_style(Style::default().fg(Color::Magenta))
|
||||
.line_set(if app.enhanced_graphics {
|
||||
symbols::line::THICK
|
||||
} else {
|
||||
symbols::line::NORMAL
|
||||
})
|
||||
.ratio(app.progress);
|
||||
f.render_widget(line_gauge, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_charts<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
|
||||
@@ -290,18 +309,21 @@ where
|
||||
let failure_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
|
||||
let header = ["Server", "Location", "Status"];
|
||||
let rows = app.servers.iter().map(|s| {
|
||||
let style = if s.status == "Up" {
|
||||
up_style
|
||||
} else {
|
||||
failure_style
|
||||
};
|
||||
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
|
||||
Row::new(vec![s.name, s.location, s.status]).style(style)
|
||||
});
|
||||
let table = Table::new(header.iter(), rows)
|
||||
let table = Table::new(rows)
|
||||
.header(
|
||||
Row::new(vec!["Server", "Location", "Status"])
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
.header_style(Style::default().fg(Color::Yellow))
|
||||
.widths(&[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
@@ -353,3 +375,51 @@ where
|
||||
.y_bounds([-90.0, 90.0]);
|
||||
f.render_widget(map, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_third_tab<B>(f: &mut Frame<B>, _app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
|
||||
.split(area);
|
||||
let colors = [
|
||||
Color::Reset,
|
||||
Color::Black,
|
||||
Color::Red,
|
||||
Color::Green,
|
||||
Color::Yellow,
|
||||
Color::Blue,
|
||||
Color::Magenta,
|
||||
Color::Cyan,
|
||||
Color::Gray,
|
||||
Color::DarkGray,
|
||||
Color::LightRed,
|
||||
Color::LightGreen,
|
||||
Color::LightYellow,
|
||||
Color::LightBlue,
|
||||
Color::LightMagenta,
|
||||
Color::LightCyan,
|
||||
Color::White,
|
||||
];
|
||||
let items: Vec<Row> = colors
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let cells = vec![
|
||||
Cell::from(Span::raw(format!("{:?}: ", c))),
|
||||
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
|
||||
Cell::from(Span::styled("Background", Style::default().bg(*c))),
|
||||
];
|
||||
Row::new(cells)
|
||||
})
|
||||
.collect();
|
||||
let table = Table::new(items)
|
||||
.block(Block::default().title("Colors").borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]);
|
||||
f.render_widget(table, chunks[0]);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)
|
||||
.percent(app.progress4)
|
||||
.label(label);
|
||||
.label(label)
|
||||
.use_unicode(true);
|
||||
f.render_widget(gauge, chunks[3]);
|
||||
})?;
|
||||
|
||||
|
||||
@@ -16,6 +16,12 @@ use tui::{
|
||||
Terminal,
|
||||
};
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is a wrapper
|
||||
/// around `ListState`. Keeping track of the items state let us render the associated widget with its state
|
||||
/// and have access to features such as natural scrolling.
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events.
|
||||
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
struct App<'a> {
|
||||
items: StatefulList<(&'a str, usize)>,
|
||||
events: Vec<(&'a str, &'a str)>,
|
||||
@@ -82,9 +88,11 @@ impl<'a> App<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate through the event list.
|
||||
/// This only exists to simulate some kind of "progress"
|
||||
fn advance(&mut self) {
|
||||
let event = self.events.pop().unwrap();
|
||||
self.events.insert(0, event);
|
||||
let event = self.events.remove(0);
|
||||
self.events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,16 +106,18 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
let events = Events::new();
|
||||
|
||||
// App
|
||||
// Create a new app with some exapmle state
|
||||
let mut app = App::new();
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
// Create two chunks with equal horizontal screen space
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Iterate through all elements in the `items` app and append some debug text to it.
|
||||
let items: Vec<ListItem> = app
|
||||
.items
|
||||
.items
|
||||
@@ -123,6 +133,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one
|
||||
let items = List::new(items)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.highlight_style(
|
||||
@@ -131,12 +143,18 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
// We can now render the item list
|
||||
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
|
||||
|
||||
// Let's do the same for the events.
|
||||
// The event list doesn't have any state and only displays the current state of the list.
|
||||
let events: Vec<ListItem> = app
|
||||
.events
|
||||
.iter()
|
||||
.map(|&(evt, level)| {
|
||||
.rev()
|
||||
.map(|&(event, level)| {
|
||||
// Colorcode the level depending on its type
|
||||
let s = match level {
|
||||
"CRITICAL" => Style::default().fg(Color::Red),
|
||||
"ERROR" => Style::default().fg(Color::Magenta),
|
||||
@@ -144,6 +162,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
"INFO" => Style::default().fg(Color::Blue),
|
||||
_ => Style::default(),
|
||||
};
|
||||
// Add a example datetime and apply proper spacing between them
|
||||
let header = Spans::from(vec![
|
||||
Span::styled(format!("{:<9}", level), s),
|
||||
Span::raw(" "),
|
||||
@@ -152,7 +171,14 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
),
|
||||
]);
|
||||
let log = Spans::from(vec![Span::raw(evt)]);
|
||||
// The event gets it's own line
|
||||
let log = Spans::from(vec![Span::raw(event)]);
|
||||
|
||||
// Here several things happen:
|
||||
// 1. Add a `---` spacing line above the final list entry
|
||||
// 2. Add the Level + datetime
|
||||
// 3. Add a spacer line
|
||||
// 4. Add the actual event
|
||||
ListItem::new(vec![
|
||||
Spans::from("-".repeat(chunks[1].width as usize)),
|
||||
header,
|
||||
@@ -167,6 +193,10 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
f.render_widget(events_list, chunks[1]);
|
||||
})?;
|
||||
|
||||
// This is a simple example on how to handle events
|
||||
// 1. This breaks the loop and exits the program on `q` button press.
|
||||
// 2. The `up`/`down` keys change the currently selected item in the App's `items` list.
|
||||
// 3. `left` unselects the current item.
|
||||
match events.next()? {
|
||||
Event::Input(input) => match input {
|
||||
Key::Char('q') => {
|
||||
|
||||
@@ -8,7 +8,7 @@ use tui::{
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Row, Table, TableState},
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ impl<'a> StatefulTable<'a> {
|
||||
vec!["Row31", "Row32", "Row33"],
|
||||
vec!["Row41", "Row42", "Row43"],
|
||||
vec!["Row51", "Row52", "Row53"],
|
||||
vec!["Row61", "Row62", "Row63"],
|
||||
vec!["Row61", "Row62\nTest", "Row63"],
|
||||
vec!["Row71", "Row72", "Row73"],
|
||||
vec!["Row81", "Row82", "Row83"],
|
||||
vec!["Row91", "Row92", "Row93"],
|
||||
@@ -93,16 +93,27 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
.margin(5)
|
||||
.split(f.size());
|
||||
|
||||
let selected_style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let normal_style = Style::default().fg(Color::White);
|
||||
let header = ["Header1", "Header2", "Header3"];
|
||||
let rows = table
|
||||
.items
|
||||
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||
let normal_style = Style::default().bg(Color::Blue);
|
||||
let header_cells = ["Header1", "Header2", "Header3"]
|
||||
.iter()
|
||||
.map(|i| Row::StyledData(i.iter(), normal_style));
|
||||
let t = Table::new(header.iter(), rows)
|
||||
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
|
||||
let header = Row::new(header_cells)
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.bottom_margin(1);
|
||||
let rows = table.items.iter().map(|item| {
|
||||
let height = item
|
||||
.iter()
|
||||
.map(|content| content.chars().filter(|c| *c == '\n').count())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 1;
|
||||
let cells = item.iter().map(|c| Cell::from(*c));
|
||||
Row::new(cells).height(height as u16).bottom_margin(1)
|
||||
});
|
||||
let t = Table::new(rows)
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ")
|
||||
|
||||
@@ -40,7 +40,7 @@ fn buffer_view(buffer: &Buffer) -> String {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
view.push_str("\n");
|
||||
view.push('\n');
|
||||
}
|
||||
view
|
||||
}
|
||||
@@ -60,6 +60,12 @@ impl TestBackend {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u16, height: u16) {
|
||||
self.buffer.resize(Rect::new(0, 0, width, height));
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
pub fn assert_buffer(&self, expected: &Buffer) {
|
||||
assert_eq!(expected.area, self.buffer.area);
|
||||
let diff = expected.diff(&self.buffer);
|
||||
@@ -68,20 +74,20 @@ impl TestBackend {
|
||||
}
|
||||
|
||||
let mut debug_info = String::from("Buffers are not equal");
|
||||
debug_info.push_str("\n");
|
||||
debug_info.push('\n');
|
||||
debug_info.push_str("Expected:");
|
||||
debug_info.push_str("\n");
|
||||
debug_info.push('\n');
|
||||
let expected_view = buffer_view(expected);
|
||||
debug_info.push_str(&expected_view);
|
||||
debug_info.push_str("\n");
|
||||
debug_info.push('\n');
|
||||
debug_info.push_str("Got:");
|
||||
debug_info.push_str("\n");
|
||||
debug_info.push('\n');
|
||||
let view = buffer_view(&self.buffer);
|
||||
debug_info.push_str(&view);
|
||||
debug_info.push_str("\n");
|
||||
debug_info.push('\n');
|
||||
|
||||
debug_info.push_str("Diff:");
|
||||
debug_info.push_str("\n");
|
||||
debug_info.push('\n');
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
|
||||
@@ -51,6 +51,13 @@ impl Cell {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(&self) -> Style {
|
||||
Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.add_modifier(self.modifier)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol.clear();
|
||||
self.symbol.push(' ');
|
||||
|
||||
@@ -401,7 +401,7 @@ impl Rect {
|
||||
}
|
||||
|
||||
pub fn right(self) -> u16 {
|
||||
self.x + self.width
|
||||
self.x.saturating_add(self.width)
|
||||
}
|
||||
|
||||
pub fn top(self) -> u16 {
|
||||
@@ -409,7 +409,7 @@ impl Rect {
|
||||
}
|
||||
|
||||
pub fn bottom(self) -> u16 {
|
||||
self.y + self.height
|
||||
self.y.saturating_add(self.height)
|
||||
}
|
||||
|
||||
pub fn inner(self, margin: &Margin) -> Rect {
|
||||
|
||||
12
src/lib.rs
12
src/lib.rs
@@ -9,7 +9,7 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! tui = "0.11"
|
||||
//! tui = "0.14"
|
||||
//! termion = "1.5"
|
||||
//! ```
|
||||
//!
|
||||
@@ -19,8 +19,8 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! crossterm = "0.17"
|
||||
//! tui = { version = "0.11", default-features = false, features = ['crossterm'] }
|
||||
//! crossterm = "0.19"
|
||||
//! tui = { version = "0.14", default-features = false, features = ['crossterm'] }
|
||||
//! ```
|
||||
//!
|
||||
//! The same logic applies for all other available backends.
|
||||
@@ -94,7 +94,8 @@
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, size);
|
||||
//! })
|
||||
//! })?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -136,7 +137,8 @@
|
||||
//! .title("Block 2")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[1]);
|
||||
//! })
|
||||
//! })?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
|
||||
66
src/style.rs
66
src/style.rs
@@ -54,8 +54,6 @@ bitflags! {
|
||||
|
||||
/// Style let you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// Style::default()
|
||||
@@ -63,6 +61,60 @@ bitflags! {
|
||||
/// .bg(Color::Green)
|
||||
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
/// ```
|
||||
///
|
||||
/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
|
||||
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default().bg(Color::Red),
|
||||
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Red),
|
||||
/// add_modifier: Modifier::BOLD,
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// The default implementation returns a `Style` that does not modify anything. If you wish to
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::reset().fg(Color::Yellow),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Reset),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
@@ -84,6 +136,16 @@ impl Default for Style {
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Returns a `Style` resetting all properties.
|
||||
pub fn reset() -> Style {
|
||||
Style {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::all(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the foreground color.
|
||||
///
|
||||
/// ## Examples
|
||||
|
||||
@@ -31,7 +31,7 @@ impl Viewport {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// Options to pass to [`Terminal::draw_with_options`]
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
pub struct TerminalOptions {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
@@ -148,6 +148,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// CompletedFrame represents the state of the terminal after all changes performed in the last
|
||||
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
|
||||
/// [`Terminal::draw`].
|
||||
pub struct CompletedFrame<'a> {
|
||||
pub buffer: &'a Buffer,
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
@@ -247,7 +255,7 @@ where
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
|
||||
where
|
||||
F: FnOnce(&mut Frame<B>),
|
||||
{
|
||||
@@ -279,7 +287,10 @@ where
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
Ok(())
|
||||
Ok(CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.viewport.area,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
|
||||
103
src/text.rs
103
src/text.rs
@@ -257,6 +257,28 @@ impl<'a> From<Spans<'a>> for String {
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
///
|
||||
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
|
||||
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
|
||||
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
/// let mut text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
///
|
||||
/// // Adding two more unstyled lines
|
||||
/// text.extend(Text::raw("These are two\nmore lines!"));
|
||||
/// assert_eq!(4, text.height());
|
||||
///
|
||||
/// // Adding a final two styled lines
|
||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
/// assert_eq!(6, text.height());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Text<'a> {
|
||||
pub lines: Vec<Spans<'a>>,
|
||||
@@ -269,6 +291,47 @@ impl<'a> Default for Text<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
/// Create some text (potentially multiple lines) with no style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// Text::raw("The first line\nThe second line");
|
||||
/// Text::raw(String::from("The first line\nThe second line"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Text {
|
||||
lines: match content.into() {
|
||||
Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
|
||||
Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let mut text = Text::raw(content);
|
||||
text.patch_style(style);
|
||||
text
|
||||
}
|
||||
|
||||
/// Returns the max width of all the lines.
|
||||
///
|
||||
/// ## Examples
|
||||
@@ -299,6 +362,21 @@ impl<'a> Text<'a> {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
/// Apply a new style to existing text.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_text = Text::raw("The first line\nThe second line");
|
||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// assert_ne!(raw_text, styled_text);
|
||||
///
|
||||
/// raw_text.patch_style(style);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for line in &mut self.lines {
|
||||
for span in &mut line.0 {
|
||||
@@ -308,11 +386,15 @@ impl<'a> Text<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Text<'a> {
|
||||
fn from(s: String) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Text<'a> {
|
||||
fn from(s: &'a str) -> Text<'a> {
|
||||
Text {
|
||||
lines: s.lines().map(Spans::from).collect(),
|
||||
}
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,3 +417,18 @@ impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
|
||||
Text { lines }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Text<'a> {
|
||||
type Item = Spans<'a>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.lines.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extend<Spans<'a>> for Text<'a> {
|
||||
fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
|
||||
self.lines.extend(iter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ impl<'a> Widget for BarChart<'a> {
|
||||
.map(|&(l, v)| {
|
||||
(
|
||||
l,
|
||||
v * u64::from(chart_area.height) * 8 / std::cmp::max(max, 1),
|
||||
v * u64::from(chart_area.height - 1) * 8 / std::cmp::max(max, 1),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(&str, u64)>>();
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
widgets::{Borders, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum BorderType {
|
||||
Plain,
|
||||
Rounded,
|
||||
@@ -41,7 +41,7 @@ impl BorderType {
|
||||
/// .border_type(BorderType::Rounded)
|
||||
/// .style(Style::default().bg(Color::Black));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Block<'a> {
|
||||
/// Optional title place on the upper left of the block
|
||||
title: Option<Spans<'a>>,
|
||||
@@ -111,23 +111,20 @@ impl<'a> Block<'a> {
|
||||
|
||||
/// Compute the inner area of a block based on its border visibility rules.
|
||||
pub fn inner(&self, area: Rect) -> Rect {
|
||||
if area.width < 2 || area.height < 2 {
|
||||
return Rect::default();
|
||||
}
|
||||
let mut inner = area;
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
inner.x += 1;
|
||||
inner.width -= 1;
|
||||
inner.x = inner.x.saturating_add(1).min(inner.right());
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::TOP) || self.title.is_some() {
|
||||
inner.y += 1;
|
||||
inner.height -= 1;
|
||||
inner.y = inner.y.saturating_add(1).min(inner.bottom());
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::RIGHT) {
|
||||
inner.width -= 1;
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::BOTTOM) {
|
||||
inner.height -= 1;
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
}
|
||||
inner
|
||||
}
|
||||
@@ -135,13 +132,12 @@ impl<'a> Block<'a> {
|
||||
|
||||
impl<'a> Widget for Block<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
if area.width < 2 || area.height < 2 {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
buf.set_style(area, self.style);
|
||||
let symbols = BorderType::line_symbols(self.border_type);
|
||||
|
||||
// Sides
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
for y in area.top()..area.bottom() {
|
||||
@@ -175,9 +171,9 @@ impl<'a> Widget for Block<'a> {
|
||||
}
|
||||
|
||||
// Corners
|
||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
buf.get_mut(area.left(), area.top())
|
||||
.set_symbol(symbols.top_left)
|
||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
.set_symbol(symbols.bottom_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
||||
@@ -190,9 +186,9 @@ impl<'a> Widget for Block<'a> {
|
||||
.set_symbol(symbols.bottom_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
.set_symbol(symbols.bottom_right)
|
||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
buf.get_mut(area.left(), area.top())
|
||||
.set_symbol(symbols.top_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
|
||||
@@ -207,8 +203,309 @@ impl<'a> Widget for Block<'a> {
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let width = area.width - lx - rx;
|
||||
let width = area.width.saturating_sub(lx).saturating_sub(rx);
|
||||
buf.set_spans(area.left() + lx, area.top(), &title, width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::layout::Rect;
|
||||
|
||||
#[test]
|
||||
fn inner_takes_into_account_the_borders() {
|
||||
// No borders
|
||||
assert_eq!(
|
||||
Block::default().inner(Rect::default()),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
"no borders, width=0, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"no borders, width=1, height=1"
|
||||
);
|
||||
|
||||
// Left border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"left, width=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"left, width=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"left, width=2"
|
||||
);
|
||||
|
||||
// Top border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"top, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"top, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 2
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"top, height=2"
|
||||
);
|
||||
|
||||
// Right border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"right, width=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"right, width=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"right, width=2"
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"bottom, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"bottom, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 2
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"bottom, height=2"
|
||||
);
|
||||
|
||||
// All borders
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.inner(Rect::default()),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
"all borders, width=0, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
"all borders, width=1, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
"all borders, width=2, height=2"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 3,
|
||||
height: 3,
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
"all borders, width=3, height=3"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_takes_into_account_the_title() {
|
||||
assert_eq!(
|
||||
Block::default().title("Test").inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ impl<'a> Chart<'a> {
|
||||
if let Some(ref title) = self.y_axis.title {
|
||||
let w = title.width() as u16;
|
||||
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||
layout.title_y = Some((x + 1, area.top()));
|
||||
layout.title_y = Some((x, area.top()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +366,15 @@ impl<'a> Chart<'a> {
|
||||
|
||||
impl<'a> Widget for Chart<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
// Sample the style of the entire widget. This sample will be used to reset the style of
|
||||
// the cells that are part of the components put on top of the grah area (i.e legend and
|
||||
// axis names).
|
||||
let original_style = buf.get(area.left(), area.top()).style();
|
||||
|
||||
let chart_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
@@ -382,16 +390,6 @@ impl<'a> Widget for Chart<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_x {
|
||||
let title = self.x_axis.title.unwrap();
|
||||
buf.set_spans(x, y, &title, graph_area.right().saturating_sub(x));
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_y {
|
||||
let title = self.y_axis.title.unwrap();
|
||||
buf.set_spans(x, y, &title, graph_area.right().saturating_sub(x));
|
||||
}
|
||||
|
||||
if let Some(y) = layout.label_x {
|
||||
let labels = self.x_axis.labels.unwrap();
|
||||
let total_width = labels.iter().map(Span::width).sum::<usize>() as u16;
|
||||
@@ -471,6 +469,7 @@ impl<'a> Widget for Chart<'a> {
|
||||
}
|
||||
|
||||
if let Some(legend_area) = layout.legend_area {
|
||||
buf.set_style(legend_area, original_style);
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.render(legend_area, buf);
|
||||
@@ -483,6 +482,36 @@ impl<'a> Widget for Chart<'a> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_x {
|
||||
let title = self.x_axis.title.unwrap();
|
||||
let width = graph_area.right().saturating_sub(x);
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height: 1,
|
||||
},
|
||||
original_style,
|
||||
);
|
||||
buf.set_spans(x, y, &title, width);
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_y {
|
||||
let title = self.y_axis.title.unwrap();
|
||||
let width = graph_area.right().saturating_sub(x);
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height: 1,
|
||||
},
|
||||
original_style,
|
||||
);
|
||||
buf.set_spans(x, y, &title, width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
@@ -23,6 +24,7 @@ pub struct Gauge<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
ratio: f64,
|
||||
label: Option<Span<'a>>,
|
||||
use_unicode: bool,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
}
|
||||
@@ -33,6 +35,7 @@ impl<'a> Default for Gauge<'a> {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
use_unicode: false,
|
||||
style: Style::default(),
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
@@ -81,6 +84,11 @@ impl<'a> Gauge<'a> {
|
||||
self.gauge_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn use_unicode(mut self, unicode: bool) -> Gauge<'a> {
|
||||
self.use_unicode = unicode;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Gauge<'a> {
|
||||
@@ -100,8 +108,14 @@ impl<'a> Widget for Gauge<'a> {
|
||||
}
|
||||
|
||||
let center = gauge_area.height / 2 + gauge_area.top();
|
||||
let width = (f64::from(gauge_area.width) * self.ratio).round() as u16;
|
||||
let end = gauge_area.left() + width;
|
||||
let width = f64::from(gauge_area.width) * self.ratio;
|
||||
//go to regular rounding behavior if we're not using unicode blocks
|
||||
let end = gauge_area.left()
|
||||
+ if self.use_unicode {
|
||||
width.floor() as u16
|
||||
} else {
|
||||
width.round() as u16
|
||||
};
|
||||
// Label
|
||||
let ratio = self.ratio;
|
||||
let label = self
|
||||
@@ -113,14 +127,25 @@ impl<'a> Widget for Gauge<'a> {
|
||||
buf.get_mut(x, y).set_symbol(" ");
|
||||
}
|
||||
|
||||
//set unicode block
|
||||
if self.use_unicode && self.ratio < 1.0 {
|
||||
buf.get_mut(end, y)
|
||||
.set_symbol(get_unicode_block(width % 1.0));
|
||||
}
|
||||
|
||||
let mut color_end = end;
|
||||
|
||||
if y == center {
|
||||
let label_width = label.width() as u16;
|
||||
let middle = (gauge_area.width - label_width) / 2 + gauge_area.left();
|
||||
buf.set_span(middle, y, &label, gauge_area.right() - middle);
|
||||
if self.use_unicode && end >= middle && end < middle + label_width {
|
||||
color_end = gauge_area.left() + (width.round() as u16); //set color on the label to the rounded gauge level
|
||||
}
|
||||
}
|
||||
|
||||
// Fix colors
|
||||
for x in gauge_area.left()..end {
|
||||
for x in gauge_area.left()..color_end {
|
||||
buf.get_mut(x, y)
|
||||
.set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
|
||||
.set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
|
||||
@@ -129,6 +154,152 @@ impl<'a> Widget for Gauge<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_unicode_block<'a>(frac: f64) -> &'a str {
|
||||
match (frac * 8.0).round() as u16 {
|
||||
//get how many eighths the fraction is closest to
|
||||
1 => symbols::block::ONE_EIGHTH,
|
||||
2 => symbols::block::ONE_QUARTER,
|
||||
3 => symbols::block::THREE_EIGHTHS,
|
||||
4 => symbols::block::HALF,
|
||||
5 => symbols::block::FIVE_EIGHTHS,
|
||||
6 => symbols::block::THREE_QUARTERS,
|
||||
7 => symbols::block::SEVEN_EIGHTHS,
|
||||
8 => symbols::block::FULL,
|
||||
_ => " ",
|
||||
}
|
||||
}
|
||||
|
||||
/// A compact widget to display a task progress over a single line.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Widget, LineGauge, Block, Borders};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # use tui::symbols;
|
||||
/// LineGauge::default()
|
||||
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
|
||||
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::BOLD))
|
||||
/// .line_set(symbols::line::THICK)
|
||||
/// .ratio(0.4);
|
||||
/// ```
|
||||
pub struct LineGauge<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
ratio: f64,
|
||||
label: Option<Spans<'a>>,
|
||||
line_set: symbols::line::Set,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Default for LineGauge<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
style: Style::default(),
|
||||
line_set: symbols::line::NORMAL,
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LineGauge<'a> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ratio(mut self, ratio: f64) -> Self {
|
||||
assert!(
|
||||
ratio <= 1.0 && ratio >= 0.0,
|
||||
"Ratio should be between 0 and 1 inclusively."
|
||||
);
|
||||
self.ratio = ratio;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn line_set(mut self, set: symbols::line::Set) -> Self {
|
||||
self.line_set = set;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label<T>(mut self, label: T) -> Self
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
{
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gauge_style(mut self, style: Style) -> Self {
|
||||
self.gauge_style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for LineGauge<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
let gauge_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if gauge_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ratio = self.ratio;
|
||||
let label = self
|
||||
.label
|
||||
.unwrap_or_else(move || Spans::from(format!("{:.0}%", ratio * 100.0)));
|
||||
let (col, row) = buf.set_spans(
|
||||
gauge_area.left(),
|
||||
gauge_area.top(),
|
||||
&label,
|
||||
gauge_area.width,
|
||||
);
|
||||
let start = col + 1;
|
||||
if start >= gauge_area.right() {
|
||||
return;
|
||||
}
|
||||
|
||||
let end = start
|
||||
+ (f64::from(gauge_area.right().saturating_sub(start)) * self.ratio).floor() as u16;
|
||||
for col in start..end {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.fg,
|
||||
bg: None,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
}
|
||||
for col in end..gauge_area.right() {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.bg,
|
||||
bg: None,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -36,7 +36,7 @@ impl ListState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ListItem<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
|
||||
@@ -23,7 +23,7 @@ mod clear;
|
||||
mod gauge;
|
||||
mod list;
|
||||
mod paragraph;
|
||||
mod reflow;
|
||||
pub mod reflow;
|
||||
mod sparkline;
|
||||
mod table;
|
||||
mod tabs;
|
||||
@@ -32,11 +32,11 @@ pub use self::barchart::BarChart;
|
||||
pub use self::block::{Block, BorderType};
|
||||
pub use self::chart::{Axis, Chart, Dataset, GraphType};
|
||||
pub use self::clear::Clear;
|
||||
pub use self::gauge::Gauge;
|
||||
pub use self::gauge::{Gauge, LineGauge};
|
||||
pub use self::list::{List, ListItem, ListState};
|
||||
pub use self::paragraph::{Paragraph, Wrap};
|
||||
pub use self::sparkline::Sparkline;
|
||||
pub use self::table::{Row, Table, TableState};
|
||||
pub use self::table::{Cell, Row, Table, TableState};
|
||||
pub use self::tabs::Tabs;
|
||||
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
@@ -57,7 +57,7 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
||||
let mut symbols_exhausted = true;
|
||||
for StyledGrapheme { symbol, style } in &mut self.symbols {
|
||||
symbols_exhausted = false;
|
||||
let symbol_whitespace = symbol.chars().all(&char::is_whitespace);
|
||||
let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
|
||||
|
||||
// Ignore characters wider that the total max width.
|
||||
if symbol.width() as u16 > self.max_line_width
|
||||
@@ -77,7 +77,7 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
||||
}
|
||||
|
||||
// Mark the previous symbol as word end.
|
||||
if symbol_whitespace && !prev_whitespace && symbol != NBSP {
|
||||
if symbol_whitespace && !prev_whitespace {
|
||||
symbols_to_last_word_end = self.current_line.len();
|
||||
width_to_last_word_end = current_line_width;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
style::Style,
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
};
|
||||
use cassowary::{
|
||||
@@ -11,11 +12,360 @@ use cassowary::{
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
iter::{self, Iterator},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
|
||||
///
|
||||
/// It can be created from anything that can be converted to a [`Text`].
|
||||
/// ```rust
|
||||
/// # use tui::widgets::Cell;
|
||||
/// # use tui::style::{Style, Modifier};
|
||||
/// # use tui::text::{Span, Spans, Text};
|
||||
/// Cell::from("simple string");
|
||||
///
|
||||
/// Cell::from(Span::from("span"));
|
||||
///
|
||||
/// Cell::from(Spans::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
|
||||
/// ]));
|
||||
///
|
||||
/// Cell::from(Text::from("a text"));
|
||||
/// ```
|
||||
///
|
||||
/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
|
||||
/// capabilities of [`Text`].
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Cell<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Cell<'a> {
|
||||
/// Set the `Style` of this cell.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Cell<'a>
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
fn from(content: T) -> Cell<'a> {
|
||||
Cell {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds data to be displayed in a [`Table`] widget.
|
||||
///
|
||||
/// A [`Row`] is a collection of cells. It can be created from simple strings:
|
||||
/// ```rust
|
||||
/// # use tui::widgets::Row;
|
||||
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
|
||||
/// ```
|
||||
///
|
||||
/// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s:
|
||||
/// ```rust
|
||||
/// # use tui::widgets::{Row, Cell};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Row<'a> {
|
||||
cells: Vec<Cell<'a>>,
|
||||
height: u16,
|
||||
style: Style,
|
||||
bottom_margin: u16,
|
||||
}
|
||||
|
||||
impl<'a> Row<'a> {
|
||||
/// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
|
||||
pub fn new<T>(cells: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<Cell<'a>>,
|
||||
{
|
||||
Self {
|
||||
height: 1,
|
||||
cells: cells.into_iter().map(|c| c.into()).collect(),
|
||||
style: Style::default(),
|
||||
bottom_margin: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
|
||||
/// height will see its content truncated.
|
||||
pub fn height(mut self, height: u16) -> Self {
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a
|
||||
/// any individual [`Cell`] or event by their [`Text`] content.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bottom margin. By default, the bottom margin is `0`.
|
||||
pub fn bottom_margin(mut self, margin: u16) -> Self {
|
||||
self.bottom_margin = margin;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the total height of the row.
|
||||
fn total_height(&self) -> u16 {
|
||||
self.height.saturating_add(self.bottom_margin)
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to display data in formatted columns.
|
||||
///
|
||||
/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
|
||||
/// ```rust
|
||||
/// # use tui::widgets::{Block, Borders, Table, Row, Cell};
|
||||
/// # use tui::layout::Constraint;
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # use tui::text::{Text, Spans, Span};
|
||||
/// Table::new(vec![
|
||||
/// // Row can be created from simple strings.
|
||||
/// Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
/// // You can style the entire row.
|
||||
/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
|
||||
/// // If you need more control over the styling you may need to create Cells directly
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Row31"),
|
||||
/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
|
||||
/// Cell::from(Spans::from(vec![
|
||||
/// Span::raw("Row"),
|
||||
/// Span::styled("33", Style::default().fg(Color::Green))
|
||||
/// ])),
|
||||
/// ]),
|
||||
/// // If a Row need to display some content over multiple lines, you just have to change
|
||||
/// // its height.
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Row\n41"),
|
||||
/// Cell::from("Row\n42"),
|
||||
/// Cell::from("Row\n43"),
|
||||
/// ]).height(2),
|
||||
/// ])
|
||||
/// // You can set the style of the entire Table.
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// // It has an optional header, which is simply a Row always visible at the top.
|
||||
/// .header(
|
||||
/// Row::new(vec!["Col1", "Col2", "Col3"])
|
||||
/// .style(Style::default().fg(Color::Yellow))
|
||||
/// // If you want some space between the header and the rest of the rows, you can always
|
||||
/// // specify some margin at the bottom.
|
||||
/// .bottom_margin(1)
|
||||
/// )
|
||||
/// // As any other widget, a Table can be wrapped in a Block.
|
||||
/// .block(Block::default().title("Table"))
|
||||
/// // Columns widths are constrained in the same way as Layout...
|
||||
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
|
||||
/// // ...and they can be separated by a fixed spacing.
|
||||
/// .column_spacing(1)
|
||||
/// // If you wish to highlight a row in any specific way when it is selected...
|
||||
/// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
/// // ...and potentially show a symbol in front of the selection.
|
||||
/// .highlight_symbol(">>");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Table<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Base style for the widget
|
||||
style: Style,
|
||||
/// Width constraints for each column
|
||||
widths: &'a [Constraint],
|
||||
/// Space between each column
|
||||
column_spacing: u16,
|
||||
/// Style used to render the selected row
|
||||
highlight_style: Style,
|
||||
/// Symbol in front of the selected rom
|
||||
highlight_symbol: Option<&'a str>,
|
||||
/// Optional header
|
||||
header: Option<Row<'a>>,
|
||||
/// Data to display in each row
|
||||
rows: Vec<Row<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Table<'a> {
|
||||
pub fn new<T>(rows: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = Row<'a>>,
|
||||
{
|
||||
Self {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
header: None,
|
||||
rows: rows.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header(mut self, header: Row<'a>) -> Self {
|
||||
self.header = Some(header);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
|
||||
let between_0_and_100 = |&w| match w {
|
||||
Constraint::Percentage(p) => p <= 100,
|
||||
_ => true,
|
||||
};
|
||||
assert!(
|
||||
widths.iter().all(between_0_and_100),
|
||||
"Percentages should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
|
||||
self.highlight_symbol = Some(highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, highlight_style: Style) -> Self {
|
||||
self.highlight_style = highlight_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Self {
|
||||
self.column_spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
|
||||
let mut solver = Solver::new();
|
||||
let mut var_indices = HashMap::new();
|
||||
let mut ccs = Vec::new();
|
||||
let mut variables = Vec::new();
|
||||
for i in 0..self.widths.len() {
|
||||
let var = cassowary::Variable::new();
|
||||
variables.push(var);
|
||||
var_indices.insert(var, i);
|
||||
}
|
||||
let spacing_width = (variables.len() as u16).saturating_sub(1) * self.column_spacing;
|
||||
let mut available_width = max_width.saturating_sub(spacing_width);
|
||||
if has_selection {
|
||||
let highlight_symbol_width =
|
||||
self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
|
||||
available_width = available_width.saturating_sub(highlight_symbol_width);
|
||||
}
|
||||
for (i, constraint) in self.widths.iter().enumerate() {
|
||||
ccs.push(variables[i] | GE(WEAK) | 0.);
|
||||
ccs.push(match *constraint {
|
||||
Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => {
|
||||
variables[i] | EQ(WEAK) | (f64::from(v * available_width) / 100.0)
|
||||
}
|
||||
Constraint::Ratio(n, d) => {
|
||||
variables[i]
|
||||
| EQ(WEAK)
|
||||
| (f64::from(available_width) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
|
||||
Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
|
||||
})
|
||||
}
|
||||
solver
|
||||
.add_constraint(
|
||||
variables
|
||||
.iter()
|
||||
.fold(Expression::from_constant(0.), |acc, v| acc + *v)
|
||||
| LE(REQUIRED)
|
||||
| f64::from(available_width),
|
||||
)
|
||||
.unwrap();
|
||||
solver.add_constraints(&ccs).unwrap();
|
||||
let mut widths = vec![0; variables.len()];
|
||||
for &(var, value) in solver.fetch_changes() {
|
||||
let index = var_indices[&var];
|
||||
let value = if value.is_sign_negative() {
|
||||
0
|
||||
} else {
|
||||
value.round() as u16
|
||||
};
|
||||
widths[index] = value;
|
||||
}
|
||||
// Cassowary could still return columns widths greater than the max width when there are
|
||||
// fixed length constraints that cannot be satisfied. Therefore, we clamp the widths from
|
||||
// left to right.
|
||||
let mut available_width = max_width;
|
||||
for w in &mut widths {
|
||||
*w = available_width.min(*w);
|
||||
available_width = available_width
|
||||
.saturating_sub(*w)
|
||||
.saturating_sub(self.column_spacing);
|
||||
}
|
||||
widths
|
||||
}
|
||||
|
||||
fn get_row_bounds(
|
||||
&self,
|
||||
selected: Option<usize>,
|
||||
offset: usize,
|
||||
max_height: u16,
|
||||
) -> (usize, usize) {
|
||||
let mut start = offset;
|
||||
let mut end = offset;
|
||||
let mut height = 0;
|
||||
for item in self.rows.iter().skip(offset) {
|
||||
if height + item.height > max_height {
|
||||
break;
|
||||
}
|
||||
height += item.total_height();
|
||||
end += 1;
|
||||
}
|
||||
|
||||
let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
|
||||
while selected >= end {
|
||||
height = height.saturating_add(self.rows[end].total_height());
|
||||
end += 1;
|
||||
while height > max_height {
|
||||
height = height.saturating_sub(self.rows[start].total_height());
|
||||
start += 1;
|
||||
}
|
||||
}
|
||||
while selected < start {
|
||||
start -= 1;
|
||||
height = height.saturating_add(self.rows[start].total_height());
|
||||
while height > max_height {
|
||||
end -= 1;
|
||||
height = height.saturating_sub(self.rows[end].total_height());
|
||||
}
|
||||
}
|
||||
(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TableState {
|
||||
offset: usize,
|
||||
@@ -44,185 +394,14 @@ impl TableState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds data to be displayed in a Table widget
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Row<D>
|
||||
where
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
{
|
||||
Data(D),
|
||||
StyledData(D, Style),
|
||||
}
|
||||
|
||||
/// A widget to display data in formatted columns
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, Borders, Table, Row};
|
||||
/// # use tui::layout::Constraint;
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// let row_style = Style::default().fg(Color::White);
|
||||
/// Table::new(
|
||||
/// ["Col1", "Col2", "Col3"].into_iter(),
|
||||
/// vec![
|
||||
/// Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), row_style),
|
||||
/// Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), row_style),
|
||||
/// Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), row_style),
|
||||
/// Row::Data(["Row41", "Row42", "Row43"].into_iter())
|
||||
/// ].into_iter()
|
||||
/// )
|
||||
/// .block(Block::default().title("Table"))
|
||||
/// .header_style(Style::default().fg(Color::Yellow))
|
||||
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .column_spacing(1);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Table<'a, H, R> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Base style for the widget
|
||||
style: Style,
|
||||
/// Header row for all columns
|
||||
header: H,
|
||||
/// Style for the header
|
||||
header_style: Style,
|
||||
/// Width constraints for each column
|
||||
widths: &'a [Constraint],
|
||||
/// Space between each column
|
||||
column_spacing: u16,
|
||||
/// Space between the header and the rows
|
||||
header_gap: u16,
|
||||
/// Style used to render the selected row
|
||||
highlight_style: Style,
|
||||
/// Symbol in front of the selected rom
|
||||
highlight_symbol: Option<&'a str>,
|
||||
/// Data to display in each row
|
||||
rows: R,
|
||||
}
|
||||
|
||||
impl<'a, H, R> Default for Table<'a, H, R>
|
||||
where
|
||||
H: Iterator + Default,
|
||||
R: Iterator + Default,
|
||||
{
|
||||
fn default() -> Table<'a, H, R> {
|
||||
Table {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
header: H::default(),
|
||||
header_style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
header_gap: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
rows: R::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a, H, D, R> Table<'a, H, R>
|
||||
where
|
||||
H: Iterator,
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
R: Iterator<Item = Row<D>>,
|
||||
{
|
||||
pub fn new(header: H, rows: R) -> Table<'a, H, R> {
|
||||
Table {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
header,
|
||||
header_style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
header_gap: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
rows,
|
||||
}
|
||||
}
|
||||
pub fn block(mut self, block: Block<'a>) -> Table<'a, H, R> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header<II>(mut self, header: II) -> Table<'a, H, R>
|
||||
where
|
||||
II: IntoIterator<Item = H::Item, IntoIter = H>,
|
||||
{
|
||||
self.header = header.into_iter();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header_style(mut self, style: Style) -> Table<'a, H, R> {
|
||||
self.header_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, H, R> {
|
||||
let between_0_and_100 = |&w| match w {
|
||||
Constraint::Percentage(p) => p <= 100,
|
||||
_ => true,
|
||||
};
|
||||
assert!(
|
||||
widths.iter().all(between_0_and_100),
|
||||
"Percentages should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rows<II>(mut self, rows: II) -> Table<'a, H, R>
|
||||
where
|
||||
II: IntoIterator<Item = Row<D>, IntoIter = R>,
|
||||
{
|
||||
self.rows = rows.into_iter();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Table<'a, H, R> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Table<'a, H, R> {
|
||||
self.highlight_symbol = Some(highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, highlight_style: Style) -> Table<'a, H, R> {
|
||||
self.highlight_style = highlight_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Table<'a, H, R> {
|
||||
self.column_spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header_gap(mut self, gap: u16) -> Table<'a, H, R> {
|
||||
self.header_gap = gap;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, H, D, R> StatefulWidget for Table<'a, H, R>
|
||||
where
|
||||
H: Iterator,
|
||||
H::Item: Display,
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
R: Iterator<Item = Row<D>>,
|
||||
{
|
||||
impl<'a> StatefulWidget for Table<'a> {
|
||||
type State = TableState;
|
||||
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
// Render block if necessary and get the drawing area
|
||||
let table_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
@@ -232,124 +411,115 @@ where
|
||||
None => area,
|
||||
};
|
||||
|
||||
let mut solver = Solver::new();
|
||||
let mut var_indices = HashMap::new();
|
||||
let mut ccs = Vec::new();
|
||||
let mut variables = Vec::new();
|
||||
for i in 0..self.widths.len() {
|
||||
let var = cassowary::Variable::new();
|
||||
variables.push(var);
|
||||
var_indices.insert(var, i);
|
||||
}
|
||||
for (i, constraint) in self.widths.iter().enumerate() {
|
||||
ccs.push(variables[i] | GE(WEAK) | 0.);
|
||||
ccs.push(match *constraint {
|
||||
Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => {
|
||||
variables[i] | EQ(WEAK) | (f64::from(v * area.width) / 100.0)
|
||||
}
|
||||
Constraint::Ratio(n, d) => {
|
||||
variables[i] | EQ(WEAK) | (f64::from(area.width) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
|
||||
Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
|
||||
})
|
||||
}
|
||||
solver
|
||||
.add_constraint(
|
||||
variables
|
||||
.iter()
|
||||
.fold(Expression::from_constant(0.), |acc, v| acc + *v)
|
||||
| LE(REQUIRED)
|
||||
| f64::from(
|
||||
area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1)),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
solver.add_constraints(&ccs).unwrap();
|
||||
let mut solved_widths = vec![0; variables.len()];
|
||||
for &(var, value) in solver.fetch_changes() {
|
||||
let index = var_indices[&var];
|
||||
let value = if value.is_sign_negative() {
|
||||
0
|
||||
} else {
|
||||
value as u16
|
||||
};
|
||||
solved_widths[index] = value
|
||||
}
|
||||
|
||||
let mut y = table_area.top();
|
||||
let mut x = table_area.left();
|
||||
|
||||
// Draw header
|
||||
if y < table_area.bottom() {
|
||||
for (w, t) in solved_widths.iter().zip(self.header.by_ref()) {
|
||||
buf.set_stringn(x, y, format!("{}", t), *w as usize, self.header_style);
|
||||
x += *w + self.column_spacing;
|
||||
}
|
||||
}
|
||||
y += 1 + self.header_gap;
|
||||
|
||||
// Use highlight_style only if something is selected
|
||||
let (selected, highlight_style) = match state.selected {
|
||||
Some(i) => (Some(i), self.highlight_style),
|
||||
None => (None, self.style),
|
||||
};
|
||||
let has_selection = state.selected.is_some();
|
||||
let columns_widths = self.get_columns_widths(table_area.width, has_selection);
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
let blank_symbol = iter::repeat(" ")
|
||||
.take(highlight_symbol.width())
|
||||
.collect::<String>();
|
||||
let mut current_height = 0;
|
||||
let mut rows_height = table_area.height;
|
||||
|
||||
// Draw header
|
||||
if let Some(ref header) = self.header {
|
||||
let max_header_height = table_area.height.min(header.total_height());
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x: table_area.left(),
|
||||
y: table_area.top(),
|
||||
width: table_area.width,
|
||||
height: table_area.height.min(header.height),
|
||||
},
|
||||
header.style,
|
||||
);
|
||||
let mut col = table_area.left();
|
||||
if has_selection {
|
||||
col += (highlight_symbol.width() as u16).min(table_area.width);
|
||||
}
|
||||
for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
|
||||
render_cell(
|
||||
buf,
|
||||
cell,
|
||||
Rect {
|
||||
x: col,
|
||||
y: table_area.top(),
|
||||
width: *width,
|
||||
height: max_header_height,
|
||||
},
|
||||
);
|
||||
col += *width + self.column_spacing;
|
||||
}
|
||||
current_height += max_header_height;
|
||||
rows_height = rows_height.saturating_sub(max_header_height);
|
||||
}
|
||||
|
||||
// Draw rows
|
||||
let default_style = Style::default();
|
||||
if y < table_area.bottom() {
|
||||
let remaining = (table_area.bottom() - y) as usize;
|
||||
|
||||
// Make sure the table shows the selected item
|
||||
state.offset = if let Some(selected) = selected {
|
||||
if selected >= remaining + state.offset - 1 {
|
||||
selected + 1 - remaining
|
||||
} else if selected < state.offset {
|
||||
selected
|
||||
} else {
|
||||
state.offset
|
||||
}
|
||||
} else {
|
||||
0
|
||||
if self.rows.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
|
||||
state.offset = start;
|
||||
for (i, table_row) in self
|
||||
.rows
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.skip(state.offset)
|
||||
.take(end - start)
|
||||
{
|
||||
let (row, col) = (table_area.top() + current_height, table_area.left());
|
||||
current_height += table_row.total_height();
|
||||
let table_row_area = Rect {
|
||||
x: col,
|
||||
y: row,
|
||||
width: table_area.width,
|
||||
height: table_row.height,
|
||||
};
|
||||
for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() {
|
||||
let (data, style, symbol) = match row {
|
||||
Row::Data(d) | Row::StyledData(d, _)
|
||||
if Some(i) == state.selected.map(|s| s - state.offset) =>
|
||||
{
|
||||
(d, highlight_style, highlight_symbol)
|
||||
}
|
||||
Row::Data(d) => (d, default_style, blank_symbol.as_ref()),
|
||||
Row::StyledData(d, s) => (d, s, blank_symbol.as_ref()),
|
||||
buf.set_style(table_row_area, table_row.style);
|
||||
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
|
||||
let table_row_start_col = if has_selection {
|
||||
let symbol = if is_selected {
|
||||
highlight_symbol
|
||||
} else {
|
||||
&blank_symbol
|
||||
};
|
||||
x = table_area.left();
|
||||
for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() {
|
||||
let s = if c == 0 {
|
||||
format!("{}{}", symbol, elt)
|
||||
} else {
|
||||
format!("{}", elt)
|
||||
};
|
||||
buf.set_stringn(x, y + i as u16, s, *w as usize, style);
|
||||
x += *w + self.column_spacing;
|
||||
}
|
||||
let (col, _) =
|
||||
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
|
||||
col
|
||||
} else {
|
||||
col
|
||||
};
|
||||
let mut col = table_row_start_col;
|
||||
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
|
||||
render_cell(
|
||||
buf,
|
||||
cell,
|
||||
Rect {
|
||||
x: col,
|
||||
y: row,
|
||||
width: *width,
|
||||
height: table_row.height,
|
||||
},
|
||||
);
|
||||
col += *width + self.column_spacing;
|
||||
}
|
||||
if is_selected {
|
||||
buf.set_style(table_row_area, self.highlight_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, H, D, R> Widget for Table<'a, H, R>
|
||||
where
|
||||
H: Iterator,
|
||||
H::Item: Display,
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
R: Iterator<Item = Row<D>>,
|
||||
{
|
||||
fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
|
||||
buf.set_style(area, cell.style);
|
||||
for (i, spans) in cell.content.lines.iter().enumerate() {
|
||||
if i as u16 >= area.height {
|
||||
break;
|
||||
}
|
||||
buf.set_spans(area.x, area.y + i as u16, spans, area.width);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Table<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default();
|
||||
StatefulWidget::render(self, area, buf, &mut state);
|
||||
@@ -363,7 +533,6 @@ mod tests {
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn table_invalid_percentages() {
|
||||
Table::new([""].iter(), vec![Row::Data([""].iter())].into_iter())
|
||||
.widths(&[Constraint::Percentage(110)]);
|
||||
Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,21 +34,21 @@ fn backend_termion_should_only_write_diffs() -> Result<(), Box<dyn std::error::E
|
||||
let mut s = String::new();
|
||||
// First draw
|
||||
write!(s, "{}", cursor::Goto(1, 1))?;
|
||||
s.push_str("a");
|
||||
s.push('a');
|
||||
write!(s, "{}", color::Fg(color::Reset))?;
|
||||
write!(s, "{}", color::Bg(color::Reset))?;
|
||||
write!(s, "{}", style::Reset)?;
|
||||
write!(s, "{}", cursor::Hide)?;
|
||||
// Second draw
|
||||
write!(s, "{}", cursor::Goto(2, 1))?;
|
||||
s.push_str("b");
|
||||
s.push('b');
|
||||
write!(s, "{}", color::Fg(color::Reset))?;
|
||||
write!(s, "{}", color::Bg(color::Reset))?;
|
||||
write!(s, "{}", style::Reset)?;
|
||||
write!(s, "{}", cursor::Hide)?;
|
||||
// Third draw
|
||||
write!(s, "{}", cursor::Goto(3, 1))?;
|
||||
s.push_str("c");
|
||||
s.push('c');
|
||||
write!(s, "{}", color::Fg(color::Reset))?;
|
||||
write!(s, "{}", color::Bg(color::Reset))?;
|
||||
write!(s, "{}", style::Reset)?;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::error::Error;
|
||||
use tui::{
|
||||
backend::{Backend, TestBackend},
|
||||
layout::Rect,
|
||||
widgets::Paragraph,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
@@ -11,3 +14,23 @@ fn terminal_buffer_size_should_be_limited() {
|
||||
assert_eq!(size.width, 255);
|
||||
assert_eq!(size.height, 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_draw_returns_the_completed_frame() -> Result<(), Box<dyn Error>> {
|
||||
let backend = TestBackend::new(10, 10);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
let frame = terminal.draw(|f| {
|
||||
let paragrah = Paragraph::new("Test");
|
||||
f.render_widget(paragrah, f.size());
|
||||
})?;
|
||||
assert_eq!(frame.buffer.get(0, 0).symbol, "T");
|
||||
assert_eq!(frame.area, Rect::new(0, 0, 10, 10));
|
||||
terminal.backend_mut().resize(8, 8);
|
||||
let frame = terminal.draw(|f| {
|
||||
let paragrah = Paragraph::new("test");
|
||||
f.render_widget(paragrah, f.size());
|
||||
})?;
|
||||
assert_eq!(frame.buffer.get(0, 0).symbol, "t");
|
||||
assert_eq!(frame.area, Rect::new(0, 0, 8, 8));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
40
tests/widgets_barchart.rs
Normal file
40
tests/widgets_barchart.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use tui::backend::TestBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::widgets::{BarChart, Block, Borders};
|
||||
use tui::Terminal;
|
||||
|
||||
#[test]
|
||||
fn widgets_barchart_not_full_below_max_value() {
|
||||
let test_case = |expected| {
|
||||
let backend = TestBackend::new(30, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.data(&[("empty", 0), ("half", 50), ("almost", 99), ("full", 100)])
|
||||
.max(100)
|
||||
.bar_width(7)
|
||||
.bar_gap(0);
|
||||
f.render_widget(barchart, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
// check that bars fill up correctly up to max value
|
||||
test_case(Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ ▇▇▇▇▇▇▇███████│",
|
||||
"│ ██████████████│",
|
||||
"│ ██████████████│",
|
||||
"│ ▄▄▄▄▄▄▄██████████████│",
|
||||
"│ █████████████████████│",
|
||||
"│ █████████████████████│",
|
||||
"│ ██50█████99█████100██│",
|
||||
"│empty half almost full │",
|
||||
"└────────────────────────────┘",
|
||||
]));
|
||||
}
|
||||
@@ -45,3 +45,169 @@ fn widgets_block_renders() {
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_block_renders_on_small_areas() {
|
||||
let test_case = |block, area: Rect, expected| {
|
||||
let backend = TestBackend::new(area.width, area.height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
f.render_widget(block, area);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
let one_cell_test_cases = [
|
||||
(Borders::NONE, "T"),
|
||||
(Borders::LEFT, "│"),
|
||||
(Borders::TOP, "T"),
|
||||
(Borders::RIGHT, "│"),
|
||||
(Borders::BOTTOM, "T"),
|
||||
(Borders::ALL, "┌"),
|
||||
];
|
||||
for (borders, symbol) in one_cell_test_cases.iter().cloned() {
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0,
|
||||
},
|
||||
Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0,
|
||||
}),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(borders),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec![symbol]),
|
||||
);
|
||||
}
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::LEFT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["│Tes"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::RIGHT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["Tes│"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::RIGHT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["Tes│"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["│Te│"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["Test"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default().title("Test").borders(Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 5,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["Test─"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.borders(Borders::LEFT | Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 5,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["┌Test"]),
|
||||
);
|
||||
test_case(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.borders(Borders::LEFT | Borders::TOP),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 6,
|
||||
height: 1,
|
||||
},
|
||||
Buffer::with_lines(vec!["┌Test─"]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
@@ -12,6 +13,40 @@ fn create_labels<'a>(labels: &'a [&'a str]) -> Vec<Span<'a>> {
|
||||
labels.iter().map(|l| Span::from(*l)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_can_render_on_small_areas() {
|
||||
let test_case = |width, height| {
|
||||
let backend = TestBackend::new(width, height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let datasets = vec![Dataset::default()
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Magenta))
|
||||
.data(&[(0.0, 0.0)])];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(Block::default().title("Plot").borders(Borders::ALL))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.bounds([0.0, 0.0])
|
||||
.labels(create_labels(&["0.0", "1.0"])),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.bounds([0.0, 0.0])
|
||||
.labels(create_labels(&["0.0", "1.0"])),
|
||||
);
|
||||
f.render_widget(chart, f.size());
|
||||
})
|
||||
.unwrap();
|
||||
};
|
||||
test_case(0, 0);
|
||||
test_case(0, 1);
|
||||
test_case(1, 0);
|
||||
test_case(1, 1);
|
||||
test_case(2, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_can_have_axis_with_zero_length_bounds() {
|
||||
let backend = TestBackend::new(100, 100);
|
||||
@@ -124,3 +159,255 @@ fn widgets_chart_can_have_empty_datasets() {
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_can_have_a_legend() {
|
||||
let backend = TestBackend::new(60, 30);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("Dataset 1")
|
||||
.style(Style::default().fg(Color::Blue))
|
||||
.data(&[
|
||||
(0.0, 0.0),
|
||||
(10.0, 1.0),
|
||||
(20.0, 2.0),
|
||||
(30.0, 3.0),
|
||||
(40.0, 4.0),
|
||||
(50.0, 5.0),
|
||||
(60.0, 6.0),
|
||||
(70.0, 7.0),
|
||||
(80.0, 8.0),
|
||||
(90.0, 9.0),
|
||||
(100.0, 10.0),
|
||||
])
|
||||
.graph_type(Line),
|
||||
Dataset::default()
|
||||
.name("Dataset 2")
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.data(&[
|
||||
(0.0, 10.0),
|
||||
(10.0, 9.0),
|
||||
(20.0, 8.0),
|
||||
(30.0, 7.0),
|
||||
(40.0, 6.0),
|
||||
(50.0, 5.0),
|
||||
(60.0, 4.0),
|
||||
(70.0, 3.0),
|
||||
(80.0, 2.0),
|
||||
(90.0, 1.0),
|
||||
(100.0, 0.0),
|
||||
])
|
||||
.graph_type(Line),
|
||||
];
|
||||
let chart = Chart::new(datasets)
|
||||
.style(Style::default().bg(Color::White))
|
||||
.block(Block::default().title("Chart Test").borders(Borders::ALL))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.bounds([0.0, 100.0])
|
||||
.title(Span::styled("X Axis", Style::default().fg(Color::Yellow)))
|
||||
.labels(create_labels(&["0.0", "50.0", "100.0"])),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.bounds([0.0, 10.0])
|
||||
.title("Y Axis")
|
||||
.labels(create_labels(&["0.0", "5.0", "10.0"])),
|
||||
);
|
||||
f.render_widget(
|
||||
chart,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 60,
|
||||
height: 30,
|
||||
},
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"┌Chart Test────────────────────────────────────────────────┐",
|
||||
"│10.0│Y Axis ┌─────────┐│",
|
||||
"│ │ •• │Dataset 1││",
|
||||
"│ │ •• │Dataset 2││",
|
||||
"│ │ •• └─────────┘│",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ ••• •• │",
|
||||
"│ │ ••• │",
|
||||
"│5.0 │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ ••• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• ••• │",
|
||||
"│ │ •• •• │",
|
||||
"│ │ •• •• │",
|
||||
"│0.0 │• X Axis│",
|
||||
"│ └─────────────────────────────────────────────────────│",
|
||||
"│ 0.0 50.0 100.0 │",
|
||||
"└──────────────────────────────────────────────────────────┘",
|
||||
]);
|
||||
|
||||
// Set expected backgound color
|
||||
for row in 0..30 {
|
||||
for col in 0..60 {
|
||||
expected.get_mut(col, row).set_bg(Color::White);
|
||||
}
|
||||
}
|
||||
|
||||
// Set expected colors of the first dataset
|
||||
let line1 = vec![
|
||||
(48, 5),
|
||||
(49, 5),
|
||||
(46, 6),
|
||||
(47, 6),
|
||||
(44, 7),
|
||||
(45, 7),
|
||||
(42, 8),
|
||||
(43, 8),
|
||||
(40, 9),
|
||||
(41, 9),
|
||||
(38, 10),
|
||||
(39, 10),
|
||||
(36, 11),
|
||||
(37, 11),
|
||||
(34, 12),
|
||||
(35, 12),
|
||||
(33, 13),
|
||||
(30, 14),
|
||||
(31, 14),
|
||||
(28, 15),
|
||||
(29, 15),
|
||||
(25, 16),
|
||||
(26, 16),
|
||||
(27, 16),
|
||||
(23, 17),
|
||||
(24, 17),
|
||||
(21, 18),
|
||||
(22, 18),
|
||||
(19, 19),
|
||||
(20, 19),
|
||||
(17, 20),
|
||||
(18, 20),
|
||||
(15, 21),
|
||||
(16, 21),
|
||||
(13, 22),
|
||||
(14, 22),
|
||||
(11, 23),
|
||||
(12, 23),
|
||||
(9, 24),
|
||||
(10, 24),
|
||||
(7, 25),
|
||||
(8, 25),
|
||||
(6, 26),
|
||||
];
|
||||
let legend1 = vec![
|
||||
(49, 2),
|
||||
(50, 2),
|
||||
(51, 2),
|
||||
(52, 2),
|
||||
(53, 2),
|
||||
(54, 2),
|
||||
(55, 2),
|
||||
(56, 2),
|
||||
(57, 2),
|
||||
];
|
||||
for (col, row) in line1 {
|
||||
expected.get_mut(col, row).set_fg(Color::Blue);
|
||||
}
|
||||
for (col, row) in legend1 {
|
||||
expected.get_mut(col, row).set_fg(Color::Blue);
|
||||
}
|
||||
|
||||
// Set expected colors of the second dataset
|
||||
let line2 = vec![
|
||||
(8, 2),
|
||||
(9, 2),
|
||||
(10, 3),
|
||||
(11, 3),
|
||||
(12, 4),
|
||||
(13, 4),
|
||||
(14, 5),
|
||||
(15, 5),
|
||||
(16, 6),
|
||||
(17, 6),
|
||||
(18, 7),
|
||||
(19, 7),
|
||||
(20, 8),
|
||||
(21, 8),
|
||||
(22, 9),
|
||||
(23, 9),
|
||||
(24, 10),
|
||||
(25, 10),
|
||||
(26, 11),
|
||||
(27, 11),
|
||||
(28, 12),
|
||||
(29, 12),
|
||||
(30, 12),
|
||||
(31, 13),
|
||||
(32, 13),
|
||||
(33, 14),
|
||||
(34, 14),
|
||||
(35, 15),
|
||||
(36, 15),
|
||||
(37, 16),
|
||||
(38, 16),
|
||||
(39, 17),
|
||||
(40, 17),
|
||||
(41, 18),
|
||||
(42, 18),
|
||||
(43, 19),
|
||||
(44, 19),
|
||||
(45, 20),
|
||||
(46, 20),
|
||||
(47, 21),
|
||||
(48, 21),
|
||||
(49, 22),
|
||||
(50, 22),
|
||||
(51, 23),
|
||||
(52, 23),
|
||||
(53, 23),
|
||||
(54, 24),
|
||||
(55, 24),
|
||||
(56, 25),
|
||||
(57, 25),
|
||||
];
|
||||
let legend2 = vec![
|
||||
(49, 3),
|
||||
(50, 3),
|
||||
(51, 3),
|
||||
(52, 3),
|
||||
(53, 3),
|
||||
(54, 3),
|
||||
(55, 3),
|
||||
(56, 3),
|
||||
(57, 3),
|
||||
];
|
||||
for (col, row) in line2 {
|
||||
expected.get_mut(col, row).set_fg(Color::Green);
|
||||
}
|
||||
for (col, row) in legend2 {
|
||||
expected.get_mut(col, row).set_fg(Color::Green);
|
||||
}
|
||||
|
||||
// Set expected colors of the x axis
|
||||
let x_axis_title = vec![(53, 26), (54, 26), (55, 26), (56, 26), (57, 26), (58, 26)];
|
||||
for (col, row) in x_axis_title {
|
||||
expected.get_mut(col, row).set_fg(Color::Yellow);
|
||||
}
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
widgets::{Block, Borders, Gauge},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
widgets::{Block, Borders, Gauge, LineGauge},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
@@ -20,11 +22,82 @@ fn widgets_gauge_renders() {
|
||||
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Percentage").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().bg(Color::Blue).fg(Color::Red))
|
||||
.use_unicode(true)
|
||||
.percent(43);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Ratio").borders(Borders::ALL))
|
||||
.ratio(0.211_313_934_313_1);
|
||||
.gauge_style(Style::default().bg(Color::Blue).fg(Color::Red))
|
||||
.use_unicode(true)
|
||||
.ratio(0.511_313_934_313_1);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ┌Percentage────────────────────────┐ ",
|
||||
" │ ▋43% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ┌Ratio─────────────────────────────┐ ",
|
||||
" │ 51% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
|
||||
for i in 3..17 {
|
||||
expected
|
||||
.get_mut(i, 3)
|
||||
.set_bg(Color::Red)
|
||||
.set_fg(Color::Blue);
|
||||
}
|
||||
for i in 17..37 {
|
||||
expected
|
||||
.get_mut(i, 3)
|
||||
.set_bg(Color::Blue)
|
||||
.set_fg(Color::Red);
|
||||
}
|
||||
|
||||
for i in 3..20 {
|
||||
expected
|
||||
.get_mut(i, 6)
|
||||
.set_bg(Color::Red)
|
||||
.set_fg(Color::Blue);
|
||||
}
|
||||
for i in 20..37 {
|
||||
expected
|
||||
.get_mut(i, 6)
|
||||
.set_bg(Color::Blue)
|
||||
.set_fg(Color::Red);
|
||||
}
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_gauge_renders_no_unicode() {
|
||||
let backend = TestBackend::new(40, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Percentage").borders(Borders::ALL))
|
||||
.percent(43)
|
||||
.use_unicode(false);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Ratio").borders(Borders::ALL))
|
||||
.ratio(0.211_313_934_313_1)
|
||||
.use_unicode(false);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -42,3 +115,55 @@ fn widgets_gauge_renders() {
|
||||
]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_line_gauge_renders() {
|
||||
let backend = TestBackend::new(20, 4);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let gauge = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Green).bg(Color::White))
|
||||
.ratio(0.43);
|
||||
f.render_widget(
|
||||
gauge,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 20,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
let gauge = LineGauge::default()
|
||||
.block(Block::default().title("Gauge 2").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Green))
|
||||
.line_set(symbols::line::THICK)
|
||||
.ratio(0.211_313_934_313_1);
|
||||
f.render_widget(
|
||||
gauge,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 20,
|
||||
height: 3,
|
||||
},
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"43% ────────────────",
|
||||
"┌Gauge 2───────────┐",
|
||||
"│21% ━━━━━━━━━━━━━━│",
|
||||
"└──────────────────┘",
|
||||
]);
|
||||
for col in 4..10 {
|
||||
expected.get_mut(col, 0).set_fg(Color::Green);
|
||||
}
|
||||
for col in 10..20 {
|
||||
expected.get_mut(col, 0).set_fg(Color::White);
|
||||
}
|
||||
for col in 5..7 {
|
||||
expected.get_mut(col, 2).set_fg(Color::Green);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Alignment,
|
||||
text::{Spans, Text},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Terminal,
|
||||
};
|
||||
@@ -141,6 +141,27 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_can_wrap_with_a_trailing_nbsp() {
|
||||
let nbsp: &str = "\u{00a0}";
|
||||
let line = Spans::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]);
|
||||
let backend = TestBackend::new(20, 3);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│NBSP\u{00a0} │",
|
||||
"└──────────────────┘",
|
||||
]);
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
|
||||
let paragraph = Paragraph::new(line).block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
#[test]
|
||||
fn widgets_paragraph_can_scroll_horizontally() {
|
||||
let test_case = |alignment, scroll, expected| {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use tui::backend::TestBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Constraint;
|
||||
use tui::widgets::{Block, Borders, Row, Table};
|
||||
use tui::Terminal;
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Constraint,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn widgets_table_column_spacing_can_be_changed() {
|
||||
@@ -13,16 +17,13 @@ fn widgets_table_column_spacing_can_be_changed() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(
|
||||
["Head1", "Head2", "Head3"].iter(),
|
||||
vec![
|
||||
Row::Data(["Row11", "Row12", "Row13"].iter()),
|
||||
Row::Data(["Row21", "Row22", "Row23"].iter()),
|
||||
Row::Data(["Row31", "Row32", "Row33"].iter()),
|
||||
Row::Data(["Row41", "Row42", "Row43"].iter()),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Length(5),
|
||||
@@ -114,16 +115,13 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(
|
||||
["Head1", "Head2", "Head3"].iter(),
|
||||
vec![
|
||||
Row::Data(["Row11", "Row12", "Row13"].iter()),
|
||||
Row::Data(["Row21", "Row22", "Row23"].iter()),
|
||||
Row::Data(["Row31", "Row32", "Row33"].iter()),
|
||||
Row::Data(["Row41", "Row42", "Row43"].iter()),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths);
|
||||
f.render_widget(table, size);
|
||||
@@ -205,16 +203,13 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(
|
||||
["Head1", "Head2", "Head3"].iter(),
|
||||
vec![
|
||||
Row::Data(["Row11", "Row12", "Row13"].iter()),
|
||||
Row::Data(["Row21", "Row22", "Row23"].iter()),
|
||||
Row::Data(["Row31", "Row32", "Row33"].iter()),
|
||||
Row::Data(["Row41", "Row42", "Row43"].iter()),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths)
|
||||
.column_spacing(0);
|
||||
@@ -248,9 +243,9 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
|
||||
// columns of not enough width trims the data
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(11),
|
||||
Constraint::Percentage(11),
|
||||
Constraint::Percentage(11),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
@@ -269,9 +264,9 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
|
||||
// columns of large width just before pushing a column off
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
@@ -292,12 +287,12 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
|
||||
&[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 │",
|
||||
"│Head1 Head2 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
@@ -314,16 +309,13 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(
|
||||
["Head1", "Head2", "Head3"].iter(),
|
||||
vec![
|
||||
Row::Data(["Row11", "Row12", "Row13"].iter()),
|
||||
Row::Data(["Row21", "Row22", "Row23"].iter()),
|
||||
Row::Data(["Row31", "Row32", "Row33"].iter()),
|
||||
Row::Data(["Row41", "Row42", "Row43"].iter()),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths);
|
||||
f.render_widget(table, size);
|
||||
@@ -356,9 +348,9 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
// columns of not enough width trims the data
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(11),
|
||||
Constraint::Length(20),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(11),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
@@ -377,9 +369,9 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
// columns of large width just before pushing a column off
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Length(10),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(33),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
@@ -416,3 +408,310 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_table_columns_widths_can_use_ratio_constraints() {
|
||||
let test_case = |widths, expected| {
|
||||
let backend = TestBackend::new(30, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths)
|
||||
.column_spacing(0);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
// columns of zero width show nothing
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Ratio(0, 1),
|
||||
Constraint::Ratio(0, 1),
|
||||
Constraint::Ratio(0, 1),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// columns of not enough width trims the data
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Ratio(1, 9),
|
||||
Constraint::Ratio(1, 9),
|
||||
Constraint::Ratio(1, 9),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│HeaHeaHea │",
|
||||
"│ │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│RowRowRow │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// columns of large width just before pushing a column off
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// percentages summing to 100 should give equal widths
|
||||
test_case(
|
||||
&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_table_can_have_rows_with_multi_lines() {
|
||||
let test_case = |state: &mut TableState, expected: Buffer| {
|
||||
let backend = TestBackend::new(30, 8);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.highlight_symbol(">> ")
|
||||
.widths(&[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, size, state);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
let mut state = TableState::default();
|
||||
// no selection
|
||||
test_case(
|
||||
&mut state,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│ │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// select first
|
||||
state.select(Some(0));
|
||||
test_case(
|
||||
&mut state,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│>> Row11 Row12 Row13 │",
|
||||
"│ Row21 Row22 Row23 │",
|
||||
"│ │",
|
||||
"│ Row31 Row32 Row33 │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// select second (we don't show partially the 4th row)
|
||||
state.select(Some(1));
|
||||
test_case(
|
||||
&mut state,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│ Row11 Row12 Row13 │",
|
||||
"│>> Row21 Row22 Row23 │",
|
||||
"│ │",
|
||||
"│ Row31 Row32 Row33 │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
// select 4th (we don't show partially the 1st row)
|
||||
state.select(Some(3));
|
||||
test_case(
|
||||
&mut state,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│ Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│ Row31 Row32 Row33 │",
|
||||
"│>> Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_table_can_have_elements_styled_individually() {
|
||||
let backend = TestBackend::new(30, 4);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = TableState::default();
|
||||
state.select(Some(0));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]).style(Style::default().fg(Color::Green)),
|
||||
Row::new(vec![
|
||||
Cell::from("Row21"),
|
||||
Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
|
||||
Cell::from(Spans::from(vec![
|
||||
Span::raw("Row"),
|
||||
Span::styled("23", Style::default().fg(Color::Blue)),
|
||||
]))
|
||||
.style(Style::default().fg(Color::Red)),
|
||||
])
|
||||
.style(Style::default().fg(Color::LightGreen)),
|
||||
])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.highlight_symbol(">> ")
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.widths(&[
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"│ Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│>> Row11 Row12 Row13 │",
|
||||
"│ Row21 Row22 Row23 │",
|
||||
]);
|
||||
// First row = row color + highlight style
|
||||
for col in 1..=28 {
|
||||
expected.get_mut(col, 2).set_style(
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
}
|
||||
// Second row:
|
||||
// 1. row color
|
||||
for col in 1..=28 {
|
||||
expected
|
||||
.get_mut(col, 3)
|
||||
.set_style(Style::default().fg(Color::LightGreen));
|
||||
}
|
||||
// 2. cell color
|
||||
for col in 11..=16 {
|
||||
expected
|
||||
.get_mut(col, 3)
|
||||
.set_style(Style::default().fg(Color::Yellow));
|
||||
}
|
||||
for col in 18..=23 {
|
||||
expected
|
||||
.get_mut(col, 3)
|
||||
.set_style(Style::default().fg(Color::Red));
|
||||
}
|
||||
// 3. text color
|
||||
for col in 21..=22 {
|
||||
expected
|
||||
.get_mut(col, 3)
|
||||
.set_style(Style::default().fg(Color::Blue));
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_table_should_render_even_if_empty() {
|
||||
let backend = TestBackend::new(30, 4);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]))
|
||||
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.widths(&[
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
]);
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user