Compare commits

..

1 Commits

Author SHA1 Message Date
Dylan Knutson
7f68927467 Add TextInput widget, Interaction* types for interactive widgets 2022-07-08 11:10:36 -07:00
75 changed files with 1460 additions and 3090 deletions

View File

@@ -7,7 +7,7 @@ assignees: ''
---
<!--
Hi there, sorry `ratatui` is not working as expected.
Hi there, sorry `tui` 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 Normal file
View File

@@ -0,0 +1,17 @@
## 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.

View File

@@ -1,19 +0,0 @@
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 }}

View File

@@ -1,76 +1,71 @@
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
name: CI
env:
CI_CARGO_MAKE_VERSION: 0.35.16
CI_CARGO_MAKE_VERSION: 0.35.8
jobs:
test:
linux:
name: Linux
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
rust: ["1.59.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 }}
rust: ["1.56.1", "stable"]
steps:
- uses: hecrj/setup-rust-action@50a120e4d34903c2c1383dec0e9b1d349a9cc2b1
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
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
- 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
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
lint:
runs-on: ubuntu-latest
windows:
name: Windows
runs-on: windows-latest
strategy:
matrix:
rust: ["1.56.1", "stable"]
steps:
- name: Checkout
if: github.event_name != 'pull_request'
uses: actions/checkout@v3
- name: Checkout
if: github.event_name == 'pull_request'
uses: actions/checkout@v3
- uses: actions/checkout@v1
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: "Check conventional commits"
uses: crate-ci/committed@master
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:
args: "-vv"
commits: "HEAD"
- name: "Check typos"
uses: crate-ci/typos@master
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

48
APPS.md
View File

@@ -1,48 +0,0 @@
# Apps using `ratatui`
| Name | Description | Author |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| [adsb_deku/radar](https://github.com/rsadsb/adsb_deku) | Rust ADS-B decoder + TUI radar application | [Rust ADS-B](https://github.com/rsadsb) |
| [bandwhich](https://github.com/imsnif/bandwhich) | Terminal utility for displaying current network utilization by process, connection and remote IP/hostname | [Aram Drevekenin](https://github.com/imsnif) |
| [battleship.rs](https://github.com/deepu105/battleship-rs) | A terminal battleship game in Rust | [Deepu K Sasidharan](https://github.com/deepu105) |
| [bottom](https://github.com/ClementTsang/bottom) | Yet another cross-platform graphical process/system monitor | [Clement Tsang](https://github.com/ClementTsang) |
| [conclusive](https://github.com/mrusme/conclusive) | Command line client for Plausible Analytics | [mrusme](https://github.com/mrusme) |
| [cotp](https://github.com/replydev/cotp) | Trustworthy, encrypted, command-line TOTP/HOTP authenticator app with import functionality | [Reply](https://github.com/replydev) |
| [cube timer](https://github.com/paarthmadan/cube) | A tui-based Rubik's cube timer written in Rust | [Paarth Madan](https://github.com/paarthmadan) |
| [desed](https://github.com/SoptikHa2/desed) | Debugger for Sed: demystify and debug your sed scripts, from comfort of your terminal | [Petr Šťastný](https://github.com/SoptikHa2) |
| [diskonaut](https://github.com/imsnif/diskonaut) | Terminal disk space navigator | [Aram Drevekenin](https://github.com/imsnif) |
| [exhaust](https://github.com/heyrict/exhaust) | Exhaust all your possibilities.. for the next coming exam | [Zhenhui Xie](https://github.com/heyrict) |
| [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 | [kachark](https://github.com/kachark) |
| [gitui](https://github.com/extrawurst/gitui) | Blazing fast terminal-ui for Git written in Rust | [extrawurst](https://github.com/extrawurst) |
| [glicol-cli](https://github.com/glicol/glicol-cli) | Music live coding in terminal | [glicol](https://github.com/glicol) |
| [gpg-tui](https://github.com/orhun/gpg-tui) | Manage your GnuPG keys with ease! | [Orhun Parmaksız](https://github.com/orhun) |
| [gping](https://github.com/orf/gping) | Ping, but with a graph | [Tom Forbes](https://github.com/orf) |
| [joshuto](https://github.com/kamiyaa/joshuto) | Ranger-like terminal file manager written in Rust | [Jeff Zhao](https://github.com/kamiyaa) |
| [kDash](https://github.com/kdash-rs/kdash) | A simple and fast dashboard for Kubernetes | [kdash-rs ](https://github.com/kdash-rs) |
| [kmon](https://github.com/orhun/kmon) | Linux Kernel Manager and Activity Monitor | [Orhun Parmaksız](https://github.com/orhun) |
| [kubectl-watch](https://github.com/imuxin/kubectl-watch) | A kubectl plugin to provide a pretty delta change view of being watched kubernetes resources | [牧心](https://github.com/imuxin) |
| [logss](https://github.com/todoesverso/logss) | A simple command line tool that helps you visualize an input stream of text. | [Victor Rosales](https://github.com/todoesverso) |
| [minesweep](https://github.com/cpcloud/minesweep-rs) | Sweep some mines for fun, and probably not for profit | [Phillip Cloud](https://github.com/cpcloud) |
| [oha](https://github.com/hatoo/oha) | HTTP load generator, inspired by rakyll/hey with tui animation | [hatoo](https://github.com/hatoo) |
| [oxker](https://github.com/mrjackwills/oxker) | a simple tui to view & control docker containers | [Jack Wills](https://github.com/mrjackwills) |
| [oxycards](https://github.com/BrookJeynes/oxycards) | Oxycards is a quiz card application built within the terminal | [Brook Jeynes](https://github.com/BrookJeynes) |
| [poketex](https://github.com/ckaznable/poketex) | A simple pokedex based on TUI | [CK Aznable](https://github.com/ckaznable) |
| [repgrep](https://github.com/acheronfail/repgrep) | An interactive find and replace app powered by ripgrep | [acheronfail](https://github.com/acheronfail) |
| [rrtop](https://github.com/wojciech-zurek/rrtop) | Redis monitoring (top like) app | [Wojciech Żurek](https://github.com/wojciech-zurek) |
| [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli) | Sadari game based on terminal | [24seconds](https://github.com/24seconds) |
| [rusty-krab-manager](https://github.com/aryakaul/rusty-krab-manager) | Time-management TUI in Rust | [Arya](https://github.com/aryakaul) |
| [spotify-tui](https://github.com/Rigellute/spotify-tui) | Spotify for the terminal written in Rust | [Alexander Keliris](https://github.com/Rigellute) |
| [systeroid](https://github.com/orhun/systeroid) | A more powerful alternative to sysctl(8) with a terminal user interface | [Orhun Parmaksız](https://github.com/orhun) |
| [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui) | A terminal user interface for Taskwarrior | [Dheepak Krishnamurthy](https://github.com/kdheepak) |
| [tenere](https://github.com/pythops/tenere) | TUI interface for LLMs written in Rust | [Badr Badri](https://github.com/pythops) |
| [termchat](https://github.com/lemunozm/termchat) | Terminal chat through the LAN with video streaming and file transfer | [Luis Enrique Muñoz Martín](https://github.com/lemunozm) |
| [termscp](https://github.com/veeso/termscp) | A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/S3 | [Christian Visintin](https://github.com/veeso) |
| [tick-rs](https://github.com/tarkah/tickrs) | Realtime ticker data in your terminal | [Cory Forsstrom](https://github.com/tarkah) |
| [trippy](https://github.com/fujiapple852/trippy) | A network diagnostic tool (combines traceroute & ping, like mtr) | [fujiapple852](https://github.com/fujiapple852) |
| [tsuchita](https://github.com/kamiyaa/tsuchita) | Client-server notification center for dbus desktop notifications | [Jeff Zhao](https://github.com/kamiyaa) |
| [tuinance](https://github.com/landchad/tuinance) | Display financial data on the terminal | [bloatoo](https://github.com/bloatoo) |
| [twitch-tui](https://github.com/Xithrius/twitch-tui) | Twitch chat in the terminal. | [Xithrius](https://github.com/Xithrius) |
| [vector](https://vector.dev) | A lightweight, ultra-fast tool for building observability pipelines | [vectordotdev](https://github.com/vectordotdev) |
| [xplr](https://github.com/sayanarijit/xplr) | A hackable, minimal, fast TUI file explorer | [Arijit Basu](https://github.com/sayanarijit/xplr) |
| [ytop](https://github.com/cjbassi/ytop) | A TUI system monitor written in Rust (no longer maintained) | [Caleb Bassi](https://github.com/cjbassi) |
| [zenith](https://github.com/bvaisvil/zenith) | Sort of like top or htop but with zoom-able charts, CPU, GPU, network, and disk usage | [Benjamin Vaisvil](https://github.com/bvaisvil) |

View File

@@ -1,121 +1,6 @@
# Changelog
## 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`
## To be released
## v0.18.0 - 2022-04-24
@@ -127,7 +12,7 @@ And most importantly, special thanks to [Florian Dehau](https://github.com/fdeha
### Features
* Add option to `widgets::List` to repeat the highlight symbol for each line of multi-line items (#533).
* Add option to `widgets::List` to repeat the hightlight 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
@@ -385,7 +270,7 @@ In this new release, you may now write this as:
```rust
Block::default()
.style(Style::default().bg(Color::Green))
// The style is not overridden anymore, we simply add new style rule for the title.
// The style is not overidden anymore, we simply add new style rule for the title.
.title(Span::styled("My title", Style::default().add_modifier(Modifier::BOLD)))
```
@@ -393,9 +278,9 @@ In addition, the crate now provides a method `patch` to combine two styles into
rules:
```rust
let style = Style::default().modifier(Modifier::BOLD);
let style = Style::default().modifer(Modifier::BOLD);
let style = style.patch(Style::default().add_modifier(Modifier::ITALIC));
// style.modifier == Modifier::BOLD | Modifier::ITALIC, the modifier has been enriched not overridden
// style.modifer == Modifier::BOLD | Modifier::ITALIC, the modifier has been enriched not overidden
```
- `Style::modifier` has been removed in favor of `Style::add_modifier` and `Style::remove_modifier`.
@@ -713,7 +598,7 @@ let style = Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD);
### Bug Fixes
* Ensure correct behavior of the alternate screens with the `Crossterm` backend.
* Ensure correct behavoir of the alternate screens with the `Crossterm` backend.
* Fix out of bounds panic when two `Buffer` are merged.
## v0.4.0 - 2019-02-03
@@ -782,7 +667,7 @@ additional `termion` features.
* Replace `Item` by a generic and flexible `Text` that can be used in both
`Paragraph` and `List` widgets.
* Remove unnecessary borrows on `Style`.
* Remove unecessary borrows on `Style`.
## v0.3.0-beta.0 - 2018-09-04
@@ -805,7 +690,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 language inside `Paragraph`. `Paragraph` now expects an iterator
* Remove markup langage inside `Paragraph`. `Paragraph` now expects an iterator
of `Text` items
## v0.2.3 - 2018-06-09
@@ -813,7 +698,7 @@ of `Text` items
### Features
* Add `start_corner` option for `List`
* Add more text alignment options for `Paragraph`
* Add more text aligment options for `Paragraph`
## v0.2.2 - 2018-05-06

View File

@@ -1,38 +1,10 @@
# 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"
`ratatui` is an ordinary Rust project where common tasks are managed with [cargo-make].
`tui` is an ordinary Rust project where common tasks are managed with [cargo-make].
It wraps common `cargo` commands with sane defaults depending on your platform of choice.
Building the project should be as easy as running `cargo make build`.
@@ -56,6 +28,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 `ratatui` is a test against the `TestBackend`.
Beside the usual doc and unit tests, one of the most valuable test you can write for `tui` is a test again 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.

View File

@@ -1,44 +1,36 @@
[package]
name = "ratatui"
version = "0.20.1"
name = "tui"
version = "0.18.0"
authors = ["Florian Dehau <work@fdehau.com>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
documentation = "https://docs.rs/ratatui/latest/ratatui/"
documentation = "https://docs.rs/tui/0.18.0/tui/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/tui-rs-revival/ratatui"
repository = "https://github.com/fdehau/tui-rs"
readme = "README.md"
license = "MIT"
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
autoexamples = true
edition = "2021"
rust-version = "1.59.0"
[badges]
[features]
default = ["crossterm"]
all-widgets = ["widget-calendar"]
widget-calendar = ["time"]
[package.metadata.docs.rs]
all-features = true
[dependencies]
bitflags = "1.3"
cassowary = "0.3"
unicode-segmentation = "1.10"
unicode-segmentation = "1.2"
unicode-width = "0.1"
termion = { version = "2.0", optional = true }
crossterm = { version = "0.26", optional = true }
termion = { version = "1.5", optional = true }
crossterm = { version = "0.23", 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"
@@ -52,10 +44,6 @@ required-features = ["crossterm"]
name = "canvas"
required-features = ["crossterm"]
[[example]]
name = "calendar"
required-features = ["crossterm", "widget-calendar"]
[[example]]
name = "chart"
required-features = ["crossterm"]
@@ -103,7 +91,3 @@ required-features = ["crossterm"]
[[example]]
name = "user_input"
required-features = ["crossterm"]
[[example]]
name = "inline"
required-features = ["crossterm"]

View File

@@ -100,11 +100,11 @@ args = [
]
[tasks.test-crossterm]
env = { TUI_FEATURES = "serde,crossterm,all-widgets" }
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "test"
[tasks.test-termion]
env = { TUI_FEATURES = "serde,termion,all-widgets" }
env = { TUI_FEATURES = "serde,termion" }
run_task = "test"
[tasks.test]

139
README.md
View File

@@ -1,42 +1,19 @@
# ratatui
# tui-rs
An actively maintained `tui-rs` fork.
[![Build Status](https://github.com/tui-rs-revival/ratatui/workflows/CI/badge.svg)](https://github.com/tui-rs-revival/ratatui/actions?query=workflow%3ACI+)
[![Crate Status](https://img.shields.io/crates/v/ratatui.svg)](https://crates.io/crates/ratatui)
[![Docs Status](https://docs.rs/ratatui/badge.svg)](https://docs.rs/crate/ratatui/)
[![Build Status](https://github.com/fdehau/tui-rs/workflows/CI/badge.svg)](https://github.com/fdehau/tui-rs/actions?query=workflow%3ACI+)
[![Crate Status](https://img.shields.io/crates/v/tui.svg)](https://crates.io/crates/tui)
[![Docs Status](https://docs.rs/tui/badge.svg)](https://docs.rs/crate/tui/)
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
# 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
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
user interfaces and dashboards. It is heavily inspired by the `Javascript`
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
`Go` library [termui](https://github.com/gizak/termui).
The library 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)
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
@@ -49,15 +26,13 @@ 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, `ratatui` requires **rustc version 1.59.0 or greater**.
Since version 0.17.0, `tui` requires **rustc version 1.56.1 or greater**.
# Documentation
### [Documentation](https://docs.rs/tui)
The documentation can be found on [docs.rs.](https://docs.rs/ratatui)
# Demo
### Demo
The demo shown in the gif can be run with all available backends.
@@ -70,8 +45,8 @@ cargo run --example demo --no-default-features --features=termion --release -- -
where `tick-rate` is the UI refresh rate in ms.
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).
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).
If the user interface contains glyphs that are not displayed correctly by your terminal, you may want to run
the demo without those symbols:
@@ -80,60 +55,72 @@ the demo without those symbols:
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
```
# Widgets
## Built in
### Widgets
The library comes with the following list of widgets:
- [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)
* [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)
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 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 libraries, bootstrapping templates and widgets
### Third-party widgets
- [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.
* [tui-logger](https://github.com/gin66/tui-logger)
# Apps
### Apps using tui
Check out the list of [close to 40 apps](./APPS.md) using `ratatui`!
* [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.
# Alternatives
### Alternatives
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an
alternative solution to build text user interfaces in Rust.
# 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
## License
[MIT](LICENSE)

View File

@@ -1,10 +0,0 @@
# 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.

View File

@@ -1,87 +0,0 @@
# 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 | trim_start_matches(pat="v") }}] - {{ 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"

View File

@@ -1,18 +0,0 @@
# 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" ]

View File

@@ -3,18 +3,18 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
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)>,

View File

@@ -3,15 +3,15 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, BorderType, Borders, Padding, Paragraph},
widgets::{Block, BorderType, Borders},
Frame, Terminal,
};
use std::{error::Error, io};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
@@ -106,32 +106,14 @@ 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)
.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]);
let block = Block::default().title("With borders").borders(Borders::ALL);
f.render_widget(block, 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)
.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]);
.border_type(BorderType::Double);
f.render_widget(block, bottom_chunks[1]);
f.render_widget(inner_block, inner_area);
}

View File

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

View File

@@ -3,11 +3,15 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
symbols::Marker,
text::Span,
widgets::{
canvas::{Canvas, Map, MapResolution, Rectangle},
@@ -15,11 +19,6 @@ use ratatui::{
},
Frame, Terminal,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
struct App {
x: f64,
@@ -30,8 +29,6 @@ struct App {
vy: f64,
dir_x: bool,
dir_y: bool,
tick_count: u64,
marker: Marker,
}
impl App {
@@ -51,22 +48,10 @@ 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
{
@@ -170,7 +155,6 @@ 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,
@@ -187,7 +171,6 @@ 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);
})

View File

@@ -3,7 +3,12 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
@@ -12,11 +17,6 @@ use ratatui::{
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] = [

View File

@@ -3,7 +3,8 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
layout::Rect,
@@ -11,7 +12,6 @@ use ratatui::{
widgets::Widget,
Frame, Terminal,
};
use std::{error::Error, io};
#[derive(Default)]
struct Label<'a> {

View File

@@ -2,7 +2,7 @@ use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::widgets::ListState;
use tui::widgets::ListState;
const TASKS: [&str; 24] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",

View File

@@ -1,18 +1,18 @@
use crate::{app::App, ui};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
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
@@ -56,15 +56,13 @@ fn run_app<B: Backend>(
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
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(),
_ => {}
}
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(),
_ => {}
}
}
}

View File

@@ -1,24 +1,21 @@
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::IntoAlternateScreen,
screen::AlternateScreen,
};
use tui::{
backend::{Backend, TermionBackend},
Terminal,
};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
let stdout = io::stdout()
.into_raw_mode()
.unwrap()
.into_alternate_screen()
.unwrap();
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;

View File

@@ -1,5 +1,5 @@
use crate::app::App;
use ratatui::{
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},

View File

@@ -3,7 +3,12 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
@@ -11,11 +16,6 @@ use ratatui::{
widgets::{Block, Borders, Gauge},
Frame, Terminal,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
struct App {
progress1: u16,

View File

@@ -1,292 +0,0 @@
use rand::distributions::{Distribution, Uniform};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans},
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(Spans::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(Spans::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,
},
);
}
}

View File

@@ -3,13 +3,13 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{error::Error, io};
use tui::{
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

View File

@@ -1,9 +1,14 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
@@ -11,11 +16,6 @@ use ratatui::{
widgets::{Block, Borders, List, ListItem, ListState},
Frame, Terminal,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
struct StatefulList<T> {
state: ListState,
@@ -186,14 +186,12 @@ fn run_app<B: Backend>(
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
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(),
_ => {}
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Left => app.items.unselect(),
KeyCode::Down => app.items.next(),
KeyCode::Up => app.items.previous(),
_ => {}
}
}
}

View File

@@ -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 ratatui::backend::{Backend, CrosstermBackend};
use ratatui::layout::Alignment;
use ratatui::text::Spans;
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::{Frame, Terminal};
use tui::backend::{Backend, CrosstermBackend};
use tui::layout::Alignment;
use tui::text::Spans;
use tui::widgets::{Block, Borders, Paragraph};
use tui::{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(())
@@ -127,7 +127,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
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 ratatui"),
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"),
];

View File

@@ -3,7 +3,12 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
@@ -11,11 +16,6 @@ use ratatui::{
widgets::{Block, Borders, Paragraph, Wrap},
Frame, Terminal,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
struct App {
scroll: u16,
@@ -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().fg(Color::Black));
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.margin(5)
.constraints(
[
Constraint::Percentage(25),
@@ -138,36 +138,34 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::Gray))
.style(Style::default().bg(Color::White).fg(Color::Black))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), no wrap"));
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Left, no wrap"))
.alignment(Alignment::Left);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Left, wrap"))
.alignment(Alignment::Left)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.clone())
.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"))
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Center, wrap"))
.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]);
}

View File

@@ -1,4 +1,5 @@
use ratatui::{
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
@@ -6,10 +7,9 @@ use ratatui::{
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame, Terminal,
};
use std::{error::Error, io};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
@@ -57,12 +57,10 @@ 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()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('p') => app.show_popup = !app.show_popup,
_ => {}
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('p') => app.show_popup = !app.show_popup,
_ => {}
}
}
}

View File

@@ -7,18 +7,18 @@ use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::{
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
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 {

View File

@@ -1,39 +1,49 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{error::Error, io};
use tui::{
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 {
struct App<'a> {
state: TableState,
items: Vec<Vec<String>>,
items: Vec<Vec<&'a str>>,
}
impl App {
fn new() -> App {
let mut items = vec![];
let max_row = 100;
items.resize(max_row, vec![]);
for row in 0..100 {
for column in 0..3 {
items[row].push(format!("{}.{}", row, column))
}
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
state: TableState::default(),
items,
items: vec![
vec!["Row11", "Row12", "Row13"],
vec!["Row21", "Row22", "Row23"],
vec!["Row31", "Row32", "Row33"],
vec!["Row41", "Row42", "Row43"],
vec!["Row51", "Row52", "Row53"],
vec!["Row61", "Row62\nTest", "Row63"],
vec!["Row71", "Row72", "Row73"],
vec!["Row81", "Row82", "Row83"],
vec!["Row91", "Row92", "Row93"],
vec!["Row101", "Row102", "Row103"],
vec!["Row111", "Row112", "Row113"],
vec!["Row121", "Row122", "Row123"],
vec!["Row131", "Row132", "Row133"],
vec!["Row141", "Row142", "Row143"],
vec!["Row151", "Row152", "Row153"],
vec!["Row161", "Row162", "Row163"],
vec!["Row171", "Row172", "Row173"],
vec!["Row181", "Row182", "Row183"],
vec!["Row191", "Row192", "Row193"],
],
}
}
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
@@ -61,37 +71,6 @@ impl App {
};
self.state.select(Some(i));
}
pub fn next_page(&mut self) {
let page_size = self.state.page_size.unwrap_or(1);
let i = match self.state.selected() {
Some(i) => {
if (i + page_size) > self.items.len() - 1 {
i + page_size - self.items.len()
} else {
i + page_size
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn previous_page(&mut self) {
let page_size = self.state.page_size.unwrap_or(1);
let i = match self.state.selected() {
Some(i) => {
if i >= page_size {
i - page_size
} else {
let remainder = page_size - i;
self.items.len() - remainder - i
}
}
None => 0,
};
self.state.select(Some(i));
}
}
fn main() -> Result<(), Box<dyn Error>> {
@@ -127,15 +106,11 @@ 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()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
KeyCode::PageDown => app.next_page(),
KeyCode::PageUp => app.previous_page(),
_ => {}
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
_ => {}
}
}
}
@@ -163,7 +138,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.max()
.unwrap_or(0)
+ 1;
let cells = item.iter().map(|c| Cell::from(c.to_owned()));
let cells = item.iter().map(|c| Cell::from(*c));
Row::new(cells).height(height as u16).bottom_margin(1)
});
let t = Table::new(rows)

View File

@@ -1,9 +1,10 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
@@ -11,7 +12,6 @@ use ratatui::{
widgets::{Block, Borders, Tabs},
Frame, Terminal,
};
use std::{error::Error, io};
struct App<'a> {
pub titles: Vec<&'a str>,
@@ -72,13 +72,11 @@ 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()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Right => app.next(),
KeyCode::Left => app.previous(),
_ => {}
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Right => app.next(),
KeyCode::Left => app.previous(),
_ => {}
}
}
}

245
examples/text_input.rs Normal file
View File

@@ -0,0 +1,245 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{
Block, Borders, Cell, InteractiveWidgetState, List, ListItem, Paragraph, Row, Table, TextInput,
TextInputState,
},
Frame, Terminal,
};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
const NUM_INPUTS: usize = 3;
#[derive(Default)]
struct App {
input_states: [TextInputState; NUM_INPUTS],
focused_input_idx: Option<usize>,
events: Vec<Event>,
}
impl App {
fn focus_next(&mut self) {
self.focused_input_idx = match self.focused_input_idx {
Some(idx) => {
if idx == (NUM_INPUTS - 1) {
None
} else {
Some(idx + 1)
}
}
None => Some(0),
};
self.set_focused();
}
fn focus_prev(&mut self) {
self.focused_input_idx = match self.focused_input_idx {
Some(idx) => {
if idx == 0 {
None
} else {
Some(idx - 1)
}
}
None => Some(NUM_INPUTS - 1),
};
self.set_focused();
}
fn set_focused(&mut self) {
for input_state in self.input_states.iter_mut() {
input_state.unfocus();
}
if let Some(idx) = self.focused_input_idx {
self.input_states[idx].focus();
}
}
fn focused_input_mut(&mut self) -> Option<&mut TextInputState> {
if let Some(idx) = self.focused_input_idx {
Some(&mut self.input_states[idx])
} else {
None
}
}
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut app = App::default();
loop {
terminal.draw(|f| ui(f, &mut app))?;
let event = event::read()?;
app.events.push(event);
if let Some(state) = app.focused_input_mut() {
if state.handle_event(event).is_consumed() {
continue;
}
}
match event {
Event::Key(key) => match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Tab => app.focus_next(),
KeyCode::BackTab => app.focus_prev(),
_ => {}
},
_ => {}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let layout = Layout::default()
.horizontal_margin(10)
.vertical_margin(2)
.constraints(
[
Constraint::Length(10),
Constraint::Length(14),
Constraint::Length(5),
Constraint::Percentage(100),
]
.as_ref(),
)
.split(f.size());
let info_block = Paragraph::new(vec![
Spans::from(Span::raw("Press 'TAB' to go to the next input")),
Spans::from(Span::raw("Press 'SHIFT+TAB' to go to the previous input")),
Spans::from(Span::raw("Press 'q' to quit when no input is focused")),
Spans::from(Span::raw(
"Supports a subset of readline keyboard shortcuts:",
)),
Spans::from(Span::raw(
" - ctrl+e / ctrl+a to jump to text input end / start",
)),
Spans::from(Span::raw(
" - ctrl+w delete to the start of the current word",
)),
Spans::from(Span::raw(
" - alt+b / alt+f to jump backwards / forwards a word",
)),
Spans::from(Span::raw(" - left / right arrow keys to move the cursor")),
])
.block(Block::default().title("Information").borders(Borders::ALL));
f.render_widget(info_block, layout[0]);
let inputs_block = Block::default().title("Inputs").borders(Borders::ALL);
let inputs_rect = inputs_block.inner(layout[1]);
f.render_widget(inputs_block, layout[1]);
let inputs_layout = Layout::default()
.constraints(
[
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
]
.as_ref(),
)
.split(inputs_rect);
{
let text_input =
TextInput::new().block(Block::default().title("Basic Input").borders(Borders::ALL));
f.render_interactive(text_input, inputs_layout[0], &mut app.input_states[0]);
}
{
let text_input = TextInput::new()
.block(
Block::default()
.title("Has Placeholder")
.borders(Borders::ALL),
)
.placeholder_text("Type something...");
f.render_interactive(text_input, inputs_layout[1], &mut app.input_states[1]);
}
{
let text_input = TextInput::new()
.text_style(Style::default().fg(Color::Yellow))
.block(Block::default().title("Is Followed").borders(Borders::ALL));
f.render_interactive(text_input, inputs_layout[2], &mut app.input_states[2]);
}
{
let text_input = TextInput::new()
.read_only(true)
.text_style(Style::default().fg(Color::LightBlue))
.block(
Block::default()
.title("Follows Above (read only)")
.borders(Borders::ALL),
);
f.render_interactive(text_input, inputs_layout[3], &mut app.input_states[2]);
}
let table = Table::new(
app.input_states
.iter()
.enumerate()
.map(|(idx, input_state)| {
Row::new(vec![
Cell::from(Span::raw(format!("Input {}", idx + 1))),
Cell::from(Span::styled(
input_state.get_value(),
Style::default().add_modifier(Modifier::BOLD),
)),
])
})
.collect::<Vec<_>>(),
)
.widths(&[Constraint::Min(10), Constraint::Percentage(100)])
.block(Block::default().title("Input Values").borders(Borders::ALL));
f.render_widget(table, layout[2]);
let events = List::new(
app.events
.iter()
.rev()
.map(|event| ListItem::new(Span::raw(format!("{:?}", event))))
.collect::<Vec<_>>(),
)
.block(Block::default().title("Events").borders(Borders::ALL));
f.render_widget(events, layout[3]);
}

View File

@@ -10,11 +10,12 @@
/// * Pressing Enter pushes the current input in the history of previous
/// messages
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
@@ -22,7 +23,6 @@ use ratatui::{
widgets::{Block, Borders, List, ListItem, Paragraph},
Frame, Terminal,
};
use std::{error::Error, io};
use unicode_width::UnicodeWidthStr;
enum InputMode {
@@ -93,7 +93,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
_ => {}
},
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
InputMode::Editing => match key.code {
KeyCode::Enter => {
app.messages.push(app.input.drain(..).collect());
}
@@ -108,7 +108,6 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
_ => {}
},
_ => {}
}
}
}
@@ -168,7 +167,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
{}
InputMode::Editing => {
// Make the cursor visible and ask ratatui to put it at the specified coordinates after rendering
// Make the cursor visible and ask tui-rs 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,
@@ -183,7 +182,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.iter()
.enumerate()
.map(|(i, m)| {
let content = Spans::from(Span::raw(format!("{}: {}", i, m)));
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
ListItem::new(content)
})
.collect();

View File

@@ -1,5 +1,5 @@
use crate::{
backend::{Backend, ClearType},
backend::Backend,
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},
terminal::{self, Clear, ClearType},
};
use std::io::{self, Write};
@@ -107,27 +107,7 @@ where
}
fn clear(&mut self) -> io::Result<()> {
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()
map_error(execute!(self.buffer, Clear(ClearType::All)))
}
fn size(&self) -> io::Result<Rect> {

View File

@@ -16,48 +16,15 @@ pub use self::crossterm::CrosstermBackend;
mod test;
pub use self::test::TestBackend;
#[derive(Debug, Clone, Copy, PartialEq)]
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>;
}

View File

@@ -1,5 +1,5 @@
use super::Backend;
use crate::{
backend::{Backend, ClearType},
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
@@ -42,25 +42,10 @@ 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<()> {
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)?;
}
write!(self.stdout, "{}", termion::clear::All)?;
write!(self.stdout, "{}", termion::cursor::Goto(1, 1))?;
self.stdout.flush()
}

View File

@@ -3,10 +3,7 @@ use crate::{
buffer::{Buffer, Cell},
layout::Rect,
};
use std::{
fmt::{Display, Write},
io,
};
use std::{fmt::Write, io};
use unicode_width::UnicodeWidthStr;
/// A backend used for the integration tests.
@@ -108,12 +105,6 @@ impl TestBackend {
}
}
impl Display for TestBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", buffer_view(&self.buffer))
}
}
impl Backend for TestBackend {
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where

View File

@@ -8,7 +8,7 @@ use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
/// A buffer cell
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Cell {
pub symbol: String,
pub fg: Color,
@@ -88,9 +88,9 @@ impl Default for Cell {
/// # Examples:
///
/// ```
/// use ratatui::buffer::{Buffer, Cell};
/// use ratatui::layout::Rect;
/// use ratatui::style::{Color, Style, Modifier};
/// use tui::buffer::{Buffer, Cell};
/// use tui::layout::Rect;
/// use tui::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 +105,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, Eq, Default)]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
@@ -176,15 +176,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 ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::buffer::Buffer;
/// # use tui::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 +196,8 @@ impl Buffer {
/// Panics when given an coordinate that is outside of this Buffer's area.
///
/// ```should_panic
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::buffer::Buffer;
/// # use tui::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
@@ -225,8 +225,8 @@ impl Buffer {
/// # Examples
///
/// ```
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::buffer::Buffer;
/// # use tui::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 +238,8 @@ impl Buffer {
/// Panics when given an index that is outside the Buffer's content.
///
/// ```should_panic
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::buffer::Buffer;
/// # use tui::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.
@@ -289,7 +289,7 @@ impl Buffer {
continue;
}
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
// change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
if width > max_offset.saturating_sub(x_offset) {
break;
}
@@ -306,7 +306,7 @@ impl Buffer {
(x_offset as u16, y)
}
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) {
pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &spans.0 {
@@ -327,7 +327,7 @@ impl Buffer {
(x, y)
}
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
pub fn set_span<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) {
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
}
@@ -431,16 +431,18 @@ 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 preceding multi-width characters:
// Cells invalidated by drawing/replacing preceeding multi-width characters:
let mut invalidated: usize = 0;
// Cells from the current buffer to skip due to preceding multi-width characters taking their
// Cells from the current buffer to skip due to preceeding multi-width characters taking their
// place (the skipped cells should be blank anyway):
let mut to_skip: usize = 0;
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
if (current != previous || invalidated > 0) && to_skip == 0 {
let (x, y) = self.pos_of(i);
let x = i as u16 % width;
let y = i as u16 / width;
updates.push((x, y, &next_buffer[i]));
}

View File

@@ -1,9 +1,8 @@
use std::cell::RefCell;
use std::cmp::{max, min};
use std::collections::HashMap;
use std::rc::Rc;
use cassowary::strength::{MEDIUM, REQUIRED, WEAK};
use cassowary::strength::{REQUIRED, WEAK};
use cassowary::WeightedRelation::*;
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
@@ -52,7 +51,7 @@ pub struct Margin {
pub horizontal: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alignment {
Left,
Center,
@@ -69,9 +68,8 @@ pub struct Layout {
expand_to_fill: bool,
}
type Cache = HashMap<(Rect, Layout), Rc<[Rect]>>;
thread_local! {
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(HashMap::new());
static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
}
impl Default for Layout {
@@ -130,7 +128,7 @@ impl Layout {
///
/// # Examples
/// ```
/// # use ratatui::layout::{Rect, Constraint, Direction, Layout};
/// # use tui::layout::{Rect, Constraint, Direction, Layout};
/// let chunks = Layout::default()
/// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
@@ -141,8 +139,8 @@ impl Layout {
/// height: 10,
/// });
/// assert_eq!(
/// chunks[..],
/// [
/// chunks,
/// vec![
/// Rect {
/// x: 2,
/// y: 2,
@@ -168,8 +166,8 @@ impl Layout {
/// height: 2,
/// });
/// assert_eq!(
/// chunks[..],
/// [
/// chunks,
/// vec![
/// Rect {
/// x: 0,
/// y: 0,
@@ -185,7 +183,7 @@ impl Layout {
/// ]
/// );
/// ```
pub fn split(&self, area: Rect) -> Rc<[Rect]> {
pub fn split(&self, area: Rect) -> Vec<Rect> {
// TODO: Maybe use a fixed size cache ?
LAYOUT_CACHE.with(|c| {
c.borrow_mut()
@@ -196,7 +194,7 @@ impl Layout {
}
}
fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
let mut solver = Solver::new();
let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
let elements = layout
@@ -204,13 +202,11 @@ fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
.iter()
.map(|_| Element::new())
.collect::<Vec<Element>>();
let mut res = layout
let mut results = layout
.constraints
.iter()
.map(|_| Rect::default())
.collect::<Rc<[Rect]>>();
let mut results = Rc::get_mut(&mut res).expect("newly created Rc should have no shared refs");
.collect::<Vec<Rect>>();
let dest_area = area.inner(&layout.margin);
for (i, e) in elements.iter().enumerate() {
@@ -252,28 +248,18 @@ fn split(area: Rect, layout: &Layout) -> Rc<[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(MEDIUM) | f64::from(v),
Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].width | EQ(MEDIUM) | (f64::from(v * dest_area.width) / 100.0)
elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
}
Constraint::Ratio(n, d) => {
elements[i].width
| EQ(MEDIUM)
| EQ(WEAK)
| (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => elements[i].width | GE(MEDIUM) | f64::from(v),
Constraint::Max(v) => elements[i].width | LE(MEDIUM) | f64::from(v),
Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
});
match *size {
Constraint::Min(v) => {
ccs.push(elements[i].width | EQ(WEAK) | f64::from(v));
}
Constraint::Max(v) => {
ccs.push(elements[i].width | EQ(WEAK) | f64::from(v));
}
_ => {}
}
}
}
Direction::Vertical => {
@@ -284,28 +270,18 @@ fn split(area: Rect, layout: &Layout) -> Rc<[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(MEDIUM) | f64::from(v),
Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].height | EQ(MEDIUM) | (f64::from(v * dest_area.height) / 100.0)
elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
}
Constraint::Ratio(n, d) => {
elements[i].height
| EQ(MEDIUM)
| EQ(WEAK)
| (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => elements[i].height | GE(MEDIUM) | f64::from(v),
Constraint::Max(v) => elements[i].height | LE(MEDIUM) | f64::from(v),
Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
});
match *size {
Constraint::Min(v) => {
ccs.push(elements[i].height | EQ(WEAK) | f64::from(v));
}
Constraint::Max(v) => {
ccs.push(elements[i].height | EQ(WEAK) | f64::from(v));
}
_ => {}
}
}
}
}
@@ -347,7 +323,7 @@ fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
}
}
}
res
results
}
/// A container used by the solver inside split
@@ -385,7 +361,7 @@ impl Element {
}
}
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
/// area they are supposed to render to.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
pub struct Rect {

View File

@@ -1,17 +1,16 @@
//! [ratatui](https://github.com/tui-rs-revival/ratatui) is a library used to build rich
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
//! terminal users interfaces and dashboards.
//!
//! ![](https://raw.githubusercontent.com/tui-rs-revival/ratatui/master/assets/demo.gif)
//! ![](https://raw.githubusercontent.com/fdehau/tui-rs/master/assets/demo.gif)
//!
//! # Get started
//!
//! ## Adding `ratatui` as a dependency
//! ## Adding `tui` as a dependency
//!
//! Add the following to your `Cargo.toml`:
//! ```toml
//! [dependencies]
//! crossterm = "0.26"
//! ratatui = "0.20"
//! tui = "0.18"
//! crossterm = "0.23"
//! ```
//!
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
@@ -21,29 +20,21 @@
//! ```toml
//! [dependencies]
//! termion = "1.5"
//! ratatui = { version = "0.20", default-features = false, features = ['termion'] }
//! tui = { version = "0.18", 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 `ratatui` should start by instantiating a `Terminal`. It is a light
//! Every application using `tui` 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 ratatui::{backend::CrosstermBackend, Terminal};
//! use tui::{backend::CrosstermBackend, Terminal};
//!
//! fn main() -> Result<(), io::Error> {
//! let stdout = io::stdout();
@@ -58,7 +49,7 @@
//!
//! ```rust,ignore
//! use std::io;
//! use ratatui::{backend::TermionBackend, Terminal};
//! use tui::{backend::TermionBackend, Terminal};
//! use termion::raw::IntoRawMode;
//!
//! fn main() -> Result<(), io::Error> {
@@ -86,13 +77,14 @@
//!
//! ```rust,no_run
//! use std::{io, thread, time::Duration};
//! use ratatui::{
//! use tui::{
//! backend::CrosstermBackend,
//! widgets::{Block, Borders},
//! widgets::{Widget, Block, Borders},
//! layout::{Layout, Constraint, Direction},
//! Terminal
//! };
//! use crossterm::{
//! event::{self, DisableMouseCapture, EnableMouseCapture},
//! event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
//! execute,
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
//! };
@@ -113,12 +105,6 @@
//! 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
@@ -141,7 +127,7 @@
//! full customization. And `Layout` is no exception:
//!
//! ```rust,no_run
//! use ratatui::{
//! use tui::{
//! backend::Backend,
//! layout::{Constraint, Direction, Layout},
//! widgets::{Block, Borders},

View File

@@ -2,7 +2,7 @@
use bitflags::bitflags;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Color {
Reset,
@@ -34,7 +34,7 @@ bitflags! {
/// ## Examples
///
/// ```rust
/// # use ratatui::style::Modifier;
/// # use tui::style::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
@@ -55,7 +55,7 @@ bitflags! {
/// Style let you control the main characteristics of the displayed elements.
///
/// ```rust
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::style::{Color, Modifier, Style};
/// Style::default()
/// .fg(Color::Black)
/// .bg(Color::Green)
@@ -67,9 +67,9 @@ bitflags! {
/// just S3.
///
/// ```rust
/// # use ratatui::style::{Color, Modifier, Style};
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::style::{Color, Modifier, Style};
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::default().bg(Color::Red),
@@ -94,9 +94,9 @@ bitflags! {
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// # use ratatui::style::{Color, Modifier, Style};
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::style::{Color, Modifier, Style};
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::reset().fg(Color::Yellow),
@@ -115,7 +115,7 @@ bitflags! {
/// buffer.get(0, 0).style(),
/// );
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Option<Color>,
@@ -126,12 +126,6 @@ pub struct Style {
impl Default for Style {
fn default() -> Style {
Style::new()
}
}
impl Style {
pub const fn new() -> Style {
Style {
fg: None,
bg: None,
@@ -139,9 +133,11 @@ impl Style {
sub_modifier: Modifier::empty(),
}
}
}
impl Style {
/// Returns a `Style` resetting all properties.
pub const fn reset() -> Style {
pub fn reset() -> Style {
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
@@ -155,12 +151,12 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::style::{Color, Style};
/// # use tui::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 const fn fg(mut self, color: Color) -> Style {
pub fn fg(mut self, color: Color) -> Style {
self.fg = Some(color);
self
}
@@ -170,12 +166,12 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::style::{Color, Style};
/// # use tui::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 const fn bg(mut self, color: Color) -> Style {
pub fn bg(mut self, color: Color) -> Style {
self.bg = Some(color);
self
}
@@ -187,7 +183,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::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);
@@ -207,7 +203,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::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);
@@ -225,7 +221,7 @@ impl Style {
///
/// ## Examples
/// ```
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::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);
@@ -282,33 +278,4 @@ 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));
}
}
}

View File

@@ -228,8 +228,6 @@ 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,
}

View File

@@ -1,16 +1,33 @@
use crate::{
backend::{Backend, ClearType},
backend::Backend,
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
widgets::{InteractiveWidget, StatefulWidget, Widget},
};
use std::io;
#[derive(Debug, Clone, PartialEq)]
pub enum Viewport {
Fullscreen,
Inline(u16),
Fixed(Rect),
/// UNSTABLE
enum ResizeBehavior {
Fixed,
Auto,
}
#[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)]
@@ -36,12 +53,6 @@ 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.
@@ -62,9 +73,9 @@ impl<'a, B> Frame<'a, B>
where
B: Backend,
{
/// Frame size, guaranteed not to change when rendering.
/// Terminal 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`].
@@ -72,10 +83,10 @@ where
/// # Examples
///
/// ```rust
/// # use ratatui::Terminal;
/// # use ratatui::backend::TestBackend;
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::Block;
/// # use tui::Terminal;
/// # use tui::backend::TestBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::Block;
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let block = Block::default();
@@ -98,10 +109,10 @@ where
/// # Examples
///
/// ```rust
/// # use ratatui::Terminal;
/// # use ratatui::backend::TestBackend;
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::{List, ListItem, ListState};
/// # use tui::Terminal;
/// # use tui::backend::TestBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::{List, ListItem, ListState};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let mut state = ListState::default();
@@ -122,6 +133,20 @@ where
widget.render(area, self.terminal.current_buffer_mut(), state);
}
pub fn render_interactive<W>(&mut self, widget: W, area: Rect, state: &W::State)
where
W: InteractiveWidget,
{
widget.render(area, self, state);
}
pub fn render_interactive_mut<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: InteractiveWidget,
{
widget.render_mut(area, self, state);
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
@@ -162,33 +187,29 @@ 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::Fullscreen,
viewport: Viewport {
area: size,
resize_behavior: ResizeBehavior::Auto,
},
},
)
}
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())),
};
/// UNSTABLE
pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
Ok(Terminal {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
buffers: [
Buffer::empty(options.viewport.area),
Buffer::empty(options.viewport.area),
],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_size: size,
last_known_cursor_pos: cursor_pos,
})
}
@@ -218,46 +239,24 @@ 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, 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) {
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport_area = area;
self.viewport.area = area;
self.clear()
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
// fixed viewports do not get autoresized
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
if self.viewport.resize_behavior == ResizeBehavior::Auto {
let size = self.size()?;
if size != self.last_known_size {
if size != self.viewport.area {
self.resize(size)?;
}
};
@@ -298,10 +297,9 @@ where
// Flush
self.backend.flush()?;
Ok(CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_size,
area: self.viewport.area,
})
}
@@ -322,27 +320,12 @@ where
}
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)?;
self.last_known_cursor_pos = (x, y);
Ok(())
self.backend.set_cursor(x, y)
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
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)?;
}
}
}
self.backend.clear()?;
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
@@ -352,130 +335,4 @@ 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::{Spans, 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(Spans::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,
))
}

View File

@@ -1,7 +1,7 @@
//! 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. `ratatui` has three ways to represent them:
//! those strings may be associated to a set of styles. `tui` 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 multiple line string where each grapheme may have its own style is represented by a
@@ -11,7 +11,7 @@
//! is a [`Spans`].
//!
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations so
//! supported for their properties. Moreover, `tui` 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.
//!
@@ -19,9 +19,9 @@
//! its `title` property (which is a [`Spans`] under the hood):
//!
//! ```rust
//! # use ratatui::widgets::Block;
//! # use ratatui::text::{Span, Spans};
//! # use ratatui::style::{Color, Style};
//! # use tui::widgets::Block;
//! # use tui::text::{Span, Spans};
//! # use tui::style::{Color, Style};
//! // A simple string with no styling.
//! // Converted to Spans(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
@@ -52,14 +52,14 @@ use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
/// A grapheme associated to a style.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct StyledGrapheme<'a> {
pub symbol: &'a str,
pub style: Style,
}
/// A string where all graphemes have the same style.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Span<'a> {
pub content: Cow<'a, str>,
pub style: Style,
@@ -71,7 +71,7 @@ impl<'a> Span<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::text::Span;
/// # use tui::text::Span;
/// Span::raw("My text");
/// Span::raw(String::from("My text"));
/// ```
@@ -90,8 +90,8 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::text::Span;
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::text::Span;
/// # use tui::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 +119,8 @@ impl<'a> Span<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, StyledGrapheme};
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::text::{Span, StyledGrapheme};
/// # use tui::style::{Color, Modifier, Style};
/// # use std::iter::Iterator;
/// let style = Style::default().fg(Color::Yellow);
/// let span = Span::styled("Text", style);
@@ -179,43 +179,6 @@ 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> {
@@ -231,7 +194,7 @@ 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, Eq)]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Spans<'a>(pub Vec<Span<'a>>);
impl<'a> Spans<'a> {
@@ -240,8 +203,8 @@ impl<'a> Spans<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style};
/// # 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"),
@@ -251,57 +214,6 @@ impl<'a> Spans<'a> {
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();
}
}
}
impl<'a> From<String> for Spans<'a> {
@@ -345,8 +257,8 @@ impl<'a> From<Spans<'a>> for String {
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::text::Text;
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
///
/// // An initial two lines of `Text` built from a `&str`
@@ -361,7 +273,7 @@ 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, Eq)]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Text<'a> {
pub lines: Vec<Spans<'a>>,
}
@@ -372,7 +284,7 @@ impl<'a> Text<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// # use tui::text::Text;
/// Text::raw("The first line\nThe second line");
/// Text::raw(String::from("The first line\nThe second line"));
/// ```
@@ -380,14 +292,12 @@ impl<'a> Text<'a> {
where
T: Into<Cow<'a, str>>,
{
let lines: Vec<_> = match content.into() {
Cow::Borrowed("") => vec![Spans::from("")],
Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
Cow::Owned(s) if s.is_empty() => vec![Spans::from("")],
Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
};
Text { lines }
Text {
lines: match content.into() {
Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
},
}
}
/// Create some text (potentially multiple lines) with a style.
@@ -395,8 +305,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::text::Text;
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style);
@@ -415,7 +325,7 @@ impl<'a> Text<'a> {
/// ## Examples
///
/// ```rust
/// use ratatui::text::Text;
/// use tui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(15, text.width());
/// ```
@@ -432,7 +342,7 @@ impl<'a> Text<'a> {
/// ## Examples
///
/// ```rust
/// use ratatui::text::Text;
/// use tui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
/// ```
@@ -440,13 +350,13 @@ impl<'a> Text<'a> {
self.lines.len()
}
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
/// Apply a new style to existing text.
///
/// # Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::text::Text;
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_text = Text::raw("The first line\nThe second line");
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
@@ -457,31 +367,9 @@ impl<'a> Text<'a> {
/// ```
pub fn patch_style(&mut self, style: Style) {
for line in &mut self.lines {
line.patch_style(style);
}
}
/// Resets the style of the Text.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans, 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.0 {
/// assert_eq!(Style::reset(), span.style);
/// }
/// }
/// ```
pub fn reset_style(&mut self) {
for line in &mut self.lines {
line.reset_style();
for span in &mut line.0 {
span.style = span.style.patch(style);
}
}
}
}

View File

@@ -13,8 +13,8 @@ use unicode_width::UnicodeWidthStr;
/// # Examples
///
/// ```
/// # use ratatui::widgets::{Block, Borders, BarChart};
/// # use ratatui::style::{Style, Color, Modifier};
/// # use tui::widgets::{Block, Borders, BarChart};
/// # use tui::style::{Style, Color, Modifier};
/// BarChart::default()
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .bar_width(3)

View File

@@ -7,7 +7,7 @@ use crate::{
widgets::{Borders, Widget},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BorderType {
Plain,
Rounded,
@@ -26,69 +26,14 @@ 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 ratatui::widgets::{Block, BorderType, Borders};
/// # use ratatui::style::{Style, Color};
/// # use tui::widgets::{Block, BorderType, Borders};
/// # use tui::style::{Style, Color};
/// Block::default()
/// .title("Block")
/// .borders(Borders::LEFT | Borders::RIGHT)
@@ -96,15 +41,13 @@ impl Padding {
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Block<'a> {
/// Optional title place on the upper left of the block
title: Option<Spans<'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
@@ -114,8 +57,6 @@ pub struct Block<'a> {
border_type: BorderType,
/// Widget style
style: Style,
/// Block padding
padding: Padding,
}
impl<'a> Default for Block<'a> {
@@ -123,12 +64,10 @@ 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_type: BorderType::Plain,
style: Default::default(),
padding: Padding::zero(),
}
}
}
@@ -159,11 +98,6 @@ 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
@@ -185,34 +119,6 @@ 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) {
@@ -229,24 +135,8 @@ 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> {
@@ -340,11 +230,7 @@ impl<'a> Widget for Block<'a> {
};
let title_x = area.left() + title_dx;
let title_y = if self.title_on_bottom {
area.bottom() - 1
} else {
area.top()
};
let title_y = area.top();
buf.set_spans(title_x, title_y, &title, title_area_width);
}

View File

@@ -1,245 +0,0 @@
//! 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, Spans},
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_spans(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 line = 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 {
line.0.push(Span::styled(" ", Style::default()));
} else {
line.0.push(Span::styled(" ", self.default_bg()));
}
line.0.push(self.format_date(curr_day));
curr_day += Duration::DAY;
}
buf.set_spans(area.x, area.y, &line, 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"
);
}
}

View File

@@ -183,7 +183,7 @@ impl<'a, 'b> Painter<'a, 'b> {
///
/// # Examples:
/// ```
/// use ratatui::{symbols, widgets::canvas::{Painter, Context}};
/// use tui::{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 +220,7 @@ impl<'a, 'b> Painter<'a, 'b> {
///
/// # Examples:
/// ```
/// use ratatui::{style::Color, symbols, widgets::canvas::{Painter, Context}};
/// use tui::{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,13 +260,9 @@ 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, dot)),
symbols::Marker::Block => Box::new(CharGrid::new(width, height, block)),
symbols::Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, '•')),
symbols::Marker::Block => Box::new(CharGrid::new(width, height, '▄')),
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
};
Context {
@@ -321,10 +317,10 @@ impl<'a> Context<'a> {
/// # Examples
///
/// ```
/// # use ratatui::widgets::{Block, Borders};
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
/// # use ratatui::style::Color;
/// # use tui::widgets::{Block, Borders};
/// # use tui::layout::Rect;
/// # use tui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
/// # use tui::style::Color;
/// Canvas::default()
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
/// .x_bounds([-180.0, 180.0])
@@ -388,18 +384,11 @@ 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
@@ -423,8 +412,8 @@ where
/// # Examples
///
/// ```
/// # use ratatui::widgets::canvas::Canvas;
/// # use ratatui::symbols;
/// # use tui::widgets::canvas::Canvas;
/// # use tui::symbols;
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
///
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
@@ -472,7 +461,7 @@ where
painter(&mut ctx);
ctx.finish();
// Retrieve painted points for each layer
// Retreive painted points for each layer
for layer in ctx.layers {
for (i, (ch, color)) in layer
.string
@@ -512,103 +501,3 @@ where
}
}
}
#[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
•••••"
),
);
}
}

View File

@@ -181,10 +181,10 @@ struct ChartLayout {
/// # Examples
///
/// ```
/// # use ratatui::symbols;
/// # use ratatui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
/// # use ratatui::style::{Style, Color};
/// # use ratatui::text::Span;
/// # use tui::symbols;
/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
/// # use tui::style::{Style, Color};
/// # use tui::text::Span;
/// let datasets = vec![
/// Dataset::default()
/// .name("data1")
@@ -265,8 +265,8 @@ impl<'a> Chart<'a> {
/// # Examples
///
/// ```
/// # use ratatui::widgets::Chart;
/// # use ratatui::layout::Constraint;
/// # use tui::widgets::Chart;
/// # use tui::layout::Constraint;
/// let constraints = (
/// Constraint::Ratio(1, 3),
/// Constraint::Ratio(1, 4)

View File

@@ -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 `ratatui` assumes the
/// This widget **cannot be used to clear the terminal on the first render** as `tui` assumes the
/// render area is empty. Use [`crate::Terminal::clear`] instead.
///
/// # Examples
///
/// ```
/// # use ratatui::widgets::{Clear, Block, Borders};
/// # use ratatui::layout::Rect;
/// # use ratatui::Frame;
/// # use ratatui::backend::Backend;
/// # use tui::widgets::{Clear, Block, Borders};
/// # use tui::layout::Rect;
/// # use tui::Frame;
/// # use tui::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

View File

@@ -0,0 +1,25 @@
use crossterm::event::Event;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum InteractionOutcome {
Consumed,
Bubble,
}
impl InteractionOutcome {
pub fn is_consumed(&self) -> bool {
matches!(self, InteractionOutcome::Consumed)
}
pub fn is_bubble(&self) -> bool {
matches!(self, InteractionOutcome::Bubble)
}
}
pub trait InteractiveWidgetState {
fn handle_event(&mut self, _event: Event) -> InteractionOutcome {
InteractionOutcome::Bubble
}
fn is_focused(&self) -> bool;
fn focus(&mut self);
fn unfocus(&mut self);
}

View File

@@ -12,8 +12,8 @@ use crate::{
/// # Examples:
///
/// ```
/// # use ratatui::widgets::{Widget, Gauge, Block, Borders};
/// # use ratatui::style::{Style, Color, Modifier};
/// # use tui::widgets::{Widget, Gauge, Block, Borders};
/// # use tui::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))
@@ -163,9 +163,9 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
/// # Examples:
///
/// ```
/// # use ratatui::widgets::{Widget, LineGauge, Block, Borders};
/// # use ratatui::style::{Style, Color, Modifier};
/// # use ratatui::symbols;
/// # use tui::widgets::{Widget, LineGauge, Block, Borders};
/// # use tui::style::{Style, Color, Modifier};
/// # use tui::symbols;
/// LineGauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::BOLD))

View File

@@ -26,7 +26,7 @@ impl ListState {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct ListItem<'a> {
content: Text<'a>,
style: Style,
@@ -51,10 +51,6 @@ 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)
@@ -62,8 +58,8 @@ impl<'a> ListItem<'a> {
/// # Examples
///
/// ```
/// # use ratatui::widgets::{Block, Borders, List, ListItem};
/// # use ratatui::style::{Style, Color, Modifier};
/// # use tui::widgets::{Block, Borders, List, ListItem};
/// # use tui::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))
@@ -231,7 +227,7 @@ impl<'a> StatefulWidget for List<'a> {
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
for (j, line) in item.content.lines.iter().enumerate() {
// if the item is selected, we need to display the highlight symbol:
// if the item is selected, we need to display the hightlight 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) {
@@ -247,11 +243,11 @@ impl<'a> StatefulWidget for List<'a> {
list_area.width as usize,
item_style,
);
(elem_x, (list_area.width - (elem_x - x)))
(elem_x, (list_area.width - (elem_x - x)) as u16)
} else {
(x, list_area.width)
};
buf.set_spans(elem_x, y + j as u16, line, max_element_width);
buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
}
if is_selected {
buf.set_style(area, self.highlight_style);

View File

@@ -13,13 +13,10 @@
//! - [`BarChart`]
//! - [`Gauge`]
//! - [`Sparkline`]
//! - [`calendar::Monthly`]
//! - [`Clear`]
mod barchart;
mod block;
#[cfg(feature = "widget-calendar")]
pub mod calendar;
pub mod canvas;
mod chart;
mod clear;
@@ -30,34 +27,44 @@ mod reflow;
mod sparkline;
mod table;
mod tabs;
mod text_input;
#[cfg(feature = "crossterm")]
mod crossterm_interactive_widget;
pub use self::barchart::BarChart;
pub use self::block::{Block, BorderType, Padding};
pub use self::block::{Block, BorderType};
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::{RenderDirection, Sparkline};
pub use self::sparkline::Sparkline;
pub use self::table::{Cell, Row, Table, TableState};
pub use self::tabs::Tabs;
pub use self::text_input::{TextInput, TextInputState};
#[cfg(feature = "crossterm")]
pub use self::crossterm_interactive_widget::{InteractiveWidgetState, InteractionOutcome};
use crate::backend::Backend;
use crate::Frame;
use crate::{buffer::Buffer, layout::Rect};
use bitflags::bitflags;
bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
pub struct Borders: u8 {
pub struct Borders: u32 {
/// Show no border (default)
const NONE = 0b0000;
const NONE = 0b0000_0001;
/// Show the top border
const TOP = 0b0001;
const TOP = 0b0000_0010;
/// Show the right border
const RIGHT = 0b0010;
const RIGHT = 0b0000_0100;
/// Show the bottom border
const BOTTOM = 0b0100;
const BOTTOM = 0b000_1000;
/// Show the left border
const LEFT = 0b1000;
const LEFT = 0b0001_0000;
/// Show all borders
const ALL = Self::TOP.bits | Self::RIGHT.bits | Self::BOTTOM.bits | Self::LEFT.bits;
}
@@ -89,9 +96,9 @@ pub trait Widget {
///
/// ```rust,no_run
/// # use std::io;
/// # use ratatui::Terminal;
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::widgets::{Widget, List, ListItem, ListState};
/// # use tui::Terminal;
/// # use tui::backend::{Backend, TestBackend};
/// # use tui::widgets::{Widget, List, ListItem, ListState};
///
/// // Let's say we have some events to display.
/// struct Events {
@@ -168,7 +175,7 @@ pub trait Widget {
/// loop {
/// terminal.draw(|f| {
/// // The items managed by the application are transformed to something
/// // that is understood by ratatui.
/// // that is understood by tui.
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_ref())).collect();
/// // The `List` widget is then built with those items.
/// let list = List::new(items);
@@ -185,3 +192,21 @@ pub trait StatefulWidget {
type State;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
}
pub trait InteractiveWidget {
type State;
fn render<'a, B: Backend + 'a>(
self,
area: Rect,
frame: &mut Frame<'a, B>,
state: &Self::State,
);
fn render_mut<'a, B: Backend + 'a>(
self,
area: Rect,
frame: &mut Frame<'a, B>,
state: &mut Self::State,
);
}

View File

@@ -24,10 +24,10 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// # Examples
///
/// ```
/// # use ratatui::text::{Text, Spans, Span};
/// # use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use ratatui::style::{Style, Color, Modifier};
/// # use ratatui::layout::{Alignment};
/// # use tui::text::{Text, Spans, Span};
/// # use tui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use tui::style::{Style, Color, Modifier};
/// # use tui::layout::{Alignment};
/// let text = vec![
/// Spans::from(vec![
/// Span::raw("First"),
@@ -63,8 +63,8 @@ pub struct Paragraph<'a> {
/// ## Examples
///
/// ```
/// # use ratatui::widgets::{Paragraph, Wrap};
/// # use ratatui::text::Text;
/// # use tui::widgets::{Paragraph, Wrap};
/// # use tui::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"#);
@@ -176,10 +176,6 @@ impl<'a> Widget for Paragraph<'a> {
if y >= self.scroll.0 {
let mut x = get_line_offset(current_line_width, text_area.width, self.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
@@ -189,7 +185,7 @@ impl<'a> Widget for Paragraph<'a> {
symbol
})
.set_style(*style);
x += width as u16;
x += symbol.width() as u16;
}
}
y += 1;
@@ -199,222 +195,3 @@ impl<'a> Widget for Paragraph<'a> {
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{
style::Color,
text::{Span, Spans},
widgets::Borders,
};
#[test]
fn zero_width_char_at_end_of_line() {
let line = "foo\0";
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 1));
Paragraph::new(line).render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["foo"]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_empty_paragraph() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
Paragraph::new("").render(Rect::new(0, 0, 10, 5), &mut buffer);
let expected_buffer = Buffer::with_lines(vec![" "; 5]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_single_line_paragraph() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
Paragraph::new("Hello, world!").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["Hello, world! "]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_multi_line_paragraph() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Paragraph::new("This is a\nmultiline\nparagraph.").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![
"This is a ",
"multiline ",
"paragraph. ",
]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_block() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Paragraph::new("Hello, world!")
.block(Block::default().title("Title").borders(Borders::ALL))
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![
"┌Title────────┐",
"│Hello, world!│",
"└─────────────┘",
]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_without_block() {
let area = Rect::new(0, 0, 15, 1);
let mut buffer = Buffer::empty(area);
Paragraph::new("Hello, world!").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["Hello, world! "]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_word_wrap() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 4));
Paragraph::new("This is a long line of text that should wrap.")
.wrap(Wrap { trim: true })
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![
"This is a long ",
"line of text ",
"that should ",
"wrap. ",
]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_line_truncation() {
let paragraph = Paragraph::new("This is a long line of text that should be truncated.");
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
paragraph.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["This is a "]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_left_alignment() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
Paragraph::new("Hello, world!")
.alignment(Alignment::Left)
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["Hello, world! "]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_center_alignment() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
Paragraph::new("Hello, world!")
.alignment(Alignment::Center)
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![" Hello, world! "]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_right_alignment() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
Paragraph::new("Hello, world!")
.alignment(Alignment::Right)
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![" Hello, world!"]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_scroll_offset() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Paragraph::new("This is a\nmultiline\nparagraph.")
.scroll((1, 0))
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![
"multiline ",
"paragraph. ",
" ",
]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_zero_width_area() {
let area = Rect::new(0, 0, 0, 3);
let mut buffer = Buffer::empty(area);
Paragraph::new("Hello, world!").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::empty(area);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_zero_height_area() {
let area = Rect::new(0, 0, 10, 0);
let mut buffer = Buffer::empty(area);
Paragraph::new("Hello, world!").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::empty(area);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_styled_text() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 13, 1));
Paragraph::new(Spans::from(vec![
Span::styled("Hello, ", Style::default().fg(Color::Red)),
Span::styled("world!", Style::default().fg(Color::Blue)),
]))
.render(buffer.area, &mut buffer);
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));
expected_buffer.set_style(Rect::new(7, 0, 6, 1), Style::default().fg(Color::Blue));
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_special_characters() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
Paragraph::new("Hello, <world>!").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["Hello, <world>!"]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_unicode_characters() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 1));
Paragraph::new("こんにちは, 世界! 😃").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["こんにちは, 世界! 😃"]);
assert_eq!(buffer, expected_buffer);
}
}

View File

@@ -127,7 +127,7 @@ pub struct LineTruncator<'a, 'b> {
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16,
current_line: Vec<StyledGrapheme<'a>>,
/// Record the offset to skip render
/// Record the offet to skip render
horizontal_offset: u16,
}
@@ -438,7 +438,7 @@ mod test {
assert_eq!(line_truncator, vec![" "]);
}
/// Tests an input starting with a letter, followed by spaces - some of the behaviour is
/// Tests an input starting with a letter, folowed by spaces - some of the behaviour is
/// incidental.
#[test]
fn line_composer_char_plus_lots_of_spaces() {

View File

@@ -12,8 +12,8 @@ use std::cmp::min;
/// # Examples
///
/// ```
/// # use ratatui::widgets::{Block, Borders, Sparkline};
/// # use ratatui::style::{Style, Color};
/// # use tui::widgets::{Block, Borders, Sparkline};
/// # use tui::style::{Style, Color};
/// Sparkline::default()
/// .block(Block::default().title("Sparkline").borders(Borders::ALL))
/// .data(&[0, 2, 3, 4, 1, 4, 10])
@@ -33,14 +33,6 @@ 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> {
@@ -51,7 +43,6 @@ impl<'a> Default for Sparkline<'a> {
data: &[],
max: None,
bar_set: symbols::bar::NINE_LEVELS,
direction: RenderDirection::LeftToRight,
}
}
}
@@ -81,11 +72,6 @@ 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> {
@@ -133,11 +119,7 @@ impl<'a> Widget for Sparkline<'a> {
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
};
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)
buf.get_mut(spark_area.left() + i as u16, spark_area.top() + j)
.set_symbol(symbol)
.set_style(self.style);
@@ -153,57 +135,21 @@ impl<'a> Widget for Sparkline<'a> {
#[cfg(test)]
mod tests {
use crate::buffer::Cell;
use super::*;
// 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 buffer = render(widget, 6);
assert_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
let area = Rect::new(0, 0, 3, 1);
let mut buffer = Buffer::empty(area);
widget.render(area, &mut buffer);
}
#[test]
fn it_does_not_panic_if_max_is_set_to_zero() {
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
let buffer = render(widget, 6);
assert_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_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_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_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "]));
let area = Rect::new(0, 0, 3, 1);
let mut buffer = Buffer::empty(area);
widget.render(area, &mut buffer);
}
}

View File

@@ -11,9 +11,9 @@ use unicode_width::UnicodeWidthStr;
///
/// It can be created from anything that can be converted to a [`Text`].
/// ```rust
/// # use ratatui::widgets::Cell;
/// # use ratatui::style::{Style, Modifier};
/// # use ratatui::text::{Span, Spans, Text};
/// # use tui::widgets::Cell;
/// # use tui::style::{Style, Modifier};
/// # use tui::text::{Span, Spans, Text};
/// # use std::borrow::Cow;
/// Cell::from("simple string");
///
@@ -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, Eq, Default)]
#[derive(Debug, Clone, PartialEq, 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 ratatui::widgets::Row;
/// # use tui::widgets::Row;
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
/// ```
///
/// But if you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
/// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s:
/// ```rust
/// # use ratatui::widgets::{Row, Cell};
/// # use ratatui::style::{Style, Color};
/// # use tui::widgets::{Row, Cell};
/// # use tui::style::{Style, Color};
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
@@ -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 ratatui::widgets::Row;
/// # use tui::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, Eq, Default)]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Row<'a> {
cells: Vec<Cell<'a>>,
height: u16,
@@ -116,7 +116,7 @@ impl<'a> Row<'a> {
self
}
/// Set the [`Style`] of the entire row. This [`Style`] can be overridden by the [`Style`] of a
/// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a
/// any individual [`Cell`] or event by their [`Text`] content.
pub fn style(mut self, style: Style) -> Self {
self.style = style;
@@ -139,10 +139,10 @@ impl<'a> Row<'a> {
///
/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
/// ```rust
/// # use ratatui::widgets::{Block, Borders, Table, Row, Cell};
/// # use ratatui::layout::Constraint;
/// # use ratatui::style::{Style, Color, Modifier};
/// # use ratatui::text::{Text, Spans, Span};
/// # use tui::widgets::{Block, Borders, Table, Row, Cell};
/// # use tui::layout::Constraint;
/// # use tui::style::{Style, Color, Modifier};
/// # use tui::text::{Text, Spans, Span};
/// Table::new(vec![
/// // Row can be created from simple strings.
/// Row::new(vec!["Row11", "Row12", "Row13"]),
@@ -186,7 +186,7 @@ impl<'a> Row<'a> {
/// // ...and potentially show a symbol in front of the selection.
/// .highlight_symbol(">>");
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Table<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
@@ -280,7 +280,7 @@ impl<'a> Table<'a> {
if !self.widths.is_empty() {
constraints.pop();
}
let chunks = Layout::default()
let mut chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.expand_to_fill(false)
@@ -290,9 +290,8 @@ impl<'a> Table<'a> {
width: max_width,
height: 1,
});
let mut chunks = &chunks[..];
if has_selection {
chunks = &chunks[1..];
chunks.remove(0);
}
chunks.iter().step_by(2).map(|c| c.width).collect()
}
@@ -340,7 +339,6 @@ impl<'a> Table<'a> {
pub struct TableState {
offset: usize,
selected: Option<usize>,
pub page_size: Option<usize>,
}
impl TableState {
@@ -354,13 +352,6 @@ impl TableState {
self.offset = 0;
}
}
/// Returns a copy of the receiver's scroll offset.
///
/// This is useful, for example, if you need to "synchronize" the scrolling of a `Table` and a `Paragraph`.
pub fn offset(&self) -> usize {
self.offset
}
}
impl<'a> StatefulWidget for Table<'a> {
@@ -426,7 +417,6 @@ impl<'a> StatefulWidget for Table<'a> {
}
let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
state.offset = start;
state.page_size = Some(end - start);
for (i, table_row) in self
.rows
.iter_mut()

View File

@@ -12,10 +12,10 @@ use crate::{
/// # Examples
///
/// ```
/// # use ratatui::widgets::{Block, Borders, Tabs};
/// # use ratatui::style::{Style, Color};
/// # use ratatui::text::{Spans};
/// # use ratatui::symbols::{DOT};
/// # 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();
/// Tabs::new(titles)
/// .block(Block::default().title("Tabs").borders(Borders::ALL))

View File

@@ -0,0 +1,315 @@
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use crate::widgets::{InteractiveWidgetState, InteractionOutcome, TextInputState};
impl InteractiveWidgetState for TextInputState {
fn handle_event(&mut self, event: Event) -> InteractionOutcome {
if !self.is_focused() {
return InteractionOutcome::Bubble;
}
match event {
Event::Key(key) => self.handle_key(key),
_ => InteractionOutcome::Bubble,
}
}
fn is_focused(&self) -> bool {
self.is_focused()
}
fn focus(&mut self) {
self.focus()
}
fn unfocus(&mut self) {
self.unfocus()
}
}
impl TextInputState {
// used in tests
#[allow(dead_code)]
fn up_to_cursor(&self) -> &str {
&self.value[0..self.cursor_pos as usize]
}
fn handle_key(&mut self, key: KeyEvent) -> InteractionOutcome {
if key.modifiers == KeyModifiers::ALT || key.modifiers == KeyModifiers::CONTROL {
self.handle_modifiers(key.modifiers, key.code)
} else {
self.handle_plain(key.code)
}
}
fn word_boundary_idx_under_cursor(&self, scan_backwards: bool) -> usize {
let value_as_chars = self.get_value().chars().collect::<Vec<_>>();
let mut char_pairs: Vec<(usize, &[char])> = value_as_chars
.windows(2) // work in doubles
.enumerate() // idx of the first char
.collect();
if scan_backwards {
char_pairs = char_pairs
.into_iter()
.take(self.cursor_pos.saturating_sub(1))
.rev()
.collect();
} else {
char_pairs = char_pairs.into_iter().skip(self.cursor_pos).collect()
}
if let Some((idx, _chars)) = char_pairs.iter().find(|(_, chars)| {
// find a boundary where we go from non-whitespace to whitespace
match (chars[0].is_whitespace(), chars[1].is_whitespace()) {
(true, true) => false,
(true, false) => scan_backwards,
(false, true) => !scan_backwards,
(false, false) => false,
}
}) {
// println!("bounry at {}: '{}{}'", idx, _chars[0], _chars[1]);
if scan_backwards {
idx + 1
} else {
idx + 2
}
} else {
// no whitespace boundary found, remove to start of string
if scan_backwards {
0
} else {
self.value.len()
}
}
}
fn handle_modifiers(&mut self, modifiers: KeyModifiers, code: KeyCode) -> InteractionOutcome {
match (modifiers, code) {
// delete to current word start
(KeyModifiers::CONTROL, KeyCode::Char('w')) => {
// find the first boundary going from non-whitespace to whitespace,
// going backwards from the cursor position
// println!("up to cursor ({}): '{}'", self.cursor_pos, self.up_to_cursor());
let remove_to = self.cursor_pos as usize;
let remove_from = self.word_boundary_idx_under_cursor(true);
// println!("removing span '{}'", &self.value.as_str()[remove_from..remove_to]);
// and collect everything that isn't between [remove_from..remove_to)
self.cursor_pos = remove_from;
self.value = self
.value
.chars()
.take(remove_from)
.chain(self.value.chars().skip(remove_to))
.collect();
}
// jump to end of line
(KeyModifiers::CONTROL, KeyCode::Char('e')) => {
self.cursor_pos = self.value.len();
}
// jump to start of line
(KeyModifiers::CONTROL, KeyCode::Char('a')) => {
self.cursor_pos = 0;
}
// jump back a word
(KeyModifiers::ALT, KeyCode::Char('b')) => {
self.cursor_pos = self.word_boundary_idx_under_cursor(true);
}
// jump forward a word
(KeyModifiers::ALT, KeyCode::Char('f')) => {
self.cursor_pos = self.word_boundary_idx_under_cursor(false);
}
_ => return InteractionOutcome::Bubble,
}
InteractionOutcome::Consumed
}
fn handle_plain(&mut self, code: KeyCode) -> InteractionOutcome {
match code {
KeyCode::Backspace => {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
self.value.remove(self.cursor_pos as usize);
}
}
KeyCode::Char(c) => {
self.value.insert(self.cursor_pos as usize, c);
self.cursor_pos += 1;
}
KeyCode::Left => {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
}
}
KeyCode::Right => {
if self.cursor_pos < self.value.len() {
self.cursor_pos += 1;
}
}
_ => return InteractionOutcome::Bubble,
};
InteractionOutcome::Consumed
}
}
#[cfg(test)]
mod test {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use crate::widgets::{InteractiveWidgetState, InteractionOutcome, TextInputState};
macro_rules! assert_consumed {
($expr:expr) => {
assert_eq!(InteractionOutcome::Consumed, $expr)
};
}
#[test]
fn test_basics() {
let mut state = TextInputState::default();
// don't change when not focused
assert_eq!(InteractionOutcome::Bubble, state.handle_event(plain('a')));
assert_eq!("", state.get_value());
assert_eq!(0, state.cursor_pos);
state.focus();
assert_consumed!(state.handle_event(code(KeyCode::Left)));
assert_eq!(0, state.cursor_pos);
assert_consumed!(state.handle_event(code(KeyCode::Right)));
assert_eq!(0, state.cursor_pos);
assert_consumed!(state.handle_event(plain('a')));
assert_eq!("a", state.get_value());
assert_eq!(1, state.cursor_pos);
// build up a multi-char value
state.handle_event(plain('s'));
state.handle_event(plain('d'));
state.handle_event(plain('f'));
assert_eq!("asdf", state.get_value());
assert_eq!(4, state.cursor_pos);
// remove from end
state.handle_event(bksp());
assert_eq!("asd", state.get_value());
assert_eq!(3, state.cursor_pos);
// move cursor to middle
assert_eq!("asd", state.up_to_cursor());
state.handle_event(code(KeyCode::Left));
assert_eq!("as", state.up_to_cursor());
assert_eq!(2, state.cursor_pos);
assert_eq!("asd", state.get_value());
// remove from middle
state.handle_event(bksp());
assert_eq!(1, state.cursor_pos);
assert_eq!("ad", state.get_value());
}
#[test]
fn test_ctrl_w_works() {
let mut state = TextInputState::default();
state.focus();
// ctrl+w word removal, from the end of a word
state.set_value("foo bar baz smaz");
state.set_cursor(18);
assert_consumed!(state.handle_event(ctrl('w')));
assert_eq!("foo bar baz ", state.get_value());
assert_eq!(14, state.cursor_pos);
// remove runs of trailing whitespace + word
state.handle_event(ctrl('w'));
assert_eq!("foo bar ", state.get_value());
assert_eq!(8, state.cursor_pos);
// remove from middle of word
state.handle_event(code(KeyCode::Left));
state.handle_event(code(KeyCode::Left));
assert_eq!("foo ba", state.up_to_cursor());
state.handle_event(ctrl('w'));
assert_eq!("foo r ", state.get_value());
assert_eq!(4, state.cursor_pos);
// remove at start of word
state.handle_event(ctrl('w'));
assert_eq!("r ", state.get_value());
assert_eq!(0, state.cursor_pos);
// remove when buffer is empty
state.set_value("");
assert_eq!(0, state.cursor_pos);
assert_consumed!(state.handle_event(ctrl('w')));
}
#[test]
fn test_cursor_movement() {
let mut state = TextInputState::default();
state.focus();
state.set_value("foo bar baz");
state.set_cursor(0);
assert_consumed!(state.handle_event(ctrl('e')));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(11, state.cursor_pos);
assert_consumed!(state.handle_event(ctrl('a')));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(0, state.cursor_pos);
assert_consumed!(state.handle_event(alt('f')));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(4, state.cursor_pos);
state.handle_event(alt('f'));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(8, state.cursor_pos);
state.handle_event(alt('f'));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(11, state.cursor_pos);
assert_consumed!(state.handle_event(alt('b')));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(8, state.cursor_pos);
state.handle_event(alt('b'));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(4, state.cursor_pos);
}
// helper macros + functions
fn ctrl(c: char) -> Event {
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::CONTROL,
})
}
fn alt(c: char) -> Event {
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::ALT,
})
}
fn plain(c: char) -> Event {
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE,
})
}
fn code(code: KeyCode) -> Event {
Event::Key(KeyEvent {
code,
modifiers: KeyModifiers::NONE,
})
}
fn bksp() -> Event {
code(KeyCode::Backspace)
}
}

View File

@@ -0,0 +1,204 @@
use std::borrow::Cow;
use crate::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Span, Text},
widgets::Block,
};
use super::{InteractiveWidget, Paragraph};
#[cfg(feature = "crossterm")]
mod crossterm_interactive;
#[derive(Debug, Clone)]
pub struct TextInput<'a> {
// Block to draw the text input inside (convenience function) - default: None
optional_block: Option<Block<'a>>,
// Placeholder text - what's shown if the state value is "" - default: None
placeholder: Option<Text<'a>>,
// Render as a read-only input - that is, it will not be focused - default: false
is_read_only: bool,
// Style to render the widget when focused - default: Bold style
focused_style: Style,
// Style to apply to displayed text - overriden by focused_style when focused
text_style: Style,
}
impl<'a> TextInput<'a> {
pub fn new() -> TextInput<'a> {
Default::default()
}
pub fn block(mut self, block: Block<'a>) -> TextInput<'a> {
self.optional_block = Some(block);
self
}
pub fn read_only(mut self, read_only: bool) -> TextInput<'a> {
self.is_read_only = read_only;
self
}
pub fn placeholder_text<T>(mut self, placeholder_text: T) -> TextInput<'a>
where
T: Into<Cow<'a, str>>,
{
self.placeholder = Some(
Span::styled(
placeholder_text,
Style::default()
.fg(Color::Black)
.add_modifier(Modifier::ITALIC),
)
.into(),
);
self
}
pub fn placeholder(mut self, placeholder: Text<'a>) -> TextInput<'a> {
self.placeholder = Some(placeholder);
self
}
pub fn focused_style(mut self, style: Style) -> TextInput<'a> {
self.focused_style = style;
self
}
pub fn text_style(mut self, style: Style) -> TextInput<'a> {
self.text_style = style;
self
}
}
impl<'a> Default for TextInput<'a> {
fn default() -> Self {
Self {
optional_block: Default::default(),
placeholder: Default::default(),
is_read_only: false,
focused_style: Style::default().add_modifier(Modifier::BOLD),
text_style: Default::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct TextInputState {
// Underlying value of the text input field
value: String,
// Position in the text input to insert / remove text from
cursor_pos: usize,
// Is the input focused?
is_focused: bool,
// Can the input take focus?
can_take_focus: bool,
}
impl TextInputState {
pub fn with_value(value: &str) -> TextInputState {
TextInputState {
value: value.to_string(),
cursor_pos: value.len(),
..Default::default()
}
}
pub fn can_take_focus(&mut self, can_take_focus: bool) {
self.can_take_focus = can_take_focus;
if !can_take_focus {
self.unfocus();
}
}
pub fn is_focused(&self) -> bool {
self.can_take_focus && self.is_focused
}
pub fn focus(&mut self) {
if self.can_take_focus {
self.is_focused = true;
}
}
pub fn unfocus(&mut self) {
self.is_focused = false;
}
pub fn set_value(&mut self, val: &str) {
self.value = val.to_string();
self.cursor_pos = std::cmp::min(self.cursor_pos, self.value.len());
}
pub fn set_cursor(&mut self, pos: usize) {
self.cursor_pos = pos;
}
pub fn get_value(&self) -> &String {
&self.value
}
}
impl Default for TextInputState {
fn default() -> Self {
Self {
value: Default::default(),
is_focused: false,
cursor_pos: 0,
can_take_focus: true,
}
}
}
impl<'a> InteractiveWidget for TextInput<'a> {
type State = TextInputState;
fn render<'b, B: crate::backend::Backend + 'b>(
mut self,
area: Rect,
frame: &mut crate::Frame<'b, B>,
state: &Self::State,
) {
let is_focused = !self.is_read_only && state.is_focused;
let area = if let Some(block) = self.optional_block.take() {
let block = if is_focused {
block.style(self.focused_style)
} else {
block
};
let inner = block.inner(area);
frame.render_widget(block, area);
inner
} else {
area
};
let contents = if state.get_value().is_empty() {
match self.placeholder {
Some(placeholder) => placeholder,
None => "".into(),
}
} else {
let value = state.get_value();
if is_focused {
Span::styled(value, self.focused_style).into()
} else {
Span::styled(value, self.text_style).into()
}
};
let paragraph = Paragraph::new(contents);
frame.render_widget(paragraph, area);
if is_focused {
frame.set_cursor(area.x + (state.cursor_pos as u16), area.y);
}
}
fn render_mut<'b, B: crate::backend::Backend + 'b>(
self,
area: Rect,
frame: &mut crate::Frame<'b, B>,
state: &mut Self::State,
) {
self.render(area, frame, state);
}
}

View File

@@ -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 ratatui::{
use tui::{
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| {

View File

@@ -1,10 +1,10 @@
use ratatui::{
use std::error::Error;
use tui::{
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 paragraph = Paragraph::new("Test");
f.render_widget(paragraph, f.size());
let paragrah = Paragraph::new("Test");
f.render_widget(paragrah, f.size());
})?;
assert_eq!(frame.buffer.get(0, 0).symbol, "T");
assert_eq!(frame.area, Rect::new(0, 0, 10, 10));
terminal.backend_mut().resize(8, 8);
let frame = terminal.draw(|f| {
let paragraph = Paragraph::new("test");
f.render_widget(paragraph, f.size());
let paragrah = Paragraph::new("test");
f.render_widget(paragrah, f.size());
})?;
assert_eq!(frame.buffer.get(0, 0).symbol, "t");
assert_eq!(frame.area, Rect::new(0, 0, 8, 8));

View File

@@ -1,7 +1,7 @@
use ratatui::backend::TestBackend;
use ratatui::buffer::Buffer;
use ratatui::widgets::{BarChart, Block, Borders};
use ratatui::Terminal;
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::widgets::{BarChart, Block, Borders};
use tui::Terminal;
#[test]
fn widgets_barchart_not_full_below_max_value() {

View File

@@ -1,4 +1,4 @@
use ratatui::{
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::{Alignment, Rect},
@@ -344,137 +344,3 @@ 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 "]),
);
}

View File

@@ -1,114 +0,0 @@
#![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));
}

View File

@@ -1,4 +1,4 @@
use ratatui::{
use tui::{
backend::TestBackend,
buffer::Buffer,
style::{Color, Style},

View File

@@ -1,5 +1,5 @@
use ratatui::layout::Alignment;
use ratatui::{
use tui::layout::Alignment;
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
@@ -469,7 +469,7 @@ fn widgets_chart_can_have_a_legend() {
"└──────────────────────────────────────────────────────────┘",
]);
// Set expected background color
// Set expected backgound color
for row in 0..30 {
for col in 0..60 {
expected.get_mut(col, row).set_bg(Color::White);

View File

@@ -1,4 +1,4 @@
use ratatui::{
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},

View File

@@ -1,4 +1,4 @@
use ratatui::{
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
@@ -198,30 +198,3 @@ 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);
}

View File

@@ -1,9 +1,9 @@
use ratatui::{
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::Alignment,
text::{Span, Spans, Text},
widgets::{Block, Borders, Padding, Paragraph, Wrap},
widgets::{Block, Borders, Paragraph, Wrap},
Terminal,
};
@@ -15,7 +15,7 @@ const SAMPLE_STRING: &str = "The library is based on the principle of immediate
#[test]
fn widgets_paragraph_can_wrap_its_content() {
let test_case = |alignment, expected| {
let backend = TestBackend::new(22, 12);
let backend = TestBackend::new(20, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
@@ -23,12 +23,7 @@ fn widgets_paragraph_can_wrap_its_content() {
let size = f.size();
let text = vec![Spans::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL).padding(Padding {
left: 2,
right: 2,
top: 1,
bottom: 1,
}))
.block(Block::default().borders(Borders::ALL))
.alignment(alignment)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, size);
@@ -40,52 +35,46 @@ fn widgets_paragraph_can_wrap_its_content() {
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 │",
"│ │",
"└────────────────────┘",
"┌──────────────────┐",
"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 │",
"│ │",
"└────────────────────┘",
"┌──────────────────┐",
"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 │",
"│ │",
"└────────────────────┘",
"┌──────────────────┐",
"The library is",
"based on the",
"principle of",
" immediate",
"rendering with",
"intermediate",
"buffers. This",
"means that at each",
"└──────────────────┘",
]),
);
}

View File

@@ -1,4 +1,4 @@
use ratatui::{
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::Constraint,

View File

@@ -1,4 +1,4 @@
use ratatui::{
use tui::{
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Spans, widgets::Tabs,
Terminal,
};