Compare commits
105 Commits
653/master
...
v0.21.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21ca38d9e9 | ||
|
|
769efc20d1 | ||
|
|
32e416ea95 | ||
|
|
49e0f4e983 | ||
|
|
e08b466166 | ||
|
|
3f9935bbcc | ||
|
|
ef8bc7c5a8 | ||
|
|
fa02cf0f2b | ||
|
|
bc66a27baf | ||
|
|
3e54ac3aca | ||
|
|
a425102cf3 | ||
|
|
60cd28005f | ||
|
|
728f82c084 | ||
|
|
4437835057 | ||
|
|
1cc405d2dc | ||
|
|
2f0d549a50 | ||
|
|
c7aca64ba1 | ||
|
|
548961f610 | ||
|
|
b3072ce354 | ||
|
|
98b6b1911c | ||
|
|
ef96109ea5 | ||
|
|
cf1a759fa5 | ||
|
|
86c3fc9fac | ||
|
|
5f12f06297 | ||
|
|
33b3f4e312 | ||
|
|
603ec3f10a | ||
|
|
37bb82e87d | ||
|
|
047e0b7e8d | ||
|
|
782820c34a | ||
|
|
26cf1f7a89 | ||
|
|
f7ab4f04ac | ||
|
|
2751f08bdc | ||
|
|
0904d448e0 | ||
|
|
fab7952af6 | ||
|
|
6af75d6d40 | ||
|
|
5f1a37f0db | ||
|
|
b7bd3051b1 | ||
|
|
7adc3fe19b | ||
|
|
4842885aa6 | ||
|
|
00e8c0d1b0 | ||
|
|
239efa5fbd | ||
|
|
a9ba23bae8 | ||
|
|
62930f2821 | ||
|
|
4334c71bc7 | ||
|
|
21a029f17e | ||
|
|
9d37c3bd45 | ||
|
|
343ec220b4 | ||
|
|
2da4c10384 | ||
|
|
829dfee9e5 | ||
|
|
26c7bcfdd5 | ||
|
|
cf1b05d16e | ||
|
|
cfa8b8042a | ||
|
|
f0c0985708 | ||
|
|
73c937bc30 | ||
|
|
33e67abbe9 | ||
|
|
c20fb723ef | ||
|
|
ccd142df97 | ||
|
|
f6dbd1c0b5 | ||
|
|
d38d185d1c | ||
|
|
ef79b72471 | ||
|
|
ed12ab16e0 | ||
|
|
24820cfcff | ||
|
|
e10f62663e | ||
|
|
02573b0ad2 | ||
|
|
0dc39434c2 | ||
|
|
33acfce083 | ||
|
|
79d0eadbd6 | ||
|
|
73f7f16298 | ||
|
|
66eb0e42fe | ||
|
|
052ae53b6e | ||
|
|
1c0ed3268b | ||
|
|
0456abb327 | ||
|
|
ec50458491 | ||
|
|
b834ccaa2f | ||
|
|
ffb3de6c36 | ||
|
|
454c8459f2 | ||
|
|
94a0d09591 | ||
|
|
9b7a6ed85d | ||
|
|
7e31035114 | ||
|
|
e15a6146f8 | ||
|
|
e49cb1126b | ||
|
|
142bc5720e | ||
|
|
feaeb7870f | ||
|
|
9df0eefe49 | ||
|
|
8e89a9377a | ||
|
|
d3df8fe7ef | ||
|
|
9feda988a5 | ||
|
|
9534d533e3 | ||
|
|
85eefe1d8b | ||
|
|
3343270680 | ||
|
|
bf9d502742 | ||
|
|
85c0779ac0 | ||
|
|
070de44069 | ||
|
|
33087e3a99 | ||
|
|
fe4eb5e771 | ||
|
|
2fead23556 | ||
|
|
bc5a9e4c06 | ||
|
|
fafad6c961 | ||
|
|
a4de409235 | ||
|
|
a05fd45959 | ||
|
|
24de2f8a96 | ||
|
|
eee37011a5 | ||
|
|
a67706bea0 | ||
|
|
faa69b6cfe | ||
|
|
ba5ea2deff |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,7 +7,7 @@ assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi there, sorry `tui` is not working as expected.
|
||||
Hi there, sorry `ratatui` is not working as expected.
|
||||
Please fill this bug report conscientiously.
|
||||
A detailed and complete issue is more likely to be processed quickly.
|
||||
-->
|
||||
|
||||
17
.github/pull_request_template.md
vendored
17
.github/pull_request_template.md
vendored
@@ -1,17 +0,0 @@
|
||||
## Description
|
||||
<!--
|
||||
A clear and concise description of what this PR changes.
|
||||
-->
|
||||
|
||||
## Testing guidelines
|
||||
<!--
|
||||
A clear and concise description of how the changes can be tested.
|
||||
For example, you can include a command to run the relevant tests or examples.
|
||||
You can also include screenshots of the expected behavior.
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] I have read the [contributing guidelines](../CONTRIBUTING.md).
|
||||
* [ ] I have added relevant tests.
|
||||
* [ ] I have documented all new additions.
|
||||
19
.github/workflows/cd.yml
vendored
Normal file
19
.github/workflows/cd.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Continuous Deployment
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish on crates.io
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Publish
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: publish
|
||||
args: --token ${{ secrets.CARGO_TOKEN }}
|
||||
117
.github/workflows/ci.yml
vendored
117
.github/workflows/ci.yml
vendored
@@ -1,71 +1,76 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
name: CI
|
||||
|
||||
env:
|
||||
CI_CARGO_MAKE_VERSION: 0.35.8
|
||||
CI_CARGO_MAKE_VERSION: 0.35.16
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
rust: ["1.65.0", "stable"]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
triple: x86_64-unknown-linux-musl
|
||||
- os: windows-latest
|
||||
triple: x86_64-pc-windows-msvc
|
||||
- os: macos-latest
|
||||
triple: x86_64-apple-darwin
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: hecrj/setup-rust-action@50a120e4d34903c2c1383dec0e9b1d349a9cc2b1
|
||||
with:
|
||||
rust-version: ${{ matrix.rust }}
|
||||
components: rustfmt,clippy
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install cargo-make on Linux or macOS
|
||||
if: ${{ runner.os != 'windows' }}
|
||||
shell: bash
|
||||
run: |
|
||||
curl -LO 'https://github.com/sagiegurari/cargo-make/releases/download/${{ env.CI_CARGO_MAKE_VERSION }}/cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
|
||||
unzip 'cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
|
||||
cp 'cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}/cargo-make' ~/.cargo/bin/
|
||||
cargo make --version
|
||||
- name: Install cargo-make on Windows
|
||||
if: ${{ runner.os == 'windows' }}
|
||||
shell: bash
|
||||
run: |
|
||||
# `cargo-make-v0.35.16-{target}/` directory is created on Linux and macOS, but it is not creatd on Windows.
|
||||
mkdir cargo-make-temporary
|
||||
cd cargo-make-temporary
|
||||
curl -LO 'https://github.com/sagiegurari/cargo-make/releases/download/${{ env.CI_CARGO_MAKE_VERSION }}/cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
|
||||
unzip 'cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
|
||||
cp cargo-make.exe ~/.cargo/bin/
|
||||
cd ..
|
||||
cargo make --version
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.56.1", "stable"]
|
||||
steps:
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
- name: Checkout
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
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
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: "Check conventional commits"
|
||||
uses: crate-ci/committed@master
|
||||
with:
|
||||
path: ${{ steps.cargo-bin-dir.outputs.dir }}/cargo-make
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
windows:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.56.1", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
with:
|
||||
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:
|
||||
path: ${{ steps.cargo-bin-dir.outputs.dir }}\cargo-make.exe
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
args: "-vv"
|
||||
commits: "HEAD"
|
||||
- name: "Check typos"
|
||||
uses: crate-ci/typos@master
|
||||
|
||||
83
APPS.md
Normal file
83
APPS.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Apps using `ratatui`
|
||||
|
||||
Here you will find a list of TUI applications that are made using `ratatui`.
|
||||
|
||||
## 📝 Table of Contents
|
||||
|
||||
- [💻 Development Tools](#-development-tools)
|
||||
- [🕹️ Games and Entertainment](#-games-and-entertainment)
|
||||
- [🚀 Productivity and Utilities](#-productivity-and-utilities)
|
||||
- [🎼 Music and Media](#-music-and-media)
|
||||
- [🌐 Networking and Internet](#-networking-and-internet)
|
||||
- [👨💻 System Administration](#-system-administration)
|
||||
- [🌌 Other](#-other)
|
||||
|
||||
#### 💻 Development Tools
|
||||
|
||||
- [desed](https://github.com/SoptikHa2/desed): Debugging tool for sed scripts
|
||||
- [gitui](https://github.com/extrawurst/gitui): Terminal UI for Git
|
||||
- [gobang](https://github.com/TaKO8Ki/gobang): Cross-platform TUI database management tool
|
||||
- [joshuto](https://github.com/kamiyaa/joshuto): Ranger-like terminal file manager written in Rust
|
||||
- [repgrep](https://github.com/acheronfail/repgrep): An interactive replacer for ripgrep that makes it easy to find and replace across files on the command line
|
||||
- [tenere](https://github.com/pythops/tenere): TUI interface for LLMs written in Rust
|
||||
|
||||
#### 🕹️ Games and Entertainment
|
||||
|
||||
- [Battleship.rs](https://github.com/deepu105/battleship-rs): Terminal-based Battleship game
|
||||
- [game-of-life-rs](https://github.com/kachark/game-of-life-rs): Conway's Game of Life implemented in Rust and visualized with tui-rs
|
||||
- [oxycards](https://github.com/BrookJeynes/oxycards): Quiz card application built within the terminal
|
||||
- [minesweep](https://github.com/cpcloud/minesweep-rs): Terminal-based Minesweeper game
|
||||
- [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli): rust sadari game based on terminal! (Ghost leg or Amidakuji in another words)
|
||||
|
||||
#### 🚀 Productivity and Utilities
|
||||
|
||||
- [diskonaut](https://github.com/imsnif/diskonaut): Terminal-based disk space navigator
|
||||
- [exhaust](https://github.com/heyrict/exhaust): Exhaust all your possibilities.. for the next coming exam
|
||||
- [gpg-tui](https://github.com/orhun/gpg-tui): Manage your GnuPG keys with ease!
|
||||
- [meteo-tui](https://github.com/16arpi/meteo-tui): French weather app in the command line
|
||||
- [rusty-krab-manager](https://github.com/aryakaul/rusty-krab-manager): time management tui in rust
|
||||
- [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui): TUI for the Taskwarrior command-line task manager
|
||||
- [tickrs](https://github.com/tarkah/tickrs): Stock market ticker in the terminal
|
||||
|
||||
#### 🎼 Music and Media
|
||||
|
||||
- [glicol-cli](https://github.com/glicol/glicol-cli): Cross-platform music live coding in terminal
|
||||
- [spotify-tui](https://github.com/Rigellute/spotify-tui): Spotify client for the terminal
|
||||
- [twitch-tui](https://github.com/Xithrius/twitch-tui): Twitch chat in the terminal
|
||||
- [ytui-music](https://github.com/sudipghimire533/ytui-music): Listen to music from YouTube in the terminal
|
||||
|
||||
#### 🌐 Networking and Internet
|
||||
|
||||
- [adsb_deku/radar](https://github.com/wcampbell0x2a/adsb_deku#radar-tui): TUI for displaying ADS-B data from aircraft
|
||||
- [bandwhich](https://github.com/imsnif/bandwhich): Displays network utilization by process
|
||||
- [conclusive](https://github.com/mrusme/conclusive): A command line client for Plausible Analytics
|
||||
- [gping](https://github.com/orf/gping/): Ping tool with a graph
|
||||
- [mqttui](https://github.com/EdJoPaTo/mqttui): MQTT client for subscribing or publishing to topics
|
||||
- [oha](https://github.com/hatoo/oha): Top-like monitoring tool for HTTP(S) traffic
|
||||
- [rrtop](https://github.com/wojciech-zurek/rrtop): Redis monitoring (top like) app. rrtop -> [r]ust [r]edis [top]
|
||||
- [termscp](https://github.com/veeso/termscp): A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/S3/SMB
|
||||
- [trippy](https://github.com/fujiapple852/trippy): Network diagnostic tool
|
||||
- [tsuchita](https://github.com/kamiyaa/tsuchita): client-server notification center for dbus desktop notifications
|
||||
- [vector](https://github.com/vectordotdev/vector): A high-performance observability data pipeline
|
||||
|
||||
#### 👨💻 System Administration
|
||||
|
||||
- [bottom](https://github.com/ClementTsang/bottom): Cross-platform graphical process/system monitor
|
||||
- [kdash](https://github.com/kdash-rs/kdash): A simple and fast dashboard for Kubernetes
|
||||
- [kmon](https://github.com/orhun/kmon): Linux Kernel Manager and Activity Monitor
|
||||
- [kubectl-watch](https://github.com/imuxin/kubectl-watch): A kubectl plugin to provide a pretty delta change view of being watched kubernetes resources
|
||||
- [logss](https://github.com/todoesverso/logss): A simple cli for logs splitting
|
||||
- [oxker](https://github.com/mrjackwills/oxker): Simple TUI to view & control docker containers
|
||||
- [systeroid](https://github.com/orhun/systeroid): A more powerful alternative to sysctl(8) with a terminal user interface
|
||||
- [xplr](https://github.com/sayanarijit/xplr): Hackable, minimal, and fast TUI file explorer
|
||||
- [ytop](https://github.com/cjbassi/ytop): TUI system monitor for Linux
|
||||
- [zenith](https://github.com/bvaisvil/zenith): Cross-platform monitoring tool for system stats
|
||||
|
||||
#### 🌌 Other
|
||||
|
||||
- [cotp](https://github.com/replydev/cotp): Command-line TOTP/HOTP authenticator app
|
||||
- [cube timer](https://github.com/paarthmadan/cube): A tui for cube timing, written in Rust
|
||||
- [hg-tui](https://github.com/kaixinbaba/hg-tui): TUI for viewing the hellogithub.com website
|
||||
- [hwatch](https://github.com/blacknon/hwatch): Alternative watch command with command history and diffs
|
||||
- [poketex](https://github.com/ckaznable/poketex): Simple Pokedex based on TUI
|
||||
- [termchat](https://github.com/lemunozm/termchat): Terminal chat through the LAN with video streaming and file transfer
|
||||
229
CHANGELOG.md
229
CHANGELOG.md
@@ -1,6 +1,217 @@
|
||||
# Changelog
|
||||
|
||||
## To be released
|
||||
## v0.21.0 - 2023-05-28
|
||||
|
||||
### Features
|
||||
|
||||
- *(backend)* Add termwiz backend and example ([#5](https://github.com/tui-rs-revival/ratatui/issues/5))
|
||||
- *(block)* Support placing the title on bottom ([#36](https://github.com/tui-rs-revival/ratatui/issues/36))
|
||||
- *(border)* Add border! macro for easy bitflag manipulation ([#11](https://github.com/tui-rs-revival/ratatui/issues/11))
|
||||
- *(calendar)* Add calendar widget ([#138](https://github.com/tui-rs-revival/ratatui/issues/138))
|
||||
- *(color)* Add `FromStr` implementation for `Color` ([#180](https://github.com/tui-rs-revival/ratatui/issues/180))
|
||||
- *(list)* Add len() to List ([#24](https://github.com/tui-rs-revival/ratatui/pull/24))
|
||||
- *(paragraph)* Allow Lines to be individually aligned ([#149](https://github.com/tui-rs-revival/ratatui/issues/149))
|
||||
- *(sparkline)* Finish #1 Sparkline directions PR ([#134](https://github.com/tui-rs-revival/ratatui/issues/134))
|
||||
- *(terminal)* Add inline viewport ([#114](https://github.com/tui-rs-revival/ratatui/issues/114)) [**breaking**]
|
||||
- *(test)* Expose test buffer ([#160](https://github.com/tui-rs-revival/ratatui/issues/160))
|
||||
- *(text)* Add `Masked` to display secure data ([#168](https://github.com/tui-rs-revival/ratatui/issues/168)) [**breaking**]
|
||||
- *(widget)* Add circle widget ([#159](https://github.com/tui-rs-revival/ratatui/issues/159))
|
||||
- *(widget)* Add style methods to Span, Spans, Text ([#148](https://github.com/tui-rs-revival/ratatui/issues/148))
|
||||
- *(widget)* Support adding padding to Block ([#20](https://github.com/tui-rs-revival/ratatui/issues/20))
|
||||
- *(widget)* Add offset() and offset_mut() for table and list state ([#12](https://github.com/tui-rs-revival/ratatui/issues/12))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- *(canvas)* Use full block for Marker::Block ([#133](https://github.com/tui-rs-revival/ratatui/issues/133)) [**breaking**]
|
||||
- *(example)* Update input in examples to only use press events ([#129](https://github.com/tui-rs-revival/ratatui/issues/129))
|
||||
- *(uncategorized)* Cleanup doc example ([#145](https://github.com/tui-rs-revival/ratatui/issues/145))
|
||||
- *(reflow)* Remove debug macro call ([#198](https://github.com/tui-rs-revival/ratatui/issues/198))
|
||||
|
||||
### Refactor
|
||||
|
||||
- *(example)* Remove redundant `vec![]` in `user_input` example ([#26](https://github.com/tui-rs-revival/ratatui/issues/26))
|
||||
- *(example)* Refactor paragraph example ([#152](https://github.com/tui-rs-revival/ratatui/issues/152))
|
||||
- *(style)* Mark some Style fns const so they can be defined globally ([#115](https://github.com/tui-rs-revival/ratatui/issues/115))
|
||||
- *(text)* Replace `Spans` with `Line` ([#178](https://github.com/tui-rs-revival/ratatui/issues/178))
|
||||
|
||||
### Documentation
|
||||
|
||||
- *(apps)* Fix rsadsb/adsb_deku radar link ([#140](https://github.com/tui-rs-revival/ratatui/issues/140))
|
||||
- *(apps)* Add tenere ([#141](https://github.com/tui-rs-revival/ratatui/issues/141))
|
||||
- *(apps)* Add twitch-tui ([#124](https://github.com/tui-rs-revival/ratatui/issues/124))
|
||||
- *(apps)* Add oxycards ([#113](https://github.com/tui-rs-revival/ratatui/issues/113))
|
||||
- *(apps)* Re-add trippy to APPS.md ([#117](https://github.com/tui-rs-revival/ratatui/issues/117))
|
||||
- *(block)* Add example for block.inner ([#158](https://github.com/tui-rs-revival/ratatui/issues/158))
|
||||
- *(changelog)* Update the empty profile link in contributors ([#112](https://github.com/tui-rs-revival/ratatui/issues/112))
|
||||
- *(readme)* Fix small typo in readme ([#186](https://github.com/tui-rs-revival/ratatui/issues/186))
|
||||
- *(readme)* Add termwiz demo to examples ([#183](https://github.com/tui-rs-revival/ratatui/issues/183))
|
||||
- *(readme)* Add acknowledgement section ([#154](https://github.com/tui-rs-revival/ratatui/issues/154))
|
||||
- *(readme)* Update project description ([#127](https://github.com/tui-rs-revival/ratatui/issues/127))
|
||||
- *(uncategorized)* Scrape example code from examples/* ([#195](https://github.com/tui-rs-revival/ratatui/issues/195))
|
||||
|
||||
### Styling
|
||||
|
||||
- *(apps)* Update the style of application list ([#184](https://github.com/tui-rs-revival/ratatui/issues/184))
|
||||
- *(readme)* Update project introduction in README.md ([#153](https://github.com/tui-rs-revival/ratatui/issues/153))
|
||||
- *(uncategorized)* Clippy's variable inlining in format macros
|
||||
|
||||
### Testing
|
||||
|
||||
- *(buffer)* Add `assert_buffer_eq!` and Debug implementation ([#161](https://github.com/tui-rs-revival/ratatui/issues/161))
|
||||
- *(list)* Add characterization tests for list ([#167](https://github.com/tui-rs-revival/ratatui/issues/167))
|
||||
- *(widget)* Add unit tests for Paragraph ([#156](https://github.com/tui-rs-revival/ratatui/issues/156))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- *(uncategorized)* Inline format args ([#190](https://github.com/tui-rs-revival/ratatui/issues/190))
|
||||
- *(uncategorized)* Minor lints, making Clippy happier ([#189](https://github.com/tui-rs-revival/ratatui/issues/189))
|
||||
|
||||
### Build
|
||||
|
||||
- *(uncategorized)* Bump MSRV to 1.65.0 ([#171](https://github.com/tui-rs-revival/ratatui/issues/171))
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- *(uncategorized)* Add ci, build, and revert to allowed commit types
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you so much to everyone that contributed to this release!
|
||||
|
||||
Here is the list of contributors who has contributed to `ratatui` for the first time!
|
||||
|
||||
- [@kpcyrd](https://github.com/kpcyrd)
|
||||
- [@fujiapple852](https://github.com/fujiapple852)
|
||||
- [@BrookJeynes](https://github.com/BrookJeynes)
|
||||
- [@Ziqi-Yang](https://github.com/Ziqi-Yang)
|
||||
- [@Xithrius](https://github.com/Xithrius)
|
||||
- [@lesleyrs](https://github.com/lesleyrs)
|
||||
- [@pythops](https://github.com/pythops)
|
||||
- [@wcampbell0x2a](https://github.com/wcampbell0x2a)
|
||||
- [@sophacles](https://github.com/sophacles)
|
||||
- [@Eyesonjune18](https://github.com/Eyesonjune18)
|
||||
- [@a-kenji](https://github.com/a-kenji)
|
||||
- [@TimerErTim](https://github.com/TimerErTim)
|
||||
- [@Mehrbod2002](https://github.com/Mehrbod2002)
|
||||
- [@thomas-mauran](https://github.com/thomas-mauran)
|
||||
- [@nyurik](https://github.com/nyurik)
|
||||
|
||||
## v0.20.1 - 2023-03-19
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- *(style)* Bold needs a bit ([#104](https://github.com/tui-rs-revival/ratatui/issues/104))
|
||||
|
||||
### Documentation
|
||||
|
||||
- *(apps)* Add "logss" to apps ([#105](https://github.com/tui-rs-revival/ratatui/issues/105))
|
||||
- *(uncategorized)* Fixup remaining tui references ([#106](https://github.com/tui-rs-revival/ratatui/issues/106))
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you so much to everyone that contributed to this release!
|
||||
|
||||
- [@joshka](https://github.com/joshka)
|
||||
- [@todoesverso](https://github.com/todoesverso)
|
||||
- [@UncleScientist](https://github.com/UncleScientist)
|
||||
|
||||
## v0.20.0 - 2023-03-19
|
||||
|
||||
This marks the first release of `ratatui`, a community-maintained fork of [tui](https://github.com/fdehau/tui-rs).
|
||||
|
||||
The purpose of this release is to include **bug fixes** and **small changes** into the repository thus **no new features** are added. We have transferred all the pull requests from the original repository and worked on the low hanging ones to incorporate them in this "maintenance" release.
|
||||
|
||||
Here is a list of changes:
|
||||
|
||||
### Features
|
||||
|
||||
- *(cd)* Add continuous deployment workflow ([#93](https://github.com/tui-rs-revival/ratatui/issues/93))
|
||||
- *(ci)* Add MacOS to CI ([#60](https://github.com/tui-rs-revival/ratatui/issues/60))
|
||||
- *(widget)* Add `offset()` to `TableState` ([#10](https://github.com/tui-rs-revival/ratatui/issues/10))
|
||||
- *(widget)* Add `width()` to ListItem ([#17](https://github.com/tui-rs-revival/ratatui/issues/17))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- *(ci)* Test MSRV compatibility on CI ([#85](https://github.com/tui-rs-revival/ratatui/issues/85))
|
||||
- *(ci)* Bump Rust version to 1.63.0 ([#80](https://github.com/tui-rs-revival/ratatui/issues/80))
|
||||
- *(ci)* Use env for the cargo-make version ([#76](https://github.com/tui-rs-revival/ratatui/issues/76))
|
||||
- *(ci)* Fix deprecation warnings on CI ([#58](https://github.com/tui-rs-revival/ratatui/issues/58))
|
||||
- *(doc)* Add 3rd party libraries accidentally removed at #21 ([#61](https://github.com/tui-rs-revival/ratatui/issues/61))
|
||||
- *(widget)* List should not ignore empty string items ([#42](https://github.com/tui-rs-revival/ratatui/issues/42)) [**breaking**]
|
||||
- *(uncategorized)* Cassowary/layouts: add extra constraints for fixing Min(v)/Max(v) combination. ([#31](https://github.com/tui-rs-revival/ratatui/issues/31))
|
||||
- *(uncategorized)* Fix user_input example double key press registered on windows
|
||||
- *(uncategorized)* Ignore zero-width symbol on rendering `Paragraph`
|
||||
- *(uncategorized)* Fix typos ([#45](https://github.com/tui-rs-revival/ratatui/issues/45))
|
||||
- *(uncategorized)* Fix typos ([#47](https://github.com/tui-rs-revival/ratatui/issues/47))
|
||||
|
||||
### Refactor
|
||||
|
||||
- *(style)* Make bitflags smaller ([#13](https://github.com/tui-rs-revival/ratatui/issues/13))
|
||||
|
||||
### Documentation
|
||||
|
||||
- *(apps)* Move 'apps using ratatui' to dedicated file ([#98](https://github.com/tui-rs-revival/ratatui/issues/98)) ([#99](https://github.com/tui-rs-revival/ratatui/issues/99))
|
||||
- *(canvas)* Add documentation for x_bounds, y_bounds ([#35](https://github.com/tui-rs-revival/ratatui/issues/35))
|
||||
- *(contributing)* Specify the use of unsafe for optimization ([#67](https://github.com/tui-rs-revival/ratatui/issues/67))
|
||||
- *(github)* Remove pull request template ([#68](https://github.com/tui-rs-revival/ratatui/issues/68))
|
||||
- *(readme)* Update crate status badge ([#102](https://github.com/tui-rs-revival/ratatui/issues/102))
|
||||
- *(readme)* Small edits before first release ([#101](https://github.com/tui-rs-revival/ratatui/issues/101))
|
||||
- *(readme)* Add install instruction and update title ([#100](https://github.com/tui-rs-revival/ratatui/issues/100))
|
||||
- *(readme)* Add systeroid to application list ([#92](https://github.com/tui-rs-revival/ratatui/issues/92))
|
||||
- *(readme)* Add glicol-cli to showcase list ([#95](https://github.com/tui-rs-revival/ratatui/issues/95))
|
||||
- *(readme)* Add oxker to application list ([#74](https://github.com/tui-rs-revival/ratatui/issues/74))
|
||||
- *(readme)* Add app kubectl-watch which uses tui ([#73](https://github.com/tui-rs-revival/ratatui/issues/73))
|
||||
- *(readme)* Add poketex to 'apps using tui' in README ([#64](https://github.com/tui-rs-revival/ratatui/issues/64))
|
||||
- *(readme)* Update README.md ([#39](https://github.com/tui-rs-revival/ratatui/issues/39))
|
||||
- *(readme)* Update README.md ([#40](https://github.com/tui-rs-revival/ratatui/issues/40))
|
||||
- *(readme)* Clarify README.md fork status update
|
||||
- *(uncategorized)* Fix: fix typos ([#90](https://github.com/tui-rs-revival/ratatui/issues/90))
|
||||
- *(uncategorized)* Update to build more backends ([#81](https://github.com/tui-rs-revival/ratatui/issues/81))
|
||||
- *(uncategorized)* Expand "Apps" and "Third-party" sections ([#21](https://github.com/tui-rs-revival/ratatui/issues/21))
|
||||
- *(uncategorized)* Add tui-input and update xplr in README.md
|
||||
- *(uncategorized)* Add hncli to list of applications made with tui-rs ([#41](https://github.com/tui-rs-revival/ratatui/issues/41))
|
||||
- *(uncategorized)* Updated readme and contributing guide with updates about the fork ([#46](https://github.com/tui-rs-revival/ratatui/issues/46))
|
||||
|
||||
### Performance
|
||||
|
||||
- *(layout)* Better safe shared layout cache ([#62](https://github.com/tui-rs-revival/ratatui/issues/62))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- *(cargo)* Update project metadata ([#94](https://github.com/tui-rs-revival/ratatui/issues/94))
|
||||
- *(ci)* Integrate `typos` for checking typos ([#91](https://github.com/tui-rs-revival/ratatui/issues/91))
|
||||
- *(ci)* Change the target branch to main ([#79](https://github.com/tui-rs-revival/ratatui/issues/79))
|
||||
- *(ci)* Re-enable clippy on CI ([#59](https://github.com/tui-rs-revival/ratatui/issues/59))
|
||||
- *(uncategorized)* Integrate `committed` for checking conventional commits ([#77](https://github.com/tui-rs-revival/ratatui/issues/77))
|
||||
- *(uncategorized)* Update `rust-version` to 1.59 in Cargo.toml ([#57](https://github.com/tui-rs-revival/ratatui/issues/57))
|
||||
- *(uncategorized)* Update deps ([#51](https://github.com/tui-rs-revival/ratatui/issues/51))
|
||||
- *(uncategorized)* Fix typo in layout.rs ([#619](https://github.com/tui-rs-revival/ratatui/issues/619))
|
||||
- *(uncategorized)* Add apps using `tui`
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you so much to everyone that contributed to this release!
|
||||
|
||||
- [@orhun](https://github.com/orhun)
|
||||
- [@mindoodoo](https://github.com/mindoodoo)
|
||||
- [@sayanarijit](https://github.com/sayanarijit)
|
||||
- [@Owletti](https://github.com/Owletti)
|
||||
- [@UncleScientist](https://github.com/UncleScientist)
|
||||
- [@rhysd](https://github.com/rhysd)
|
||||
- [@ckaznable](https://github.com/ckaznable)
|
||||
- [@imuxin](https://github.com/imuxin)
|
||||
- [@mrjackwills](https://github.com/mrjackwills)
|
||||
- [@conradludgate](https://github.com/conradludgate)
|
||||
- [@kianmeng](https://github.com/kianmeng)
|
||||
- [@chaosprint](https://github.com/chaosprint)
|
||||
|
||||
And most importantly, special thanks to [Florian Dehau](https://github.com/fdehau) for creating this awesome library 💖 We look forward to building on the strong foundations that the original crate laid out.
|
||||
|
||||
## v0.19.0 - 2022-08-14
|
||||
|
||||
### Features
|
||||
|
||||
* Bump `crossterm` to `0.25`
|
||||
|
||||
## v0.18.0 - 2022-04-24
|
||||
|
||||
@@ -12,7 +223,7 @@
|
||||
|
||||
### Features
|
||||
|
||||
* Add option to `widgets::List` to repeat the hightlight symbol for each line of multi-line items (#533).
|
||||
* Add option to `widgets::List` to repeat the highlight symbol for each line of multi-line items (#533).
|
||||
* Add option to control the alignment of `Axis` labels in the `Chart` widget (#568).
|
||||
|
||||
### Breaking changes
|
||||
@@ -270,7 +481,7 @@ In this new release, you may now write this as:
|
||||
```rust
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::Green))
|
||||
// The style is not overidden anymore, we simply add new style rule for the title.
|
||||
// The style is not overridden anymore, we simply add new style rule for the title.
|
||||
.title(Span::styled("My title", Style::default().add_modifier(Modifier::BOLD)))
|
||||
```
|
||||
|
||||
@@ -278,9 +489,9 @@ In addition, the crate now provides a method `patch` to combine two styles into
|
||||
rules:
|
||||
|
||||
```rust
|
||||
let style = Style::default().modifer(Modifier::BOLD);
|
||||
let style = Style::default().modifier(Modifier::BOLD);
|
||||
let style = style.patch(Style::default().add_modifier(Modifier::ITALIC));
|
||||
// style.modifer == Modifier::BOLD | Modifier::ITALIC, the modifier has been enriched not overidden
|
||||
// style.modifier == Modifier::BOLD | Modifier::ITALIC, the modifier has been enriched not overridden
|
||||
```
|
||||
|
||||
- `Style::modifier` has been removed in favor of `Style::add_modifier` and `Style::remove_modifier`.
|
||||
@@ -598,7 +809,7 @@ let style = Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Ensure correct behavoir of the alternate screens with the `Crossterm` backend.
|
||||
* Ensure correct behavior of the alternate screens with the `Crossterm` backend.
|
||||
* Fix out of bounds panic when two `Buffer` are merged.
|
||||
|
||||
## v0.4.0 - 2019-02-03
|
||||
@@ -667,7 +878,7 @@ additional `termion` features.
|
||||
|
||||
* Replace `Item` by a generic and flexible `Text` that can be used in both
|
||||
`Paragraph` and `List` widgets.
|
||||
* Remove unecessary borrows on `Style`.
|
||||
* Remove unnecessary borrows on `Style`.
|
||||
|
||||
## v0.3.0-beta.0 - 2018-09-04
|
||||
|
||||
@@ -690,7 +901,7 @@ widgets on the given `Frame`
|
||||
* All widgets use the consumable builder pattern
|
||||
* `SelectableList` can have no selected item and the highlight symbol is hidden
|
||||
in this case
|
||||
* Remove markup langage inside `Paragraph`. `Paragraph` now expects an iterator
|
||||
* Remove markup language inside `Paragraph`. `Paragraph` now expects an iterator
|
||||
of `Text` items
|
||||
|
||||
## v0.2.3 - 2018-06-09
|
||||
@@ -698,7 +909,7 @@ of `Text` items
|
||||
### Features
|
||||
|
||||
* Add `start_corner` option for `List`
|
||||
* Add more text aligment options for `Paragraph`
|
||||
* Add more text alignment options for `Paragraph`
|
||||
|
||||
## v0.2.2 - 2018-05-06
|
||||
|
||||
|
||||
@@ -1,10 +1,38 @@
|
||||
# Fork Status
|
||||
|
||||
## Pull Requests
|
||||
|
||||
**All** pull requests opened on the original repository have been imported. We'll be going through any open PRs in a timely manner, starting with the **smallest bug fixes and README updates**. If you have an open PR make sure to let us know about it on our [discord](https://discord.gg/pMCEU9hNEj) as it helps to know you are still active.
|
||||
|
||||
## Issues
|
||||
|
||||
We have been unsuccessful in importing all issues opened on the previous repository.
|
||||
For that reason, anyone wanting to **work on or discuss** an issue will have to follow the following workflow :
|
||||
|
||||
- Recreate the issue
|
||||
- Start by referencing the **original issue**: ```Referencing issue #[<issue number>](<original issue link>)```
|
||||
- Then, paste the original issues **opening** text
|
||||
|
||||
You can then resume the conversation by replying to this new issue you have created.
|
||||
|
||||
### Closing Issues
|
||||
|
||||
If you close an issue that you have "imported" to this fork, please make sure that you add the issue to the **CLOSED_ISSUES.md**. This will enable us to keep track of which issues have been closed from the original repo, in case we are able to have the original repository transferred.
|
||||
|
||||
# Contributing
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Use of unsafe for optimization purposes
|
||||
|
||||
**Do not** use unsafe to achieve better performances. This is subject to change, [see.](https://github.com/tui-rs-revival/tui-rs-revival/discussions/66)
|
||||
The only exception to this rule is if it's to fix **reproducible slowness.**
|
||||
|
||||
## Building
|
||||
|
||||
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
|
||||
|
||||
`tui` is an ordinary Rust project where common tasks are managed with [cargo-make].
|
||||
`ratatui` is an ordinary Rust project where common tasks are managed with [cargo-make].
|
||||
It wraps common `cargo` commands with sane defaults depending on your platform of choice.
|
||||
Building the project should be as easy as running `cargo make build`.
|
||||
|
||||
@@ -28,6 +56,6 @@ You can also check most of those things yourself locally using `cargo make ci` w
|
||||
## Tests
|
||||
|
||||
The test coverage of the crate is far from being ideal but we already have a fair amount of tests in place.
|
||||
Beside the usual doc and unit tests, one of the most valuable test you can write for `tui` is a test again the `TestBackend`.
|
||||
Beside the usual doc and unit tests, one of the most valuable test you can write for `ratatui` is a test against the `TestBackend`.
|
||||
It allows you to assert the content of the output buffer that would have been flushed to the terminal after a given draw call.
|
||||
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.
|
||||
|
||||
56
Cargo.toml
56
Cargo.toml
@@ -1,93 +1,135 @@
|
||||
[package]
|
||||
name = "tui"
|
||||
version = "0.18.0"
|
||||
name = "ratatui"
|
||||
version = "0.21.0"
|
||||
authors = ["Florian Dehau <work@fdehau.com>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
"""
|
||||
documentation = "https://docs.rs/tui/0.18.0/tui/"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/fdehau/tui-rs"
|
||||
repository = "https://github.com/tui-rs-revival/ratatui"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
autoexamples = true
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
[badges]
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
all-widgets = ["widget-calendar"]
|
||||
widget-calendar = ["time"]
|
||||
macros = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.3"
|
||||
cassowary = "0.3"
|
||||
unicode-segmentation = "1.2"
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-width = "0.1"
|
||||
termion = { version = "1.5", optional = true }
|
||||
crossterm = { version = "0.23", optional = true }
|
||||
termion = { version = "2.0", optional = true }
|
||||
crossterm = { version = "0.26", optional = true }
|
||||
termwiz = { version = "0.20.0", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"]}
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"]}
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
argh = "0.1"
|
||||
indoc = "2.0"
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calendar"
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "chart"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "demo"
|
||||
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "panic"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "paragraph"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "popup"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "sparkline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "table"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "tabs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
@@ -13,10 +13,13 @@ dependencies = [
|
||||
"fmt",
|
||||
"check-crossterm",
|
||||
"check-termion",
|
||||
"check-termwiz",
|
||||
"test-crossterm",
|
||||
"test-termion",
|
||||
"test-termwiz",
|
||||
"clippy-crossterm",
|
||||
"clippy-termion",
|
||||
"clippy-termwiz",
|
||||
"test-doc",
|
||||
]
|
||||
|
||||
@@ -25,8 +28,11 @@ private = true
|
||||
dependencies = [
|
||||
"fmt",
|
||||
"check-crossterm",
|
||||
"check-termwiz",
|
||||
"test-crossterm",
|
||||
"test-termwiz",
|
||||
"clippy-crossterm",
|
||||
"clippy-termwiz",
|
||||
"test-doc",
|
||||
]
|
||||
|
||||
@@ -47,6 +53,10 @@ run_task = "check"
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "check"
|
||||
|
||||
[tasks.check-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz" }
|
||||
run_task = "check"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
@@ -66,6 +76,10 @@ run_task = "build"
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "build"
|
||||
|
||||
[tasks.build-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz" }
|
||||
run_task = "build"
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
@@ -85,6 +99,10 @@ run_task = "clippy"
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "clippy"
|
||||
|
||||
[tasks.clippy-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz" }
|
||||
run_task = "clippy"
|
||||
|
||||
[tasks.clippy]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
@@ -100,11 +118,15 @@ args = [
|
||||
]
|
||||
|
||||
[tasks.test-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
env = { TUI_FEATURES = "serde,crossterm,all-widgets,macros" }
|
||||
run_task = "test"
|
||||
|
||||
[tasks.test-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
env = { TUI_FEATURES = "serde,termion,all-widgets,macros" }
|
||||
run_task = "test"
|
||||
|
||||
[tasks.test-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz,all-widgets,macros" }
|
||||
run_task = "test"
|
||||
|
||||
[tasks.test]
|
||||
|
||||
142
README.md
142
README.md
@@ -1,19 +1,43 @@
|
||||
# tui-rs
|
||||
# ratatui
|
||||
|
||||
[](https://github.com/fdehau/tui-rs/actions?query=workflow%3ACI+)
|
||||
[](https://crates.io/crates/tui)
|
||||
[](https://docs.rs/crate/tui/)
|
||||
An actively maintained `tui-rs` fork.
|
||||
|
||||
[](https://github.com/tui-rs-revival/ratatui/actions?query=workflow%3ACI+)
|
||||
[](https://crates.io/crates/ratatui)
|
||||
[](https://docs.rs/crate/ratatui/)
|
||||
|
||||
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
|
||||
|
||||
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
|
||||
# Install
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tui = { package = "ratatui" }
|
||||
```
|
||||
|
||||
# What is this fork?
|
||||
|
||||
This fork was created to continue maintenance on the original TUI project. The original maintainer had created an [issue](https://github.com/fdehau/tui-rs/issues/654) explaining how he couldn't find time to continue development, which led to us creating this fork.
|
||||
|
||||
With that in mind, **we the community** look forward to continuing the work started by [**Florian Dehau.**](https://github.com/fdehau) :rocket:
|
||||
|
||||
In order to organize ourselves, we currently use a [discord server](https://discord.gg/pMCEU9hNEj), feel free to join and come chat ! There are also plans to implement a [matrix](https://matrix.org/) bridge in the near future.
|
||||
**Discord is not a MUST to contribute,** we follow a pretty standard github centered open source workflow keeping the most important conversations on github, open an issue or PR and it will be addressed. :smile:
|
||||
|
||||
Please make sure you read the updated contributing guidelines, especially if you are interested in working on a PR or issue opened in the previous repository.
|
||||
|
||||
# Introduction
|
||||
|
||||
`ratatui` is a [Rust](https://www.rust-lang.org) library to build rich terminal
|
||||
user interfaces and dashboards. It is heavily inspired by the `Javascript`
|
||||
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
|
||||
`Go` library [termui](https://github.com/gizak/termui).
|
||||
|
||||
The library supports multiple backends:
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||
- [termion](https://github.com/ticki/termion)
|
||||
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||
- [termion](https://github.com/ticki/termion)
|
||||
- [termwiz](https://github.com/wez/wezterm/tree/master/termwiz)
|
||||
|
||||
The library is based on the principle of immediate rendering with intermediate
|
||||
buffers. This means that at each new frame you should build all widgets that are
|
||||
@@ -26,13 +50,15 @@ comes from the terminal emulator than the library itself.
|
||||
Moreover, the library does not provide any input handling nor any event system and
|
||||
you may rely on the previously cited libraries to achieve such features.
|
||||
|
||||
### Rust version requirements
|
||||
## Rust version requirements
|
||||
|
||||
Since version 0.17.0, `tui` requires **rustc version 1.56.1 or greater**.
|
||||
Since version 0.21.0, `ratatui` requires **rustc version 1.65.0 or greater**.
|
||||
|
||||
### [Documentation](https://docs.rs/tui)
|
||||
# Documentation
|
||||
|
||||
### Demo
|
||||
The documentation can be found on [docs.rs.](https://docs.rs/ratatui)
|
||||
|
||||
# Demo
|
||||
|
||||
The demo shown in the gif can be run with all available backends.
|
||||
|
||||
@@ -41,12 +67,14 @@ The demo shown in the gif can be run with all available backends.
|
||||
cargo run --example demo --release -- --tick-rate 200
|
||||
# termion
|
||||
cargo run --example demo --no-default-features --features=termion --release -- --tick-rate 200
|
||||
# termwiz
|
||||
cargo run --example demo --no-default-features --features=termwiz --release -- --tick-rate 200
|
||||
```
|
||||
|
||||
where `tick-rate` is the UI refresh rate in ms.
|
||||
|
||||
The UI code is in [examples/demo/ui.rs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/demo/ui.rs) while the
|
||||
application state is in [examples/demo/app.rs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/demo/app.rs).
|
||||
The UI code is in [examples/demo/ui.rs](https://github.com/tui-rs-revival/ratatui/blob/main/examples/demo/ui.rs) while the
|
||||
application state is in [examples/demo/app.rs](https://github.com/tui-rs-revival/ratatui/blob/main/examples/demo/app.rs).
|
||||
|
||||
If the user interface contains glyphs that are not displayed correctly by your terminal, you may want to run
|
||||
the demo without those symbols:
|
||||
@@ -55,72 +83,60 @@ the demo without those symbols:
|
||||
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
|
||||
```
|
||||
|
||||
### Widgets
|
||||
# Widgets
|
||||
|
||||
## Built in
|
||||
|
||||
The library comes with the following list of widgets:
|
||||
|
||||
* [Block](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/block.rs)
|
||||
* [Gauge](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/gauge.rs)
|
||||
* [Sparkline](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/sparkline.rs)
|
||||
* [Chart](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/chart.rs)
|
||||
* [BarChart](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/barchart.rs)
|
||||
* [List](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/list.rs)
|
||||
* [Table](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/table.rs)
|
||||
* [Paragraph](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/paragraph.rs)
|
||||
* [Canvas (with line, point cloud, map)](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/canvas.rs)
|
||||
* [Tabs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/tabs.rs)
|
||||
- [Block](https://github.com/tui-rs-revival/ratatui/blob/main/examples/block.rs)
|
||||
- [Gauge](https://github.com/tui-rs-revival/ratatui/blob/main/examples/gauge.rs)
|
||||
- [Sparkline](https://github.com/tui-rs-revival/ratatui/blob/main/examples/sparkline.rs)
|
||||
- [Chart](https://github.com/tui-rs-revival/ratatui/blob/main/examples/chart.rs)
|
||||
- [BarChart](https://github.com/tui-rs-revival/ratatui/blob/main/examples/barchart.rs)
|
||||
- [List](https://github.com/tui-rs-revival/ratatui/blob/main/examples/list.rs)
|
||||
- [Table](https://github.com/tui-rs-revival/ratatui/blob/main/examples/table.rs)
|
||||
- [Paragraph](https://github.com/tui-rs-revival/ratatui/blob/main/examples/paragraph.rs)
|
||||
- [Canvas (with line, point cloud, map)](https://github.com/tui-rs-revival/ratatui/blob/main/examples/canvas.rs)
|
||||
- [Tabs](https://github.com/tui-rs-revival/ratatui/blob/main/examples/tabs.rs)
|
||||
|
||||
Click on each item to see the source of the example. Run the examples with with
|
||||
Click on each item to see the source of the example. Run the examples with
|
||||
cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
|
||||
|
||||
You can run all examples by running `cargo make run-examples` (require
|
||||
`cargo-make` that can be installed with `cargo install cargo-make`).
|
||||
|
||||
### Third-party widgets
|
||||
### Third-party libraries, bootstrapping templates and widgets
|
||||
|
||||
* [tui-logger](https://github.com/gin66/tui-logger)
|
||||
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to `tui::text::Text`
|
||||
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to `tui::style::Color`
|
||||
- [rust-tui-template](https://github.com/orhun/rust-tui-template) — A template for bootstrapping a Rust TUI application with Tui-rs & crossterm
|
||||
- [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
|
||||
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for Tui-rs + Crossterm apps
|
||||
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
|
||||
- [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
|
||||
- [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
|
||||
- [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications with a React/Elm inspired approach
|
||||
- [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for Tui-realm
|
||||
- [tui tree widget](https://github.com/EdJoPaTo/tui-rs-tree-widget) — Tree Widget for Tui-rs
|
||||
- [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple windows and their rendering
|
||||
- [tui-textarea](https://github.com/rhysd/tui-textarea): Simple yet powerful multi-line text editor widget supporting several key shortcuts, undo/redo, text search, etc.
|
||||
- [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget): Widget for tree data structures.
|
||||
- [tui-input](https://github.com/sayanarijit/tui-input): TUI input library supporting multiple backends and tui-rs.
|
||||
|
||||
### Apps using tui
|
||||
# Apps
|
||||
|
||||
* [spotify-tui](https://github.com/Rigellute/spotify-tui)
|
||||
* [bandwhich](https://github.com/imsnif/bandwhich)
|
||||
* [kmon](https://github.com/orhun/kmon)
|
||||
* [gpg-tui](https://github.com/orhun/gpg-tui)
|
||||
* [ytop](https://github.com/cjbassi/ytop)
|
||||
* [zenith](https://github.com/bvaisvil/zenith)
|
||||
* [bottom](https://github.com/ClementTsang/bottom)
|
||||
* [oha](https://github.com/hatoo/oha)
|
||||
* [gitui](https://github.com/extrawurst/gitui)
|
||||
* [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli)
|
||||
* [desed](https://github.com/SoptikHa2/desed)
|
||||
* [diskonaut](https://github.com/imsnif/diskonaut)
|
||||
* [tickrs](https://github.com/tarkah/tickrs)
|
||||
* [rusty-krab-manager](https://github.com/aryakaul/rusty-krab-manager)
|
||||
* [termchat](https://github.com/lemunozm/termchat)
|
||||
* [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui)
|
||||
* [gping](https://github.com/orf/gping/)
|
||||
* [Vector](https://vector.dev)
|
||||
* [KDash](https://github.com/kdash-rs/kdash)
|
||||
* [xplr](https://github.com/sayanarijit/xplr)
|
||||
* [minesweep](https://github.com/cpcloud/minesweep-rs)
|
||||
* [Battleship.rs](https://github.com/deepu105/battleship-rs)
|
||||
* [termscp](https://github.com/veeso/termscp)
|
||||
* [joshuto](https://github.com/kamiyaa/joshuto)
|
||||
* [adsb_deku/radar](https://github.com/wcampbell0x2a/adsb_deku#radar-tui)
|
||||
* [hoard](https://github.com/Hyde46/hoard)
|
||||
* [tokio-console](https://github.com/tokio-rs/console): a diagnostics and debugging tool for asynchronous Rust programs.
|
||||
* [hwatch](https://github.com/blacknon/hwatch): a alternative watch command that records the result of command execution and can display its history and diffs.
|
||||
* [ytui-music](https://github.com/sudipghimire533/ytui-music): listen to music from youtube inside your terminal.
|
||||
* [mqttui](https://github.com/EdJoPaTo/mqttui): subscribe or publish to a MQTT Topic quickly from the terminal.
|
||||
* [meteo-tui](https://github.com/16arpi/meteo-tui): french weather via the command line.
|
||||
* [picterm](https://github.com/ksk001100/picterm): preview images in your terminal.
|
||||
* [gobang](https://github.com/TaKO8Ki/gobang): a cross-platform TUI database management tool.
|
||||
Check out the list of [close to 40 apps](./APPS.md) using `ratatui`!
|
||||
|
||||
### Alternatives
|
||||
# Alternatives
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an
|
||||
alternative solution to build text user interfaces in Rust.
|
||||
|
||||
## License
|
||||
# Acknowledgements
|
||||
|
||||
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an awesome logo** for the ratatui project and tui-rs-revival organization.
|
||||
|
||||
# License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
10
RELEASE.md
Normal file
10
RELEASE.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Creating a Release
|
||||
|
||||
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub actions](.github/workflows/cd.yml) and triggered by pushing a tag.
|
||||
|
||||
1. Bump the version in [Cargo.toml](Cargo.toml).
|
||||
2. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff) can be used for generating the entries.
|
||||
3. Commit and push the changes.
|
||||
4. Create a new tag: `git tag -a v[X.Y.Z]`
|
||||
5. Push the tag: `git push --tags`
|
||||
6. Wait for [Continuous Deployment](https://github.com/tui-rs-revival/ratatui/actions) workflow to finish.
|
||||
87
cliff.toml
Normal file
87
cliff.toml
Normal file
@@ -0,0 +1,87 @@
|
||||
# configuration file for git-cliff
|
||||
# see https://github.com/orhun/git-cliff#configuration-file
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits
|
||||
| filter(attribute="scope")
|
||||
| sort(attribute="scope") %}
|
||||
- *({{commit.scope}})* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- endfor -%}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{%- for commit in commits %}
|
||||
{%- if commit.scope -%}
|
||||
{% else -%}
|
||||
- *(uncategorized)* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/tui-rs-revival/ratatui/issues/${2}))"},
|
||||
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
|
||||
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" }
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 00 -->Features"},
|
||||
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes"},
|
||||
{ message = "^refactor", group = "<!-- 02 -->Refactor"},
|
||||
{ message = "^doc", group = "<!-- 03 -->Documentation"},
|
||||
{ message = "^perf", group = "<!-- 04 -->Performance"},
|
||||
{ message = "^style", group = "<!-- 05 -->Styling"},
|
||||
{ message = "^test", group = "<!-- 06 -->Testing"},
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true},
|
||||
{ message = "^chore\\(pr\\)", skip = true},
|
||||
{ message = "^chore\\(pull\\)", skip = true},
|
||||
{ message = "^chore", group = "<!-- 07 -->Miscellaneous Tasks"},
|
||||
{ body = ".*security", group = "<!-- 08 -->Security"},
|
||||
{ message = "^build", group = "<!-- 09 -->Build"},
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration"},
|
||||
{ message = "^revert", group = "<!-- 11 -->Reverted Commits"},
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-rc.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "newest"
|
||||
18
committed.toml
Normal file
18
committed.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
# configuration for https://github.com/crate-ci/committed
|
||||
|
||||
# https://www.conventionalcommits.org
|
||||
style="conventional"
|
||||
# disallow merge commits
|
||||
merge_commit = false
|
||||
# subject is not required to be capitalized
|
||||
subject_capitalized = false
|
||||
# subject should start with an imperative verb
|
||||
imperative_subject = true
|
||||
# subject should not end with a punctuation
|
||||
subject_not_punctuated = true
|
||||
# disable line length
|
||||
line_length = 0
|
||||
# disable subject length
|
||||
subject_length = 0
|
||||
# default allowed_types [ "chore", "docs", "feat", "fix", "perf", "refactor", "style", "test" ]
|
||||
allowed_types = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test" ]
|
||||
@@ -3,18 +3,18 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{BarChart, Block, Borders},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
data: Vec<(&'a str, u64)>,
|
||||
@@ -81,7 +81,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -3,15 +3,15 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, BorderType, Borders},
|
||||
widgets::{Block, BorderType, Borders, Padding, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
@@ -34,7 +34,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -106,14 +106,32 @@ fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
.split(chunks[1]);
|
||||
|
||||
// Bottom left block with all default borders
|
||||
let block = Block::default().title("With borders").borders(Borders::ALL);
|
||||
f.render_widget(block, bottom_chunks[0]);
|
||||
let block = Block::default()
|
||||
.title("With borders")
|
||||
.borders(Borders::ALL)
|
||||
.padding(Padding {
|
||||
left: 4,
|
||||
right: 4,
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
});
|
||||
|
||||
let text = Paragraph::new("text inside padded block").block(block);
|
||||
f.render_widget(text, bottom_chunks[0]);
|
||||
|
||||
// Bottom right block with styled left and right border
|
||||
let block = Block::default()
|
||||
.title("With styled borders and doubled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.border_type(BorderType::Double);
|
||||
.border_type(BorderType::Double)
|
||||
.padding(Padding::uniform(1));
|
||||
|
||||
let inner_block = Block::default()
|
||||
.title("Block inside padded block")
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let inner_area = block.inner(bottom_chunks[1]);
|
||||
f.render_widget(block, bottom_chunks[1]);
|
||||
f.render_widget(inner_block, inner_area);
|
||||
}
|
||||
|
||||
286
examples/calendar.rs
Normal file
286
examples/calendar.rs
Normal file
@@ -0,0 +1,286 @@
|
||||
use std::{error::Error, io, rc::Rc};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use time::{Date, Month, OffsetDateTime};
|
||||
|
||||
use ratatui::widgets::calendar::{CalendarEventStore, DateStyler, Monthly};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
loop {
|
||||
let _ = terminal.draw(|f| draw(f));
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
#[allow(clippy::single_match)]
|
||||
match key.code {
|
||||
KeyCode::Char(_) => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw<B: Backend>(f: &mut Frame<B>) {
|
||||
let app_area = f.size();
|
||||
|
||||
let calarea = Rect {
|
||||
x: app_area.x + 1,
|
||||
y: app_area.y + 1,
|
||||
height: app_area.height - 1,
|
||||
width: app_area.width - 1,
|
||||
};
|
||||
|
||||
let mut start = OffsetDateTime::now_local()
|
||||
.unwrap()
|
||||
.date()
|
||||
.replace_month(Month::January)
|
||||
.unwrap()
|
||||
.replace_day(1)
|
||||
.unwrap();
|
||||
|
||||
let list = make_dates(start.year());
|
||||
|
||||
for chunk in split_rows(&calarea)
|
||||
.iter()
|
||||
.flat_map(|row| split_cols(row).to_vec())
|
||||
{
|
||||
let cal = cals::get_cal(start.month(), start.year(), &list);
|
||||
f.render_widget(cal, chunk);
|
||||
start = start.replace_month(start.month().next()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn split_rows(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
|
||||
fn split_cols(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
|
||||
fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
let mut list = CalendarEventStore::today(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Blue),
|
||||
);
|
||||
|
||||
// Holidays
|
||||
let holiday_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
|
||||
// new year's
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::January, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// next new_year's for December "show surrounding"
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year + 1, Month::January, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// groundhog day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::February, 2).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// april fool's
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::April, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// earth day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::April, 22).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// star wars day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::May, 4).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// festivus
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 23).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// new year's eve
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 31).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
|
||||
// seasons
|
||||
let season_style = Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
// spring equinox
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::March, 22).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
// summer solstice
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::June, 21).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
// fall equinox
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::September, 22).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 21).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
list
|
||||
}
|
||||
|
||||
mod cals {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
use Month::*;
|
||||
match m {
|
||||
May => example1(m, y, es),
|
||||
June => example2(m, y, es),
|
||||
July => example3(m, y, es),
|
||||
December => example3(m, y, es),
|
||||
February => example4(m, y, es),
|
||||
November => example5(m, y, es),
|
||||
_ => default(m, y, es),
|
||||
}
|
||||
}
|
||||
|
||||
fn default<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_month_header(Style::default())
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example1<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_surrounding(default_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example2<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::DIM)
|
||||
.fg(Color::LightYellow);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example3<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_surrounding(Style::default().add_modifier(Modifier::DIM))
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example4<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example5<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_month_header(header_style)
|
||||
.default_style(default_style)
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,11 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
symbols::Marker,
|
||||
text::Span,
|
||||
widgets::{
|
||||
canvas::{Canvas, Map, MapResolution, Rectangle},
|
||||
@@ -19,6 +15,11 @@ use tui::{
|
||||
},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
struct App {
|
||||
x: f64,
|
||||
@@ -29,6 +30,8 @@ struct App {
|
||||
vy: f64,
|
||||
dir_x: bool,
|
||||
dir_y: bool,
|
||||
tick_count: u64,
|
||||
marker: Marker,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -48,10 +51,22 @@ impl App {
|
||||
vy: 1.0,
|
||||
dir_x: true,
|
||||
dir_y: true,
|
||||
tick_count: 0,
|
||||
marker: Marker::Dot,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.tick_count += 1;
|
||||
// only change marker every 4 ticks (1s) to avoid stroboscopic effect
|
||||
if (self.tick_count % 4) == 0 {
|
||||
self.marker = match self.marker {
|
||||
Marker::Dot => Marker::Block,
|
||||
Marker::Block => Marker::Bar,
|
||||
Marker::Bar => Marker::Braille,
|
||||
Marker::Braille => Marker::Dot,
|
||||
};
|
||||
}
|
||||
if self.ball.x < self.playground.left() as f64
|
||||
|| self.ball.x + self.ball.width > self.playground.right() as f64
|
||||
{
|
||||
@@ -100,7 +115,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -155,6 +170,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
.split(f.size());
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.marker(app.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
@@ -171,6 +187,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
f.render_widget(canvas, chunks[0]);
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.marker(app.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&app.ball);
|
||||
})
|
||||
|
||||
@@ -3,12 +3,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -17,6 +12,11 @@ use tui::{
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
|
||||
const DATA2: [(f64, f64); 7] = [
|
||||
@@ -117,7 +117,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -3,8 +3,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
@@ -12,6 +11,7 @@ use tui::{
|
||||
widgets::Widget,
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Label<'a> {
|
||||
@@ -52,7 +52,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,7 +2,7 @@ use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use tui::widgets::ListState;
|
||||
use ratatui::widgets::ListState;
|
||||
|
||||
const TASKS: [&str; 24] = [
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use crate::{app::App, ui};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
Terminal,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
@@ -36,7 +36,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -56,13 +56,15 @@ fn run_app<B: Backend>(
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,18 @@ mod app;
|
||||
mod crossterm;
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termwiz")]
|
||||
mod termwiz;
|
||||
|
||||
mod ui;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
use crate::crossterm::run;
|
||||
#[cfg(feature = "termion")]
|
||||
use crate::termion::run;
|
||||
#[cfg(feature = "termwiz")]
|
||||
use crate::termwiz::run;
|
||||
|
||||
use argh::FromArgs;
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
use crate::{app::App, ui};
|
||||
use ratatui::{
|
||||
backend::{Backend, TermionBackend},
|
||||
Terminal,
|
||||
};
|
||||
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
|
||||
use termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::AlternateScreen,
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, TermionBackend},
|
||||
Terminal,
|
||||
screen::IntoAlternateScreen,
|
||||
};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
let stdout = io::stdout().into_raw_mode()?;
|
||||
let stdout = io::stdout()
|
||||
.into_raw_mode()
|
||||
.unwrap()
|
||||
.into_alternate_screen()
|
||||
.unwrap();
|
||||
let stdout = MouseTerminal::from(stdout);
|
||||
let stdout = AlternateScreen::from(stdout);
|
||||
let backend = TermionBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
@@ -64,14 +67,14 @@ fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
|
||||
let stdin = io::stdin();
|
||||
for key in stdin.keys().flatten() {
|
||||
if let Err(err) = keys_tx.send(Event::Input(key)) {
|
||||
eprintln!("{}", err);
|
||||
eprintln!("{err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
thread::spawn(move || loop {
|
||||
if let Err(err) = tx.send(Event::Tick) {
|
||||
eprintln!("{}", err);
|
||||
eprintln!("{err}");
|
||||
break;
|
||||
}
|
||||
thread::sleep(tick_rate);
|
||||
|
||||
75
examples/demo/termwiz.rs
Normal file
75
examples/demo/termwiz.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use ratatui::{backend::TermwizBackend, Terminal};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
let backend = TermwizBackend::new()?;
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Termwiz Demo", enhanced_graphics);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
terminal.show_cursor()?;
|
||||
terminal.flush()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app(
|
||||
terminal: &mut Terminal<TermwizBackend>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if let Ok(Some(input)) = terminal
|
||||
.backend_mut()
|
||||
.buffered_terminal_mut()
|
||||
.terminal()
|
||||
.poll_input(Some(timeout))
|
||||
{
|
||||
match input {
|
||||
InputEvent::Key(key_code) => match key_code.key {
|
||||
KeyCode::UpArrow => app.on_up(),
|
||||
KeyCode::DownArrow => app.on_down(),
|
||||
KeyCode::LeftArrow => app.on_left(),
|
||||
KeyCode::RightArrow => app.on_right(),
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
_ => {}
|
||||
},
|
||||
InputEvent::Resized { cols, rows } => {
|
||||
terminal
|
||||
.backend_mut()
|
||||
.buffered_terminal_mut()
|
||||
.resize(cols, rows);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::app::App;
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
|
||||
text::{Line, Span},
|
||||
widgets::canvas::{Canvas, Circle, Line as CanvasLine, Map, MapResolution, Rectangle},
|
||||
widgets::{
|
||||
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
|
||||
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
|
||||
@@ -21,7 +21,7 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
.tabs
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| Spans::from(Span::styled(*t, Style::default().fg(Color::Green))))
|
||||
.map(|t| Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title(app.title))
|
||||
@@ -137,7 +137,7 @@ where
|
||||
.tasks
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| ListItem::new(vec![Spans::from(Span::raw(*i))]))
|
||||
.map(|i| ListItem::new(vec![Line::from(Span::raw(*i))]))
|
||||
.collect();
|
||||
let tasks = List::new(tasks)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
@@ -161,8 +161,8 @@ where
|
||||
"WARNING" => warning_style,
|
||||
_ => info_style,
|
||||
};
|
||||
let content = vec![Spans::from(vec![
|
||||
Span::styled(format!("{:<9}", level), s),
|
||||
let content = vec![Line::from(vec![
|
||||
Span::styled(format!("{level:<9}"), s),
|
||||
Span::raw(evt),
|
||||
])];
|
||||
ListItem::new(content)
|
||||
@@ -261,9 +261,9 @@ where
|
||||
B: Backend,
|
||||
{
|
||||
let text = vec![
|
||||
Spans::from("This is a paragraph with several lines. You can change style your text the way you want"),
|
||||
Spans::from(""),
|
||||
Spans::from(vec![
|
||||
Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::from("For example: "),
|
||||
Span::styled("under", Style::default().fg(Color::Red)),
|
||||
Span::raw(" "),
|
||||
@@ -272,7 +272,7 @@ where
|
||||
Span::styled("rainbow", Style::default().fg(Color::Blue)),
|
||||
Span::raw("."),
|
||||
]),
|
||||
Spans::from(vec![
|
||||
Line::from(vec![
|
||||
Span::raw("Oh and if you didn't "),
|
||||
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
|
||||
Span::raw(" you can "),
|
||||
@@ -283,7 +283,7 @@ where
|
||||
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
|
||||
Span::raw(".")
|
||||
]),
|
||||
Spans::from(
|
||||
Line::from(
|
||||
"One more thing is that it should display unicode characters: 10€"
|
||||
),
|
||||
];
|
||||
@@ -346,9 +346,15 @@ where
|
||||
height: 10.0,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Circle {
|
||||
x: app.servers[2].coords.1,
|
||||
y: app.servers[2].coords.0,
|
||||
radius: 10.0,
|
||||
color: Color::Green,
|
||||
});
|
||||
for (i, s1) in app.servers.iter().enumerate() {
|
||||
for s2 in &app.servers[i + 1..] {
|
||||
ctx.draw(&Line {
|
||||
ctx.draw(&CanvasLine {
|
||||
x1: s1.coords.1,
|
||||
y1: s1.coords.0,
|
||||
y2: s2.coords.0,
|
||||
@@ -411,7 +417,7 @@ where
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let cells = vec![
|
||||
Cell::from(Span::raw(format!("{:?}: ", c))),
|
||||
Cell::from(Span::raw(format!("{c:?}: "))),
|
||||
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
|
||||
Cell::from(Span::styled("Background", Style::default().bg(*c))),
|
||||
];
|
||||
|
||||
@@ -3,12 +3,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -16,6 +11,11 @@ use tui::{
|
||||
widgets::{Block, Borders, Gauge},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
struct App {
|
||||
progress1: u16,
|
||||
@@ -77,7 +77,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
292
examples/inline.rs
Normal file
292
examples/inline.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
|
||||
Frame, Terminal, TerminalOptions, Viewport,
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
error::Error,
|
||||
io,
|
||||
sync::mpsc,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const NUM_DOWNLOADS: usize = 10;
|
||||
|
||||
type DownloadId = usize;
|
||||
type WorkerId = usize;
|
||||
|
||||
enum Event {
|
||||
Input(crossterm::event::KeyEvent),
|
||||
Tick,
|
||||
Resize,
|
||||
DownloadUpdate(WorkerId, DownloadId, f64),
|
||||
DownloadDone(WorkerId, DownloadId),
|
||||
}
|
||||
|
||||
struct Downloads {
|
||||
pending: VecDeque<Download>,
|
||||
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
|
||||
}
|
||||
|
||||
impl Downloads {
|
||||
fn next(&mut self, worker_id: WorkerId) -> Option<Download> {
|
||||
match self.pending.pop_front() {
|
||||
Some(d) => {
|
||||
self.in_progress.insert(
|
||||
worker_id,
|
||||
DownloadInProgress {
|
||||
id: d.id,
|
||||
started_at: Instant::now(),
|
||||
progress: 0.0,
|
||||
},
|
||||
);
|
||||
Some(d)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadInProgress {
|
||||
id: DownloadId,
|
||||
started_at: Instant,
|
||||
progress: f64,
|
||||
}
|
||||
|
||||
struct Download {
|
||||
id: DownloadId,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
id: WorkerId,
|
||||
tx: mpsc::Sender<Download>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
let stdout = io::stdout();
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(8),
|
||||
},
|
||||
)?;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
input_handling(tx.clone());
|
||||
let workers = workers(tx);
|
||||
let mut downloads = downloads();
|
||||
|
||||
for w in &workers {
|
||||
let d = downloads.next(w.id).unwrap();
|
||||
w.tx.send(d).unwrap();
|
||||
}
|
||||
|
||||
run_app(&mut terminal, workers, downloads, rx)?;
|
||||
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
terminal.clear()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_handling(tx: mpsc::Sender<Event>) {
|
||||
let tick_rate = Duration::from_millis(200);
|
||||
thread::spawn(move || {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
// poll for tick rate duration, if no events, sent tick event.
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout).unwrap() {
|
||||
match crossterm::event::read().unwrap() {
|
||||
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
|
||||
crossterm::event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
|
||||
(0..4)
|
||||
.map(|id| {
|
||||
let (worker_tx, worker_rx) = mpsc::channel::<Download>();
|
||||
let tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
while let Ok(download) = worker_rx.recv() {
|
||||
let mut remaining = download.size;
|
||||
while remaining > 0 {
|
||||
let wait = (remaining as u64).min(10);
|
||||
thread::sleep(Duration::from_millis(wait * 10));
|
||||
remaining = remaining.saturating_sub(10);
|
||||
let progress = (download.size - remaining) * 100 / download.size;
|
||||
tx.send(Event::DownloadUpdate(id, download.id, progress as f64))
|
||||
.unwrap();
|
||||
}
|
||||
tx.send(Event::DownloadDone(id, download.id)).unwrap();
|
||||
}
|
||||
});
|
||||
Worker { id, tx: worker_tx }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn downloads() -> Downloads {
|
||||
let distribution = Uniform::new(0, 1000);
|
||||
let mut rng = rand::thread_rng();
|
||||
let pending = (0..NUM_DOWNLOADS)
|
||||
.map(|id| {
|
||||
let size = distribution.sample(&mut rng);
|
||||
Download { id, size }
|
||||
})
|
||||
.collect();
|
||||
Downloads {
|
||||
pending,
|
||||
in_progress: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
workers: Vec<Worker>,
|
||||
mut downloads: Downloads,
|
||||
rx: mpsc::Receiver<Event>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut redraw = true;
|
||||
loop {
|
||||
if redraw {
|
||||
terminal.draw(|f| ui(f, &downloads))?;
|
||||
}
|
||||
redraw = true;
|
||||
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => {
|
||||
if event.code == crossterm::event::KeyCode::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Resize => {
|
||||
terminal.autoresize()?;
|
||||
}
|
||||
Event::Tick => {}
|
||||
Event::DownloadUpdate(worker_id, _download_id, progress) => {
|
||||
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
|
||||
download.progress = progress;
|
||||
redraw = false
|
||||
}
|
||||
Event::DownloadDone(worker_id, download_id) => {
|
||||
let download = downloads.in_progress.remove(&worker_id).unwrap();
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::from("Finished "),
|
||||
Span::styled(
|
||||
format!("download {download_id}"),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::from(format!(
|
||||
" in {}ms",
|
||||
download.started_at.elapsed().as_millis()
|
||||
)),
|
||||
]))
|
||||
.render(buf.area, buf);
|
||||
})?;
|
||||
match downloads.next(worker_id) {
|
||||
Some(d) => workers[worker_id].tx.send(d).unwrap(),
|
||||
None => {
|
||||
if downloads.in_progress.is_empty() {
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new("Done !").render(buf.area, buf);
|
||||
})?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
|
||||
let size = f.size();
|
||||
|
||||
let block = Block::default()
|
||||
.title("Progress")
|
||||
.title_alignment(Alignment::Center);
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.constraints(vec![Constraint::Length(2), Constraint::Length(4)])
|
||||
.margin(1)
|
||||
.split(size);
|
||||
|
||||
// total progress
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
let progress = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Blue))
|
||||
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
||||
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
||||
f.render_widget(progress, chunks[0]);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.split(chunks[1]);
|
||||
|
||||
// in progress downloads
|
||||
let items: Vec<ListItem> = downloads
|
||||
.in_progress
|
||||
.values()
|
||||
.map(|download| {
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::raw(symbols::DOT),
|
||||
Span::styled(
|
||||
format!(" download {:>2}", download.id),
|
||||
Style::default()
|
||||
.fg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!(
|
||||
" ({}ms)",
|
||||
download.started_at.elapsed().as_millis()
|
||||
)),
|
||||
]))
|
||||
})
|
||||
.collect();
|
||||
let list = List::new(items);
|
||||
f.render_widget(list, chunks[0]);
|
||||
|
||||
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
|
||||
let gauge = Gauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(download.progress / 100.0);
|
||||
if chunks[1].top().saturating_add(i as u16) > size.bottom() {
|
||||
continue;
|
||||
}
|
||||
f.render_widget(
|
||||
gauge,
|
||||
Rect {
|
||||
x: chunks[1].left(),
|
||||
y: chunks[1].top().saturating_add(i as u16),
|
||||
width: chunks[1].width,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
widgets::{Block, Borders},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
@@ -32,7 +32,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
@@ -166,7 +166,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -186,12 +186,14 @@ fn run_app<B: Backend>(
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Left => app.items.unselect(),
|
||||
KeyCode::Down => app.items.next(),
|
||||
KeyCode::Up => app.items.previous(),
|
||||
_ => {}
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Left => app.items.unselect(),
|
||||
KeyCode::Down => app.items.next(),
|
||||
KeyCode::Up => app.items.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,9 +217,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let mut lines = vec![Spans::from(i.0)];
|
||||
let mut lines = vec![Line::from(i.0)];
|
||||
for _ in 0..i.1 {
|
||||
lines.push(Spans::from(Span::styled(
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
)));
|
||||
@@ -255,8 +257,8 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
_ => Style::default(),
|
||||
};
|
||||
// Add a example datetime and apply proper spacing between them
|
||||
let header = Spans::from(vec![
|
||||
Span::styled(format!("{:<9}", level), s),
|
||||
let header = Line::from(vec![
|
||||
Span::styled(format!("{level:<9}"), s),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
"2020-01-01 10:00:00",
|
||||
@@ -264,7 +266,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
),
|
||||
]);
|
||||
// The event gets its own line
|
||||
let log = Spans::from(vec![Span::raw(event)]);
|
||||
let log = Line::from(vec![Span::raw(event)]);
|
||||
|
||||
// Here several things happen:
|
||||
// 1. Add a `---` spacing line above the final list entry
|
||||
@@ -272,9 +274,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
// 3. Add a spacer line
|
||||
// 4. Add the actual event
|
||||
ListItem::new(vec![
|
||||
Spans::from("-".repeat(chunks[1].width as usize)),
|
||||
Line::from("-".repeat(chunks[1].width as usize)),
|
||||
header,
|
||||
Spans::from(""),
|
||||
Line::from(""),
|
||||
log,
|
||||
])
|
||||
})
|
||||
|
||||
@@ -24,11 +24,11 @@ use crossterm::event::{self, Event, KeyCode};
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||
|
||||
use tui::backend::{Backend, CrosstermBackend};
|
||||
use tui::layout::Alignment;
|
||||
use tui::text::Spans;
|
||||
use tui::widgets::{Block, Borders, Paragraph};
|
||||
use tui::{Frame, Terminal};
|
||||
use ratatui::backend::{Backend, CrosstermBackend};
|
||||
use ratatui::layout::Alignment;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
use ratatui::{Frame, Terminal};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
@@ -59,7 +59,7 @@ fn main() -> Result<()> {
|
||||
reset_terminal()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err);
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -113,23 +113,23 @@ fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let text = vec![
|
||||
if app.hook_enabled {
|
||||
Spans::from("HOOK IS CURRENTLY **ENABLED**")
|
||||
Line::from("HOOK IS CURRENTLY **ENABLED**")
|
||||
} else {
|
||||
Spans::from("HOOK IS CURRENTLY **DISABLED**")
|
||||
Line::from("HOOK IS CURRENTLY **DISABLED**")
|
||||
},
|
||||
Spans::from(""),
|
||||
Spans::from("press `p` to panic"),
|
||||
Spans::from("press `e` to enable the terminal-resetting panic hook"),
|
||||
Spans::from("press any other key to quit without panic"),
|
||||
Spans::from(""),
|
||||
Spans::from("when you panic without the chained hook,"),
|
||||
Spans::from("you will likely have to reset your terminal afterwards"),
|
||||
Spans::from("with the `reset` command"),
|
||||
Spans::from(""),
|
||||
Spans::from("with the chained panic hook enabled,"),
|
||||
Spans::from("you should see the panic report as you would without tui"),
|
||||
Spans::from(""),
|
||||
Spans::from("try first without the panic handler to see the difference"),
|
||||
Line::from(""),
|
||||
Line::from("press `p` to panic"),
|
||||
Line::from("press `e` to enable the terminal-resetting panic hook"),
|
||||
Line::from("press any other key to quit without panic"),
|
||||
Line::from(""),
|
||||
Line::from("when you panic without the chained hook,"),
|
||||
Line::from("you will likely have to reset your terminal afterwards"),
|
||||
Line::from("with the `reset` command"),
|
||||
Line::from(""),
|
||||
Line::from("with the chained panic hook enabled,"),
|
||||
Line::from("you should see the panic report as you would without ratatui"),
|
||||
Line::from(""),
|
||||
Line::from("try first without the panic handler to see the difference"),
|
||||
];
|
||||
|
||||
let b = Block::default()
|
||||
|
||||
@@ -3,19 +3,19 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
scroll: u16,
|
||||
@@ -55,7 +55,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -95,12 +95,12 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
let block = Block::default().style(Style::default().fg(Color::Black));
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
@@ -113,59 +113,68 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
.split(size);
|
||||
|
||||
let text = vec![
|
||||
Spans::from("This is a line "),
|
||||
Spans::from(Span::styled(
|
||||
Line::from("This is a line "),
|
||||
Line::from(Span::styled(
|
||||
"This is a line ",
|
||||
Style::default().fg(Color::Red),
|
||||
)),
|
||||
Spans::from(Span::styled(
|
||||
Line::from(Span::styled(
|
||||
"This is a line",
|
||||
Style::default().bg(Color::Blue),
|
||||
)),
|
||||
Spans::from(Span::styled(
|
||||
Line::from(Span::styled(
|
||||
"This is a longer line",
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT),
|
||||
)),
|
||||
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
|
||||
Spans::from(Span::styled(
|
||||
Line::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
|
||||
Line::from(Span::styled(
|
||||
"This is a line",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
];
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Left, no wrap"))
|
||||
.alignment(Alignment::Left);
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), no wrap"));
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Left, wrap"))
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), with wrap"))
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Center, wrap"))
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Right alignment, with wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Center alignment, with wrap, with scroll"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((app.scroll, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Right, wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -7,9 +6,10 @@ use tui::{
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
@@ -46,7 +46,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -57,10 +57,12 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => app.show_popup = !app.show_popup,
|
||||
_ => {}
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => app.show_popup = !app.show_popup,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,18 @@ use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
@@ -99,7 +99,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
|
||||
struct App<'a> {
|
||||
state: TableState,
|
||||
@@ -95,7 +95,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -106,11 +106,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
_ => {}
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Tabs},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
|
||||
struct App<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
@@ -61,7 +61,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -72,11 +72,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => app.previous(),
|
||||
_ => {}
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +99,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let (first, rest) = t.split_at(1);
|
||||
Spans::from(vec![
|
||||
Line::from(vec![
|
||||
Span::styled(first, Style::default().fg(Color::Yellow)),
|
||||
Span::styled(rest, Style::default().fg(Color::Green)),
|
||||
])
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
/// * Pressing Enter pushes the current input in the history of previous
|
||||
/// messages
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
enum InputMode {
|
||||
@@ -72,7 +72,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -93,7 +93,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Editing => match key.code {
|
||||
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.messages.push(app.input.drain(..).collect());
|
||||
}
|
||||
@@ -108,6 +108,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,12 +150,12 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
Style::default(),
|
||||
),
|
||||
};
|
||||
let mut text = Text::from(Spans::from(msg));
|
||||
let mut text = Text::from(Line::from(msg));
|
||||
text.patch_style(style);
|
||||
let help_message = Paragraph::new(text);
|
||||
f.render_widget(help_message, chunks[0]);
|
||||
|
||||
let input = Paragraph::new(app.input.as_ref())
|
||||
let input = Paragraph::new(app.input.as_str())
|
||||
.style(match app.input_mode {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
@@ -167,7 +168,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
{}
|
||||
|
||||
InputMode::Editing => {
|
||||
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
|
||||
// Make the cursor visible and ask ratatui to put it at the specified coordinates after rendering
|
||||
f.set_cursor(
|
||||
// Put cursor past the end of the input text
|
||||
chunks[1].x + app.input.width() as u16 + 1,
|
||||
@@ -182,7 +183,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| {
|
||||
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
|
||||
let content = Line::from(Span::raw(format!("{i}: {m}")));
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
@@ -11,7 +11,7 @@ use crossterm::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear, ClearType},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
|
||||
@@ -107,7 +107,27 @@ where
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Clear(ClearType::All)))
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
map_error(execute!(
|
||||
self.buffer,
|
||||
Clear(match clear_type {
|
||||
ClearType::All => crossterm::terminal::ClearType::All,
|
||||
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
|
||||
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
|
||||
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
|
||||
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
map_error(queue!(self.buffer, Print("\n")))?;
|
||||
}
|
||||
self.buffer.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
|
||||
@@ -13,18 +13,56 @@ mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
#[cfg(feature = "termwiz")]
|
||||
mod termwiz;
|
||||
#[cfg(feature = "termwiz")]
|
||||
pub use self::termwiz::TermwizBackend;
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ClearType {
|
||||
All,
|
||||
AfterCursor,
|
||||
BeforeCursor,
|
||||
CurrentLine,
|
||||
UntilNewLine,
|
||||
}
|
||||
|
||||
pub trait Backend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||
|
||||
/// Insert `n` line breaks to the terminal screen
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
// to get around the unused warning
|
||||
let _n = n;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error>;
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
||||
|
||||
/// Clears the whole terminal screen
|
||||
fn clear(&mut self) -> Result<(), io::Error>;
|
||||
|
||||
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), io::Error> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear(),
|
||||
ClearType::AfterCursor
|
||||
| ClearType::BeforeCursor
|
||||
| ClearType::CurrentLine
|
||||
| ClearType::UntilNewLine => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("clear_type [{clear_type:?}] not supported with this backend"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::Backend;
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
@@ -42,10 +42,25 @@ impl<W> Backend for TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Clears the entire screen and move the cursor to the top left of the screen
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::clear::All)?;
|
||||
write!(self.stdout, "{}", termion::cursor::Goto(1, 1))?;
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => write!(self.stdout, "{}", termion::clear::All)?,
|
||||
ClearType::AfterCursor => write!(self.stdout, "{}", termion::clear::AfterCursor)?,
|
||||
ClearType::BeforeCursor => write!(self.stdout, "{}", termion::clear::BeforeCursor)?,
|
||||
ClearType::CurrentLine => write!(self.stdout, "{}", termion::clear::CurrentLine)?,
|
||||
ClearType::UntilNewLine => write!(self.stdout, "{}", termion::clear::UntilNewline)?,
|
||||
};
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
writeln!(self.stdout)?;
|
||||
}
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
@@ -113,8 +128,7 @@ where
|
||||
}
|
||||
write!(
|
||||
self.stdout,
|
||||
"{}{}{}{}",
|
||||
string,
|
||||
"{string}{}{}{}",
|
||||
Fg(Color::Reset),
|
||||
Bg(Color::Reset),
|
||||
termion::style::Reset,
|
||||
|
||||
199
src/backend/termwiz.rs
Normal file
199
src/backend/termwiz.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, SystemTerminal, Terminal},
|
||||
};
|
||||
|
||||
pub struct TermwizBackend {
|
||||
buffered_terminal: BufferedTerminal<SystemTerminal>,
|
||||
}
|
||||
|
||||
impl TermwizBackend {
|
||||
pub fn new() -> Result<TermwizBackend, Box<dyn Error>> {
|
||||
let mut buffered_terminal =
|
||||
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
|
||||
buffered_terminal.terminal().set_raw_mode()?;
|
||||
buffered_terminal.terminal().enter_alternate_screen()?;
|
||||
Ok(TermwizBackend { buffered_terminal })
|
||||
}
|
||||
|
||||
pub fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> TermwizBackend {
|
||||
TermwizBackend {
|
||||
buffered_terminal: instance,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
|
||||
&self.buffered_terminal
|
||||
}
|
||||
|
||||
pub fn buffered_terminal_mut(&mut self) -> &mut BufferedTerminal<SystemTerminal> {
|
||||
&mut self.buffered_terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TermwizBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, cell) in content {
|
||||
self.buffered_terminal.add_changes(vec![
|
||||
Change::CursorPosition {
|
||||
x: Position::Absolute(x as usize),
|
||||
y: Position::Absolute(y as usize),
|
||||
},
|
||||
Change::Attribute(AttributeChange::Foreground(cell.fg.into())),
|
||||
Change::Attribute(AttributeChange::Background(cell.bg.into())),
|
||||
]);
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Intensity(
|
||||
if cell.modifier.contains(Modifier::BOLD) {
|
||||
Intensity::Bold
|
||||
} else if cell.modifier.contains(Modifier::DIM) {
|
||||
Intensity::Half
|
||||
} else {
|
||||
Intensity::Normal
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Italic(
|
||||
cell.modifier.contains(Modifier::ITALIC),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Underline(
|
||||
if cell.modifier.contains(Modifier::UNDERLINED) {
|
||||
Underline::Single
|
||||
} else {
|
||||
Underline::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Reverse(
|
||||
cell.modifier.contains(Modifier::REVERSED),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Invisible(
|
||||
cell.modifier.contains(Modifier::HIDDEN),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::StrikeThrough(
|
||||
cell.modifier.contains(Modifier::CROSSED_OUT),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Blink(
|
||||
if cell.modifier.contains(Modifier::SLOW_BLINK) {
|
||||
Blink::Slow
|
||||
} else if cell.modifier.contains(Modifier::RAPID_BLINK) {
|
||||
Blink::Rapid
|
||||
} else {
|
||||
Blink::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal.add_change(&cell.symbol);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let (x, y) = self.buffered_terminal.cursor_position();
|
||||
Ok((x as u16, y as u16))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.buffered_terminal.add_change(Change::CursorPosition {
|
||||
x: Position::Absolute(x as usize),
|
||||
y: Position::Absolute(y as usize),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
let (term_width, term_height) = self.buffered_terminal.dimensions();
|
||||
let max = u16::max_value();
|
||||
Ok(Rect::new(
|
||||
0,
|
||||
0,
|
||||
if term_width > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_width as u16
|
||||
},
|
||||
if term_height > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_height as u16
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.flush()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for ColorAttribute {
|
||||
fn from(color: Color) -> ColorAttribute {
|
||||
match color {
|
||||
Color::Reset => ColorAttribute::Default,
|
||||
Color::Black => AnsiColor::Black.into(),
|
||||
Color::Gray | Color::DarkGray => AnsiColor::Grey.into(),
|
||||
Color::Red => AnsiColor::Maroon.into(),
|
||||
Color::LightRed => AnsiColor::Red.into(),
|
||||
Color::Green => AnsiColor::Green.into(),
|
||||
Color::LightGreen => AnsiColor::Lime.into(),
|
||||
Color::Yellow => AnsiColor::Olive.into(),
|
||||
Color::LightYellow => AnsiColor::Yellow.into(),
|
||||
Color::Magenta => AnsiColor::Purple.into(),
|
||||
Color::LightMagenta => AnsiColor::Fuchsia.into(),
|
||||
Color::Cyan => AnsiColor::Teal.into(),
|
||||
Color::LightCyan => AnsiColor::Aqua.into(),
|
||||
Color::White => AnsiColor::White.into(),
|
||||
Color::Blue => AnsiColor::Navy.into(),
|
||||
Color::LightBlue => AnsiColor::Blue.into(),
|
||||
Color::Indexed(i) => ColorAttribute::PaletteIndex(i),
|
||||
Color::Rgb(r, g, b) => {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@ use crate::{
|
||||
buffer::{Buffer, Cell},
|
||||
layout::Rect,
|
||||
};
|
||||
use std::{fmt::Write, io};
|
||||
use std::{
|
||||
fmt::{Display, Write},
|
||||
io,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// A backend used for the integration tests.
|
||||
@@ -27,18 +30,13 @@ fn buffer_view(buffer: &Buffer) -> String {
|
||||
if skip == 0 {
|
||||
view.push_str(&c.symbol);
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol))
|
||||
overwritten.push((x, &c.symbol));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
}
|
||||
view.push('"');
|
||||
if !overwritten.is_empty() {
|
||||
write!(
|
||||
&mut view,
|
||||
" Hidden by multi-width symbols: {:?}",
|
||||
overwritten
|
||||
)
|
||||
.unwrap();
|
||||
write!(&mut view, " Hidden by multi-width symbols: {overwritten:?}").unwrap();
|
||||
}
|
||||
view.push('\n');
|
||||
}
|
||||
@@ -93,15 +91,18 @@ impl TestBackend {
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
format!(
|
||||
"{}: at ({}, {}) expected {:?} got {:?}",
|
||||
i, x, y, expected_cell, cell
|
||||
)
|
||||
format!("{i}: at ({x}, {y}) expected {expected_cell:?} got {cell:?}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
debug_info.push_str(&nice_diff);
|
||||
panic!("{}", debug_info);
|
||||
panic!("{debug_info}");
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TestBackend {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", buffer_view(&self.buffer))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
290
src/buffer.rs
290
src/buffer.rs
@@ -1,14 +1,18 @@
|
||||
#[allow(deprecated)]
|
||||
use crate::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
text::{Line, Span, Spans},
|
||||
};
|
||||
use std::{
|
||||
cmp::min,
|
||||
fmt::{Debug, Formatter, Result},
|
||||
};
|
||||
use std::cmp::min;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Cell {
|
||||
pub symbol: String,
|
||||
pub fg: Color,
|
||||
@@ -88,9 +92,9 @@ impl Default for Cell {
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use tui::buffer::{Buffer, Cell};
|
||||
/// use tui::layout::Rect;
|
||||
/// use tui::style::{Color, Style, Modifier};
|
||||
/// use ratatui::buffer::{Buffer, Cell};
|
||||
/// use ratatui::layout::Rect;
|
||||
/// use ratatui::style::{Color, Style, Modifier};
|
||||
///
|
||||
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
|
||||
/// buf.get_mut(0, 2).set_symbol("x");
|
||||
@@ -105,7 +109,7 @@ impl Default for Cell {
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol, "x");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Clone, PartialEq, Eq, Default)]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
@@ -117,7 +121,7 @@ pub struct Buffer {
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
let cell: Cell = Default::default();
|
||||
let cell = Cell::default();
|
||||
Buffer::filled(area, &cell)
|
||||
}
|
||||
|
||||
@@ -176,15 +180,15 @@ impl Buffer {
|
||||
&mut self.content[i]
|
||||
}
|
||||
|
||||
/// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
|
||||
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Global coordinates to the top corner of this buffer's area
|
||||
@@ -196,8 +200,8 @@ impl Buffer {
|
||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
||||
@@ -210,9 +214,7 @@ impl Buffer {
|
||||
&& x < self.area.right()
|
||||
&& y >= self.area.top()
|
||||
&& y < self.area.bottom(),
|
||||
"Trying to access position outside the buffer: x={}, y={}, area={:?}",
|
||||
x,
|
||||
y,
|
||||
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
|
||||
self.area
|
||||
);
|
||||
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
|
||||
@@ -225,8 +227,8 @@ impl Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
@@ -238,8 +240,8 @@ impl Buffer {
|
||||
/// Panics when given an index that is outside the Buffer's content.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
||||
@@ -248,13 +250,12 @@ impl Buffer {
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={} len={}",
|
||||
i,
|
||||
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
|
||||
self.content.len()
|
||||
);
|
||||
(
|
||||
self.area.x + i as u16 % self.area.width,
|
||||
self.area.y + i as u16 / self.area.width,
|
||||
self.area.x + (i as u16) % self.area.width,
|
||||
self.area.y + (i as u16) / self.area.width,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -289,7 +290,7 @@ impl Buffer {
|
||||
continue;
|
||||
}
|
||||
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
||||
// change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
if width > max_offset.saturating_sub(x_offset) {
|
||||
break;
|
||||
}
|
||||
@@ -306,7 +307,9 @@ impl Buffer {
|
||||
(x_offset as u16, y)
|
||||
}
|
||||
|
||||
pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) {
|
||||
#[allow(deprecated)]
|
||||
#[deprecated(note = "Use `Buffer::set_line` instead")]
|
||||
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &spans.0 {
|
||||
@@ -327,7 +330,28 @@ impl Buffer {
|
||||
(x, y)
|
||||
}
|
||||
|
||||
pub fn set_span<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) {
|
||||
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &line.spans {
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = self.set_stringn(
|
||||
x,
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
remaining_width = remaining_width.saturating_sub(w);
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
|
||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
|
||||
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
|
||||
}
|
||||
|
||||
@@ -358,7 +382,7 @@ impl Buffer {
|
||||
if self.content.len() > length {
|
||||
self.content.truncate(length);
|
||||
} else {
|
||||
self.content.resize(length, Default::default());
|
||||
self.content.resize(length, Cell::default());
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
@@ -373,7 +397,7 @@ impl Buffer {
|
||||
/// Merge an other buffer into this one
|
||||
pub fn merge(&mut self, other: &Buffer) {
|
||||
let area = self.area.union(other.area);
|
||||
let cell: Cell = Default::default();
|
||||
let cell = Cell::default();
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
|
||||
// Move original content to the appropriate space
|
||||
@@ -431,18 +455,16 @@ impl Buffer {
|
||||
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
let width = self.area.width;
|
||||
|
||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||
// Cells invalidated by drawing/replacing preceeding multi-width characters:
|
||||
// Cells invalidated by drawing/replacing preceding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceeding multi-width characters taking their
|
||||
// Cells from the current buffer to skip due to preceding multi-width characters taking their
|
||||
// place (the skipped cells should be blank anyway):
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||
if (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let x = i as u16 % width;
|
||||
let y = i as u16 / width;
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
|
||||
@@ -455,6 +477,109 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that two buffers are equal by comparing their areas and content.
|
||||
///
|
||||
/// On panic, displays the areas or the content and a diff of the contents.
|
||||
#[macro_export]
|
||||
macro_rules! assert_buffer_eq {
|
||||
($actual_expr:expr, $expected_expr:expr) => {
|
||||
match (&$actual_expr, &$expected_expr) {
|
||||
(actual, expected) => {
|
||||
if actual.area != expected.area {
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer areas not equal
|
||||
expected: {:?}
|
||||
actual: {:?}"
|
||||
),
|
||||
expected, actual
|
||||
);
|
||||
}
|
||||
let diff = expected.diff(&actual);
|
||||
if !diff.is_empty() {
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
format!("{i}: at ({x}, {y})\n expected: {expected_cell:?}\n actual: {cell:?}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer contents not equal
|
||||
expected: {:?}
|
||||
actual: {:?}
|
||||
diff:
|
||||
{}"
|
||||
),
|
||||
expected, actual, nice_diff
|
||||
);
|
||||
}
|
||||
// shouldn't get here, but this guards against future behavior
|
||||
// that changes equality but not area or content
|
||||
assert_eq!(actual, expected, "buffers not equal");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Debug for Buffer {
|
||||
/// Writes a debug representation of the buffer to the given formatter.
|
||||
///
|
||||
/// The format is like a pretty printed struct, with the following fields:
|
||||
/// area: displayed as Rect { x: 1, y: 2, width: 3, height: 4 }
|
||||
/// content: displayed as a list of strings representing the content of the
|
||||
/// buffer
|
||||
/// styles: displayed as a list of
|
||||
/// { x: 1, y: 2, fg: Color::Red, bg: Color::Blue, modifier: Modifier::BOLD }
|
||||
/// only showing a value when there is a change in style.
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.write_fmt(format_args!(
|
||||
"Buffer {{\n area: {:?},\n content: [\n",
|
||||
&self.area
|
||||
))?;
|
||||
let mut last_style = None;
|
||||
let mut styles = vec![];
|
||||
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
f.write_str(" \"")?;
|
||||
for (x, c) in line.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
f.write_str(&c.symbol)?;
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
let style = (c.fg, c.bg, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.modifier));
|
||||
}
|
||||
}
|
||||
if !overwritten.is_empty() {
|
||||
f.write_fmt(format_args!(
|
||||
"// hidden by multi-width symbols: {overwritten:?}"
|
||||
))?;
|
||||
}
|
||||
f.write_str("\",\n")?;
|
||||
}
|
||||
f.write_str(" ],\n styles: [\n")?;
|
||||
for s in styles {
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4
|
||||
))?;
|
||||
}
|
||||
f.write_str(" ]\n}")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -465,6 +590,62 @@ mod tests {
|
||||
cell
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_implements_debug() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
|
||||
buf.set_string(0, 0, "Hello World!", Style::default());
|
||||
buf.set_string(
|
||||
0,
|
||||
1,
|
||||
"G'day World!",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, modifier: (empty),
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_area() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_style() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_translates_to_and_from_coordinates() {
|
||||
let rect = Rect::new(200, 100, 50, 80);
|
||||
@@ -506,21 +687,38 @@ mod tests {
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
|
||||
// Width limit:
|
||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// Width truncation:
|
||||
buffer.set_string(0, 0, "123456", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// multi-line
|
||||
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
buffer.set_string(0, 1, "67890", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string_multi_width_overwrite() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// multi-width overwrite
|
||||
buffer.set_string(0, 0, "aaaaa", Style::default());
|
||||
buffer.set_string(0, 0, "称号", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -531,12 +729,12 @@ mod tests {
|
||||
// Leading grapheme with zero width
|
||||
let s = "\u{1}a";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
|
||||
// Trailing grapheme with zero with
|
||||
let s = "a\u{1}";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -544,11 +742,11 @@ mod tests {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer.set_string(0, 0, "コン", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -673,7 +871,7 @@ mod tests {
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -697,7 +895,7 @@ mod tests {
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_eq!(
|
||||
assert_buffer_eq!(
|
||||
one,
|
||||
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
|
||||
);
|
||||
@@ -731,6 +929,6 @@ mod tests {
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_eq!(one, merged);
|
||||
assert_buffer_eq!(one, merged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use cassowary::strength::{REQUIRED, WEAK};
|
||||
use cassowary::WeightedRelation::*;
|
||||
use cassowary::strength::{MEDIUM, REQUIRED, WEAK};
|
||||
use cassowary::WeightedRelation::{EQ, GE, LE};
|
||||
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
|
||||
|
||||
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -51,7 +52,7 @@ pub struct Margin {
|
||||
pub horizontal: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
Center,
|
||||
@@ -68,8 +69,9 @@ pub struct Layout {
|
||||
expand_to_fill: bool,
|
||||
}
|
||||
|
||||
type Cache = HashMap<(Rect, Layout), Rc<[Rect]>>;
|
||||
thread_local! {
|
||||
static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
|
||||
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
@@ -128,7 +130,7 @@ impl Layout {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use tui::layout::{Rect, Constraint, Direction, Layout};
|
||||
/// # use ratatui::layout::{Rect, Constraint, Direction, Layout};
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
|
||||
@@ -139,8 +141,8 @@ impl Layout {
|
||||
/// height: 10,
|
||||
/// });
|
||||
/// assert_eq!(
|
||||
/// chunks,
|
||||
/// vec![
|
||||
/// chunks[..],
|
||||
/// [
|
||||
/// Rect {
|
||||
/// x: 2,
|
||||
/// y: 2,
|
||||
@@ -166,8 +168,8 @@ impl Layout {
|
||||
/// height: 2,
|
||||
/// });
|
||||
/// assert_eq!(
|
||||
/// chunks,
|
||||
/// vec![
|
||||
/// chunks[..],
|
||||
/// [
|
||||
/// Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
@@ -183,7 +185,7 @@ impl Layout {
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn split(&self, area: Rect) -> Vec<Rect> {
|
||||
pub fn split(&self, area: Rect) -> Rc<[Rect]> {
|
||||
// TODO: Maybe use a fixed size cache ?
|
||||
LAYOUT_CACHE.with(|c| {
|
||||
c.borrow_mut()
|
||||
@@ -194,7 +196,7 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
|
||||
let mut solver = Solver::new();
|
||||
let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
|
||||
let elements = layout
|
||||
@@ -202,11 +204,13 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
.iter()
|
||||
.map(|_| Element::new())
|
||||
.collect::<Vec<Element>>();
|
||||
let mut results = layout
|
||||
let mut res = layout
|
||||
.constraints
|
||||
.iter()
|
||||
.map(|_| Rect::default())
|
||||
.collect::<Vec<Rect>>();
|
||||
.collect::<Rc<[Rect]>>();
|
||||
|
||||
let mut results = Rc::get_mut(&mut res).expect("newly created Rc should have no shared refs");
|
||||
|
||||
let dest_area = area.inner(&layout.margin);
|
||||
for (i, e) in elements.iter().enumerate() {
|
||||
@@ -248,18 +252,25 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
|
||||
ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
|
||||
ccs.push(match *size {
|
||||
Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
|
||||
Constraint::Length(v) => elements[i].width | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => {
|
||||
elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
|
||||
elements[i].width | EQ(MEDIUM) | (f64::from(v * dest_area.width) / 100.0)
|
||||
}
|
||||
Constraint::Ratio(n, d) => {
|
||||
elements[i].width
|
||||
| EQ(WEAK)
|
||||
| EQ(MEDIUM)
|
||||
| (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
|
||||
Constraint::Min(v) => elements[i].width | GE(MEDIUM) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].width | LE(MEDIUM) | f64::from(v),
|
||||
});
|
||||
|
||||
match *size {
|
||||
Constraint::Min(v) | Constraint::Max(v) => {
|
||||
ccs.push(elements[i].width | EQ(WEAK) | f64::from(v));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Direction::Vertical => {
|
||||
@@ -270,18 +281,25 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
|
||||
ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
|
||||
ccs.push(match *size {
|
||||
Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
|
||||
Constraint::Length(v) => elements[i].height | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => {
|
||||
elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
|
||||
elements[i].height | EQ(MEDIUM) | (f64::from(v * dest_area.height) / 100.0)
|
||||
}
|
||||
Constraint::Ratio(n, d) => {
|
||||
elements[i].height
|
||||
| EQ(WEAK)
|
||||
| EQ(MEDIUM)
|
||||
| (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
|
||||
Constraint::Min(v) => elements[i].height | GE(MEDIUM) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].height | LE(MEDIUM) | f64::from(v),
|
||||
});
|
||||
|
||||
match *size {
|
||||
Constraint::Min(v) | Constraint::Max(v) => {
|
||||
ccs.push(elements[i].height | EQ(WEAK) | f64::from(v));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,7 +341,7 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
res
|
||||
}
|
||||
|
||||
/// A container used by the solver inside split
|
||||
@@ -361,7 +379,7 @@ impl Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
|
||||
pub struct Rect {
|
||||
|
||||
42
src/lib.rs
42
src/lib.rs
@@ -1,16 +1,17 @@
|
||||
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
|
||||
//! [ratatui](https://github.com/tui-rs-revival/ratatui) is a library used to build rich
|
||||
//! terminal users interfaces and dashboards.
|
||||
//!
|
||||
//! 
|
||||
//! 
|
||||
//!
|
||||
//! # Get started
|
||||
//!
|
||||
//! ## Adding `tui` as a dependency
|
||||
//! ## Adding `ratatui` as a dependency
|
||||
//!
|
||||
//! Add the following to your `Cargo.toml`:
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! tui = "0.18"
|
||||
//! crossterm = "0.23"
|
||||
//! crossterm = "0.26"
|
||||
//! ratatui = "0.20"
|
||||
//! ```
|
||||
//!
|
||||
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
|
||||
@@ -20,21 +21,29 @@
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! termion = "1.5"
|
||||
//! tui = { version = "0.18", default-features = false, features = ['termion'] }
|
||||
//! ratatui = { version = "0.20", default-features = false, features = ['termion'] }
|
||||
//!
|
||||
//! ```
|
||||
//!
|
||||
//! The same logic applies for all other available backends.
|
||||
//!
|
||||
//! ### Features
|
||||
//!
|
||||
//! Widgets which add dependencies are gated behind feature flags to prevent unused transitive
|
||||
//! dependencies. The available features are:
|
||||
//!
|
||||
//! * `widget-calendar` - enables [`widgets::calendar`] and adds a dependency on the [time
|
||||
//! crate](https://crates.io/crates/time).
|
||||
//!
|
||||
//! ## Creating a `Terminal`
|
||||
//!
|
||||
//! Every application using `tui` should start by instantiating a `Terminal`. It is a light
|
||||
//! Every application using `ratatui` should start by instantiating a `Terminal`. It is a light
|
||||
//! abstraction over available backends that provides basic functionalities such as clearing the
|
||||
//! screen, hiding the cursor, etc.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use tui::{backend::CrosstermBackend, Terminal};
|
||||
//! use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout();
|
||||
@@ -49,7 +58,7 @@
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use std::io;
|
||||
//! use tui::{backend::TermionBackend, Terminal};
|
||||
//! use ratatui::{backend::TermionBackend, Terminal};
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
@@ -77,14 +86,13 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::{io, thread, time::Duration};
|
||||
//! use tui::{
|
||||
//! use ratatui::{
|
||||
//! backend::CrosstermBackend,
|
||||
//! widgets::{Widget, Block, Borders},
|
||||
//! layout::{Layout, Constraint, Direction},
|
||||
//! widgets::{Block, Borders},
|
||||
//! Terminal
|
||||
//! };
|
||||
//! use crossterm::{
|
||||
//! event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
//! event::{self, DisableMouseCapture, EnableMouseCapture},
|
||||
//! execute,
|
||||
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
//! };
|
||||
@@ -105,6 +113,12 @@
|
||||
//! f.render_widget(block, size);
|
||||
//! })?;
|
||||
//!
|
||||
//! // Start a thread to discard any input events. Without handling events, the
|
||||
//! // stdin buffer will fill up, and be read into the shell when the program exits.
|
||||
//! thread::spawn(|| loop {
|
||||
//! event::read();
|
||||
//! });
|
||||
//!
|
||||
//! thread::sleep(Duration::from_millis(5000));
|
||||
//!
|
||||
//! // restore terminal
|
||||
@@ -127,7 +141,7 @@
|
||||
//! full customization. And `Layout` is no exception:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use tui::{
|
||||
//! use ratatui::{
|
||||
//! backend::Backend,
|
||||
//! layout::{Constraint, Direction, Layout},
|
||||
//! widgets::{Block, Borders},
|
||||
|
||||
190
src/style.rs
190
src/style.rs
@@ -1,8 +1,9 @@
|
||||
//! `style` contains the primitives used to control how your user interface will look.
|
||||
|
||||
use bitflags::bitflags;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Color {
|
||||
Reset,
|
||||
@@ -34,7 +35,7 @@ bitflags! {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::Modifier;
|
||||
/// # use ratatui::style::Modifier;
|
||||
///
|
||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
||||
/// ```
|
||||
@@ -55,7 +56,7 @@ bitflags! {
|
||||
/// Style let you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
/// .bg(Color::Green)
|
||||
@@ -67,9 +68,9 @@ bitflags! {
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default().bg(Color::Red),
|
||||
@@ -94,9 +95,9 @@ bitflags! {
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::reset().fg(Color::Yellow),
|
||||
@@ -115,7 +116,7 @@ bitflags! {
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
pub fg: Option<Color>,
|
||||
@@ -126,6 +127,12 @@ pub struct Style {
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Style {
|
||||
Style::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
pub const fn new() -> Style {
|
||||
Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
@@ -133,11 +140,9 @@ impl Default for Style {
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Returns a `Style` resetting all properties.
|
||||
pub fn reset() -> Style {
|
||||
pub const fn reset() -> Style {
|
||||
Style {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
@@ -151,12 +156,12 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Style};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let style = Style::default().fg(Color::Blue);
|
||||
/// let diff = Style::default().fg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
||||
/// ```
|
||||
pub fn fg(mut self, color: Color) -> Style {
|
||||
pub const fn fg(mut self, color: Color) -> Style {
|
||||
self.fg = Some(color);
|
||||
self
|
||||
}
|
||||
@@ -166,12 +171,12 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Style};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let style = Style::default().bg(Color::Blue);
|
||||
/// let diff = Style::default().bg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
||||
/// ```
|
||||
pub fn bg(mut self, color: Color) -> Style {
|
||||
pub const fn bg(mut self, color: Color) -> Style {
|
||||
self.bg = Some(color);
|
||||
self
|
||||
}
|
||||
@@ -183,7 +188,7 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD);
|
||||
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
@@ -203,7 +208,7 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
||||
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
@@ -221,7 +226,7 @@ impl Style {
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style_1 = Style::default().fg(Color::Yellow);
|
||||
/// let style_2 = Style::default().bg(Color::Red);
|
||||
/// let combined = style_1.patch(style_2);
|
||||
@@ -242,6 +247,85 @@ impl Style {
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type indicating a failure to parse a color string.
|
||||
#[derive(Debug)]
|
||||
pub struct ParseColorError;
|
||||
|
||||
impl std::fmt::Display for ParseColorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Failed to parse Colors")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParseColorError {}
|
||||
|
||||
/// Converts a string representation to a `Color` instance.
|
||||
///
|
||||
/// The `from_str` function attempts to parse the given string and convert it
|
||||
/// to the corresponding `Color` variant. It supports named colors, RGB values,
|
||||
/// and indexed colors. If the string cannot be parsed, a `ParseColorError` is returned.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use ratatui::style::Color;
|
||||
/// let color: Color = Color::from_str("blue").unwrap();
|
||||
/// assert_eq!(color, Color::Blue);
|
||||
///
|
||||
/// let color: Color = Color::from_str("#FF0000").unwrap();
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
///
|
||||
/// let color: Color = Color::from_str("10").unwrap();
|
||||
/// assert_eq!(color, Color::Indexed(10));
|
||||
///
|
||||
/// let color: Result<Color, _> = Color::from_str("invalid_color");
|
||||
/// assert!(color.is_err());
|
||||
/// ```
|
||||
impl FromStr for Color {
|
||||
type Err = ParseColorError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s.to_lowercase().as_ref() {
|
||||
"reset" => Self::Reset,
|
||||
"black" => Self::Black,
|
||||
"red" => Self::Red,
|
||||
"green" => Self::Green,
|
||||
"yellow" => Self::Yellow,
|
||||
"blue" => Self::Blue,
|
||||
"magenta" => Self::Magenta,
|
||||
"cyan" => Self::Cyan,
|
||||
"gray" => Self::Gray,
|
||||
"darkgray" | "dark gray" => Self::DarkGray,
|
||||
"lightred" | "light red" => Self::LightRed,
|
||||
"lightgreen" | "light green" => Self::LightGreen,
|
||||
"lightyellow" | "light yellow" => Self::LightYellow,
|
||||
"lightblue" | "light blue" => Self::LightBlue,
|
||||
"lightmagenta" | "light magenta" => Self::LightMagenta,
|
||||
"lightcyan" | "light cyan" => Self::LightCyan,
|
||||
"white" => Self::White,
|
||||
_ => {
|
||||
if let Ok(index) = s.parse::<u8>() {
|
||||
Self::Indexed(index)
|
||||
} else if let (Ok(r), Ok(g), Ok(b)) = {
|
||||
if !s.starts_with('#') || s.len() != 7 {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
(
|
||||
u8::from_str_radix(&s[1..3], 16),
|
||||
u8::from_str_radix(&s[3..5], 16),
|
||||
u8::from_str_radix(&s[5..7], 16),
|
||||
)
|
||||
} {
|
||||
Self::Rgb(r, g, b)
|
||||
} else {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -278,4 +362,70 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_individual_modifiers() {
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
let mods = vec![
|
||||
Modifier::BOLD,
|
||||
Modifier::DIM,
|
||||
Modifier::ITALIC,
|
||||
Modifier::UNDERLINED,
|
||||
Modifier::SLOW_BLINK,
|
||||
Modifier::RAPID_BLINK,
|
||||
Modifier::REVERSED,
|
||||
Modifier::HIDDEN,
|
||||
Modifier::CROSSED_OUT,
|
||||
];
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
|
||||
for m in &mods {
|
||||
buffer.get_mut(0, 0).set_style(Style::reset());
|
||||
buffer
|
||||
.get_mut(0, 0)
|
||||
.set_style(Style::default().add_modifier(*m));
|
||||
let style = buffer.get(0, 0).style();
|
||||
assert!(style.add_modifier.contains(*m));
|
||||
assert!(!style.sub_modifier.contains(*m));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgb_color() {
|
||||
let color: Color = Color::from_str("#FF0000").unwrap();
|
||||
assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexed_color() {
|
||||
let color: Color = Color::from_str("10").unwrap();
|
||||
assert_eq!(color, Color::Indexed(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_color() {
|
||||
let color: Color = Color::from_str("lightblue").unwrap();
|
||||
assert_eq!(color, Color::LightBlue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_colors() {
|
||||
let bad_colors = [
|
||||
"invalid_color", // not a color string
|
||||
"abcdef0", // 7 chars is not a color
|
||||
" bcdefa", // doesn't start with a '#'
|
||||
"blue ", // has space at end
|
||||
" blue", // has space at start
|
||||
"#abcdef00", // too many chars
|
||||
];
|
||||
|
||||
for bad_color in bad_colors {
|
||||
assert!(
|
||||
Color::from_str(bad_color).is_err(),
|
||||
"bad color: '{bad_color}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +228,8 @@ pub enum Marker {
|
||||
Dot,
|
||||
/// One point per cell in shape of a block
|
||||
Block,
|
||||
/// One point per cell in the shape of a bar
|
||||
Bar,
|
||||
/// Up to 8 points per cell
|
||||
Braille,
|
||||
}
|
||||
|
||||
265
src/terminal.rs
265
src/terminal.rs
@@ -1,36 +1,19 @@
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// UNSTABLE
|
||||
enum ResizeBehavior {
|
||||
Fixed,
|
||||
Auto,
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Viewport {
|
||||
Fullscreen,
|
||||
Inline(u16),
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// UNSTABLE
|
||||
pub struct Viewport {
|
||||
area: Rect,
|
||||
resize_behavior: ResizeBehavior,
|
||||
}
|
||||
|
||||
impl Viewport {
|
||||
/// UNSTABLE
|
||||
pub fn fixed(area: Rect) -> Viewport {
|
||||
Viewport {
|
||||
area,
|
||||
resize_behavior: ResizeBehavior::Fixed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
pub struct TerminalOptions {
|
||||
/// Viewport used to draw to the terminal
|
||||
@@ -53,6 +36,12 @@ where
|
||||
hidden_cursor: bool,
|
||||
/// Viewport
|
||||
viewport: Viewport,
|
||||
viewport_area: Rect,
|
||||
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
|
||||
last_known_size: Rect,
|
||||
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
|
||||
/// and the terminal resized.
|
||||
last_known_cursor_pos: (u16, u16),
|
||||
}
|
||||
|
||||
/// Represents a consistent terminal interface for rendering.
|
||||
@@ -73,9 +62,9 @@ impl<'a, B> Frame<'a, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Terminal size, guaranteed not to change when rendering.
|
||||
/// Frame size, guaranteed not to change when rendering.
|
||||
pub fn size(&self) -> Rect {
|
||||
self.terminal.viewport.area
|
||||
self.terminal.viewport_area
|
||||
}
|
||||
|
||||
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
|
||||
@@ -83,10 +72,10 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::Block;
|
||||
/// # use ratatui::Terminal;
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::widgets::Block;
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let block = Block::default();
|
||||
@@ -109,10 +98,10 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::{List, ListItem, ListState};
|
||||
/// # use ratatui::Terminal;
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::widgets::{List, ListItem, ListState};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let mut state = ListState::default();
|
||||
@@ -160,7 +149,7 @@ where
|
||||
// Attempt to restore the cursor state
|
||||
if self.hidden_cursor {
|
||||
if let Err(err) = self.show_cursor() {
|
||||
eprintln!("Failed to show the cursor: {}", err);
|
||||
eprintln!("Failed to show the cursor: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,29 +162,33 @@ where
|
||||
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
||||
/// default colors for the foreground and the background
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
let size = backend.size()?;
|
||||
Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport {
|
||||
area: size,
|
||||
resize_behavior: ResizeBehavior::Auto,
|
||||
},
|
||||
viewport: Viewport::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// UNSTABLE
|
||||
pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
let size = match options.viewport {
|
||||
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
let (viewport_area, cursor_pos) = match options.viewport {
|
||||
Viewport::Fullscreen => (size, (0, 0)),
|
||||
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
|
||||
Viewport::Fixed(area) => (area, (area.left(), area.top())),
|
||||
};
|
||||
Ok(Terminal {
|
||||
backend,
|
||||
buffers: [
|
||||
Buffer::empty(options.viewport.area),
|
||||
Buffer::empty(options.viewport.area),
|
||||
],
|
||||
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
viewport: options.viewport,
|
||||
viewport_area,
|
||||
last_known_size: size,
|
||||
last_known_cursor_pos: cursor_pos,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -225,24 +218,46 @@ where
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
if let Some((col, row, _)) = updates.last() {
|
||||
self.last_known_cursor_pos = (*col, *row);
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
|
||||
/// be saved so the size can remain consistent when rendering.
|
||||
/// This leads to a full clear of the screen.
|
||||
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
|
||||
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
|
||||
let next_area = match self.viewport {
|
||||
Viewport::Fullscreen => size,
|
||||
Viewport::Inline(height) => {
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.1
|
||||
.saturating_sub(self.viewport_area.top());
|
||||
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
|
||||
}
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
|
||||
self.last_known_size = size;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_viewport_area(&mut self, area: Rect) {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.viewport.area = area;
|
||||
self.clear()
|
||||
self.viewport_area = area;
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
if self.viewport.resize_behavior == ResizeBehavior::Auto {
|
||||
// fixed viewports do not get autoresized
|
||||
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
|
||||
let size = self.size()?;
|
||||
if size != self.viewport.area {
|
||||
if size != self.last_known_size {
|
||||
self.resize(size)?;
|
||||
}
|
||||
};
|
||||
@@ -283,9 +298,10 @@ where
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
|
||||
Ok(CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.viewport.area,
|
||||
area: self.last_known_size,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -306,12 +322,27 @@ where
|
||||
}
|
||||
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)
|
||||
self.backend.set_cursor(x, y)?;
|
||||
self.last_known_cursor_pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
self.backend.clear()?;
|
||||
match self.viewport {
|
||||
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
|
||||
Viewport::Inline(_) => {
|
||||
self.backend
|
||||
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
Viewport::Fixed(area) => {
|
||||
for row in area.top()..area.bottom() {
|
||||
self.backend.set_cursor(0, row)?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
@@ -321,4 +352,130 @@ where
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
}
|
||||
|
||||
/// Insert some content before the current inline viewport. This has no effect when the
|
||||
/// viewport is fullscreen.
|
||||
///
|
||||
/// This function scrolls down the current viewport by the given height. The newly freed space is
|
||||
/// then made available to the `draw_fn` closure through a writable `Buffer`.
|
||||
///
|
||||
/// Before:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | buffer |
|
||||
/// +-------------------+
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Insert a single line before the current viewport
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::{Paragraph, Widget};
|
||||
/// # use ratatui::text::{Line, Span};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// # use ratatui::{Terminal};
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// terminal.insert_before(1, |buf| {
|
||||
/// Paragraph::new(Line::from(vec![
|
||||
/// Span::raw("This line will be added "),
|
||||
/// Span::styled("before", Style::default().fg(Color::Blue)),
|
||||
/// Span::raw(" the current viewport")
|
||||
/// ])).render(buf.area, buf);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Buffer),
|
||||
{
|
||||
if !matches!(self.viewport, Viewport::Inline(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.clear()?;
|
||||
let height = height.min(self.last_known_size.height);
|
||||
self.backend.append_lines(height)?;
|
||||
let missing_lines =
|
||||
height.saturating_sub(self.last_known_size.bottom() - self.viewport_area.top());
|
||||
let area = Rect {
|
||||
x: self.viewport_area.left(),
|
||||
y: self.viewport_area.top().saturating_sub(missing_lines),
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
draw_fn(&mut buffer);
|
||||
|
||||
let iter = buffer.content.iter().enumerate().map(|(i, c)| {
|
||||
let (x, y) = buffer.pos_of(i);
|
||||
(x, y, c)
|
||||
});
|
||||
self.backend.draw(iter)?;
|
||||
self.backend.flush()?;
|
||||
|
||||
let remaining_lines = self.last_known_size.height - area.bottom();
|
||||
let missing_lines = self.viewport_area.height.saturating_sub(remaining_lines);
|
||||
self.backend.append_lines(self.viewport_area.height)?;
|
||||
|
||||
self.set_viewport_area(Rect {
|
||||
x: area.left(),
|
||||
y: area.bottom().saturating_sub(missing_lines),
|
||||
width: area.width,
|
||||
height: self.viewport_area.height,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inline_size<B: Backend>(
|
||||
backend: &mut B,
|
||||
height: u16,
|
||||
size: Rect,
|
||||
offset_in_previous_viewport: u16,
|
||||
) -> io::Result<(Rect, (u16, u16))> {
|
||||
let pos = backend.get_cursor()?;
|
||||
let mut row = pos.1;
|
||||
|
||||
let max_height = size.height.min(height);
|
||||
|
||||
let lines_after_cursor = height
|
||||
.saturating_sub(offset_in_previous_viewport)
|
||||
.saturating_sub(1);
|
||||
|
||||
backend.append_lines(lines_after_cursor)?;
|
||||
|
||||
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
|
||||
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
row = row.saturating_sub(offset_in_previous_viewport);
|
||||
|
||||
Ok((
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
},
|
||||
pos,
|
||||
))
|
||||
}
|
||||
|
||||
245
src/text.rs
245
src/text.rs
@@ -1,35 +1,35 @@
|
||||
//! Primitives for styled text.
|
||||
//!
|
||||
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
|
||||
//! those strings may be associated to a set of styles. `tui` has three ways to represent them:
|
||||
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
|
||||
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
|
||||
//! - A single line string where each grapheme may have its own style is represented by [`Spans`].
|
||||
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
|
||||
//! - A multiple line string where each grapheme may have its own style is represented by a
|
||||
//! [`Text`].
|
||||
//!
|
||||
//! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`]
|
||||
//! is a [`Spans`].
|
||||
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
|
||||
//! is a [`Line`].
|
||||
//!
|
||||
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
|
||||
//! supported for their properties. Moreover, `tui` provides convenient `From` implementations so
|
||||
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations so
|
||||
//! that you can start by using simple `String` or `&str` and then promote them to the previous
|
||||
//! primitives when you need additional styling capabilities.
|
||||
//!
|
||||
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
|
||||
//! its `title` property (which is a [`Spans`] under the hood):
|
||||
//! its `title` property (which is a [`Line`] under the hood):
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use tui::widgets::Block;
|
||||
//! # use tui::text::{Span, Spans};
|
||||
//! # use tui::style::{Color, Style};
|
||||
//! # use ratatui::widgets::Block;
|
||||
//! # use ratatui::text::{Span, Line};
|
||||
//! # use ratatui::style::{Color, Style};
|
||||
//! // A simple string with no styling.
|
||||
//! // Converted to Spans(vec![
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
||||
//! // ])
|
||||
//! let block = Block::default().title("My title");
|
||||
//!
|
||||
//! // A simple string with a unique style.
|
||||
//! // Converted to Spans(vec![
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(
|
||||
@@ -37,7 +37,7 @@
|
||||
//! );
|
||||
//!
|
||||
//! // A string with multiple styles.
|
||||
//! // Converted to Spans(vec![
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
|
||||
//! // Span { content: Cow::Borrowed(" title"), .. }
|
||||
//! // ])
|
||||
@@ -47,19 +47,25 @@
|
||||
//! ]);
|
||||
//! ```
|
||||
use crate::style::Style;
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
mod line;
|
||||
mod masked;
|
||||
mod spans;
|
||||
#[allow(deprecated)]
|
||||
pub use {line::Line, masked::Masked, spans::Spans};
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct StyledGrapheme<'a> {
|
||||
pub symbol: &'a str,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
/// A string where all graphemes have the same style.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Span<'a> {
|
||||
pub content: Cow<'a, str>,
|
||||
pub style: Style,
|
||||
@@ -71,7 +77,7 @@ impl<'a> Span<'a> {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Span;
|
||||
/// # use ratatui::text::Span;
|
||||
/// Span::raw("My text");
|
||||
/// Span::raw(String::from("My text"));
|
||||
/// ```
|
||||
@@ -90,8 +96,8 @@ impl<'a> Span<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Span;
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Span::styled("My text", style);
|
||||
/// Span::styled(String::from("My text"), style);
|
||||
@@ -119,8 +125,8 @@ impl<'a> Span<'a> {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::{Span, StyledGrapheme};
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::text::{Span, StyledGrapheme};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use std::iter::Iterator;
|
||||
/// let style = Style::default().fg(Color::Yellow);
|
||||
/// let span = Span::styled("Text", style);
|
||||
@@ -179,6 +185,43 @@ impl<'a> Span<'a> {
|
||||
})
|
||||
.filter(|s| s.symbol != "\n")
|
||||
}
|
||||
|
||||
/// Patches the style an existing Span, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_span = Span::raw("My text");
|
||||
/// let mut styled_span = Span::styled("My text", style);
|
||||
///
|
||||
/// assert_ne!(raw_span, styled_span);
|
||||
///
|
||||
/// raw_span.patch_style(style);
|
||||
/// assert_eq!(raw_span, styled_span);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
self.style = self.style.patch(style);
|
||||
}
|
||||
|
||||
/// Resets the style of the Span.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut span = Span::styled("My text", Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC));
|
||||
///
|
||||
/// span.reset_style();
|
||||
/// assert_eq!(Style::reset(), span.style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
self.patch_style(Style::reset());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Span<'a> {
|
||||
@@ -193,62 +236,6 @@ impl<'a> From<&'a str> for Span<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A string composed of clusters of graphemes, each with their own style.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Spans<'a>(pub Vec<Span<'a>>);
|
||||
|
||||
impl<'a> Spans<'a> {
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::{Span, Spans};
|
||||
/// # use tui::style::{Color, Style};
|
||||
/// let spans = Spans::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// assert_eq!(7, spans.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.iter().map(Span::width).sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Spans<'a> {
|
||||
fn from(s: String) -> Spans<'a> {
|
||||
Spans(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Spans<'a> {
|
||||
fn from(s: &'a str) -> Spans<'a> {
|
||||
Spans(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
|
||||
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
|
||||
Spans(spans)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Spans<'a> {
|
||||
fn from(span: Span<'a>) -> Spans<'a> {
|
||||
Spans(vec![span])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Spans<'a>> for String {
|
||||
fn from(line: Spans<'a>) -> String {
|
||||
line.0.iter().fold(String::new(), |mut acc, s| {
|
||||
acc.push_str(s.content.as_ref());
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
///
|
||||
@@ -257,8 +244,8 @@ impl<'a> From<Spans<'a>> for String {
|
||||
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::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`
|
||||
@@ -273,9 +260,9 @@ impl<'a> From<Spans<'a>> for String {
|
||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
/// assert_eq!(6, text.height());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
||||
pub struct Text<'a> {
|
||||
pub lines: Vec<Spans<'a>>,
|
||||
pub lines: Vec<Line<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
@@ -284,7 +271,7 @@ impl<'a> Text<'a> {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// # use ratatui::text::Text;
|
||||
/// Text::raw("The first line\nThe second line");
|
||||
/// Text::raw(String::from("The first line\nThe second line"));
|
||||
/// ```
|
||||
@@ -292,12 +279,14 @@ impl<'a> 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(),
|
||||
},
|
||||
}
|
||||
let lines: Vec<_> = match content.into() {
|
||||
Cow::Borrowed("") => vec![Line::from("")],
|
||||
Cow::Borrowed(s) => s.lines().map(Line::from).collect(),
|
||||
Cow::Owned(s) if s.is_empty() => vec![Line::from("")],
|
||||
Cow::Owned(s) => s.lines().map(|l| Line::from(l.to_owned())).collect(),
|
||||
};
|
||||
|
||||
Text::from(lines)
|
||||
}
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
@@ -305,8 +294,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::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);
|
||||
@@ -325,16 +314,12 @@ impl<'a> Text<'a> {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use tui::text::Text;
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(15, text.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.lines
|
||||
.iter()
|
||||
.map(Spans::width)
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
self.lines.iter().map(Line::width).max().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the height.
|
||||
@@ -342,7 +327,7 @@ impl<'a> Text<'a> {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use tui::text::Text;
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
/// ```
|
||||
@@ -350,13 +335,13 @@ impl<'a> Text<'a> {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
/// Apply a new style to existing text.
|
||||
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::text::Text;
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::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);
|
||||
@@ -367,9 +352,31 @@ impl<'a> Text<'a> {
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for line in &mut self.lines {
|
||||
for span in &mut line.0 {
|
||||
span.style = span.style.patch(style);
|
||||
}
|
||||
line.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of the Text.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line, Text};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut text = Text::styled("The first line\nThe second line", style);
|
||||
///
|
||||
/// text.reset_style();
|
||||
/// for line in &text.lines {
|
||||
/// for span in &line.spans {
|
||||
/// assert_eq!(Style::reset(), span.style);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for line in &mut self.lines {
|
||||
line.reset_style();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,25 +402,43 @@ impl<'a> From<Cow<'a, str>> for Text<'a> {
|
||||
impl<'a> From<Span<'a>> for Text<'a> {
|
||||
fn from(span: Span<'a>) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![Spans::from(span)],
|
||||
lines: vec![Line::from(span)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<'a> From<Spans<'a>> for Text<'a> {
|
||||
fn from(spans: Spans<'a>) -> Text<'a> {
|
||||
Text { lines: vec![spans] }
|
||||
Text {
|
||||
lines: vec![spans.into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Line<'a>> for Text<'a> {
|
||||
fn from(line: Line<'a>) -> Text<'a> {
|
||||
Text { lines: vec![line] }
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
|
||||
Text {
|
||||
lines: lines.into_iter().map(|l| l.0.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
|
||||
Text { lines }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Text<'a> {
|
||||
type Item = Spans<'a>;
|
||||
type Item = Line<'a>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
@@ -421,8 +446,12 @@ impl<'a> IntoIterator for Text<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extend<Spans<'a>> for Text<'a> {
|
||||
fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
|
||||
self.lines.extend(iter);
|
||||
impl<'a, T> Extend<T> for Text<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
|
||||
let lines = iter.into_iter().map(Into::into);
|
||||
self.lines.extend(lines);
|
||||
}
|
||||
}
|
||||
|
||||
249
src/text/line.rs
Normal file
249
src/text/line.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
#![allow(deprecated)]
|
||||
use super::{Span, Spans, Style};
|
||||
use crate::layout::Alignment;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
||||
pub struct Line<'a> {
|
||||
pub spans: Vec<Span<'a>>,
|
||||
pub alignment: Option<Alignment>,
|
||||
}
|
||||
|
||||
impl<'a> Line<'a> {
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let line = Line::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// assert_eq!(7, line.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.spans.iter().map(Span::width).sum()
|
||||
}
|
||||
|
||||
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_line = Line::from(vec![
|
||||
/// Span::raw("My"),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// let mut styled_line = Line::from(vec![
|
||||
/// Span::styled("My", style),
|
||||
/// Span::styled(" text", style),
|
||||
/// ]);
|
||||
///
|
||||
/// assert_ne!(raw_line, styled_line);
|
||||
///
|
||||
/// raw_line.patch_style(style);
|
||||
/// assert_eq!(raw_line, styled_line);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for span in &mut self.spans {
|
||||
span.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of each Span in the Line.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut line = Line::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
/// ]);
|
||||
///
|
||||
/// line.reset_style();
|
||||
/// assert_eq!(Style::reset(), line.spans[0].style);
|
||||
/// assert_eq!(Style::reset(), line.spans[1].style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for span in &mut self.spans {
|
||||
span.reset_style();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::borrow::Cow;
|
||||
/// # use ratatui::layout::Alignment;
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut line = Line::from("Hi, what's up?");
|
||||
/// assert_eq!(None, line.alignment);
|
||||
/// assert_eq!(Some(Alignment::Right), line.alignment(Alignment::Right).alignment)
|
||||
/// ```
|
||||
pub fn alignment(self, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
alignment: Some(alignment),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Line<'a> {
|
||||
fn from(s: String) -> Self {
|
||||
Self::from(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Line<'a> {
|
||||
fn from(s: &'a str) -> Self {
|
||||
Self::from(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Span<'a>>> for Line<'a> {
|
||||
fn from(spans: Vec<Span<'a>>) -> Self {
|
||||
Self {
|
||||
spans,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Line<'a> {
|
||||
fn from(span: Span<'a>) -> Self {
|
||||
Self::from(vec![span])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Line<'a>> for String {
|
||||
fn from(line: Line<'a>) -> String {
|
||||
line.spans.iter().fold(String::new(), |mut acc, s| {
|
||||
acc.push_str(s.content.as_ref());
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Spans<'a>> for Line<'a> {
|
||||
fn from(value: Spans<'a>) -> Self {
|
||||
Self::from(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::layout::Alignment;
|
||||
use crate::style::{Color, Modifier, Style};
|
||||
use crate::text::{Line, Span, Spans};
|
||||
|
||||
#[test]
|
||||
fn test_width() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::raw(" text"),
|
||||
]);
|
||||
assert_eq!(7, line.width());
|
||||
|
||||
let empty_line = Line::default();
|
||||
assert_eq!(0, empty_line.width());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patch_style() {
|
||||
let style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
let mut raw_line = Line::from(vec![Span::raw("My"), Span::raw(" text")]);
|
||||
let styled_line = Line::from(vec![
|
||||
Span::styled("My", style),
|
||||
Span::styled(" text", style),
|
||||
]);
|
||||
|
||||
assert_ne!(raw_line, styled_line);
|
||||
|
||||
raw_line.patch_style(style);
|
||||
assert_eq!(raw_line, styled_line);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_style() {
|
||||
let mut line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]);
|
||||
|
||||
line.reset_style();
|
||||
assert_eq!(Style::reset(), line.spans[0].style);
|
||||
assert_eq!(Style::reset(), line.spans[1].style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
let s = "Hello, world!";
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec() {
|
||||
let spans = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
];
|
||||
let line = Line::from(spans.clone());
|
||||
assert_eq!(spans, line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
let line = Line::from(span.clone());
|
||||
assert_eq!(vec![span], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_spans() {
|
||||
let spans = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
];
|
||||
assert_eq!(Line::from(Spans::from(spans.clone())), Line::from(spans));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_string() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
]);
|
||||
let s: String = line.into();
|
||||
assert_eq!("Hello, world!", s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alignment() {
|
||||
let line = Line::from("This is left").alignment(Alignment::Left);
|
||||
assert_eq!(Some(Alignment::Left), line.alignment);
|
||||
|
||||
let line = Line::from("This is default");
|
||||
assert_eq!(None, line.alignment);
|
||||
}
|
||||
}
|
||||
127
src/text/masked.rs
Normal file
127
src/text/masked.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{self, Debug, Display},
|
||||
};
|
||||
|
||||
use super::Text;
|
||||
|
||||
/// A wrapper around a string that is masked when displayed.
|
||||
///
|
||||
/// The masked string is displayed as a series of the same character.
|
||||
/// This might be used to display a password field or similar secure data.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{buffer::Buffer, layout::Rect, text::Masked, widgets::{Paragraph, Widget}};
|
||||
///
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
/// let password = Masked::new("12345", 'x');
|
||||
///
|
||||
/// Paragraph::new(password).render(buffer.area, &mut buffer);
|
||||
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct Masked<'a> {
|
||||
inner: Cow<'a, str>,
|
||||
mask_char: char,
|
||||
}
|
||||
|
||||
impl<'a> Masked<'a> {
|
||||
pub fn new(s: impl Into<Cow<'a, str>>, mask_char: char) -> Self {
|
||||
Self {
|
||||
inner: s.into(),
|
||||
mask_char,
|
||||
}
|
||||
}
|
||||
|
||||
/// The character to use for masking.
|
||||
pub fn mask_char(&self) -> char {
|
||||
self.mask_char
|
||||
}
|
||||
|
||||
/// The underlying string, with all characters masked.
|
||||
pub fn value(&self) -> Cow<'a, str> {
|
||||
self.inner.chars().map(|_| self.mask_char).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Masked<'_> {
|
||||
/// Debug representation of a masked string is the underlying string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.inner).map_err(|_| fmt::Error)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Masked<'_> {
|
||||
/// Display representation of a masked string is the masked string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.value()).map_err(|_| fmt::Error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
|
||||
fn from(masked: &'a Masked) -> Cow<'a, str> {
|
||||
masked.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Masked<'a>> for Cow<'a, str> {
|
||||
fn from(masked: Masked<'a>) -> Cow<'a, str> {
|
||||
masked.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Masked<'_>> for Text<'a> {
|
||||
fn from(masked: &'a Masked) -> Text<'a> {
|
||||
Text::raw(masked.value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Masked<'a>> for Text<'a> {
|
||||
fn from(masked: Masked<'a>) -> Text<'a> {
|
||||
Text::raw(masked.value())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::text::Line;
|
||||
use std::borrow::Borrow;
|
||||
|
||||
#[test]
|
||||
fn test_masked_value() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(masked.value(), "xxxxx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_masked_debug() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked:?}"), "12345");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_masked_display() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked}"), "xxxxx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_masked_conversions() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
|
||||
let text: Text = masked.borrow().into();
|
||||
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
|
||||
|
||||
let text: Text = masked.to_owned().into();
|
||||
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
|
||||
|
||||
let cow: Cow<str> = masked.borrow().into();
|
||||
assert_eq!(cow, "xxxxx");
|
||||
|
||||
let cow: Cow<str> = masked.to_owned().into();
|
||||
assert_eq!(cow, "xxxxx");
|
||||
}
|
||||
}
|
||||
224
src/text/spans.rs
Normal file
224
src/text/spans.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use super::{Span, Style};
|
||||
use crate::layout::Alignment;
|
||||
use crate::text::Line;
|
||||
|
||||
/// A string composed of clusters of graphemes, each with their own style.
|
||||
///
|
||||
/// `Spans` has been deprecated in favor of `Line`, and will be removed in the
|
||||
/// future. All methods that accept Spans have been replaced with methods that
|
||||
/// accept Into<Line<'a>> (which is implemented on `Spans`) to allow users of
|
||||
/// this crate to gradually transition to Line.
|
||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
||||
#[deprecated(note = "Use `ratatui::text::Line` instead")]
|
||||
pub struct Spans<'a>(pub Vec<Span<'a>>);
|
||||
|
||||
impl<'a> Spans<'a> {
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let spans = Spans::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// assert_eq!(7, spans.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.iter().map(Span::width).sum()
|
||||
}
|
||||
|
||||
/// Patches the style of each Span in an existing Spans, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_spans = Spans::from(vec![
|
||||
/// Span::raw("My"),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// let mut styled_spans = Spans::from(vec![
|
||||
/// Span::styled("My", style),
|
||||
/// Span::styled(" text", style),
|
||||
/// ]);
|
||||
///
|
||||
/// assert_ne!(raw_spans, styled_spans);
|
||||
///
|
||||
/// raw_spans.patch_style(style);
|
||||
/// assert_eq!(raw_spans, styled_spans);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for span in &mut self.0 {
|
||||
span.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of each Span in the Spans.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut spans = Spans::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
/// ]);
|
||||
///
|
||||
/// spans.reset_style();
|
||||
/// assert_eq!(Style::reset(), spans.0[0].style);
|
||||
/// assert_eq!(Style::reset(), spans.0[1].style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for span in &mut self.0 {
|
||||
span.reset_style();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::borrow::Cow;
|
||||
/// # use ratatui::layout::Alignment;
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut line = Spans::from("Hi, what's up?").alignment(Alignment::Right);
|
||||
/// assert_eq!(Some(Alignment::Right), line.alignment)
|
||||
/// ```
|
||||
pub fn alignment(self, alignment: Alignment) -> Line<'a> {
|
||||
let line = Line::from(self);
|
||||
line.alignment(alignment)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Spans<'a> {
|
||||
fn from(s: String) -> Spans<'a> {
|
||||
Spans(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Spans<'a> {
|
||||
fn from(s: &'a str) -> Spans<'a> {
|
||||
Spans(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
|
||||
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
|
||||
Spans(spans)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Spans<'a> {
|
||||
fn from(span: Span<'a>) -> Spans<'a> {
|
||||
Spans(vec![span])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Spans<'a>> for String {
|
||||
fn from(line: Spans<'a>) -> String {
|
||||
line.0.iter().fold(String::new(), |mut acc, s| {
|
||||
acc.push_str(s.content.as_ref());
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::style::{Color, Modifier, Style};
|
||||
use crate::text::{Span, Spans};
|
||||
|
||||
#[test]
|
||||
fn test_width() {
|
||||
let spans = Spans::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::raw(" text"),
|
||||
]);
|
||||
assert_eq!(7, spans.width());
|
||||
|
||||
let empty_spans = Spans::default();
|
||||
assert_eq!(0, empty_spans.width());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patch_style() {
|
||||
let style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
let mut raw_spans = Spans::from(vec![Span::raw("My"), Span::raw(" text")]);
|
||||
let styled_spans = Spans::from(vec![
|
||||
Span::styled("My", style),
|
||||
Span::styled(" text", style),
|
||||
]);
|
||||
|
||||
assert_ne!(raw_spans, styled_spans);
|
||||
|
||||
raw_spans.patch_style(style);
|
||||
assert_eq!(raw_spans, styled_spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_style() {
|
||||
let mut spans = Spans::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]);
|
||||
|
||||
spans.reset_style();
|
||||
assert_eq!(Style::reset(), spans.0[0].style);
|
||||
assert_eq!(Style::reset(), spans.0[1].style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
let spans = Spans::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
let s = "Hello, world!";
|
||||
let spans = Spans::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec() {
|
||||
let spans_vec = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
];
|
||||
let spans = Spans::from(spans_vec.clone());
|
||||
assert_eq!(spans_vec, spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
let spans = Spans::from(span.clone());
|
||||
assert_eq!(vec![span], spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_string() {
|
||||
let spans = Spans::from(vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
]);
|
||||
let s: String = spans.into();
|
||||
assert_eq!("Hello, world!", s);
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ use unicode_width::UnicodeWidthStr;
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, Borders, BarChart};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::widgets::{Block, Borders, BarChart};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// BarChart::default()
|
||||
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
|
||||
/// .bar_width(3)
|
||||
@@ -63,9 +63,9 @@ impl<'a> Default for BarChart<'a> {
|
||||
bar_width: 1,
|
||||
bar_gap: 1,
|
||||
bar_set: symbols::bar::NINE_LEVELS,
|
||||
value_style: Default::default(),
|
||||
label_style: Default::default(),
|
||||
style: Default::default(),
|
||||
value_style: Style::default(),
|
||||
label_style: Style::default(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ impl<'a> BarChart<'a> {
|
||||
self.data = data;
|
||||
self.values = Vec::with_capacity(self.data.len());
|
||||
for &(_, v) in self.data {
|
||||
self.values.push(format!("{}", v));
|
||||
self.values.push(format!("{v}"));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ use crate::{
|
||||
layout::{Alignment, Rect},
|
||||
style::Style,
|
||||
symbols::line,
|
||||
text::{Span, Spans},
|
||||
text::{Line, Span},
|
||||
widgets::{Borders, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BorderType {
|
||||
Plain,
|
||||
Rounded,
|
||||
@@ -26,14 +26,69 @@ impl BorderType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Padding {
|
||||
pub left: u16,
|
||||
pub right: u16,
|
||||
pub top: u16,
|
||||
pub bottom: u16,
|
||||
}
|
||||
|
||||
impl Padding {
|
||||
pub fn new(left: u16, right: u16, top: u16, bottom: u16) -> Self {
|
||||
Padding {
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Padding {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn horizontal(value: u16) -> Self {
|
||||
Padding {
|
||||
left: value,
|
||||
right: value,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(value: u16) -> Self {
|
||||
Padding {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: value,
|
||||
bottom: value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uniform(value: u16) -> Self {
|
||||
Padding {
|
||||
left: value,
|
||||
right: value,
|
||||
top: value,
|
||||
bottom: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Base widget to be used with all upper level ones. It may be used to display a box border around
|
||||
/// the widget and/or add a title.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, BorderType, Borders};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # use ratatui::widgets::{Block, BorderType, Borders};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// Block::default()
|
||||
/// .title("Block")
|
||||
/// .borders(Borders::LEFT | Borders::RIGHT)
|
||||
@@ -41,13 +96,15 @@ impl BorderType {
|
||||
/// .border_type(BorderType::Rounded)
|
||||
/// .style(Style::default().bg(Color::Black));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Block<'a> {
|
||||
/// Optional title place on the upper left of the block
|
||||
title: Option<Spans<'a>>,
|
||||
title: Option<Line<'a>>,
|
||||
/// Title alignment. The default is top left of the block, but one can choose to place
|
||||
/// title in the top middle, or top right of the block
|
||||
title_alignment: Alignment,
|
||||
/// Whether or not title goes on top or bottom row of the block
|
||||
title_on_bottom: bool,
|
||||
/// Visible borders
|
||||
borders: Borders,
|
||||
/// Border style
|
||||
@@ -57,6 +114,8 @@ pub struct Block<'a> {
|
||||
border_type: BorderType,
|
||||
/// Widget style
|
||||
style: Style,
|
||||
/// Block padding
|
||||
padding: Padding,
|
||||
}
|
||||
|
||||
impl<'a> Default for Block<'a> {
|
||||
@@ -64,10 +123,12 @@ impl<'a> Default for Block<'a> {
|
||||
Block {
|
||||
title: None,
|
||||
title_alignment: Alignment::Left,
|
||||
title_on_bottom: false,
|
||||
borders: Borders::NONE,
|
||||
border_style: Default::default(),
|
||||
border_style: Style::default(),
|
||||
border_type: BorderType::Plain,
|
||||
style: Default::default(),
|
||||
style: Style::default(),
|
||||
padding: Padding::zero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +136,7 @@ impl<'a> Default for Block<'a> {
|
||||
impl<'a> Block<'a> {
|
||||
pub fn title<T>(mut self, title: T) -> Block<'a>
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
@@ -83,12 +144,12 @@ impl<'a> Block<'a> {
|
||||
|
||||
#[deprecated(
|
||||
since = "0.10.0",
|
||||
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
|
||||
note = "You should use styling capabilities of `text::Line` given as argument of the `title` method to apply styling to the title."
|
||||
)]
|
||||
pub fn title_style(mut self, style: Style) -> Block<'a> {
|
||||
if let Some(t) = self.title {
|
||||
let title = String::from(t);
|
||||
self.title = Some(Spans::from(Span::styled(title, style)));
|
||||
self.title = Some(Line::from(Span::styled(title, style)));
|
||||
}
|
||||
self
|
||||
}
|
||||
@@ -98,6 +159,11 @@ impl<'a> Block<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title_on_bottom(mut self) -> Block<'a> {
|
||||
self.title_on_bottom = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn border_style(mut self, style: Style) -> Block<'a> {
|
||||
self.border_style = style;
|
||||
self
|
||||
@@ -119,6 +185,34 @@ impl<'a> Block<'a> {
|
||||
}
|
||||
|
||||
/// Compute the inner area of a block based on its border visibility rules.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// // Draw a block nested within another block
|
||||
/// use ratatui::{backend::TestBackend, buffer::Buffer, terminal::Terminal, widgets::{Block, Borders}};
|
||||
/// let backend = TestBackend::new(15, 5);
|
||||
/// let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let outer_block = Block::default()
|
||||
/// .title("Outer Block")
|
||||
/// .borders(Borders::ALL);
|
||||
/// let inner_block = Block::default()
|
||||
/// .title("Inner Block")
|
||||
/// .borders(Borders::ALL);
|
||||
/// terminal.draw(|f| {
|
||||
/// let inner_area = outer_block.inner(f.size());
|
||||
/// f.render_widget(outer_block, f.size());
|
||||
/// f.render_widget(inner_block, inner_area);
|
||||
/// });
|
||||
/// let expected = Buffer::with_lines(vec![
|
||||
/// "┌Outer Block──┐",
|
||||
/// "│┌Inner Block┐│",
|
||||
/// "││ ││",
|
||||
/// "│└───────────┘│",
|
||||
/// "└─────────────┘",
|
||||
/// ]);
|
||||
/// terminal.backend().assert_buffer(&expected);
|
||||
/// ```
|
||||
pub fn inner(&self, area: Rect) -> Rect {
|
||||
let mut inner = area;
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
@@ -135,8 +229,24 @@ impl<'a> Block<'a> {
|
||||
if self.borders.intersects(Borders::BOTTOM) {
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
}
|
||||
|
||||
inner.x = inner.x.saturating_add(self.padding.left);
|
||||
inner.y = inner.y.saturating_add(self.padding.top);
|
||||
|
||||
inner.width = inner
|
||||
.width
|
||||
.saturating_sub(self.padding.left + self.padding.right);
|
||||
inner.height = inner
|
||||
.height
|
||||
.saturating_sub(self.padding.top + self.padding.bottom);
|
||||
|
||||
inner
|
||||
}
|
||||
|
||||
pub fn padding(mut self, padding: Padding) -> Block<'a> {
|
||||
self.padding = padding;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Block<'a> {
|
||||
@@ -203,17 +313,8 @@ impl<'a> Widget for Block<'a> {
|
||||
|
||||
// Title
|
||||
if let Some(title) = self.title {
|
||||
let left_border_dx = if self.borders.intersects(Borders::LEFT) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let right_border_dx = if self.borders.intersects(Borders::RIGHT) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let left_border_dx = u16::from(self.borders.intersects(Borders::LEFT));
|
||||
let right_border_dx = u16::from(self.borders.intersects(Borders::RIGHT));
|
||||
|
||||
let title_area_width = area
|
||||
.width
|
||||
@@ -230,9 +331,13 @@ impl<'a> Widget for Block<'a> {
|
||||
};
|
||||
|
||||
let title_x = area.left() + title_dx;
|
||||
let title_y = area.top();
|
||||
let title_y = if self.title_on_bottom {
|
||||
area.bottom() - 1
|
||||
} else {
|
||||
area.top()
|
||||
};
|
||||
|
||||
buf.set_spans(title_x, title_y, &title, title_area_width);
|
||||
buf.set_line(title_x, title_y, &title, title_area_width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
245
src/widgets/calendar.rs
Normal file
245
src/widgets/calendar.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
//! A simple calendar widget. `(feature: widget-calendar)`
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! The [`Monthly`] widget will display a calendar for the monh provided in `display_date`. Days are
|
||||
//! styled using the default style unless:
|
||||
//! * `show_surrounding` is set, then days not in the `display_date` month will use that style.
|
||||
//! * a style is returned by the [`DateStyler`] for the day
|
||||
//!
|
||||
//! [`Monthly`] has several controls for what should be displayed
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
text::Span,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
use time::{Date, Duration, OffsetDateTime};
|
||||
|
||||
/// Display a month calendar for the month containing `display_date`
|
||||
pub struct Monthly<'a, S: DateStyler> {
|
||||
display_date: Date,
|
||||
events: S,
|
||||
show_surrounding: Option<Style>,
|
||||
show_weekday: Option<Style>,
|
||||
show_month: Option<Style>,
|
||||
default_style: Style,
|
||||
block: Option<Block<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, S: DateStyler> Monthly<'a, S> {
|
||||
/// Construct a calendar for the `display_date` and highlight the `events`
|
||||
pub fn new(display_date: Date, events: S) -> Self {
|
||||
Self {
|
||||
display_date,
|
||||
events,
|
||||
show_surrounding: None,
|
||||
show_weekday: None,
|
||||
show_month: None,
|
||||
default_style: Style::default(),
|
||||
block: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill the calendar slots for days not in the current month also, this causes each line to be
|
||||
/// completely filled. If there is an event style for a date, this style will be patched with
|
||||
/// the event's style
|
||||
pub fn show_surrounding(mut self, style: Style) -> Self {
|
||||
self.show_surrounding = Some(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Display a header containing weekday abbreviations
|
||||
pub fn show_weekdays_header(mut self, style: Style) -> Self {
|
||||
self.show_weekday = Some(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Display a header containing the month and year
|
||||
pub fn show_month_header(mut self, style: Style) -> Self {
|
||||
self.show_month = Some(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// How to render otherwise unstyled dates
|
||||
pub fn default_style(mut self, s: Style) -> Self {
|
||||
self.default_style = s;
|
||||
self
|
||||
}
|
||||
|
||||
/// Render the calendar within a [Block](ratatui::widgets::Block)
|
||||
pub fn block(mut self, b: Block<'a>) -> Self {
|
||||
self.block = Some(b);
|
||||
self
|
||||
}
|
||||
|
||||
/// Return a style with only the background from the default style
|
||||
fn default_bg(&self) -> Style {
|
||||
match self.default_style.bg {
|
||||
None => Style::default(),
|
||||
Some(c) => Style::default().bg(c),
|
||||
}
|
||||
}
|
||||
|
||||
/// All logic to style a date goes here.
|
||||
fn format_date(&self, date: Date) -> Span {
|
||||
if date.month() != self.display_date.month() {
|
||||
match self.show_surrounding {
|
||||
None => Span::styled(" ", self.default_bg()),
|
||||
Some(s) => {
|
||||
let style = self
|
||||
.default_style
|
||||
.patch(s)
|
||||
.patch(self.events.get_style(date));
|
||||
Span::styled(format!("{:2?}", date.day()), style)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Span::styled(
|
||||
format!("{:2?}", date.day()),
|
||||
self.default_style.patch(self.events.get_style(date)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S: DateStyler> Widget for Monthly<'a, S> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
// Block is used for borders and such
|
||||
// Draw that first, and use the blank area inside the block for our own purposes
|
||||
let mut area = match self.block.take() {
|
||||
None => area,
|
||||
Some(b) => {
|
||||
let inner = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner
|
||||
}
|
||||
};
|
||||
|
||||
// Draw the month name and year
|
||||
if let Some(style) = self.show_month {
|
||||
let line = Span::styled(
|
||||
format!("{} {}", self.display_date.month(), self.display_date.year()),
|
||||
style,
|
||||
);
|
||||
// cal is 21 cells wide, so hard code the 11
|
||||
let x_off = 11_u16.saturating_sub(line.width() as u16 / 2);
|
||||
buf.set_line(area.x + x_off, area.y, &line.into(), area.width);
|
||||
area.y += 1
|
||||
}
|
||||
|
||||
// Draw days of week
|
||||
if let Some(style) = self.show_weekday {
|
||||
let days = String::from(" Su Mo Tu We Th Fr Sa");
|
||||
buf.set_string(area.x, area.y, days, style);
|
||||
area.y += 1;
|
||||
}
|
||||
|
||||
// Set the start of the calendar to the Sunday before the 1st (or the sunday of the first)
|
||||
let first_of_month = self.display_date.replace_day(1).unwrap();
|
||||
let offset = Duration::days(first_of_month.weekday().number_days_from_sunday().into());
|
||||
let mut curr_day = first_of_month - offset;
|
||||
|
||||
// go through all the weeks containing a day in the target month.
|
||||
while curr_day.month() as u8 != self.display_date.month().next() as u8 {
|
||||
let mut spans = Vec::with_capacity(14);
|
||||
for i in 0..7 {
|
||||
// Draw the gutter. Do it here so we can avoid worrying about
|
||||
// styling the ' ' in the format_date method
|
||||
if i == 0 {
|
||||
spans.push(Span::styled(" ", Style::default()));
|
||||
} else {
|
||||
spans.push(Span::styled(" ", self.default_bg()));
|
||||
}
|
||||
spans.push(self.format_date(curr_day));
|
||||
curr_day += Duration::DAY;
|
||||
}
|
||||
buf.set_line(area.x, area.y, &spans.into(), area.width);
|
||||
area.y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a method for styling a given date. [Month] is generic on this trait, so any type
|
||||
/// that implements this trait can be used.
|
||||
pub trait DateStyler {
|
||||
/// Given a date, return a style for that date
|
||||
fn get_style(&self, date: Date) -> Style;
|
||||
}
|
||||
|
||||
/// A simple `DateStyler` based on a [`HashMap`]
|
||||
pub struct CalendarEventStore(pub HashMap<Date, Style>);
|
||||
|
||||
impl CalendarEventStore {
|
||||
/// Construct a store that has the current date styled.
|
||||
pub fn today(style: Style) -> Self {
|
||||
let mut res = Self::default();
|
||||
res.add(OffsetDateTime::now_local().unwrap().date(), style);
|
||||
res
|
||||
}
|
||||
|
||||
/// Add a date and style to the store
|
||||
pub fn add(&mut self, date: Date, style: Style) {
|
||||
// to simplify style nonsense, last write wins
|
||||
let _ = self.0.insert(date, style);
|
||||
}
|
||||
|
||||
/// Helper for trait impls
|
||||
fn lookup_style(&self, date: Date) -> Style {
|
||||
self.0.get(&date).copied().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl DateStyler for CalendarEventStore {
|
||||
fn get_style(&self, date: Date) -> Style {
|
||||
self.lookup_style(date)
|
||||
}
|
||||
}
|
||||
|
||||
impl DateStyler for &CalendarEventStore {
|
||||
fn get_style(&self, date: Date) -> Style {
|
||||
self.lookup_style(date)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CalendarEventStore {
|
||||
fn default() -> Self {
|
||||
Self(HashMap::with_capacity(4))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Color;
|
||||
use time::Month;
|
||||
|
||||
#[test]
|
||||
fn event_store() {
|
||||
let a = (
|
||||
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
|
||||
Style::default(),
|
||||
);
|
||||
let b = (
|
||||
Date::from_calendar_date(2023, Month::January, 2).unwrap(),
|
||||
Style::default().bg(Color::Red).fg(Color::Blue),
|
||||
);
|
||||
let mut s = CalendarEventStore::default();
|
||||
s.add(b.0, b.1);
|
||||
|
||||
assert_eq!(
|
||||
s.get_style(a.0),
|
||||
a.1,
|
||||
"Date not added to the styler should look up as Style::default()"
|
||||
);
|
||||
assert_eq!(
|
||||
s.get_style(b.0),
|
||||
b.1,
|
||||
"Date added to styler should return the provided style"
|
||||
);
|
||||
}
|
||||
}
|
||||
62
src/widgets/canvas/circle.rs
Normal file
62
src/widgets/canvas/circle.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
|
||||
/// Shape to draw a circle with a given center and radius and with the given color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Circle {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub radius: f64,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Shape for Circle {
|
||||
fn draw(&self, painter: &mut Painter<'_, '_>) {
|
||||
for angle in 0..360 {
|
||||
let radians = f64::from(angle).to_radians();
|
||||
let circle_x = self.radius.mul_add(radians.cos(), self.x);
|
||||
let circle_y = self.radius.mul_add(radians.sin(), self.y);
|
||||
if let Some((x, y)) = painter.get_point(circle_x, circle_y) {
|
||||
painter.paint(x, y, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::Color;
|
||||
use crate::symbols::Marker;
|
||||
use crate::widgets::canvas::{Canvas, Circle};
|
||||
use crate::widgets::Widget;
|
||||
|
||||
#[test]
|
||||
fn test_it_draws_a_circle() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
|
||||
let canvas = Canvas::default()
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Circle {
|
||||
x: 5.0,
|
||||
y: 2.0,
|
||||
radius: 5.0,
|
||||
color: Color::Reset,
|
||||
});
|
||||
})
|
||||
.marker(Marker::Braille)
|
||||
.x_bounds([-10.0, 10.0])
|
||||
.y_bounds([-10.0, 10.0]);
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" ⢀⣠⢤⣀ ",
|
||||
" ⢰⠋ ⠈⣇",
|
||||
" ⠘⣆⡀ ⣠⠇",
|
||||
" ⠉⠉⠁ ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,8 @@ pub struct Line {
|
||||
|
||||
impl Shape for Line {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
let (x1, y1) = match painter.get_point(self.x1, self.y1) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
let (x2, y2) = match painter.get_point(self.x2, self.y2) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
let Some((x1, y1)) = painter.get_point(self.x1, self.y1) else { return };
|
||||
let Some((x2, y2)) = painter.get_point(self.x2, self.y2) else { return };
|
||||
let (dx, x_range) = if x2 >= x1 {
|
||||
(x2 - x1, x1..=x2)
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
mod circle;
|
||||
mod line;
|
||||
mod map;
|
||||
mod points;
|
||||
mod rectangle;
|
||||
mod world;
|
||||
|
||||
pub use self::circle::Circle;
|
||||
pub use self::line::Line;
|
||||
pub use self::map::{Map, MapResolution};
|
||||
pub use self::points::Points;
|
||||
@@ -14,7 +16,7 @@ use crate::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Spans,
|
||||
text::Line as TextLine,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
@@ -29,7 +31,7 @@ pub trait Shape {
|
||||
pub struct Label<'a> {
|
||||
x: f64,
|
||||
y: f64,
|
||||
spans: Spans<'a>,
|
||||
line: TextLine<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -183,7 +185,7 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use tui::{symbols, widgets::canvas::{Painter, Context}};
|
||||
/// use ratatui::{symbols, widgets::canvas::{Painter, Context}};
|
||||
///
|
||||
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
@@ -220,7 +222,7 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use tui::{style::Color, symbols, widgets::canvas::{Painter, Context}};
|
||||
/// use ratatui::{style::Color, symbols, widgets::canvas::{Painter, Context}};
|
||||
///
|
||||
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
@@ -260,9 +262,13 @@ impl<'a> Context<'a> {
|
||||
y_bounds: [f64; 2],
|
||||
marker: symbols::Marker,
|
||||
) -> Context<'a> {
|
||||
let dot = symbols::DOT.chars().next().unwrap();
|
||||
let block = symbols::block::FULL.chars().next().unwrap();
|
||||
let bar = symbols::bar::HALF.chars().next().unwrap();
|
||||
let grid: Box<dyn Grid> = match marker {
|
||||
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, '•')),
|
||||
symbols::Marker::Block => Box::new(CharGrid::new(width, height, '▄')),
|
||||
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
|
||||
symbols::Marker::Block => Box::new(CharGrid::new(width, height, block)),
|
||||
symbols::Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
|
||||
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
|
||||
};
|
||||
Context {
|
||||
@@ -293,21 +299,21 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
|
||||
/// Print a string on the canvas at the given position
|
||||
pub fn print<T>(&mut self, x: f64, y: f64, spans: T)
|
||||
pub fn print<T>(&mut self, x: f64, y: f64, line: T)
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
T: Into<TextLine<'a>>,
|
||||
{
|
||||
self.labels.push(Label {
|
||||
x,
|
||||
y,
|
||||
spans: spans.into(),
|
||||
line: line.into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Push the last layer if necessary
|
||||
fn finish(&mut self) {
|
||||
if self.dirty {
|
||||
self.layer()
|
||||
self.layer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,10 +323,10 @@ impl<'a> Context<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, Borders};
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
|
||||
/// # use tui::style::Color;
|
||||
/// # use ratatui::widgets::{Block, Borders};
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
|
||||
/// # use ratatui::style::Color;
|
||||
/// Canvas::default()
|
||||
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
|
||||
/// .x_bounds([-180.0, 180.0])
|
||||
@@ -384,11 +390,18 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Define the viewport of the canvas.
|
||||
/// If you were to "zoom" to a certain part of the world you may want to choose different
|
||||
/// bounds.
|
||||
pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
|
||||
self.x_bounds = bounds;
|
||||
self
|
||||
}
|
||||
|
||||
/// Define the viewport of the canvas.
|
||||
///
|
||||
/// If you were to "zoom" to a certain part of the world you may want to choose different
|
||||
/// bounds.
|
||||
pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
|
||||
self.y_bounds = bounds;
|
||||
self
|
||||
@@ -412,8 +425,8 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::canvas::Canvas;
|
||||
/// # use tui::symbols;
|
||||
/// # use ratatui::widgets::canvas::Canvas;
|
||||
/// # use ratatui::symbols;
|
||||
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
|
||||
///
|
||||
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
|
||||
@@ -444,10 +457,7 @@ where
|
||||
|
||||
let width = canvas_area.width as usize;
|
||||
|
||||
let painter = match self.painter {
|
||||
Some(ref p) => p,
|
||||
None => return,
|
||||
};
|
||||
let Some(ref painter) = self.painter else { return };
|
||||
|
||||
// Create a blank context that match the size of the canvas
|
||||
let mut ctx = Context::new(
|
||||
@@ -461,7 +471,7 @@ where
|
||||
painter(&mut ctx);
|
||||
ctx.finish();
|
||||
|
||||
// Retreive painted points for each layer
|
||||
// Retrieve painted points for each layer
|
||||
for layer in ctx.layers {
|
||||
for (i, (ch, color)) in layer
|
||||
.string
|
||||
@@ -497,7 +507,107 @@ where
|
||||
{
|
||||
let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
|
||||
let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
|
||||
buf.set_spans(x, y, &label.spans, canvas_area.right() - x);
|
||||
buf.set_line(x, y, &label.line, canvas_area.right() - x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{buffer::Cell, symbols::Marker};
|
||||
use indoc::indoc;
|
||||
|
||||
// helper to test the canvas checks that drawing a vertical and horizontal line
|
||||
// results in the expected output
|
||||
fn test_marker(marker: Marker, expected: &str) {
|
||||
let area = Rect::new(0, 0, 5, 5);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_char('x');
|
||||
let mut buf = Buffer::filled(area, &cell);
|
||||
let horizontal_line = Line {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: 10.0,
|
||||
y2: 0.0,
|
||||
color: Color::Reset,
|
||||
};
|
||||
let vertical_line = Line {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: 0.0,
|
||||
y2: 10.0,
|
||||
color: Color::Reset,
|
||||
};
|
||||
Canvas::default()
|
||||
.marker(marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&vertical_line);
|
||||
ctx.draw(&horizontal_line);
|
||||
})
|
||||
.x_bounds([0.0, 10.0])
|
||||
.y_bounds([0.0, 10.0])
|
||||
.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(expected.lines().collect()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bar_marker() {
|
||||
test_marker(
|
||||
Marker::Bar,
|
||||
indoc!(
|
||||
"
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄▄▄▄▄"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_marker() {
|
||||
test_marker(
|
||||
Marker::Block,
|
||||
indoc!(
|
||||
"
|
||||
█xxxx
|
||||
█xxxx
|
||||
█xxxx
|
||||
█xxxx
|
||||
█████"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_braille_marker() {
|
||||
test_marker(
|
||||
Marker::Braille,
|
||||
indoc!(
|
||||
"
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⣇⣀⣀⣀⣀"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dot_marker() {
|
||||
test_marker(
|
||||
Marker::Dot,
|
||||
indoc!(
|
||||
"
|
||||
•xxxx
|
||||
•xxxx
|
||||
•xxxx
|
||||
•xxxx
|
||||
•••••"
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
layout::{Constraint, Rect},
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
text::{Line as TextLine, Span},
|
||||
widgets::{
|
||||
canvas::{Canvas, Line, Points},
|
||||
Block, Borders, Widget,
|
||||
@@ -19,7 +19,7 @@ use crate::{
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Axis<'a> {
|
||||
/// Title displayed next to axis end
|
||||
title: Option<Spans<'a>>,
|
||||
title: Option<TextLine<'a>>,
|
||||
/// Bounds for the axis (all data points outside these limits will not be represented)
|
||||
bounds: [f64; 2],
|
||||
/// A list of labels to put to the left or below the axis
|
||||
@@ -36,7 +36,7 @@ impl<'a> Default for Axis<'a> {
|
||||
title: None,
|
||||
bounds: [0.0, 0.0],
|
||||
labels: None,
|
||||
style: Default::default(),
|
||||
style: Style::default(),
|
||||
labels_alignment: Alignment::Left,
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ impl<'a> Default for Axis<'a> {
|
||||
impl<'a> Axis<'a> {
|
||||
pub fn title<T>(mut self, title: T) -> Axis<'a>
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
T: Into<TextLine<'a>>,
|
||||
{
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
@@ -53,12 +53,12 @@ impl<'a> Axis<'a> {
|
||||
|
||||
#[deprecated(
|
||||
since = "0.10.0",
|
||||
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
|
||||
note = "You should use styling capabilities of `text::Line` given as argument of the `title` method to apply styling to the title."
|
||||
)]
|
||||
pub fn title_style(mut self, style: Style) -> Axis<'a> {
|
||||
if let Some(t) = self.title {
|
||||
let title = String::from(t);
|
||||
self.title = Some(Spans::from(Span::styled(title, style)));
|
||||
self.title = Some(TextLine::from(Span::styled(title, style)));
|
||||
}
|
||||
self
|
||||
}
|
||||
@@ -181,10 +181,10 @@ struct ChartLayout {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::symbols;
|
||||
/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # use tui::text::Span;
|
||||
/// # use ratatui::symbols;
|
||||
/// # use ratatui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// # use ratatui::text::Span;
|
||||
/// let datasets = vec![
|
||||
/// Dataset::default()
|
||||
/// .name("data1")
|
||||
@@ -234,7 +234,7 @@ impl<'a> Chart<'a> {
|
||||
block: None,
|
||||
x_axis: Axis::default(),
|
||||
y_axis: Axis::default(),
|
||||
style: Default::default(),
|
||||
style: Style::default(),
|
||||
datasets,
|
||||
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
|
||||
}
|
||||
@@ -265,8 +265,8 @@ impl<'a> Chart<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::Chart;
|
||||
/// # use tui::layout::Constraint;
|
||||
/// # use ratatui::widgets::Chart;
|
||||
/// # use ratatui::layout::Constraint;
|
||||
/// let constraints = (
|
||||
/// Constraint::Ratio(1, 3),
|
||||
/// Constraint::Ratio(1, 4)
|
||||
@@ -366,7 +366,7 @@ impl<'a> Chart<'a> {
|
||||
let width_left_of_y_axis = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => {
|
||||
// The last character of the label should be below the Y-Axis when it exists, not on its left
|
||||
let y_axis_offset = if has_y_axis { 1 } else { 0 };
|
||||
let y_axis_offset = u16::from(has_y_axis);
|
||||
first_label_width.saturating_sub(y_axis_offset)
|
||||
}
|
||||
Alignment::Center => first_label_width / 2,
|
||||
@@ -385,10 +385,7 @@ impl<'a> Chart<'a> {
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) {
|
||||
let y = match layout.label_x {
|
||||
Some(y) => y,
|
||||
None => return,
|
||||
};
|
||||
let Some(y) = layout.label_x else { return };
|
||||
let labels = self.x_axis.labels.as_ref().unwrap();
|
||||
let labels_len = labels.len() as u16;
|
||||
if labels_len < 2 {
|
||||
@@ -470,10 +467,7 @@ impl<'a> Chart<'a> {
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) {
|
||||
let x = match layout.label_y {
|
||||
Some(x) => x,
|
||||
None => return,
|
||||
};
|
||||
let Some(x) = layout.label_y else { return };
|
||||
let labels = self.y_axis.labels.as_ref().unwrap();
|
||||
let labels_len = labels.len() as u16;
|
||||
for (i, label) in labels.iter().enumerate() {
|
||||
@@ -563,7 +557,7 @@ impl<'a> Widget for Chart<'a> {
|
||||
x2: data[1].0,
|
||||
y2: data[1].1,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -597,7 +591,7 @@ impl<'a> Widget for Chart<'a> {
|
||||
},
|
||||
original_style,
|
||||
);
|
||||
buf.set_spans(x, y, &title, width);
|
||||
buf.set_line(x, y, &title, width);
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_y {
|
||||
@@ -612,7 +606,7 @@ impl<'a> Widget for Chart<'a> {
|
||||
},
|
||||
original_style,
|
||||
);
|
||||
buf.set_spans(x, y, &title, width);
|
||||
buf.set_line(x, y, &title, width);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,7 +639,7 @@ mod tests {
|
||||
for case in &cases {
|
||||
let datasets = (0..10)
|
||||
.map(|i| {
|
||||
let name = format!("Dataset #{}", i);
|
||||
let name = format!("Dataset #{i}");
|
||||
Dataset::default().name(name).data(&data)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -2,16 +2,16 @@ use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
|
||||
/// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups).
|
||||
///
|
||||
/// This widget **cannot be used to clear the terminal on the first render** as `tui` assumes the
|
||||
/// This widget **cannot be used to clear the terminal on the first render** as `ratatui` assumes the
|
||||
/// render area is empty. Use [`crate::Terminal::clear`] instead.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Clear, Block, Borders};
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::Frame;
|
||||
/// # use tui::backend::Backend;
|
||||
/// # use ratatui::widgets::{Clear, Block, Borders};
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::Frame;
|
||||
/// # use ratatui::backend::Backend;
|
||||
/// fn draw_on_clear<B: Backend>(f: &mut Frame<B>, area: Rect) {
|
||||
/// let block = Block::default().title("Block").borders(Borders::ALL);
|
||||
/// f.render_widget(Clear, area); // <- this will clear/reset the area first
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
@@ -12,8 +12,8 @@ use crate::{
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Widget, Gauge, Block, Borders};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::widgets::{Widget, Gauge, Block, Borders};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// Gauge::default()
|
||||
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
|
||||
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::ITALIC))
|
||||
@@ -111,8 +111,7 @@ impl<'a> Widget for Gauge<'a> {
|
||||
// label is put at the center of the gauge_area
|
||||
let label = {
|
||||
let pct = f64::round(self.ratio * 100.0);
|
||||
self.label
|
||||
.unwrap_or_else(|| Span::from(format!("{}%", pct)))
|
||||
self.label.unwrap_or_else(|| Span::from(format!("{pct}%")))
|
||||
};
|
||||
let clamped_label_width = gauge_area.width.min(label.width() as u16);
|
||||
let label_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2;
|
||||
@@ -139,7 +138,7 @@ impl<'a> Widget for Gauge<'a> {
|
||||
.set_symbol(get_unicode_block(filled_width % 1.0));
|
||||
}
|
||||
}
|
||||
// set the span
|
||||
// set the line
|
||||
buf.set_span(label_col, label_row, &label, clamped_label_width);
|
||||
}
|
||||
}
|
||||
@@ -163,9 +162,9 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Widget, LineGauge, Block, Borders};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # use tui::symbols;
|
||||
/// # use ratatui::widgets::{Widget, LineGauge, Block, Borders};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::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))
|
||||
@@ -175,7 +174,7 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
|
||||
pub struct LineGauge<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
ratio: f64,
|
||||
label: Option<Spans<'a>>,
|
||||
label: Option<Line<'a>>,
|
||||
line_set: symbols::line::Set,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
@@ -216,7 +215,7 @@ impl<'a> LineGauge<'a> {
|
||||
|
||||
pub fn label<T>(mut self, label: T) -> Self
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
@@ -252,8 +251,8 @@ impl<'a> Widget for LineGauge<'a> {
|
||||
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(
|
||||
.unwrap_or_else(move || Line::from(format!("{:.0}%", ratio * 100.0)));
|
||||
let (col, row) = buf.set_line(
|
||||
gauge_area.left(),
|
||||
gauge_area.top(),
|
||||
&label,
|
||||
|
||||
@@ -14,6 +14,24 @@ pub struct ListState {
|
||||
}
|
||||
|
||||
impl ListState {
|
||||
pub fn offset(&self) -> usize {
|
||||
self.offset
|
||||
}
|
||||
|
||||
pub fn offset_mut(&mut self) -> &mut usize {
|
||||
&mut self.offset
|
||||
}
|
||||
|
||||
pub fn with_selected(mut self, selected: Option<usize>) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_offset(mut self, offset: usize) -> Self {
|
||||
self.offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
@@ -26,7 +44,7 @@ impl ListState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListItem<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
@@ -51,6 +69,10 @@ impl<'a> ListItem<'a> {
|
||||
pub fn height(&self) -> usize {
|
||||
self.content.height()
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to display several items among which one can be selected (optional)
|
||||
@@ -58,8 +80,8 @@ impl<'a> ListItem<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, Borders, List, ListItem};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::widgets::{Block, Borders, List, ListItem};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")];
|
||||
/// List::new(items)
|
||||
/// .block(Block::default().title("List").borders(Borders::ALL))
|
||||
@@ -128,6 +150,14 @@ impl<'a> List<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.items.is_empty()
|
||||
}
|
||||
|
||||
fn get_items_bounds(
|
||||
&self,
|
||||
selected: Option<usize>,
|
||||
@@ -205,16 +235,13 @@ impl<'a> StatefulWidget for List<'a> {
|
||||
.skip(state.offset)
|
||||
.take(end - start)
|
||||
{
|
||||
let (x, y) = match self.start_corner {
|
||||
Corner::BottomLeft => {
|
||||
current_height += item.height() as u16;
|
||||
(list_area.left(), list_area.bottom() - current_height)
|
||||
}
|
||||
_ => {
|
||||
let pos = (list_area.left(), list_area.top() + current_height);
|
||||
current_height += item.height() as u16;
|
||||
pos
|
||||
}
|
||||
let (x, y) = if self.start_corner == Corner::BottomLeft {
|
||||
current_height += item.height() as u16;
|
||||
(list_area.left(), list_area.bottom() - current_height)
|
||||
} else {
|
||||
let pos = (list_area.left(), list_area.top() + current_height);
|
||||
current_height += item.height() as u16;
|
||||
pos
|
||||
};
|
||||
let area = Rect {
|
||||
x,
|
||||
@@ -225,9 +252,9 @@ impl<'a> StatefulWidget for List<'a> {
|
||||
let item_style = self.style.patch(item.style);
|
||||
buf.set_style(area, item_style);
|
||||
|
||||
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
|
||||
let is_selected = state.selected.map_or(false, |s| s == i);
|
||||
for (j, line) in item.content.lines.iter().enumerate() {
|
||||
// if the item is selected, we need to display the hightlight symbol:
|
||||
// if the item is selected, we need to display the highlight symbol:
|
||||
// - either for the first line of the item only,
|
||||
// - or for each line of the item if the appropriate option is set
|
||||
let symbol = if is_selected && (j == 0 || self.repeat_highlight_symbol) {
|
||||
@@ -243,11 +270,11 @@ impl<'a> StatefulWidget for List<'a> {
|
||||
list_area.width as usize,
|
||||
item_style,
|
||||
);
|
||||
(elem_x, (list_area.width - (elem_x - x)) as u16)
|
||||
(elem_x, (list_area.width - (elem_x - x)))
|
||||
} else {
|
||||
(x, list_area.width)
|
||||
};
|
||||
buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
|
||||
buf.set_line(elem_x, y + j as u16, line, max_element_width);
|
||||
}
|
||||
if is_selected {
|
||||
buf.set_style(area, self.highlight_style);
|
||||
@@ -262,3 +289,595 @@ impl<'a> Widget for List<'a> {
|
||||
StatefulWidget::render(self, area, buf, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
assert_buffer_eq,
|
||||
style::Color,
|
||||
text::{Line, Span},
|
||||
widgets::{Borders, StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_list_state_selected() {
|
||||
let mut state = ListState::default();
|
||||
assert_eq!(state.selected(), None);
|
||||
|
||||
state.select(Some(1));
|
||||
assert_eq!(state.selected(), Some(1));
|
||||
|
||||
state.select(None);
|
||||
assert_eq!(state.selected(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_state_select() {
|
||||
let mut state = ListState::default();
|
||||
assert_eq!(state.selected, None);
|
||||
assert_eq!(state.offset, 0);
|
||||
|
||||
state.select(Some(2));
|
||||
assert_eq!(state.selected, Some(2));
|
||||
assert_eq!(state.offset, 0);
|
||||
|
||||
state.select(None);
|
||||
assert_eq!(state.selected, None);
|
||||
assert_eq!(state.offset, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_item_new_from_str() {
|
||||
let item = ListItem::new("Test item");
|
||||
assert_eq!(item.content, Text::from("Test item"));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_item_new_from_string() {
|
||||
let item = ListItem::new("Test item".to_string());
|
||||
assert_eq!(item.content, Text::from("Test item"));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_item_new_from_cow_str() {
|
||||
let item = ListItem::new(Cow::Borrowed("Test item"));
|
||||
assert_eq!(item.content, Text::from("Test item"));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_item_new_from_span() {
|
||||
let span = Span::styled("Test item", Style::default().fg(Color::Blue));
|
||||
let item = ListItem::new(span.clone());
|
||||
assert_eq!(item.content, Text::from(span));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_item_new_from_spans() {
|
||||
let spans = Line::from(vec![
|
||||
Span::styled("Test ", Style::default().fg(Color::Blue)),
|
||||
Span::styled("item", Style::default().fg(Color::Red)),
|
||||
]);
|
||||
let item = ListItem::new(spans.clone());
|
||||
assert_eq!(item.content, Text::from(spans));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_item_new_from_vec_spans() {
|
||||
let lines = vec![
|
||||
Line::from(vec![
|
||||
Span::styled("Test ", Style::default().fg(Color::Blue)),
|
||||
Span::styled("item", Style::default().fg(Color::Red)),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("Second ", Style::default().fg(Color::Green)),
|
||||
Span::styled("line", Style::default().fg(Color::Yellow)),
|
||||
]),
|
||||
];
|
||||
let item = ListItem::new(lines.clone());
|
||||
assert_eq!(item.content, Text::from(lines));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_item_style() {
|
||||
let item = ListItem::new("Test item").style(Style::default().bg(Color::Red));
|
||||
assert_eq!(item.content, Text::from("Test item"));
|
||||
assert_eq!(item.style, Style::default().bg(Color::Red));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_item_height() {
|
||||
let item = ListItem::new("Test item");
|
||||
assert_eq!(item.height(), 1);
|
||||
|
||||
let item = ListItem::new("Test item\nSecond line");
|
||||
assert_eq!(item.height(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_item_width() {
|
||||
let item = ListItem::new("Test item");
|
||||
assert_eq!(item.width(), 9);
|
||||
}
|
||||
|
||||
/// helper method to take a vector of strings and return a vector of list items
|
||||
fn list_items(items: Vec<&str>) -> Vec<ListItem> {
|
||||
items.iter().map(|i| ListItem::new(i.to_string())).collect()
|
||||
}
|
||||
|
||||
/// helper method to render a widget to an empty buffer with the default state
|
||||
fn render_widget(widget: List<'_>, width: u16, height: u16) -> Buffer {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
|
||||
Widget::render(widget, buffer.area, &mut buffer);
|
||||
buffer
|
||||
}
|
||||
|
||||
/// helper method to render a widget to an empty buffer with a given state
|
||||
fn render_stateful_widget(
|
||||
widget: List<'_>,
|
||||
state: &mut ListState,
|
||||
width: u16,
|
||||
height: u16,
|
||||
) -> Buffer {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
|
||||
StatefulWidget::render(widget, buffer.area, &mut buffer, state);
|
||||
buffer
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_does_not_render_in_small_space() {
|
||||
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
|
||||
let list = List::new(items.clone()).highlight_symbol(">>");
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
|
||||
// attempt to render into an area of the buffer with 0 width
|
||||
Widget::render(list.clone(), Rect::new(0, 0, 0, 3), &mut buffer);
|
||||
assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
|
||||
|
||||
// attempt to render into an area of the buffer with 0 height
|
||||
Widget::render(list.clone(), Rect::new(0, 0, 15, 0), &mut buffer);
|
||||
assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
|
||||
|
||||
let list = List::new(items)
|
||||
.highlight_symbol(">>")
|
||||
.block(Block::default().borders(Borders::all()));
|
||||
// attempt to render into an area of the buffer with zero height after
|
||||
// setting the block borders
|
||||
Widget::render(list, Rect::new(0, 0, 15, 2), &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
"┌─────────────┐",
|
||||
"└─────────────┘",
|
||||
" "
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_combinations() {
|
||||
fn test_case_render(items: &[ListItem], expected_lines: Vec<&str>) {
|
||||
let list = List::new(items.to_owned()).highlight_symbol(">>");
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
|
||||
|
||||
Widget::render(list, buffer.area, &mut buffer);
|
||||
|
||||
let expected = Buffer::with_lines(expected_lines);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
fn test_case_render_stateful(
|
||||
items: &[ListItem],
|
||||
selected: Option<usize>,
|
||||
expected_lines: Vec<&str>,
|
||||
) {
|
||||
let list = List::new(items.to_owned()).highlight_symbol(">>");
|
||||
let mut state = ListState::default().with_selected(selected);
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
|
||||
|
||||
StatefulWidget::render(list, buffer.area, &mut buffer, &mut state);
|
||||
|
||||
let expected = Buffer::with_lines(expected_lines);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
let empty_items: Vec<ListItem> = Vec::new();
|
||||
let single_item = list_items(vec!["Item 0"]);
|
||||
let multiple_items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
|
||||
let multi_line_items = list_items(vec!["Item 0\nLine 2", "Item 1", "Item 2"]);
|
||||
|
||||
// empty list
|
||||
test_case_render(
|
||||
&empty_items,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&empty_items,
|
||||
None,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&empty_items,
|
||||
Some(0),
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
|
||||
// single item
|
||||
test_case_render(
|
||||
&single_item,
|
||||
vec![
|
||||
"Item 0 ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&single_item,
|
||||
None,
|
||||
vec![
|
||||
"Item 0 ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&single_item,
|
||||
Some(0),
|
||||
vec![
|
||||
">>Item 0 ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&single_item,
|
||||
Some(1),
|
||||
vec![
|
||||
" Item 0 ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
|
||||
// multiple items
|
||||
test_case_render(
|
||||
&multiple_items,
|
||||
vec![
|
||||
"Item 0 ",
|
||||
"Item 1 ",
|
||||
"Item 2 ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&multiple_items,
|
||||
None,
|
||||
vec![
|
||||
"Item 0 ",
|
||||
"Item 1 ",
|
||||
"Item 2 ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&multiple_items,
|
||||
Some(0),
|
||||
vec![
|
||||
">>Item 0 ",
|
||||
" Item 1 ",
|
||||
" Item 2 ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&multiple_items,
|
||||
Some(1),
|
||||
vec![
|
||||
" Item 0 ",
|
||||
">>Item 1 ",
|
||||
" Item 2 ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&multiple_items,
|
||||
Some(3),
|
||||
vec![
|
||||
" Item 0 ",
|
||||
" Item 1 ",
|
||||
" Item 2 ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
|
||||
// multi line items
|
||||
test_case_render(
|
||||
&multi_line_items,
|
||||
vec![
|
||||
"Item 0 ",
|
||||
"Line 2 ",
|
||||
"Item 1 ",
|
||||
"Item 2 ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&multi_line_items,
|
||||
None,
|
||||
vec![
|
||||
"Item 0 ",
|
||||
"Line 2 ",
|
||||
"Item 1 ",
|
||||
"Item 2 ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&multi_line_items,
|
||||
Some(0),
|
||||
vec![
|
||||
">>Item 0 ",
|
||||
" Line 2 ",
|
||||
" Item 1 ",
|
||||
" Item 2 ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
test_case_render_stateful(
|
||||
&multi_line_items,
|
||||
Some(1),
|
||||
vec![
|
||||
" Item 0 ",
|
||||
" Line 2 ",
|
||||
">>Item 1 ",
|
||||
" Item 2 ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_with_empty_strings() {
|
||||
let items = list_items(vec!["Item 0", "", "", "Item 1", "Item 2"]);
|
||||
let list = List::new(items).block(Block::default().title("List").borders(Borders::ALL));
|
||||
let buffer = render_widget(list, 10, 7);
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"┌List────┐",
|
||||
"│Item 0 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│Item 1 │",
|
||||
"│Item 2 │",
|
||||
"└────────┘",
|
||||
]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_block() {
|
||||
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
|
||||
let list = List::new(items).block(Block::default().title("List").borders(Borders::ALL));
|
||||
let buffer = render_widget(list, 10, 7);
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"┌List────┐",
|
||||
"│Item 0 │",
|
||||
"│Item 1 │",
|
||||
"│Item 2 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────┘",
|
||||
]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_style() {
|
||||
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
|
||||
let list = List::new(items).style(Style::default().fg(Color::Red));
|
||||
let buffer = render_widget(list, 10, 5);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"Item 0 ",
|
||||
"Item 1 ",
|
||||
"Item 2 ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
expected.set_style(buffer.area, Style::default().fg(Color::Red));
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_highlight_symbol_and_style() {
|
||||
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
|
||||
let list = List::new(items)
|
||||
.highlight_symbol(">>")
|
||||
.highlight_style(Style::default().fg(Color::Yellow));
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(1));
|
||||
|
||||
let buffer = render_stateful_widget(list, &mut state, 10, 5);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" Item 0 ",
|
||||
">>Item 1 ",
|
||||
" Item 2 ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 1, 10, 1), Style::default().fg(Color::Yellow));
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_repeat_highlight_symbol() {
|
||||
let items = list_items(vec!["Item 0\nLine 2", "Item 1", "Item 2"]);
|
||||
let list = List::new(items)
|
||||
.highlight_symbol(">>")
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.repeat_highlight_symbol(true);
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(0));
|
||||
|
||||
let buffer = render_stateful_widget(list, &mut state, 10, 5);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
">>Item 0 ",
|
||||
">>Line 2 ",
|
||||
" Item 1 ",
|
||||
" Item 2 ",
|
||||
" ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 10, 2), Style::default().fg(Color::Yellow));
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_start_corner_top_left() {
|
||||
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
|
||||
let list = List::new(items).start_corner(Corner::TopLeft);
|
||||
let buffer = render_widget(list, 10, 5);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"Item 0 ",
|
||||
"Item 1 ",
|
||||
"Item 2 ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_start_corner_bottom_left() {
|
||||
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
|
||||
let list = List::new(items).start_corner(Corner::BottomLeft);
|
||||
let buffer = render_widget(list, 10, 5);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
"Item 2 ",
|
||||
"Item 1 ",
|
||||
"Item 0 ",
|
||||
]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_truncate_items() {
|
||||
let items = list_items(vec!["Item 0", "Item 1", "Item 2", "Item 3", "Item 4"]);
|
||||
let list = List::new(items);
|
||||
let buffer = render_widget(list, 10, 3);
|
||||
let expected = Buffer::with_lines(vec!["Item 0 ", "Item 1 ", "Item 2 "]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_long_lines() {
|
||||
let items = list_items(vec![
|
||||
"Item 0 with a very long line that will be truncated",
|
||||
"Item 1",
|
||||
"Item 2",
|
||||
]);
|
||||
let list = List::new(items).highlight_symbol(">>");
|
||||
|
||||
fn test_case(list: List, selected: Option<usize>, expected_lines: Vec<&str>) {
|
||||
let mut state = ListState::default();
|
||||
state.select(selected);
|
||||
let buffer = render_stateful_widget(list.clone(), &mut state, 15, 3);
|
||||
let expected = Buffer::with_lines(expected_lines);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
test_case(
|
||||
list.clone(),
|
||||
None,
|
||||
vec!["Item 0 with a v", "Item 1 ", "Item 2 "],
|
||||
);
|
||||
test_case(
|
||||
list,
|
||||
Some(0),
|
||||
vec![">>Item 0 with a", " Item 1 ", " Item 2 "],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_selected_item_ensures_selected_item_is_visible_when_offset_is_before_visible_range(
|
||||
) {
|
||||
let items = list_items(vec![
|
||||
"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6",
|
||||
]);
|
||||
let list = List::new(items).highlight_symbol(">>");
|
||||
// Set the initial visible range to items 3, 4, and 5
|
||||
let mut state = ListState::default().with_selected(Some(1)).with_offset(3);
|
||||
let buffer = render_stateful_widget(list, &mut state, 10, 3);
|
||||
|
||||
let expected = Buffer::with_lines(vec![">>Item 1 ", " Item 2 ", " Item 3 "]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(state.selected, Some(1));
|
||||
assert_eq!(
|
||||
state.offset, 1,
|
||||
"did not scroll the selected item into view"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_selected_item_ensures_selected_item_is_visible_when_offset_is_after_visible_range()
|
||||
{
|
||||
let items = list_items(vec![
|
||||
"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6",
|
||||
]);
|
||||
let list = List::new(items).highlight_symbol(">>");
|
||||
// Set the initial visible range to items 3, 4, and 5
|
||||
let mut state = ListState::default().with_selected(Some(6)).with_offset(3);
|
||||
let buffer = render_stateful_widget(list, &mut state, 10, 3);
|
||||
|
||||
let expected = Buffer::with_lines(vec![" Item 4 ", " Item 5 ", ">>Item 6 "]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(state.selected, Some(6));
|
||||
assert_eq!(
|
||||
state.offset, 4,
|
||||
"did not scroll the selected item into view"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,13 @@
|
||||
//! - [`BarChart`]
|
||||
//! - [`Gauge`]
|
||||
//! - [`Sparkline`]
|
||||
//! - [`calendar::Monthly`]
|
||||
//! - [`Clear`]
|
||||
|
||||
mod barchart;
|
||||
mod block;
|
||||
#[cfg(feature = "widget-calendar")]
|
||||
pub mod calendar;
|
||||
pub mod canvas;
|
||||
mod chart;
|
||||
mod clear;
|
||||
@@ -29,13 +32,13 @@ mod table;
|
||||
mod tabs;
|
||||
|
||||
pub use self::barchart::BarChart;
|
||||
pub use self::block::{Block, BorderType};
|
||||
pub use self::block::{Block, BorderType, Padding};
|
||||
pub use self::chart::{Axis, Chart, Dataset, GraphType};
|
||||
pub use self::clear::Clear;
|
||||
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::sparkline::{RenderDirection, Sparkline};
|
||||
pub use self::table::{Cell, Row, Table, TableState};
|
||||
pub use self::tabs::Tabs;
|
||||
|
||||
@@ -44,17 +47,17 @@ use bitflags::bitflags;
|
||||
|
||||
bitflags! {
|
||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
||||
pub struct Borders: u32 {
|
||||
pub struct Borders: u8 {
|
||||
/// Show no border (default)
|
||||
const NONE = 0b0000_0001;
|
||||
const NONE = 0b0000;
|
||||
/// Show the top border
|
||||
const TOP = 0b0000_0010;
|
||||
const TOP = 0b0001;
|
||||
/// Show the right border
|
||||
const RIGHT = 0b0000_0100;
|
||||
const RIGHT = 0b0010;
|
||||
/// Show the bottom border
|
||||
const BOTTOM = 0b000_1000;
|
||||
const BOTTOM = 0b0100;
|
||||
/// Show the left border
|
||||
const LEFT = 0b0001_0000;
|
||||
const LEFT = 0b1000;
|
||||
/// Show all borders
|
||||
const ALL = Self::TOP.bits | Self::RIGHT.bits | Self::BOTTOM.bits | Self::LEFT.bits;
|
||||
}
|
||||
@@ -86,9 +89,9 @@ pub trait Widget {
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::{Backend, TestBackend};
|
||||
/// # use tui::widgets::{Widget, List, ListItem, ListState};
|
||||
/// # use ratatui::Terminal;
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::widgets::{Widget, List, ListItem, ListState};
|
||||
///
|
||||
/// // Let's say we have some events to display.
|
||||
/// struct Events {
|
||||
@@ -165,8 +168,8 @@ pub trait Widget {
|
||||
/// loop {
|
||||
/// terminal.draw(|f| {
|
||||
/// // The items managed by the application are transformed to something
|
||||
/// // that is understood by tui.
|
||||
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_ref())).collect();
|
||||
/// // that is understood by ratatui.
|
||||
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_str())).collect();
|
||||
/// // The `List` widget is then built with those items.
|
||||
/// let list = List::new(items);
|
||||
/// // Finally the widget is rendered using the associated state. `events.state` is
|
||||
@@ -182,3 +185,41 @@ pub trait StatefulWidget {
|
||||
type State;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
||||
|
||||
/// Macro that constructs and returns a [`Borders`] object from TOP, BOTTOM, LEFT, RIGHT, NONE, and ALL.
|
||||
/// Internally it creates an empty `Borders` object and then inserts each bit flag specified
|
||||
/// into it using `Borders::insert()`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
///```
|
||||
/// # use tui::widgets::{Block, Borders};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # use tui::border;
|
||||
///
|
||||
/// Block::default()
|
||||
/// //Construct a `Borders` object and use it in place
|
||||
/// .borders(border!(TOP, BOTTOM));
|
||||
///
|
||||
/// //`border!` can be called with any order of individual sides
|
||||
/// let bottom_first = border!(BOTTOM, LEFT, TOP);
|
||||
/// //with the ALL keyword which works as expected
|
||||
/// let all = border!(ALL);
|
||||
/// //or with nothing to return a `Borders::NONE' bitflag.
|
||||
/// let none = border!(NONE);
|
||||
///
|
||||
///```
|
||||
#[cfg(feature = "macros")]
|
||||
#[macro_export]
|
||||
macro_rules! border {
|
||||
( $($b:tt), +) => {{
|
||||
let mut border = Borders::empty();
|
||||
$(
|
||||
border.insert(Borders::$b);
|
||||
)*
|
||||
border
|
||||
}};
|
||||
() =>{
|
||||
Borders::NONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
@@ -8,8 +10,6 @@ use crate::{
|
||||
Block, Widget,
|
||||
},
|
||||
};
|
||||
use std::iter;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
|
||||
match alignment {
|
||||
@@ -24,17 +24,17 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::text::{Text, Spans, Span};
|
||||
/// # use tui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # use tui::layout::{Alignment};
|
||||
/// # use ratatui::text::{Text, Line, Span};
|
||||
/// # use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::layout::{Alignment};
|
||||
/// let text = vec![
|
||||
/// Spans::from(vec![
|
||||
/// Line::from(vec![
|
||||
/// Span::raw("First"),
|
||||
/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)),
|
||||
/// Span::raw("."),
|
||||
/// ]),
|
||||
/// Spans::from(Span::styled("Second line", Style::default().fg(Color::Red))),
|
||||
/// Line::from(Span::styled("Second line", Style::default().fg(Color::Red))),
|
||||
/// ];
|
||||
/// Paragraph::new(text)
|
||||
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
|
||||
@@ -63,8 +63,8 @@ pub struct Paragraph<'a> {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Paragraph, Wrap};
|
||||
/// # use tui::text::Text;
|
||||
/// # use ratatui::widgets::{Paragraph, Wrap};
|
||||
/// # use ratatui::text::Text;
|
||||
/// let bullet_points = Text::from(r#"Some indented points:
|
||||
/// - First thing goes here and is long so that it wraps
|
||||
/// - Here is another point that is long enough to wrap"#);
|
||||
@@ -98,7 +98,7 @@ impl<'a> Paragraph<'a> {
|
||||
{
|
||||
Paragraph {
|
||||
block: None,
|
||||
style: Default::default(),
|
||||
style: Style::default(),
|
||||
wrap: None,
|
||||
text: text.into(),
|
||||
scroll: (0, 0),
|
||||
@@ -149,33 +149,34 @@ impl<'a> Widget for Paragraph<'a> {
|
||||
}
|
||||
|
||||
let style = self.style;
|
||||
let mut styled = self.text.lines.iter().flat_map(|spans| {
|
||||
spans
|
||||
.0
|
||||
.iter()
|
||||
.flat_map(|span| span.styled_graphemes(style))
|
||||
// Required given the way composers work but might be refactored out if we change
|
||||
// composers to operate on lines instead of a stream of graphemes.
|
||||
.chain(iter::once(StyledGrapheme {
|
||||
symbol: "\n",
|
||||
style: self.style,
|
||||
}))
|
||||
let styled = self.text.lines.iter().map(|line| {
|
||||
(
|
||||
line.spans
|
||||
.iter()
|
||||
.flat_map(|span| span.styled_graphemes(style)),
|
||||
line.alignment.unwrap_or(self.alignment),
|
||||
)
|
||||
});
|
||||
|
||||
let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
|
||||
Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
|
||||
Box::new(WordWrapper::new(styled, text_area.width, trim))
|
||||
} else {
|
||||
let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
|
||||
if let Alignment::Left = self.alignment {
|
||||
line_composer.set_horizontal_offset(self.scroll.1);
|
||||
}
|
||||
let mut line_composer = Box::new(LineTruncator::new(styled, text_area.width));
|
||||
line_composer.set_horizontal_offset(self.scroll.1);
|
||||
line_composer
|
||||
};
|
||||
let mut y = 0;
|
||||
while let Some((current_line, current_line_width)) = line_composer.next_line() {
|
||||
while let Some((current_line, current_line_width, current_line_alignment)) =
|
||||
line_composer.next_line()
|
||||
{
|
||||
if y >= self.scroll.0 {
|
||||
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
|
||||
let mut x =
|
||||
get_line_offset(current_line_width, text_area.width, current_line_alignment);
|
||||
for StyledGrapheme { symbol, style } in current_line {
|
||||
let width = symbol.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
|
||||
.set_symbol(if symbol.is_empty() {
|
||||
// If the symbol is empty, the last char which rendered last time will
|
||||
@@ -185,7 +186,7 @@ impl<'a> Widget for Paragraph<'a> {
|
||||
symbol
|
||||
})
|
||||
.set_style(*style);
|
||||
x += symbol.width() as u16;
|
||||
x += width as u16;
|
||||
}
|
||||
}
|
||||
y += 1;
|
||||
@@ -195,3 +196,496 @@ impl<'a> Widget for Paragraph<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::backend::TestBackend;
|
||||
use crate::{
|
||||
style::Color,
|
||||
text::{Line, Span},
|
||||
widgets::Borders,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
/// Tests the [`Paragraph`] widget against the expected [`Buffer`] by rendering it onto an equal area
|
||||
/// and comparing the rendered and expected content.
|
||||
/// This can be used for easy testing of varying configured paragraphs with the same expected
|
||||
/// buffer or any other test case really.
|
||||
fn test_case(paragraph: &Paragraph, expected: Buffer) {
|
||||
let backend = TestBackend::new(expected.area.width, expected.area.height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
f.render_widget(paragraph.clone(), size);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_width_char_at_end_of_line() {
|
||||
let line = "foo\0";
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(line),
|
||||
Paragraph::new(line).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(line).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(¶graph, Buffer::with_lines(vec!["foo"]));
|
||||
test_case(¶graph, Buffer::with_lines(vec!["foo "]));
|
||||
test_case(¶graph, Buffer::with_lines(vec!["foo ", " "]));
|
||||
test_case(¶graph, Buffer::with_lines(vec!["foo", " "]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_empty_paragraph() {
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(""),
|
||||
Paragraph::new("").wrap(Wrap { trim: false }),
|
||||
Paragraph::new("").wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(¶graph, Buffer::with_lines(vec![" "]));
|
||||
test_case(¶graph, Buffer::with_lines(vec![" "]));
|
||||
test_case(¶graph, Buffer::with_lines(vec![" "; 10]));
|
||||
test_case(¶graph, Buffer::with_lines(vec![" ", " "]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_single_line_paragraph() {
|
||||
let text = "Hello, world!";
|
||||
let truncated_paragraph = Paragraph::new(text);
|
||||
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world! "]));
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec!["Hello, world! ", " "]),
|
||||
);
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec!["Hello, world!", " "]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_multi_line_paragraph() {
|
||||
let text = "This is a\nmultiline\nparagraph.";
|
||||
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(text),
|
||||
Paragraph::new(text).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(text).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(
|
||||
¶graph,
|
||||
Buffer::with_lines(vec!["This is a ", "multiline ", "paragraph."]),
|
||||
);
|
||||
test_case(
|
||||
¶graph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a ",
|
||||
"multiline ",
|
||||
"paragraph. ",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
¶graph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a ",
|
||||
"multiline ",
|
||||
"paragraph. ",
|
||||
" ",
|
||||
" ",
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_block() {
|
||||
let text = "Hello, world!";
|
||||
let truncated_paragraph =
|
||||
Paragraph::new(text).block(Block::default().title("Title").borders(Borders::ALL));
|
||||
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title────────┐",
|
||||
"│Hello, world!│",
|
||||
"└─────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title───────────┐",
|
||||
"│Hello, world! │",
|
||||
"└────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title────────────┐",
|
||||
"│Hello, world! │",
|
||||
"│ │",
|
||||
"└─────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
test_case(
|
||||
&truncated_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title──────┐",
|
||||
"│Hello, worl│",
|
||||
"│ │",
|
||||
"└───────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title──────┐",
|
||||
"│Hello, │",
|
||||
"│world! │",
|
||||
"└───────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌Title──────┐",
|
||||
"│Hello, │",
|
||||
"│world! │",
|
||||
"└───────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_word_wrap() {
|
||||
let text = "This is a long line of text that should wrap and contains a superultramegagigalong word.";
|
||||
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
|
||||
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a long line",
|
||||
"of text that should",
|
||||
"wrap and ",
|
||||
"contains a ",
|
||||
"superultramegagigal",
|
||||
"ong word. ",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a ",
|
||||
"long line of",
|
||||
"text that ",
|
||||
"should wrap ",
|
||||
" and ",
|
||||
"contains a ",
|
||||
"superultrame",
|
||||
"gagigalong ",
|
||||
"word. ",
|
||||
]),
|
||||
);
|
||||
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a long line",
|
||||
"of text that should",
|
||||
"wrap and ",
|
||||
"contains a ",
|
||||
"superultramegagigal",
|
||||
"ong word. ",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"This is a ",
|
||||
"long line of",
|
||||
"text that ",
|
||||
"should wrap ",
|
||||
"and contains",
|
||||
"a ",
|
||||
"superultrame",
|
||||
"gagigalong ",
|
||||
"word. ",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_line_truncation() {
|
||||
let text = "This is a long line of text that should be truncated.";
|
||||
let truncated_paragraph = Paragraph::new(text);
|
||||
|
||||
test_case(
|
||||
&truncated_paragraph,
|
||||
Buffer::with_lines(vec!["This is a long line of"]),
|
||||
);
|
||||
test_case(
|
||||
&truncated_paragraph,
|
||||
Buffer::with_lines(vec!["This is a long line of te"]),
|
||||
);
|
||||
test_case(
|
||||
&truncated_paragraph,
|
||||
Buffer::with_lines(vec!["This is a long line of "]),
|
||||
);
|
||||
test_case(
|
||||
&truncated_paragraph.clone().scroll((0, 2)),
|
||||
Buffer::with_lines(vec!["is is a long line of te"]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_left_alignment() {
|
||||
let text = "Hello, world!";
|
||||
let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Left);
|
||||
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world! "]));
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
|
||||
}
|
||||
|
||||
test_case(&truncated_paragraph, Buffer::with_lines(vec!["Hello, wor"]));
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec!["Hello, ", "world! "]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec!["Hello, ", "world! "]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_center_alignment() {
|
||||
let text = "Hello, world!";
|
||||
let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Center);
|
||||
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(paragraph, Buffer::with_lines(vec![" Hello, world! "]));
|
||||
test_case(paragraph, Buffer::with_lines(vec![" Hello, world! "]));
|
||||
test_case(paragraph, Buffer::with_lines(vec![" Hello, world! "]));
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
|
||||
}
|
||||
|
||||
test_case(&truncated_paragraph, Buffer::with_lines(vec!["Hello, wor"]));
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec![" Hello, ", " world! "]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec![" Hello, ", " world! "]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_right_alignment() {
|
||||
let text = "Hello, world!";
|
||||
let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Right);
|
||||
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(paragraph, Buffer::with_lines(vec![" Hello, world!"]));
|
||||
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
|
||||
}
|
||||
|
||||
test_case(&truncated_paragraph, Buffer::with_lines(vec!["Hello, wor"]));
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec![" Hello,", " world!"]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec![" Hello,", " world!"]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_scroll_offset() {
|
||||
let text = "This is a\ncool\nmultiline\nparagraph.";
|
||||
let truncated_paragraph = Paragraph::new(text).scroll((2, 0));
|
||||
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec!["multiline ", "paragraph. ", " "]),
|
||||
);
|
||||
test_case(paragraph, Buffer::with_lines(vec!["multiline "]));
|
||||
}
|
||||
|
||||
test_case(
|
||||
&truncated_paragraph.clone().scroll((2, 4)),
|
||||
Buffer::with_lines(vec!["iline ", "graph. "]),
|
||||
);
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec!["cool ", "multili", "ne "]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_zero_width_area() {
|
||||
let text = "Hello, world!";
|
||||
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(text),
|
||||
Paragraph::new(text).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(text).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
let area = Rect::new(0, 0, 0, 3);
|
||||
for paragraph in paragraphs {
|
||||
test_case(¶graph, Buffer::empty(area));
|
||||
test_case(¶graph.clone().scroll((2, 4)), Buffer::empty(area));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_zero_height_area() {
|
||||
let text = "Hello, world!";
|
||||
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(text),
|
||||
Paragraph::new(text).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(text).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
let area = Rect::new(0, 0, 10, 0);
|
||||
for paragraph in paragraphs {
|
||||
test_case(¶graph, Buffer::empty(area));
|
||||
test_case(¶graph.clone().scroll((2, 4)), Buffer::empty(area));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_styled_text() {
|
||||
let text = Line::from(vec![
|
||||
Span::styled("Hello, ", Style::default().fg(Color::Red)),
|
||||
Span::styled("world!", Style::default().fg(Color::Blue)),
|
||||
]);
|
||||
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(text.clone()),
|
||||
Paragraph::new(text.clone()).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(text.clone()).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
let mut expected_buffer = Buffer::with_lines(vec!["Hello, world!"]);
|
||||
expected_buffer.set_style(
|
||||
Rect::new(0, 0, 7, 1),
|
||||
Style::default().fg(Color::Red).bg(Color::Green),
|
||||
);
|
||||
expected_buffer.set_style(
|
||||
Rect::new(7, 0, 6, 1),
|
||||
Style::default().fg(Color::Blue).bg(Color::Green),
|
||||
);
|
||||
for paragraph in paragraphs {
|
||||
test_case(
|
||||
¶graph.style(Style::default().bg(Color::Green)),
|
||||
expected_buffer.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_special_characters() {
|
||||
let text = "Hello, <world>!";
|
||||
let paragraphs = vec![
|
||||
Paragraph::new(text),
|
||||
Paragraph::new(text).wrap(Wrap { trim: false }),
|
||||
Paragraph::new(text).wrap(Wrap { trim: true }),
|
||||
];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(¶graph, Buffer::with_lines(vec!["Hello, <world>!"]));
|
||||
test_case(¶graph, Buffer::with_lines(vec!["Hello, <world>! "]));
|
||||
test_case(
|
||||
¶graph,
|
||||
Buffer::with_lines(vec!["Hello, <world>! ", " "]),
|
||||
);
|
||||
test_case(
|
||||
¶graph,
|
||||
Buffer::with_lines(vec!["Hello, <world>!", " "]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_unicode_characters() {
|
||||
let text = "こんにちは, 世界! 😃";
|
||||
let truncated_paragraph = Paragraph::new(text);
|
||||
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
|
||||
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
|
||||
|
||||
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
|
||||
|
||||
for paragraph in paragraphs {
|
||||
test_case(paragraph, Buffer::with_lines(vec!["こんにちは, 世界! 😃"]));
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec!["こんにちは, 世界! 😃 "]),
|
||||
);
|
||||
}
|
||||
|
||||
test_case(
|
||||
&truncated_paragraph,
|
||||
Buffer::with_lines(vec!["こんにちは, 世 "]),
|
||||
);
|
||||
test_case(
|
||||
&wrapped_paragraph,
|
||||
Buffer::with_lines(vec!["こんにちは, ", "世界! 😃 "]),
|
||||
);
|
||||
test_case(
|
||||
&trimmed_paragraph,
|
||||
Buffer::with_lines(vec!["こんにちは, ", "世界! 😃 "]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,143 +1,228 @@
|
||||
use crate::text::StyledGrapheme;
|
||||
use std::collections::VecDeque;
|
||||
use std::vec::IntoIter;
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::layout::Alignment;
|
||||
use crate::text::StyledGrapheme;
|
||||
|
||||
const NBSP: &str = "\u{00a0}";
|
||||
|
||||
/// A state machine to pack styled symbols into lines.
|
||||
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
|
||||
/// iterators for that).
|
||||
pub trait LineComposer<'a> {
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>;
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)>;
|
||||
}
|
||||
|
||||
/// A state machine that wraps lines on word boundaries.
|
||||
pub struct WordWrapper<'a, 'b> {
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
pub struct WordWrapper<'a, O, I>
|
||||
where
|
||||
O: Iterator<Item = (I, Alignment)>, // Outer iterator providing the individual lines
|
||||
I: Iterator<Item = StyledGrapheme<'a>>, // Inner iterator providing the styled symbols of a line
|
||||
// Each line consists of an alignment and a series of symbols
|
||||
{
|
||||
/// The given, unprocessed lines
|
||||
input_lines: O,
|
||||
max_line_width: u16,
|
||||
wrapped_lines: Option<IntoIter<Vec<StyledGrapheme<'a>>>>,
|
||||
current_alignment: Alignment,
|
||||
current_line: Vec<StyledGrapheme<'a>>,
|
||||
next_line: Vec<StyledGrapheme<'a>>,
|
||||
/// Removes the leading whitespace from lines
|
||||
trim: bool,
|
||||
}
|
||||
|
||||
impl<'a, 'b> WordWrapper<'a, 'b> {
|
||||
pub fn new(
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
max_line_width: u16,
|
||||
trim: bool,
|
||||
) -> WordWrapper<'a, 'b> {
|
||||
impl<'a, O, I> WordWrapper<'a, O, I>
|
||||
where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
pub fn new(lines: O, max_line_width: u16, trim: bool) -> WordWrapper<'a, O, I> {
|
||||
WordWrapper {
|
||||
symbols,
|
||||
input_lines: lines,
|
||||
max_line_width,
|
||||
wrapped_lines: None,
|
||||
current_alignment: Alignment::Left,
|
||||
current_line: vec![],
|
||||
next_line: vec![],
|
||||
trim,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
|
||||
impl<'a, O, I> LineComposer<'a> for WordWrapper<'a, O, I>
|
||||
where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
std::mem::swap(&mut self.current_line, &mut self.next_line);
|
||||
self.next_line.truncate(0);
|
||||
|
||||
let mut current_line_width = self
|
||||
.current_line
|
||||
.iter()
|
||||
.map(|StyledGrapheme { symbol, .. }| symbol.width() as u16)
|
||||
.sum();
|
||||
let mut current_line: Option<Vec<StyledGrapheme<'a>>> = None;
|
||||
let mut line_width: u16 = 0;
|
||||
|
||||
let mut symbols_to_last_word_end: usize = 0;
|
||||
let mut width_to_last_word_end: u16 = 0;
|
||||
let mut prev_whitespace = false;
|
||||
let mut symbols_exhausted = true;
|
||||
for StyledGrapheme { symbol, style } in &mut self.symbols {
|
||||
symbols_exhausted = false;
|
||||
let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
|
||||
|
||||
// Ignore characters wider that the total max width.
|
||||
if symbol.width() as u16 > self.max_line_width
|
||||
// Skip leading whitespace when trim is enabled.
|
||||
|| self.trim && symbol_whitespace && symbol != "\n" && current_line_width == 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break on newline and discard it.
|
||||
if symbol == "\n" {
|
||||
if prev_whitespace {
|
||||
current_line_width = width_to_last_word_end;
|
||||
self.current_line.truncate(symbols_to_last_word_end);
|
||||
// Try to repeatedly retrieve next line
|
||||
while current_line.is_none() {
|
||||
// Retrieve next preprocessed wrapped line
|
||||
if let Some(line_iterator) = &mut self.wrapped_lines {
|
||||
if let Some(line) = line_iterator.next() {
|
||||
line_width = line
|
||||
.iter()
|
||||
.map(|grapheme| grapheme.symbol.width())
|
||||
.sum::<usize>() as u16;
|
||||
current_line = Some(line);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Mark the previous symbol as word end.
|
||||
if symbol_whitespace && !prev_whitespace {
|
||||
symbols_to_last_word_end = self.current_line.len();
|
||||
width_to_last_word_end = current_line_width;
|
||||
}
|
||||
// When no more preprocessed wrapped lines
|
||||
if current_line.is_none() {
|
||||
// Try to calculate next wrapped lines based on current whole line
|
||||
if let Some((line_symbols, line_alignment)) = &mut self.input_lines.next() {
|
||||
// Save the whole line's alignment
|
||||
self.current_alignment = *line_alignment;
|
||||
let mut wrapped_lines = vec![]; // Saves the wrapped lines
|
||||
let (mut current_line, mut current_line_width) = (vec![], 0); // Saves the unfinished wrapped line
|
||||
let (mut unfinished_word, mut word_width) = (vec![], 0); // Saves the partially processed word
|
||||
let (mut unfinished_whitespaces, mut whitespace_width) =
|
||||
(VecDeque::<StyledGrapheme>::new(), 0); // Saves the whitespaces of the partially unfinished word
|
||||
|
||||
self.current_line.push(StyledGrapheme { symbol, style });
|
||||
current_line_width += symbol.width() as u16;
|
||||
let mut has_seen_non_whitespace = false;
|
||||
for StyledGrapheme { symbol, style } in line_symbols {
|
||||
let symbol_whitespace =
|
||||
symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
|
||||
let symbol_width = symbol.width() as u16;
|
||||
// Ignore characters wider than the total max width
|
||||
if symbol_width > self.max_line_width {
|
||||
continue;
|
||||
}
|
||||
|
||||
if current_line_width > self.max_line_width {
|
||||
// If there was no word break in the text, wrap at the end of the line.
|
||||
let (truncate_at, truncated_width) = if symbols_to_last_word_end != 0 {
|
||||
(symbols_to_last_word_end, width_to_last_word_end)
|
||||
} else {
|
||||
(self.current_line.len() - 1, self.max_line_width)
|
||||
};
|
||||
// Append finished word to current line
|
||||
if has_seen_non_whitespace && symbol_whitespace
|
||||
// Append if trimmed (whitespaces removed) word would overflow
|
||||
|| word_width + symbol_width > self.max_line_width && current_line.is_empty() && self.trim
|
||||
// Append if removed whitespace would overflow -> reset whitespace counting to prevent overflow
|
||||
|| whitespace_width + symbol_width > self.max_line_width && current_line.is_empty() && self.trim
|
||||
// Append if complete word would overflow
|
||||
|| word_width + whitespace_width + symbol_width > self.max_line_width && current_line.is_empty() && !self.trim
|
||||
{
|
||||
if !current_line.is_empty() || !self.trim {
|
||||
// Also append whitespaces if not trimming or current line is not empty
|
||||
current_line.extend(
|
||||
std::mem::take(&mut unfinished_whitespaces).into_iter(),
|
||||
);
|
||||
current_line_width += whitespace_width;
|
||||
}
|
||||
// Append trimmed word
|
||||
current_line.append(&mut unfinished_word);
|
||||
current_line_width += word_width;
|
||||
|
||||
// Push the remainder to the next line but strip leading whitespace:
|
||||
{
|
||||
let remainder = &self.current_line[truncate_at..];
|
||||
if let Some(remainder_nonwhite) =
|
||||
remainder.iter().position(|StyledGrapheme { symbol, .. }| {
|
||||
!symbol.chars().all(&char::is_whitespace)
|
||||
})
|
||||
{
|
||||
self.next_line
|
||||
.extend_from_slice(&remainder[remainder_nonwhite..]);
|
||||
// Clear whitespace buffer
|
||||
unfinished_whitespaces.clear();
|
||||
whitespace_width = 0;
|
||||
word_width = 0;
|
||||
}
|
||||
|
||||
// Append the unfinished wrapped line to wrapped lines if it is as wide as max line width
|
||||
if current_line_width >= self.max_line_width
|
||||
// or if it would be too long with the current partially processed word added
|
||||
|| current_line_width + whitespace_width + word_width >= self.max_line_width && symbol_width > 0
|
||||
{
|
||||
let mut remaining_width =
|
||||
(self.max_line_width as i32 - current_line_width as i32).max(0)
|
||||
as u16;
|
||||
wrapped_lines.push(std::mem::take(&mut current_line));
|
||||
current_line_width = 0;
|
||||
|
||||
// Remove all whitespaces till end of just appended wrapped line + next whitespace
|
||||
let mut first_whitespace = unfinished_whitespaces.pop_front();
|
||||
while let Some(grapheme) = first_whitespace.as_ref() {
|
||||
let symbol_width = grapheme.symbol.width() as u16;
|
||||
whitespace_width -= symbol_width;
|
||||
|
||||
if symbol_width > remaining_width {
|
||||
break;
|
||||
}
|
||||
remaining_width -= symbol_width;
|
||||
first_whitespace = unfinished_whitespaces.pop_front();
|
||||
}
|
||||
// In case all whitespaces have been exhausted
|
||||
if symbol_whitespace && first_whitespace.is_none() {
|
||||
// Prevent first whitespace to count towards next word
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Append symbol to unfinished, partially processed word
|
||||
if symbol_whitespace {
|
||||
whitespace_width += symbol_width;
|
||||
unfinished_whitespaces.push_back(StyledGrapheme { symbol, style });
|
||||
} else {
|
||||
word_width += symbol_width;
|
||||
unfinished_word.push(StyledGrapheme { symbol, style });
|
||||
}
|
||||
|
||||
has_seen_non_whitespace = !symbol_whitespace;
|
||||
}
|
||||
}
|
||||
self.current_line.truncate(truncate_at);
|
||||
current_line_width = truncated_width;
|
||||
break;
|
||||
}
|
||||
|
||||
prev_whitespace = symbol_whitespace;
|
||||
// Append remaining text parts
|
||||
if !unfinished_word.is_empty() || !unfinished_whitespaces.is_empty() {
|
||||
if current_line.is_empty() && unfinished_word.is_empty() {
|
||||
wrapped_lines.push(vec![]);
|
||||
} else if !self.trim || !current_line.is_empty() {
|
||||
current_line.extend(unfinished_whitespaces.into_iter());
|
||||
}
|
||||
current_line.append(&mut unfinished_word);
|
||||
}
|
||||
if !current_line.is_empty() {
|
||||
wrapped_lines.push(current_line);
|
||||
}
|
||||
if wrapped_lines.is_empty() {
|
||||
// Append empty line if there was nothing to wrap in the first place
|
||||
wrapped_lines.push(vec![]);
|
||||
}
|
||||
|
||||
self.wrapped_lines = Some(wrapped_lines.into_iter());
|
||||
} else {
|
||||
// No more whole lines available -> stop repeatedly retrieving next wrapped line
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Even if the iterator is exhausted, pass the previous remainder.
|
||||
if symbols_exhausted && self.current_line.is_empty() {
|
||||
None
|
||||
if let Some(line) = current_line {
|
||||
self.current_line = line;
|
||||
Some((&self.current_line[..], line_width, self.current_alignment))
|
||||
} else {
|
||||
Some((&self.current_line[..], current_line_width))
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A state machine that truncates overhanging lines.
|
||||
pub struct LineTruncator<'a, 'b> {
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
pub struct LineTruncator<'a, O, I>
|
||||
where
|
||||
O: Iterator<Item = (I, Alignment)>, // Outer iterator providing the individual lines
|
||||
I: Iterator<Item = StyledGrapheme<'a>>, // Inner iterator providing the styled symbols of a line
|
||||
// Each line consists of an alignment and a series of symbols
|
||||
{
|
||||
/// The given, unprocessed lines
|
||||
input_lines: O,
|
||||
max_line_width: u16,
|
||||
current_line: Vec<StyledGrapheme<'a>>,
|
||||
/// Record the offet to skip render
|
||||
/// Record the offset to skip render
|
||||
horizontal_offset: u16,
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineTruncator<'a, 'b> {
|
||||
pub fn new(
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
max_line_width: u16,
|
||||
) -> LineTruncator<'a, 'b> {
|
||||
impl<'a, O, I> LineTruncator<'a, O, I>
|
||||
where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
pub fn new(lines: O, max_line_width: u16) -> LineTruncator<'a, O, I> {
|
||||
LineTruncator {
|
||||
symbols,
|
||||
input_lines: lines,
|
||||
max_line_width,
|
||||
horizontal_offset: 0,
|
||||
current_line: vec![],
|
||||
@@ -149,8 +234,12 @@ impl<'a, 'b> LineTruncator<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
|
||||
impl<'a, O, I> LineComposer<'a> for LineTruncator<'a, O, I>
|
||||
where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
@@ -158,57 +247,50 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
|
||||
self.current_line.truncate(0);
|
||||
let mut current_line_width = 0;
|
||||
|
||||
let mut skip_rest = false;
|
||||
let mut symbols_exhausted = true;
|
||||
let mut lines_exhausted = true;
|
||||
let mut horizontal_offset = self.horizontal_offset as usize;
|
||||
for StyledGrapheme { symbol, style } in &mut self.symbols {
|
||||
symbols_exhausted = false;
|
||||
let mut current_alignment = Alignment::Left;
|
||||
if let Some((current_line, alignment)) = &mut self.input_lines.next() {
|
||||
lines_exhausted = false;
|
||||
current_alignment = *alignment;
|
||||
|
||||
// Ignore characters wider that the total max width.
|
||||
if symbol.width() as u16 > self.max_line_width {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break on newline and discard it.
|
||||
if symbol == "\n" {
|
||||
break;
|
||||
}
|
||||
|
||||
if current_line_width + symbol.width() as u16 > self.max_line_width {
|
||||
// Exhaust the remainder of the line.
|
||||
skip_rest = true;
|
||||
break;
|
||||
}
|
||||
|
||||
let symbol = if horizontal_offset == 0 {
|
||||
symbol
|
||||
} else {
|
||||
let w = symbol.width();
|
||||
if w > horizontal_offset {
|
||||
let t = trim_offset(symbol, horizontal_offset);
|
||||
horizontal_offset = 0;
|
||||
t
|
||||
} else {
|
||||
horizontal_offset -= w;
|
||||
""
|
||||
for StyledGrapheme { symbol, style } in current_line {
|
||||
// Ignore characters wider that the total max width.
|
||||
if symbol.width() as u16 > self.max_line_width {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
current_line_width += symbol.width() as u16;
|
||||
self.current_line.push(StyledGrapheme { symbol, style });
|
||||
}
|
||||
|
||||
if skip_rest {
|
||||
for StyledGrapheme { symbol, .. } in &mut self.symbols {
|
||||
if symbol == "\n" {
|
||||
if current_line_width + symbol.width() as u16 > self.max_line_width {
|
||||
// Truncate line
|
||||
break;
|
||||
}
|
||||
|
||||
let symbol = if horizontal_offset == 0 || Alignment::Left != *alignment {
|
||||
symbol
|
||||
} else {
|
||||
let w = symbol.width();
|
||||
if w > horizontal_offset {
|
||||
let t = trim_offset(symbol, horizontal_offset);
|
||||
horizontal_offset = 0;
|
||||
t
|
||||
} else {
|
||||
horizontal_offset -= w;
|
||||
""
|
||||
}
|
||||
};
|
||||
current_line_width += symbol.width() as u16;
|
||||
self.current_line.push(StyledGrapheme { symbol, style });
|
||||
}
|
||||
}
|
||||
|
||||
if symbols_exhausted && self.current_line.is_empty() {
|
||||
if lines_exhausted {
|
||||
None
|
||||
} else {
|
||||
Some((&self.current_line[..], current_line_width))
|
||||
Some((
|
||||
&self.current_line[..],
|
||||
current_line_width,
|
||||
current_alignment,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,6 +314,8 @@ fn trim_offset(src: &str, mut offset: usize) -> &str {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::style::Style;
|
||||
use crate::text::{Line, Text};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
enum Composer {
|
||||
@@ -239,19 +323,31 @@ mod test {
|
||||
LineTruncator,
|
||||
}
|
||||
|
||||
fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
|
||||
let style = Default::default();
|
||||
let mut styled =
|
||||
UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
|
||||
fn run_composer<'a>(
|
||||
which: Composer,
|
||||
text: impl Into<Text<'a>>,
|
||||
text_area_width: u16,
|
||||
) -> (Vec<String>, Vec<u16>, Vec<Alignment>) {
|
||||
let text = text.into();
|
||||
let styled_lines = text.lines.iter().map(|line| {
|
||||
(
|
||||
line.spans
|
||||
.iter()
|
||||
.flat_map(|span| span.styled_graphemes(Style::default())),
|
||||
line.alignment.unwrap_or(Alignment::Left),
|
||||
)
|
||||
});
|
||||
|
||||
let mut composer: Box<dyn LineComposer> = match which {
|
||||
Composer::WordWrapper { trim } => {
|
||||
Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
|
||||
Box::new(WordWrapper::new(styled_lines, text_area_width, trim))
|
||||
}
|
||||
Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
|
||||
Composer::LineTruncator => Box::new(LineTruncator::new(styled_lines, text_area_width)),
|
||||
};
|
||||
let mut lines = vec![];
|
||||
let mut widths = vec![];
|
||||
while let Some((styled, width)) = composer.next_line() {
|
||||
let mut alignments = vec![];
|
||||
while let Some((styled, width, alignment)) = composer.next_line() {
|
||||
let line = styled
|
||||
.iter()
|
||||
.map(|StyledGrapheme { symbol, .. }| *symbol)
|
||||
@@ -259,8 +355,9 @@ mod test {
|
||||
assert!(width <= text_area_width);
|
||||
lines.push(line);
|
||||
widths.push(width);
|
||||
alignments.push(alignment);
|
||||
}
|
||||
(lines, widths)
|
||||
(lines, widths, alignments)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -268,9 +365,13 @@ mod test {
|
||||
let width = 40;
|
||||
for i in 1..width {
|
||||
let text = "a".repeat(i);
|
||||
let (word_wrapper, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, &text, width as u16);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
|
||||
let (word_wrapper, _, _) = run_composer(
|
||||
Composer::WordWrapper { trim: true },
|
||||
&text[..],
|
||||
width as u16,
|
||||
);
|
||||
let (line_truncator, _, _) =
|
||||
run_composer(Composer::LineTruncator, &text[..], width as u16);
|
||||
let expected = vec![text];
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, expected);
|
||||
@@ -282,8 +383,8 @@ mod test {
|
||||
let width = 20;
|
||||
let text =
|
||||
"abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let wrapped: Vec<&str> = text.split('\n').collect();
|
||||
assert_eq!(word_wrapper, wrapped);
|
||||
@@ -294,9 +395,9 @@ mod test {
|
||||
fn line_composer_long_word() {
|
||||
let width = 20;
|
||||
let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
|
||||
let (word_wrapper, _) =
|
||||
let (word_wrapper, _, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let wrapped = vec![
|
||||
&text[..width],
|
||||
@@ -320,14 +421,14 @@ mod test {
|
||||
let text_multi_space =
|
||||
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
|
||||
m n o";
|
||||
let (word_wrapper_single_space, _) =
|
||||
let (word_wrapper_single_space, _, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
||||
let (word_wrapper_multi_space, _) = run_composer(
|
||||
let (word_wrapper_multi_space, _, _) = run_composer(
|
||||
Composer::WordWrapper { trim: true },
|
||||
text_multi_space,
|
||||
width as u16,
|
||||
);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let word_wrapped = vec![
|
||||
"abcd efghij",
|
||||
@@ -346,8 +447,8 @@ mod test {
|
||||
fn line_composer_zero_width() {
|
||||
let width = 0;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = Vec::new();
|
||||
assert_eq!(word_wrapper, expected);
|
||||
@@ -358,8 +459,8 @@ mod test {
|
||||
fn line_composer_max_line_width_of_1() {
|
||||
let width = 1;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
|
||||
.filter(|g| g.chars().any(|c| !c.is_whitespace()))
|
||||
@@ -371,20 +472,21 @@ mod test {
|
||||
#[test]
|
||||
fn line_composer_max_line_width_of_1_double_width_characters() {
|
||||
let width = 1;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
|
||||
let text =
|
||||
"コンピュータ上で文字を扱う場合、典型的には文字\naaa\naによる通信を行う場合にその\
|
||||
両端点では、";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
|
||||
assert_eq!(line_truncator, vec!["", "a"]);
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["", "a", "a", "a", "a"]);
|
||||
assert_eq!(line_truncator, vec!["", "a", "a"]);
|
||||
}
|
||||
|
||||
/// Tests WordWrapper with words some of which exceed line length and some not.
|
||||
/// Tests `WordWrapper` with words some of which exceed line length and some not.
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_mixed_length() {
|
||||
let width = 20;
|
||||
let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
@@ -402,9 +504,9 @@ mod test {
|
||||
let width = 20;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
|
||||
では、";
|
||||
let (word_wrapper, word_wrapper_width) =
|
||||
let (word_wrapper, word_wrapper_width, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
|
||||
let wrapped = vec![
|
||||
"コンピュータ上で文字",
|
||||
@@ -421,8 +523,8 @@ mod test {
|
||||
fn line_composer_leading_whitespace_removal() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
|
||||
assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
|
||||
}
|
||||
@@ -432,20 +534,20 @@ mod test {
|
||||
fn line_composer_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = " ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec![""]);
|
||||
assert_eq!(line_truncator, vec![" "]);
|
||||
}
|
||||
|
||||
/// Tests an input starting with a letter, folowed by spaces - some of the behaviour is
|
||||
/// Tests an input starting with a letter, followed by spaces - some of the behaviour is
|
||||
/// incidental.
|
||||
#[test]
|
||||
fn line_composer_char_plus_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = "a ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
// What's happening below is: the first line gets consumed, trailing spaces discarded,
|
||||
// after 20 of which a word break occurs (probably shouldn't). The second line break
|
||||
// discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
|
||||
@@ -463,7 +565,7 @@ mod test {
|
||||
// hiragana and katakana...
|
||||
// This happens to also be a test case for mixed width because regular spaces are single width.
|
||||
let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
|
||||
let (word_wrapper, word_wrapper_width) =
|
||||
let (word_wrapper, word_wrapper_width, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
@@ -485,21 +587,24 @@ mod test {
|
||||
fn line_composer_word_wrapper_nbsp() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (word_wrapper, word_wrapper_widths, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
|
||||
assert_eq!(word_wrapper_widths, vec![15, 8]);
|
||||
|
||||
// Ensure that if the character was a regular space, it would be wrapped differently.
|
||||
let text_space = text.replace('\u{00a0}', " ");
|
||||
let (word_wrapper_space, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
|
||||
let (word_wrapper_space, word_wrapper_widths, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text_space, width);
|
||||
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
|
||||
assert_eq!(word_wrapper_widths, vec![20, 3])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_preserve_indentation() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
|
||||
}
|
||||
|
||||
@@ -507,7 +612,7 @@ mod test {
|
||||
fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
|
||||
let width = 10;
|
||||
let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"]
|
||||
@@ -518,7 +623,7 @@ mod test {
|
||||
fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
|
||||
let width = 10;
|
||||
let text = " 4 Indent\n must wrap!";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
@@ -531,4 +636,43 @@ mod test {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_zero_width_at_end() {
|
||||
let width = 3;
|
||||
let line = "foo\0";
|
||||
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, line, width);
|
||||
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, line, width);
|
||||
assert_eq!(word_wrapper, vec!["foo\0"]);
|
||||
assert_eq!(line_truncator, vec!["foo\0"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_preserves_line_alignment() {
|
||||
let width = 20;
|
||||
let lines = vec![
|
||||
Line::from("Something that is left aligned.").alignment(Alignment::Left),
|
||||
Line::from("This is right aligned and half short.").alignment(Alignment::Right),
|
||||
Line::from("This should sit in the center.").alignment(Alignment::Center),
|
||||
];
|
||||
let (_, _, wrapped_alignments) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, lines.clone(), width);
|
||||
let (_, _, truncated_alignments) = run_composer(Composer::LineTruncator, lines, width);
|
||||
assert_eq!(
|
||||
wrapped_alignments,
|
||||
vec![
|
||||
Alignment::Left,
|
||||
Alignment::Left,
|
||||
Alignment::Right,
|
||||
Alignment::Right,
|
||||
Alignment::Right,
|
||||
Alignment::Center,
|
||||
Alignment::Center
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
truncated_alignments,
|
||||
vec![Alignment::Left, Alignment::Right, Alignment::Center]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ use std::cmp::min;
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, Borders, Sparkline};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # use ratatui::widgets::{Block, Borders, Sparkline};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// Sparkline::default()
|
||||
/// .block(Block::default().title("Sparkline").borders(Borders::ALL))
|
||||
/// .data(&[0, 2, 3, 4, 1, 4, 10])
|
||||
@@ -33,16 +33,25 @@ pub struct Sparkline<'a> {
|
||||
max: Option<u64>,
|
||||
/// A set of bar symbols used to represent the give data
|
||||
bar_set: symbols::bar::Set,
|
||||
// The direction to render the sparkine, either from left to right, or from right to left
|
||||
direction: RenderDirection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RenderDirection {
|
||||
LeftToRight,
|
||||
RightToLeft,
|
||||
}
|
||||
|
||||
impl<'a> Default for Sparkline<'a> {
|
||||
fn default() -> Sparkline<'a> {
|
||||
Sparkline {
|
||||
block: None,
|
||||
style: Default::default(),
|
||||
style: Style::default(),
|
||||
data: &[],
|
||||
max: None,
|
||||
bar_set: symbols::bar::NINE_LEVELS,
|
||||
direction: RenderDirection::LeftToRight,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +81,11 @@ impl<'a> Sparkline<'a> {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn direction(mut self, direction: RenderDirection) -> Sparkline<'a> {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Sparkline<'a> {
|
||||
@@ -99,10 +113,10 @@ impl<'a> Widget for Sparkline<'a> {
|
||||
.iter()
|
||||
.take(max_index)
|
||||
.map(|e| {
|
||||
if max != 0 {
|
||||
e * u64::from(spark_area.height) * 8 / max
|
||||
} else {
|
||||
if max == 0 {
|
||||
0
|
||||
} else {
|
||||
e * u64::from(spark_area.height) * 8 / max
|
||||
}
|
||||
})
|
||||
.collect::<Vec<u64>>();
|
||||
@@ -119,7 +133,11 @@ impl<'a> Widget for Sparkline<'a> {
|
||||
7 => self.bar_set.seven_eighths,
|
||||
_ => self.bar_set.full,
|
||||
};
|
||||
buf.get_mut(spark_area.left() + i as u16, spark_area.top() + j)
|
||||
let x = match self.direction {
|
||||
RenderDirection::LeftToRight => spark_area.left() + i as u16,
|
||||
RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
|
||||
};
|
||||
buf.get_mut(x, spark_area.top() + j)
|
||||
.set_symbol(symbol)
|
||||
.set_style(self.style);
|
||||
|
||||
@@ -136,20 +154,55 @@ impl<'a> Widget for Sparkline<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, buffer::Cell};
|
||||
|
||||
// Helper function to render a sparkline to a buffer with a given width
|
||||
// filled with x symbols to make it easier to assert on the result
|
||||
fn render(widget: Sparkline, width: u16) -> Buffer {
|
||||
let area = Rect::new(0, 0, width, 1);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("x");
|
||||
let mut buffer = Buffer::filled(area, &cell);
|
||||
widget.render(area, &mut buffer);
|
||||
buffer
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_does_not_panic_if_max_is_zero() {
|
||||
let widget = Sparkline::default().data(&[0, 0, 0]);
|
||||
let area = Rect::new(0, 0, 3, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
widget.render(area, &mut buffer);
|
||||
let buffer = render(widget, 6);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_does_not_panic_if_max_is_set_to_zero() {
|
||||
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
|
||||
let area = Rect::new(0, 0, 3, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
widget.render(area, &mut buffer);
|
||||
let buffer = render(widget, 6);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_draws() {
|
||||
let widget = Sparkline::default().data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
let buffer = render(widget, 12);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_left_to_right() {
|
||||
let widget = Sparkline::default()
|
||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.direction(RenderDirection::LeftToRight);
|
||||
let buffer = render(widget, 12);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_right_to_left() {
|
||||
let widget = Sparkline::default()
|
||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.direction(RenderDirection::RightToLeft);
|
||||
let buffer = render(widget, 12);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ use unicode_width::UnicodeWidthStr;
|
||||
///
|
||||
/// It can be created from anything that can be converted to a [`Text`].
|
||||
/// ```rust
|
||||
/// # use tui::widgets::Cell;
|
||||
/// # use tui::style::{Style, Modifier};
|
||||
/// # use tui::text::{Span, Spans, Text};
|
||||
/// # use ratatui::widgets::Cell;
|
||||
/// # use ratatui::style::{Style, Modifier};
|
||||
/// # use ratatui::text::{Span, Line, Text};
|
||||
/// # use std::borrow::Cow;
|
||||
/// Cell::from("simple string");
|
||||
///
|
||||
/// Cell::from(Span::from("span"));
|
||||
///
|
||||
/// Cell::from(Spans::from(vec![
|
||||
/// Cell::from(Line::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
|
||||
/// ]));
|
||||
@@ -31,7 +31,7 @@ use unicode_width::UnicodeWidthStr;
|
||||
///
|
||||
/// 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)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Cell<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
@@ -61,14 +61,14 @@ where
|
||||
///
|
||||
/// A [`Row`] is a collection of cells. It can be created from simple strings:
|
||||
/// ```rust
|
||||
/// # use tui::widgets::Row;
|
||||
/// # use ratatui::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:
|
||||
/// But if you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
|
||||
/// ```rust
|
||||
/// # use tui::widgets::{Row, Cell};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # use ratatui::widgets::{Row, Cell};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
|
||||
@@ -78,7 +78,7 @@ where
|
||||
/// You can also construct a row from any type that can be converted into [`Text`]:
|
||||
/// ```rust
|
||||
/// # use std::borrow::Cow;
|
||||
/// # use tui::widgets::Row;
|
||||
/// # use ratatui::widgets::Row;
|
||||
/// Row::new(vec![
|
||||
/// Cow::Borrowed("hello"),
|
||||
/// Cow::Owned("world".to_uppercase()),
|
||||
@@ -86,7 +86,7 @@ where
|
||||
/// ```
|
||||
///
|
||||
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Row<'a> {
|
||||
cells: Vec<Cell<'a>>,
|
||||
height: u16,
|
||||
@@ -103,7 +103,7 @@ impl<'a> Row<'a> {
|
||||
{
|
||||
Self {
|
||||
height: 1,
|
||||
cells: cells.into_iter().map(|c| c.into()).collect(),
|
||||
cells: cells.into_iter().map(Into::into).collect(),
|
||||
style: Style::default(),
|
||||
bottom_margin: 0,
|
||||
}
|
||||
@@ -116,7 +116,7 @@ impl<'a> Row<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a
|
||||
/// Set the [`Style`] of the entire row. This [`Style`] can be overridden 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;
|
||||
@@ -139,10 +139,10 @@ impl<'a> Row<'a> {
|
||||
///
|
||||
/// 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};
|
||||
/// # use ratatui::widgets::{Block, Borders, Table, Row, Cell};
|
||||
/// # use ratatui::layout::Constraint;
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::text::{Text, Line, Span};
|
||||
/// Table::new(vec![
|
||||
/// // Row can be created from simple strings.
|
||||
/// Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
@@ -152,7 +152,7 @@ impl<'a> Row<'a> {
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Row31"),
|
||||
/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
|
||||
/// Cell::from(Spans::from(vec![
|
||||
/// Cell::from(Line::from(vec![
|
||||
/// Span::raw("Row"),
|
||||
/// Span::styled("33", Style::default().fg(Color::Green))
|
||||
/// ])),
|
||||
@@ -186,7 +186,7 @@ impl<'a> Row<'a> {
|
||||
/// // ...and potentially show a symbol in front of the selection.
|
||||
/// .highlight_symbol(">>");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Table<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
@@ -269,8 +269,7 @@ impl<'a> Table<'a> {
|
||||
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
|
||||
let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
|
||||
if has_selection {
|
||||
let highlight_symbol_width =
|
||||
self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
|
||||
let highlight_symbol_width = self.highlight_symbol.map_or(0, |s| s.width() as u16);
|
||||
constraints.push(Constraint::Length(highlight_symbol_width));
|
||||
}
|
||||
for constraint in self.widths {
|
||||
@@ -280,7 +279,7 @@ impl<'a> Table<'a> {
|
||||
if !self.widths.is_empty() {
|
||||
constraints.pop();
|
||||
}
|
||||
let mut chunks = Layout::default()
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(constraints)
|
||||
.expand_to_fill(false)
|
||||
@@ -290,8 +289,9 @@ impl<'a> Table<'a> {
|
||||
width: max_width,
|
||||
height: 1,
|
||||
});
|
||||
let mut chunks = &chunks[..];
|
||||
if has_selection {
|
||||
chunks.remove(0);
|
||||
chunks = &chunks[1..];
|
||||
}
|
||||
chunks.iter().step_by(2).map(|c| c.width).collect()
|
||||
}
|
||||
@@ -342,6 +342,24 @@ pub struct TableState {
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
pub fn offset(&self) -> usize {
|
||||
self.offset
|
||||
}
|
||||
|
||||
pub fn offset_mut(&mut self) -> &mut usize {
|
||||
&mut self.offset
|
||||
}
|
||||
|
||||
pub fn with_selected(mut self, selected: Option<usize>) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_offset(mut self, offset: usize) -> Self {
|
||||
self.offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
@@ -433,7 +451,7 @@ impl<'a> StatefulWidget for Table<'a> {
|
||||
height: table_row.height,
|
||||
};
|
||||
buf.set_style(table_row_area, table_row.style);
|
||||
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
|
||||
let is_selected = state.selected.map_or(false, |s| s == i);
|
||||
let table_row_start_col = if has_selection {
|
||||
let symbol = if is_selected {
|
||||
highlight_symbol
|
||||
@@ -469,11 +487,11 @@ impl<'a> StatefulWidget for Table<'a> {
|
||||
|
||||
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() {
|
||||
for (i, line) 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);
|
||||
buf.set_line(area.x, area.y + i as u16, line, area.width);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
@@ -12,11 +12,11 @@ use crate::{
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, Borders, Tabs};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # use tui::text::{Spans};
|
||||
/// # use tui::symbols::{DOT};
|
||||
/// let titles = ["Tab1", "Tab2", "Tab3", "Tab4"].iter().cloned().map(Spans::from).collect();
|
||||
/// # use ratatui::widgets::{Block, Borders, Tabs};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// # use ratatui::text::{Line};
|
||||
/// # use ratatui::symbols::{DOT};
|
||||
/// let titles = ["Tab1", "Tab2", "Tab3", "Tab4"].iter().cloned().map(Line::from).collect();
|
||||
/// Tabs::new(titles)
|
||||
/// .block(Block::default().title("Tabs").borders(Borders::ALL))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
@@ -28,7 +28,7 @@ pub struct Tabs<'a> {
|
||||
/// A block to wrap this widget in if necessary
|
||||
block: Option<Block<'a>>,
|
||||
/// One title for each tab
|
||||
titles: Vec<Spans<'a>>,
|
||||
titles: Vec<Line<'a>>,
|
||||
/// The index of the selected tabs
|
||||
selected: usize,
|
||||
/// The style used to draw the text
|
||||
@@ -40,13 +40,16 @@ pub struct Tabs<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Tabs<'a> {
|
||||
pub fn new(titles: Vec<Spans<'a>>) -> Tabs<'a> {
|
||||
pub fn new<T>(titles: Vec<T>) -> Tabs<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
Tabs {
|
||||
block: None,
|
||||
titles,
|
||||
titles: titles.into_iter().map(Into::into).collect(),
|
||||
selected: 0,
|
||||
style: Default::default(),
|
||||
highlight_style: Default::default(),
|
||||
style: Style::default(),
|
||||
highlight_style: Style::default(),
|
||||
divider: Span::raw(symbols::line::VERTICAL),
|
||||
}
|
||||
}
|
||||
@@ -105,7 +108,7 @@ impl<'a> Widget for Tabs<'a> {
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = buf.set_spans(x, tabs_area.top(), &title, remaining_width);
|
||||
let pos = buf.set_line(x, tabs_area.top(), &title, remaining_width);
|
||||
if i == self.selected {
|
||||
buf.set_style(
|
||||
Rect {
|
||||
|
||||
@@ -6,7 +6,7 @@ fn backend_termion_should_only_write_diffs() -> Result<(), Box<dyn std::error::E
|
||||
let mut bytes = Vec::new();
|
||||
let mut stdout = Cursor::new(&mut bytes);
|
||||
{
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::TermionBackend, layout::Rect, widgets::Paragraph, Terminal, TerminalOptions,
|
||||
Viewport,
|
||||
};
|
||||
@@ -15,7 +15,7 @@ fn backend_termion_should_only_write_diffs() -> Result<(), Box<dyn std::error::E
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::fixed(area),
|
||||
viewport: Viewport::Fixed(area),
|
||||
},
|
||||
)?;
|
||||
terminal.draw(|f| {
|
||||
|
||||
19
tests/border_macro.rs
Normal file
19
tests/border_macro.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
#![cfg(feature = "macros")]
|
||||
use ratatui::{border, widgets::Borders};
|
||||
|
||||
#[test]
|
||||
fn border_empty_test() {
|
||||
let empty = Borders::NONE;
|
||||
assert_eq!(empty, border!());
|
||||
}
|
||||
#[test]
|
||||
fn border_all_test() {
|
||||
let all = Borders::ALL;
|
||||
assert_eq!(all, border!(ALL));
|
||||
assert_eq!(all, border!(TOP, BOTTOM, LEFT, RIGHT));
|
||||
}
|
||||
#[test]
|
||||
fn border_left_right_test() {
|
||||
let left_right = Borders::from_bits(Borders::LEFT.bits() | Borders::RIGHT.bits());
|
||||
assert_eq!(left_right, Some(border!(RIGHT, LEFT)));
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::error::Error;
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::{Backend, TestBackend},
|
||||
layout::Rect,
|
||||
widgets::Paragraph,
|
||||
Terminal,
|
||||
};
|
||||
use std::error::Error;
|
||||
|
||||
#[test]
|
||||
fn terminal_buffer_size_should_be_limited() {
|
||||
@@ -20,15 +20,15 @@ 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());
|
||||
let paragraph = Paragraph::new("Test");
|
||||
f.render_widget(paragraph, 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());
|
||||
let paragraph = Paragraph::new("test");
|
||||
f.render_widget(paragraph, f.size());
|
||||
})?;
|
||||
assert_eq!(frame.buffer.get(0, 0).symbol, "t");
|
||||
assert_eq!(frame.area, Rect::new(0, 0, 8, 8));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use tui::backend::TestBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::widgets::{BarChart, Block, Borders};
|
||||
use tui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::widgets::{BarChart, Block, Borders};
|
||||
use ratatui::Terminal;
|
||||
|
||||
#[test]
|
||||
fn widgets_barchart_not_full_below_max_value() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
@@ -344,3 +344,137 @@ fn widgets_block_title_alignment() {
|
||||
Buffer::with_lines(vec![" Title ", " "]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_block_title_alignment_bottom() {
|
||||
let test_case = |alignment, borders, expected| {
|
||||
let backend = TestBackend::new(15, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let block = Block::default()
|
||||
.title(Span::styled("Title", Style::default()))
|
||||
.title_alignment(alignment)
|
||||
.title_on_bottom()
|
||||
.borders(borders);
|
||||
|
||||
let area = Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 13,
|
||||
height: 2,
|
||||
};
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
f.render_widget(block, area);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
// title bottom-left with all borders
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " └Title──────┘ "]),
|
||||
);
|
||||
|
||||
// title bottom-left without bottom border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::LEFT | Borders::TOP | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " │Title │ "]),
|
||||
);
|
||||
|
||||
// title bottom-left with no left border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ────────────┐ ", " Title───────┘ "]),
|
||||
);
|
||||
|
||||
// title bottom-left without right border
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌──────────── ", " └Title─────── "]),
|
||||
);
|
||||
|
||||
// title bottom-left without borders
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" ", " Title "]),
|
||||
);
|
||||
|
||||
// title center with all borders
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " └───Title───┘ "]),
|
||||
);
|
||||
|
||||
// title center without bottom border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::LEFT | Borders::TOP | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " │ Title │ "]),
|
||||
);
|
||||
|
||||
// title center with no left border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ────────────┐ ", " ────Title───┘ "]),
|
||||
);
|
||||
|
||||
// title center without right border
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌──────────── ", " └───Title──── "]),
|
||||
);
|
||||
|
||||
// title center without borders
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" ", " Title "]),
|
||||
);
|
||||
|
||||
// title bottom-right with all borders
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::ALL,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " └──────Title┘ "]),
|
||||
);
|
||||
|
||||
// title bottom-right without bottom border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::LEFT | Borders::TOP | Borders::RIGHT,
|
||||
Buffer::with_lines(vec![" ┌───────────┐ ", " │ Title│ "]),
|
||||
);
|
||||
|
||||
// title bottom-right with no left border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ────────────┐ ", " ───────Title┘ "]),
|
||||
);
|
||||
|
||||
// title bottom-right without right border
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
|
||||
Buffer::with_lines(vec![" ┌──────────── ", " └───────Title "]),
|
||||
);
|
||||
|
||||
// title bottom-right without borders
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Borders::NONE,
|
||||
Buffer::with_lines(vec![" ", " Title "]),
|
||||
);
|
||||
}
|
||||
|
||||
114
tests/widgets_calendar.rs
Normal file
114
tests/widgets_calendar.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
#![cfg(feature = "widget-calendar")]
|
||||
use ratatui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
style::Style,
|
||||
widgets::{
|
||||
calendar::{CalendarEventStore, Monthly},
|
||||
Widget,
|
||||
},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
use time::{Date, Month};
|
||||
|
||||
fn test_render<W: Widget>(widget: W, expected: Buffer, size: (u16, u16)) {
|
||||
let backend = TestBackend::new(size.0, size.1);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| f.render_widget(widget, f.size()))
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn days_layout() {
|
||||
let c = Monthly::new(
|
||||
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
|
||||
CalendarEventStore::default(),
|
||||
);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" 1 2 3 4 5 6 7",
|
||||
" 8 9 10 11 12 13 14",
|
||||
" 15 16 17 18 19 20 21",
|
||||
" 22 23 24 25 26 27 28",
|
||||
" 29 30 31",
|
||||
]);
|
||||
test_render(c, expected, (21, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn days_layout_show_surrounding() {
|
||||
let c = Monthly::new(
|
||||
Date::from_calendar_date(2023, Month::December, 1).unwrap(),
|
||||
CalendarEventStore::default(),
|
||||
)
|
||||
.show_surrounding(Style::default());
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" 26 27 28 29 30 1 2",
|
||||
" 3 4 5 6 7 8 9",
|
||||
" 10 11 12 13 14 15 16",
|
||||
" 17 18 19 20 21 22 23",
|
||||
" 24 25 26 27 28 29 30",
|
||||
" 31 1 2 3 4 5 6",
|
||||
]);
|
||||
test_render(c, expected, (21, 6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_month_header() {
|
||||
let c = Monthly::new(
|
||||
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
|
||||
CalendarEventStore::default(),
|
||||
)
|
||||
.show_month_header(Style::default());
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" January 2023 ",
|
||||
" 1 2 3 4 5 6 7",
|
||||
" 8 9 10 11 12 13 14",
|
||||
" 15 16 17 18 19 20 21",
|
||||
" 22 23 24 25 26 27 28",
|
||||
" 29 30 31",
|
||||
]);
|
||||
test_render(c, expected, (21, 6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_weekdays_header() {
|
||||
let c = Monthly::new(
|
||||
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
|
||||
CalendarEventStore::default(),
|
||||
)
|
||||
.show_weekdays_header(Style::default());
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" Su Mo Tu We Th Fr Sa",
|
||||
" 1 2 3 4 5 6 7",
|
||||
" 8 9 10 11 12 13 14",
|
||||
" 15 16 17 18 19 20 21",
|
||||
" 22 23 24 25 26 27 28",
|
||||
" 29 30 31",
|
||||
]);
|
||||
test_render(c, expected, (21, 6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_combo() {
|
||||
let c = Monthly::new(
|
||||
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
|
||||
CalendarEventStore::default(),
|
||||
)
|
||||
.show_weekdays_header(Style::default())
|
||||
.show_month_header(Style::default())
|
||||
.show_surrounding(Style::default());
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" January 2023 ",
|
||||
" Su Mo Tu We Th Fr Sa",
|
||||
" 1 2 3 4 5 6 7",
|
||||
" 8 9 10 11 12 13 14",
|
||||
" 15 16 17 18 19 20 21",
|
||||
" 22 23 24 25 26 27 28",
|
||||
" 29 30 31 1 2 3 4",
|
||||
]);
|
||||
test_render(c, expected, (21, 7));
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
style::{Color, Style},
|
||||
@@ -38,5 +38,5 @@ fn widgets_canvas_draw_labels() {
|
||||
for col in 0..4 {
|
||||
expected.get_mut(col, 4).set_fg(Color::Blue);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected)
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use tui::layout::Alignment;
|
||||
use tui::{
|
||||
use ratatui::layout::Alignment;
|
||||
use ratatui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
@@ -469,7 +469,7 @@ fn widgets_chart_can_have_a_legend() {
|
||||
"└──────────────────────────────────────────────────────────┘",
|
||||
]);
|
||||
|
||||
// Set expected backgound color
|
||||
// Set expected background color
|
||||
for row in 0..30 {
|
||||
for col in 0..60 {
|
||||
expected.get_mut(col, row).set_bg(Color::White);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use tui::{
|
||||
use ratatui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
use tui::{
|
||||
#![allow(deprecated)]
|
||||
|
||||
use ratatui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Spans,
|
||||
text::Line,
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn list_should_shows_the_length() {
|
||||
let items = vec![
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
];
|
||||
let list = List::new(items);
|
||||
assert_eq!(list.len(), 3);
|
||||
assert!(!list.is_empty());
|
||||
|
||||
let empty_list = List::new(vec![]);
|
||||
assert_eq!(empty_list.len(), 0);
|
||||
assert!(empty_list.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_list_should_highlight_the_selected_item() {
|
||||
let backend = TestBackend::new(10, 3);
|
||||
@@ -138,9 +156,9 @@ fn widgets_list_should_display_multiline_items() {
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new(vec![Spans::from("Item 1"), Spans::from("Item 1a")]),
|
||||
ListItem::new(vec![Spans::from("Item 2"), Spans::from("Item 2b")]),
|
||||
ListItem::new(vec![Spans::from("Item 3"), Spans::from("Item 3c")]),
|
||||
ListItem::new(vec![Line::from("Item 1"), Line::from("Item 1a")]),
|
||||
ListItem::new(vec![Line::from("Item 2"), Line::from("Item 2b")]),
|
||||
ListItem::new(vec![Line::from("Item 3"), Line::from("Item 3c")]),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_style(Style::default().bg(Color::Yellow))
|
||||
@@ -173,9 +191,9 @@ fn widgets_list_should_repeat_highlight_symbol() {
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new(vec![Spans::from("Item 1"), Spans::from("Item 1a")]),
|
||||
ListItem::new(vec![Spans::from("Item 2"), Spans::from("Item 2b")]),
|
||||
ListItem::new(vec![Spans::from("Item 3"), Spans::from("Item 3c")]),
|
||||
ListItem::new(vec![Line::from("Item 1"), Line::from("Item 1a")]),
|
||||
ListItem::new(vec![Line::from("Item 2"), Line::from("Item 2b")]),
|
||||
ListItem::new(vec![Line::from("Item 3"), Line::from("Item 3c")]),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_style(Style::default().bg(Color::Yellow))
|
||||
@@ -198,3 +216,30 @@ fn widgets_list_should_repeat_highlight_symbol() {
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widget_list_should_not_ignore_empty_string_items() {
|
||||
let backend = TestBackend::new(6, 4);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let items = vec![
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new(""),
|
||||
ListItem::new(""),
|
||||
ListItem::new("Item 4"),
|
||||
];
|
||||
|
||||
let list = List::new(items)
|
||||
.style(Style::default())
|
||||
.highlight_style(Style::default());
|
||||
|
||||
f.render_widget(list, f.size());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = Buffer::with_lines(vec!["Item 1", "", "", "Item 4"]);
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
@@ -1,116 +1,56 @@
|
||||
use tui::{
|
||||
#![allow(deprecated)]
|
||||
|
||||
use ratatui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Alignment,
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, Padding, Paragraph, Wrap},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
const SAMPLE_STRING: &str = "The library is based on the principle of immediate rendering with \
|
||||
intermediate buffers. This means that at each new frame you should build all widgets that are \
|
||||
supposed to be part of the UI. While providing a great flexibility for rich and \
|
||||
interactive UI, this may introduce overhead for highly dynamic content.";
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_can_wrap_its_content() {
|
||||
let test_case = |alignment, expected| {
|
||||
let backend = TestBackend::new(20, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let text = vec![Spans::from(SAMPLE_STRING)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.alignment(alignment)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│The library is │",
|
||||
"│based on the │",
|
||||
"│principle of │",
|
||||
"│immediate │",
|
||||
"│rendering with │",
|
||||
"│intermediate │",
|
||||
"│buffers. This │",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ The library is│",
|
||||
"│ based on the│",
|
||||
"│ principle of│",
|
||||
"│ immediate│",
|
||||
"│ rendering with│",
|
||||
"│ intermediate│",
|
||||
"│ buffers. This│",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ The library is │",
|
||||
"│ based on the │",
|
||||
"│ principle of │",
|
||||
"│ immediate │",
|
||||
"│ rendering with │",
|
||||
"│ intermediate │",
|
||||
"│ buffers. This │",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_renders_double_width_graphemes() {
|
||||
let backend = TestBackend::new(10, 10);
|
||||
/// Tests the [`Paragraph`] widget against the expected [`Buffer`] by rendering it onto an equal area
|
||||
/// and comparing the rendered and expected content.
|
||||
fn test_case(paragraph: Paragraph, expected: Buffer) {
|
||||
let backend = TestBackend::new(expected.area.width, expected.area.height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、";
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let text = vec![Spans::from(s)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"┌────────┐",
|
||||
"│コンピュ│",
|
||||
"│ータ上で│",
|
||||
"│文字を扱│",
|
||||
"│う場合、│",
|
||||
"│典型的に│",
|
||||
"│は文字に│",
|
||||
"│よる通信│",
|
||||
"│を行う場│",
|
||||
"└────────┘",
|
||||
]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_renders_double_width_graphemes() {
|
||||
let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、";
|
||||
|
||||
let text = vec![Line::from(s)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(Wrap { trim: true });
|
||||
|
||||
test_case(
|
||||
paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────┐",
|
||||
"│コンピュ│",
|
||||
"│ータ上で│",
|
||||
"│文字を扱│",
|
||||
"│う場合、│",
|
||||
"│典型的に│",
|
||||
"│は文字に│",
|
||||
"│よる通信│",
|
||||
"│を行う場│",
|
||||
"└────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_renders_mixed_width_graphemes() {
|
||||
let backend = TestBackend::new(10, 7);
|
||||
@@ -120,7 +60,7 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let text = vec![Spans::from(s)];
|
||||
let text = vec![Line::from(s)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(Wrap { trim: true });
|
||||
@@ -144,49 +84,27 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
|
||||
#[test]
|
||||
fn widgets_paragraph_can_wrap_with_a_trailing_nbsp() {
|
||||
let nbsp: &str = "\u{00a0}";
|
||||
let line = Spans::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]);
|
||||
let backend = TestBackend::new(20, 3);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│NBSP\u{00a0} │",
|
||||
"└──────────────────┘",
|
||||
]);
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
|
||||
let paragraph = Paragraph::new(line).block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
#[test]
|
||||
fn widgets_paragraph_can_scroll_horizontally() {
|
||||
let test_case = |alignment, scroll, expected| {
|
||||
let backend = TestBackend::new(20, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let text = Text::from(
|
||||
"段落现在可以水平滚动了!\nParagraph can scroll horizontally!\nShort line",
|
||||
);
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.alignment(alignment)
|
||||
.scroll(scroll);
|
||||
f.render_widget(paragraph, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
let line = Line::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]);
|
||||
let paragraph = Paragraph::new(line).block(Block::default().borders(Borders::ALL));
|
||||
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
(0, 7),
|
||||
paragraph,
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│NBSP\u{00a0} │",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_can_scroll_horizontally() {
|
||||
let text =
|
||||
Text::from("段落现在可以水平滚动了!\nParagraph can scroll horizontally!\nShort line");
|
||||
let paragraph = Paragraph::new(text).block(Block::default().borders(Borders::ALL));
|
||||
|
||||
test_case(
|
||||
paragraph.clone().alignment(Alignment::Left).scroll((0, 7)),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│在可以水平滚动了!│",
|
||||
@@ -202,8 +120,7 @@ fn widgets_paragraph_can_scroll_horizontally() {
|
||||
);
|
||||
// only support Alignment::Left
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
(0, 7),
|
||||
paragraph.clone().alignment(Alignment::Right).scroll((0, 7)),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│段落现在可以水平滚│",
|
||||
@@ -218,3 +135,234 @@ fn widgets_paragraph_can_scroll_horizontally() {
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
const SAMPLE_STRING: &str = "The library is based on the principle of immediate rendering with \
|
||||
intermediate buffers. This means that at each new frame you should build all widgets that are \
|
||||
supposed to be part of the UI. While providing a great flexibility for rich and \
|
||||
interactive UI, this may introduce overhead for highly dynamic content.";
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_can_wrap_its_content() {
|
||||
let text = vec![Line::from(SAMPLE_STRING)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(Wrap { trim: true });
|
||||
|
||||
test_case(
|
||||
paragraph.clone().alignment(Alignment::Left),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│The library is │",
|
||||
"│based on the │",
|
||||
"│principle of │",
|
||||
"│immediate │",
|
||||
"│rendering with │",
|
||||
"│intermediate │",
|
||||
"│buffers. This │",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
paragraph.clone().alignment(Alignment::Center),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ The library is │",
|
||||
"│ based on the │",
|
||||
"│ principle of │",
|
||||
"│ immediate │",
|
||||
"│ rendering with │",
|
||||
"│ intermediate │",
|
||||
"│ buffers. This │",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
paragraph.clone().alignment(Alignment::Right),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ The library is│",
|
||||
"│ based on the│",
|
||||
"│ principle of│",
|
||||
"│ immediate│",
|
||||
"│ rendering with│",
|
||||
"│ intermediate│",
|
||||
"│ buffers. This│",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_works_with_padding() {
|
||||
let text = vec![Line::from(SAMPLE_STRING)];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL).padding(Padding {
|
||||
left: 2,
|
||||
right: 2,
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
}))
|
||||
.wrap(Wrap { trim: true });
|
||||
|
||||
test_case(
|
||||
paragraph.clone().alignment(Alignment::Left),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────┐",
|
||||
"│ │",
|
||||
"│ The library is │",
|
||||
"│ based on the │",
|
||||
"│ principle of │",
|
||||
"│ immediate │",
|
||||
"│ rendering with │",
|
||||
"│ intermediate │",
|
||||
"│ buffers. This │",
|
||||
"│ means that at │",
|
||||
"│ │",
|
||||
"└────────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
paragraph.clone().alignment(Alignment::Right),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────┐",
|
||||
"│ │",
|
||||
"│ The library is │",
|
||||
"│ based on the │",
|
||||
"│ principle of │",
|
||||
"│ immediate │",
|
||||
"│ rendering with │",
|
||||
"│ intermediate │",
|
||||
"│ buffers. This │",
|
||||
"│ means that at │",
|
||||
"│ │",
|
||||
"└────────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
let mut text = vec![Line::from("This is always centered.").alignment(Alignment::Center)];
|
||||
text.push(Line::from(SAMPLE_STRING));
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL).padding(Padding {
|
||||
left: 2,
|
||||
right: 2,
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
}))
|
||||
.wrap(Wrap { trim: true });
|
||||
|
||||
test_case(
|
||||
paragraph.alignment(Alignment::Right),
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────┐",
|
||||
"│ │",
|
||||
"│ This is always │",
|
||||
"│ centered. │",
|
||||
"│ The library is │",
|
||||
"│ based on the │",
|
||||
"│ principle of │",
|
||||
"│ immediate │",
|
||||
"│ rendering with │",
|
||||
"│ intermediate │",
|
||||
"│ buffers. This │",
|
||||
"│ means that at │",
|
||||
"│ │",
|
||||
"└────────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_can_align_spans() {
|
||||
let right_s = "This string will override the paragraph alignment to be right aligned.";
|
||||
let default_s = "This string will be aligned based on the alignment of the paragraph.";
|
||||
|
||||
let text = vec![
|
||||
Line::from(right_s).alignment(Alignment::Right),
|
||||
Line::from(default_s),
|
||||
];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(Wrap { trim: true });
|
||||
|
||||
test_case(
|
||||
paragraph.clone().alignment(Alignment::Left),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ This string will│",
|
||||
"│ override the│",
|
||||
"│ paragraph│",
|
||||
"│ alignment to be│",
|
||||
"│ right aligned.│",
|
||||
"│This string will │",
|
||||
"│be aligned based │",
|
||||
"│on the alignment │",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
paragraph.alignment(Alignment::Center),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ This string will│",
|
||||
"│ override the│",
|
||||
"│ paragraph│",
|
||||
"│ alignment to be│",
|
||||
"│ right aligned.│",
|
||||
"│ This string will │",
|
||||
"│ be aligned based │",
|
||||
"│ on the alignment │",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
|
||||
let left_lines = vec!["This string", "will override the paragraph alignment"]
|
||||
.into_iter()
|
||||
.map(|s| Line::from(s).alignment(Alignment::Left))
|
||||
.collect::<Vec<_>>();
|
||||
let mut lines = vec![
|
||||
"This",
|
||||
"must be pretty long",
|
||||
"in order to effectively show",
|
||||
"truncation.",
|
||||
]
|
||||
.into_iter()
|
||||
.map(Line::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut text = left_lines.clone();
|
||||
text.append(&mut lines);
|
||||
let paragraph = Paragraph::new(text).block(Block::default().borders(Borders::ALL));
|
||||
|
||||
test_case(
|
||||
paragraph.clone().alignment(Alignment::Right),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│This string │",
|
||||
"│will override the │",
|
||||
"│ This│",
|
||||
"│must be pretty lon│",
|
||||
"│in order to effect│",
|
||||
"│ truncation.│",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
test_case(
|
||||
paragraph.alignment(Alignment::Left),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│This string │",
|
||||
"│will override the │",
|
||||
"│This │",
|
||||
"│must be pretty lon│",
|
||||
"│in order to effect│",
|
||||
"│truncation. │",
|
||||
"│ │",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use tui::{
|
||||
#![allow(deprecated)]
|
||||
|
||||
use ratatui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Constraint,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Terminal,
|
||||
};
|
||||
@@ -623,7 +625,7 @@ fn widgets_table_can_have_elements_styled_individually() {
|
||||
Row::new(vec![
|
||||
Cell::from("Row21"),
|
||||
Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
|
||||
Cell::from(Spans::from(vec![
|
||||
Cell::from(Line::from(vec![
|
||||
Span::raw("Row"),
|
||||
Span::styled("23", Style::default().fg(Color::Blue)),
|
||||
]))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use tui::{
|
||||
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Spans, widgets::Tabs,
|
||||
#![allow(deprecated)]
|
||||
|
||||
use ratatui::{
|
||||
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Line, widgets::Tabs,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
@@ -9,7 +11,7 @@ fn widgets_tabs_should_not_panic_on_narrow_areas() {
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Spans::from).collect());
|
||||
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Line::from).collect());
|
||||
f.render_widget(
|
||||
tabs,
|
||||
Rect {
|
||||
@@ -31,7 +33,7 @@ fn widgets_tabs_should_truncate_the_last_item() {
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Spans::from).collect());
|
||||
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Line::from).collect());
|
||||
f.render_widget(
|
||||
tabs,
|
||||
Rect {
|
||||
|
||||
Reference in New Issue
Block a user