Compare commits

...

42 Commits

Author SHA1 Message Date
Orhun Parmaksız
c70a84b3fe chore(release): configure git-cliff 2023-07-31 16:37:23 +03:00
Orhun Parmaksız
8ed244eeb9 feat(release): add automation for creating nightly releases 2023-07-31 16:30:39 +03:00
tieway59
440f62ff54 Chore: implement Clone & Copy common traits (#350)
Implement `Clone & Copy` common traits for most structs in src.

Only implement `Copy` for structs that are simple and trivial to copy.

Reorder the derive fields to be more consistent:

    Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash

see: https://github.com/ratatui-org/ratatui/issues/307
2023-07-28 11:04:29 +00:00
Josh McKinney
6f659cfb07 ci: add coverage token (#352) 2023-07-28 06:57:53 +00:00
tieway59
bf4944683d Chore: implement Debug & Default common traits (#339)
Implement `Debug & Default` common traits for most structs in src.

Reorder the derive fields to be more consistent:

    Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash

see: https://github.com/ratatui-org/ratatui/issues/307
2023-07-27 01:40:07 +00:00
Josh McKinney
7539f775fe fix(scrollbar)!: move symbols to symbols module (#330)
The symbols and sets are moved from `widgets::scrollbar` to
`symbols::scrollbar`. This makes it consistent with the other symbol
sets and allows us to make the scrollbar module private rather than
re-exporting it.

BREAKING CHANGE: The symbols are now in the `symbols` module. To update
your code, add an import for `ratatui::symbols::scrollbar::*` (or the
specific symbols you need). The scrollbar module is no longer public. To
update your code, remove any `widgets::scrollbar` imports and replace it
with `ratatui::widgets::Scrollbar`.
2023-07-26 11:33:39 +00:00
nathan
8db9fb4aeb fix(cargo): adjust minimum paste version (#348)
ratatui is using features that are currently only available in paste 1.0.2; specifying the minimum version to be 1.0 will consequently cause a compilation error if cargo is only able to use a version less than 1.0.2.
2023-07-26 07:05:12 +00:00
Florian Meißner
d05ab6fb70 fix(readme): fix typo in readme (#344)
Co-authored-by: josh rotenberg <joshrotenberg@users.noreply.github.com>
2023-07-25 21:47:28 +00:00
Josh McKinney
2920e045ba docs(readme): fix widget docs links (#346)
Add scrollbar, clear. Fix Block link. Sort
2023-07-25 21:44:50 +00:00
Josh McKinney
add578a7d6 docs(examples): Add examples readme with gifs (#303)
This commit adds a readme to the examples directory with gifs of each
example. This should make it easier to see what each example does
without having to run it.

I modified the examples to fit better in the gifs. Mostly this was just
removing the margins, but for the block example I cleaned up the code a
bit to make it more readable and changed it so the background bug is not
triggered.

For the table example, the combination of Min, Length, and Percent
constraints was causing the table to panic when the terminal was too
small. I changed the example to use the Max constraint instead of the
Length constraint.

The layout example now shows information about how the layout is
constrained on each block (which is now a paragraph with a block).
2023-07-24 19:05:37 +00:00
Orhun Parmaksız
60a4131384 chore(github): add kdheepak as a maintainer (#343) 2023-07-22 22:12:08 +00:00
Orhun Parmaksız
964190a859 chore(github): rename tui-rs-revival references to ratatui-org (#340) 2023-07-22 10:34:11 +00:00
josh rotenberg
b9290b35d1 fix(readme): fix incorrect template link (#338) 2023-07-20 21:36:15 +00:00
josh rotenberg
daf5890152 fix(example): Fix typo (#337)
the existential feels
2023-07-20 20:00:42 +00:00
josh rotenberg
7e37a96678 fix(readme): fix typo in readme (#336) 2023-07-20 04:39:46 +00:00
josh rotenberg
bcb7417785 update version in README.md (#335) 2023-07-20 04:29:08 +00:00
Hichem
9c956733f7 fix(barchart): empty groups causes panic (#333)
This unlikely to happen, since nobody wants to add an empty group.
Even we fix the panic, things will not render correctly.
So it is better to just not add them to the BarChart.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-07-19 09:37:59 +00:00
Markus
13fb11a62c fix: Correct minor typos in documentation (#331) 2023-07-18 10:06:59 +00:00
EdJoPaTo
0fb1ed85c6 build: forbid unsafe code (#332)
This indicates good (high level) code and is used by tools like cargo-geiger.
2023-07-18 10:03:00 +00:00
Josh McKinney
e2cb11cc30 build(examples): fix cargo make run-examples (#327)
Enables the all-widgets feature so that the calendar example runs correctly
2023-07-17 15:59:56 +00:00
a-kenji
c3f87f245a docs: improve scrollbar doc comment (#329) 2023-07-17 12:05:28 +00:00
Orhun Parmaksız
df90982632 chore(release): prepare for 0.22.0 (#326) 2023-07-17 10:41:45 +00:00
Josh McKinney
bb061fdab6 ci: parallelize CI jobs (#318)
* ci: parallelize CI jobs

- remove the dependency on the lint job from all other jobs
- implement workflow concurrency
- reorder the workflow so that the lint, clippy and coverage jobs are
  scheduled before the test jobs
- run jobs which run for each backend in parallel by calling e.g.
  cargo make test-termion, instead of cargo make test
- add a coverage task to the makefile
- change "cargo-make check" to check all features valid for OS in
  parallel
- run clippy only on the ubuntu-latest runner and check all features
  valid in parallel
- tidy up the workflow file

* ci: simplify Makefile OS detection

Use platform overrides to significantly simplify the Makefile logic
See https://github.com/sagiegurari/cargo-make\#platform-override

* fix(termwiz): skip doc test that requires stdout
2023-07-17 10:31:31 +00:00
Orhun Parmaksız
1ff85535c8 fix(title): remove default alignment and position (#323)
* fix(title): remove default alignment and position

* test(block): add test cases for alignment

* test(block): extend the unit tests for block title alignment
2023-07-17 10:27:58 +00:00
Josh McKinney
33f3212cbf fix: rust-tui-template became a revival project (#320)
Changed the URL https://github.com/orhun/rust-tui-template
into https://github.com/rust-tui-revival/rust-tui-template

Co-authored-by: Geert Stappers <stappers@stappers.it>
2023-07-17 06:31:26 +00:00
Josh McKinney
fb6d4b2f51 refactor(text): simplify reflow implementation (#290)
* refactor(text): split text::* into separate files

* feat(text): expose graphemes on Line

- add `Line::styled()`
- add `Line::styled_graphemes()`
- add `StyledGrapheme::new()`

---------

Co-authored-by: Eyesonjune18 <lowellashton@gmail.com>
2023-07-17 06:27:45 +00:00
Josh McKinney
446efae185 fix(prelude): remove widgets module from prelude (#317)
This helps to keep the prelude small and less likely to conflict with
other crates.

- remove widgets module from prelude as the entire module can be just as
  easily imported with `use ratatui::widgets::*;`
- move prelude module into its own file
- update examples to import widgets module instead of just prelude
- added several modules to prelude to make it possible to qualify
  imports that collide with other types that have similar names
2023-07-16 09:11:59 +00:00
Mano Ségransan
b347201b9f feat(style): Enable setting the underline color for crossterm (#308) (#310)
This commit adds the underline_color() function to the Style and Cell
structs. This enables setting the underline color of text on the
crossterm backend. This is a no-op for the termion and termwiz backends
as they do not support this feature.
2023-07-15 09:57:15 +00:00
Josh McKinney
9f1f59a51c feat(stylize): allow all widgets to be styled (#289)
* feat(stylize): allow all widgets to be styled

- Add styled impl to:
  - Barchart
  - Chart (including Axis and Dataset),
  - Guage and LineGuage
  - List and ListItem
  - Sparkline
  - Table, Row, and Cell
  - Tabs
  - Style
- Allow modifiers to be removed (e.g. .not_italic())
- Allow .bg() to recieve Into<Color>
- Made shorthand methods consistent with modifier names (e.g. dim() not
  dimmed() and underlined() not underline())
- Simplify integration tests
- Add doc comments
- Simplified stylize macros with https://crates.io/crates/paste

* build: run clippy before tests

Runny clippy first means that we fail fast when there is an issue that
can easily be fixed rather than having to wait 30-40s for the failure
2023-07-14 08:37:30 +00:00
Josh McKinney
6f6c355c5c chore(tests): add coverage job to bacon (#312)
- Add two jobs to bacon.toml (one for unit tests, one for all tests)
- Remove "run" job as it doesn't work well with bacon due to no stdin
- Document coverage tooling in CONTRIBUTING.md
2023-07-14 08:37:00 +00:00
Hichem
60150f6236 feat(barchart): set custom text value in the bar (#309)
for now the value is converted to a string and then printed. in many
cases the values are too wide or double values. so it make sense
to set a custom value text instead of the default behavior.

this patch suggests to add a method
"fn text_value(mut self, text_value: String)"
to the Bar, which allows to override the value printed in the bar

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-07-14 04:38:54 +00:00
Josh McKinney
2889c7d084 fix(lint): suspicious_double_ref_op is new in 1.71 (#311)
Fixed tests and completed coverage for `Masked` type.
2023-07-14 02:18:07 +00:00
Florian
57678a5fe8 feat(examples): user_input example cursor movement (#302)
The user_input example now responds to left/right and allows the
character at the cursor position to be deleted / inserted.

Co-authored-by: Leon Sautour <leon1.sautour@epitech.eu>
2023-07-13 10:41:10 +00:00
Hichem
ae8ed8867d feat(barchart): enable barchart groups (#288)
* feat(barchart): allow to add a group of bars

Example: to show the revenue of different companies:
┌────────────────────────┐
│             ████       │
│             ████       │
│      ████   ████       │
│ ▄▄▄▄ ████   ████ ████  │
│ ████ ████   ████ ████  │
│ ████ ████   ████ ████  │
│ █50█ █60█   █90█ █55█  │
│    Mars       April    │
└────────────────────────┘
new structs are introduced: Group and Bar.
the data function is modified to accept "impl Into<Group<'a>>".

a new function "group_gap" is introduced to set the gap between each group

unit test changed to allow the label to be in the center

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>

* feat(barchart)!: center labels by default

The bar labels are currently printed string from the left side of
bar. This commit centers the labels under the bar.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>

---------

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-07-13 10:27:08 +00:00
Josh McKinney
e66d5cdee0 docs(color): parse more color formats and add docs (#306) 2023-07-12 13:49:28 +00:00
Josh McKinney
804115ac6f feat(prelude): add a prelude (#304)
This allows users of the library to easily use ratatui without a huge amount of imports
2023-07-10 22:59:01 +00:00
Josh McKinney
a1813af297 test(barchart): add unit tests (#301) 2023-07-09 01:17:34 +00:00
Orhun Parmaksız
085fde7d4a chore(github): add EditorConfig config (#300) 2023-07-08 20:21:24 +00:00
Orhun Parmaksız
860a40c13a style(readme): update the style of badges in README.md (#299) 2023-07-08 20:19:56 +00:00
Josh McKinney
0833c9018b docs: improve CONTRIBUTING.md (#277) 2023-07-08 19:02:22 +00:00
a-kenji
f7c4b44962 feat(style): allow Modifiers add/remove in const (#287)
Allows Modifiers to be added or removed from `Style` in a const context.
This can be used in the following way:

```
const DEFAULT_MODIFIER: Modifier = Modifier::BOLD.union(Modifier::ITALIC);
const DEFAULT_STYLE: Style = Style::new()
.fg(Color::Red).bg(Color::Black).add_modifier(DEFAULT_MODIFIER);
```
2023-07-08 10:12:48 +00:00
Josh McKinney
56e44a0efa chore(license): add Ratatui developers to license (#297) 2023-07-06 12:28:48 +00:00
105 changed files with 4507 additions and 2001 deletions

11
.editorconfig Normal file
View File

@@ -0,0 +1,11 @@
# configuration for https://editorconfig.org
root = true
[*.rs]
indent_style = space
indent_size = 4
[*.yml]
indent_style = space
indent_size = 2

2
.github/CODEOWNERS vendored
View File

@@ -5,4 +5,4 @@
# https://git-scm.com/docs/gitignore#_pattern_format
# Maintainers
* @orhun @mindoodoo @sayanarijit @sophacles @joshka
* @orhun @mindoodoo @sayanarijit @sophacles @joshka @kdheepak

View File

@@ -1,19 +1,80 @@
name: Continuous Deployment
on:
workflow_dispatch:
schedule:
# At 00:00 on Saturday
# https://crontab.guru/#0_0_*_*_6
- cron: "0 0 * * 6"
push:
tags:
- "v*.*.*"
defaults:
run:
shell: bash
jobs:
publish:
name: Publish on crates.io
publish-nightly:
name: Create a nightly release
runs-on: ubuntu-latest
if: ${{ !startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Publish
with:
fetch-depth: 0
- name: Calculate the next release
run: |
suffix="-alpha"
last_tag="$(git describe --abbrev=0 --tags `git rev-list --tags --max-count=1`)"
if [[ "${last_tag}" = *"${suffix}"* ]]; then
# increment the alpha version
alpha=$(echo "${last_tag}" | grep -oE '([0-9]+)$')
next_alpha=$((alpha + 1))
next_tag=$(echo "${last_tag}" | sed "s/\.[0-9]\+$/\.${next_alpha}/")
else
# start the alpha version from 0
next_tag="${last_tag}${suffix}.0"
fi
# update the crate version
msg="# crate version"
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
echo "Next nightly release: ${next_tag} 🐭"
- name: Publish on crates.io
uses: actions-rs/cargo@v1
with:
command: publish
args: --token ${{ secrets.CARGO_TOKEN }}
args: --dry-run --allow-dirty
- name: Generate a changelog
uses: orhun/git-cliff-action@v2
with:
config: cliff.toml
args: --unreleased --strip header
env:
OUTPUT: BODY.md
- name: Publish on GitHub
uses: ncipollo/release-action@v1
with:
tag: ${{ env.NEXT_TAG }}
prerelease: true
bodyFile: BODY.md
publish-stable:
name: Create a stable release
runs-on: ubuntu-latest
if: ${{ startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Publish on crates.io
uses: actions-rs/cargo@v1
with:
command: publish
args: --dry-run

View File

@@ -1,78 +1,30 @@
name: CI
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
push:
branches:
- feat-wrapping
- main
pull_request:
branches:
- main
- feat-wrapping
merge_group:
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: CI
env:
# don't install husky hooks during CI as they are only needed for for pre-push
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
# lint, clippy and coveraget jobs are intentionally early in the workflow to catch simple
# formatting, typos, and missing tests as early as possible. This allows us to fix these and
# resubmit the PR without having to wait for the comprehensive matrix of tests to complete.
jobs:
build:
strategy:
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.65.0", "stable" ]
runs-on: ${{ matrix.os }}
needs: lint
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: "Check"
run: cargo make check
env:
RUST_BACKTRACE: full
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
clippy:
name: Clippy
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: "Clippy"
run: cargo make clippy
env:
RUST_BACKTRACE: full
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
test:
strategy:
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.65.0", "stable" ]
runs-on: ${{ matrix.os }}
needs: lint
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: "Test"
run: cargo make test
env:
RUST_BACKTRACE: full
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
lint:
runs-on: ubuntu-latest
steps:
@@ -84,38 +36,117 @@ jobs:
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: "Check conventional commits"
- name: Check conventional commits
uses: crate-ci/committed@master
with:
args: "-vv"
commits: "HEAD"
- name: "Check typos"
commits: HEAD
- name: Check typos
uses: crate-ci/typos@master
- name: "Lint dependencies"
- name: Lint dependencies
uses: EmbarkStudios/cargo-deny-action@v1
- name: Install Rust
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: "Formatting"
run: cargo fmt --all --check
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Check formatting
run: cargo make fmt
clippy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Run cargo make clippy-all
run: cargo make clippy
coverage:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v3
- name: Install Rust
- name: Checkout
uses: actions/checkout@v3
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools
- name: cargo install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: cargo llvm-cov
run: cargo llvm-cov --all-features --lcov --output-path lcov.info
env:
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
- name: Install cargo-llvm-cov and cargo-make
uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov,cargo-make
- name: Generate coverage
run: cargo make coverage
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
check:
strategy:
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.65.0", "stable" ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Rust {{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Run cargo make check
run: cargo make check
env:
RUST_BACKTRACE: full
test-doc:
strategy:
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Test docs
run: cargo make test-doc
env:
RUST_BACKTRACE: full
test:
strategy:
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.65.0", "stable" ]
backend: [ crossterm, termion, termwiz ]
exclude:
# termion is not supported on windows
- os: windows-latest
backend: termion
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Rust ${{ matrix.toolchain }}}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Test ${{ matrix.backend }}
run: cargo make test-backend ${{ matrix.backend }}
env:
RUST_BACKTRACE: full

View File

@@ -1,74 +1,184 @@
# Changelog
## v0.22.0 - 2023-07-17
### Features
- *(barchart)* Set custom text value in the bar ([#309](https://github.com/ratatui-org/ratatui/issues/309))
- *(barchart)* Enable barchart groups ([#288](https://github.com/ratatui-org/ratatui/issues/288))
- *(block)* Support for having more than one title ([#232](https://github.com/ratatui-org/ratatui/issues/232))
- *(examples)* User_input example cursor movement ([#302](https://github.com/ratatui-org/ratatui/issues/302))
- *(misc)* Make builder fn const ([#275](https://github.com/ratatui-org/ratatui/issues/275)) ([#275](https://github.com/ratatui-org/ratatui/issues/275))
- *(prelude)* Add a prelude ([#304](https://github.com/ratatui-org/ratatui/issues/304))
- *(style)* Enable setting the underline color for crossterm ([#308](https://github.com/ratatui-org/ratatui/issues/308)) ([#310](https://github.com/ratatui-org/ratatui/issues/310))
- *(style)* Allow Modifiers add/remove in const ([#287](https://github.com/ratatui-org/ratatui/issues/287))
- *(stylize)* Allow all widgets to be styled ([#289](https://github.com/ratatui-org/ratatui/issues/289))
- *(terminal)* Expose 'swap_buffers' method
- *(uncategorized)* Stylization shorthands ([#283](https://github.com/ratatui-org/ratatui/issues/283))
- *(uncategorized)* Add scrollbar widget ([#228](https://github.com/ratatui-org/ratatui/issues/228))
### Bug Fixes
- *(clippy)* Unused_mut lint for layout ([#285](https://github.com/ratatui-org/ratatui/issues/285))
- *(examples)* Correct progress label in gague example ([#263](https://github.com/ratatui-org/ratatui/issues/263))
- *(layout)* Cap Constraint::apply to 100% length ([#264](https://github.com/ratatui-org/ratatui/issues/264))
- *(lint)* Suspicious_double_ref_op is new in 1.71 ([#311](https://github.com/ratatui-org/ratatui/issues/311))
- *(prelude)* Remove widgets module from prelude ([#317](https://github.com/ratatui-org/ratatui/issues/317))
- *(title)* Remove default alignment and position ([#323](https://github.com/ratatui-org/ratatui/issues/323))
- *(typos)* Configure typos linter ([#233](https://github.com/ratatui-org/ratatui/issues/233))
- *(uncategorized)* Rust-tui-template became a revival project ([#320](https://github.com/ratatui-org/ratatui/issues/320))
- *(uncategorized)* Revert removal of WTFPL from deny.toml ([#266](https://github.com/ratatui-org/ratatui/issues/266))
### Refactor
- *(ci)* Simplify cargo-make installation ([#240](https://github.com/ratatui-org/ratatui/issues/240))
- *(text)* Simplify reflow implementation ([#290](https://github.com/ratatui-org/ratatui/issues/290))
### Documentation
- *(color)* Parse more color formats and add docs ([#306](https://github.com/ratatui-org/ratatui/issues/306))
- *(lib)* Add `tui-term` a pseudoterminal library ([#268](https://github.com/ratatui-org/ratatui/issues/268))
- *(lib)* Fixup tui refs in widgets/mod.rs ([#216](https://github.com/ratatui-org/ratatui/issues/216))
- *(lib)* Add backend docs ([#213](https://github.com/ratatui-org/ratatui/issues/213))
- *(readme)* Remove duplicated mention of tui-rs-tree-widgets ([#223](https://github.com/ratatui-org/ratatui/issues/223))
- *(uncategorized)* Improve CONTRIBUTING.md ([#277](https://github.com/ratatui-org/ratatui/issues/277))
- *(uncategorized)* Fix scrollbar ascii illustrations and calendar doc paths ([#272](https://github.com/ratatui-org/ratatui/issues/272))
- *(uncategorized)* README tweaks ([#225](https://github.com/ratatui-org/ratatui/issues/225))
- *(uncategorized)* Add CODEOWNERS file ([#212](https://github.com/ratatui-org/ratatui/issues/212))
- *(uncategorized)* Update README.md and add hello_world example ([#204](https://github.com/ratatui-org/ratatui/issues/204))
### Styling
- *(comments)* Set comment length to wrap at 100 chars ([#218](https://github.com/ratatui-org/ratatui/issues/218))
- *(config)* Apply formatting to config files ([#238](https://github.com/ratatui-org/ratatui/issues/238))
- *(manifest)* Apply formatting to Cargo.toml ([#237](https://github.com/ratatui-org/ratatui/issues/237))
- *(readme)* Update the style of badges in README.md ([#299](https://github.com/ratatui-org/ratatui/issues/299))
- *(widget)* Inline format arguments ([#279](https://github.com/ratatui-org/ratatui/issues/279))
- *(uncategorized)* Fix formatting ([#292](https://github.com/ratatui-org/ratatui/issues/292))
- *(uncategorized)* Reformat imports ([#219](https://github.com/ratatui-org/ratatui/issues/219))
### Testing
- *(barchart)* Add unit tests ([#301](https://github.com/ratatui-org/ratatui/issues/301))
- *(paragraph)* Simplify paragraph benchmarks ([#282](https://github.com/ratatui-org/ratatui/issues/282))
- *(uncategorized)* Add benchmarks for paragraph ([#262](https://github.com/ratatui-org/ratatui/issues/262))
### Miscellaneous Tasks
- *(ci)* Bump cargo-make version ([#239](https://github.com/ratatui-org/ratatui/issues/239))
- *(ci)* Enable merge queue for builds ([#235](https://github.com/ratatui-org/ratatui/issues/235))
- *(ci)* Integrate cargo-deny for linting dependencies ([#221](https://github.com/ratatui-org/ratatui/issues/221))
- *(commitizen)* Add commitizen config ([#222](https://github.com/ratatui-org/ratatui/issues/222))
- *(demo)* Update demo gif ([#234](https://github.com/ratatui-org/ratatui/issues/234))
- *(demo)* Update demo gif with a fixed unicode gauge ([#227](https://github.com/ratatui-org/ratatui/issues/227))
- *(features)* Enable building with all-features ([#286](https://github.com/ratatui-org/ratatui/issues/286))
- *(github)* Add EditorConfig config ([#300](https://github.com/ratatui-org/ratatui/issues/300))
- *(github)* Simplify the CODEOWNERS file ([#271](https://github.com/ratatui-org/ratatui/issues/271))
- *(github)* Add pull request template ([#269](https://github.com/ratatui-org/ratatui/issues/269))
- *(github)* Fix the syntax in CODEOWNERS file ([#236](https://github.com/ratatui-org/ratatui/issues/236))
- *(license)* Add Ratatui developers to license ([#297](https://github.com/ratatui-org/ratatui/issues/297))
- *(tests)* Add coverage job to bacon ([#312](https://github.com/ratatui-org/ratatui/issues/312))
- *(uncategorized)* Lint and doc cleanup ([#191](https://github.com/ratatui-org/ratatui/issues/191))
### Build
- *(deps)* Upgrade bitflags to 2.3 ([#205](https://github.com/ratatui-org/ratatui/issues/205)) [**breaking**]
- *(uncategorized)* Add git pre-push hooks using cargo-husky ([#274](https://github.com/ratatui-org/ratatui/issues/274))
### Continuous Integration
- *(makefile)* Split CI jobs ([#278](https://github.com/ratatui-org/ratatui/issues/278))
- *(uncategorized)* Parallelize CI jobs ([#318](https://github.com/ratatui-org/ratatui/issues/318))
- *(uncategorized)* Add feat-wrapping on push and on pull request ci triggers ([#267](https://github.com/ratatui-org/ratatui/issues/267))
- *(uncategorized)* Add code coverage action ([#209](https://github.com/ratatui-org/ratatui/issues/209))
### Contributors
Thank you so much to everyone that contributed to this release!
Here is the list of contributors who have contributed to `ratatui` for the first time!
- [@Nydragon](https://github.com/Nydragon)
- [@snpefk](https://github.com/snpefk)
- [@Philipp-M](https://github.com/Philipp-M)
- [@mrbcmorris](https://github.com/mrbcmorris)
- [@endepointe](https://github.com/endepointe)
- [@kdheepak](https://github.com/kdheepak)
- [@samyosm](https://github.com/samyosm)
- [@SLASHLogin](https://github.com/SLASHLogin)
- [@karthago1](https://github.com/karthago1)
- [@BoolPurist](https://github.com/BoolPurist)
- [@Nogesma](https://github.com/Nogesma)
## v0.21.0 - 2023-05-28
### Features
- *(backend)* Add termwiz backend and example ([#5](https://github.com/tui-rs-revival/ratatui/issues/5))
- *(block)* Support placing the title on bottom ([#36](https://github.com/tui-rs-revival/ratatui/issues/36))
- *(border)* Add border! macro for easy bitflag manipulation ([#11](https://github.com/tui-rs-revival/ratatui/issues/11))
- *(calendar)* Add calendar widget ([#138](https://github.com/tui-rs-revival/ratatui/issues/138))
- *(color)* Add `FromStr` implementation for `Color` ([#180](https://github.com/tui-rs-revival/ratatui/issues/180))
- *(list)* Add len() to List ([#24](https://github.com/tui-rs-revival/ratatui/pull/24))
- *(paragraph)* Allow Lines to be individually aligned ([#149](https://github.com/tui-rs-revival/ratatui/issues/149))
- *(sparkline)* Finish #1 Sparkline directions PR ([#134](https://github.com/tui-rs-revival/ratatui/issues/134))
- *(terminal)* Add inline viewport ([#114](https://github.com/tui-rs-revival/ratatui/issues/114)) [**breaking**]
- *(test)* Expose test buffer ([#160](https://github.com/tui-rs-revival/ratatui/issues/160))
- *(text)* Add `Masked` to display secure data ([#168](https://github.com/tui-rs-revival/ratatui/issues/168)) [**breaking**]
- *(widget)* Add circle widget ([#159](https://github.com/tui-rs-revival/ratatui/issues/159))
- *(widget)* Add style methods to Span, Spans, Text ([#148](https://github.com/tui-rs-revival/ratatui/issues/148))
- *(widget)* Support adding padding to Block ([#20](https://github.com/tui-rs-revival/ratatui/issues/20))
- *(widget)* Add offset() and offset_mut() for table and list state ([#12](https://github.com/tui-rs-revival/ratatui/issues/12))
- *(backend)* Add termwiz backend and example ([#5](https://github.com/ratatui-org/ratatui/issues/5))
- *(block)* Support placing the title on bottom ([#36](https://github.com/ratatui-org/ratatui/issues/36))
- *(border)* Add border! macro for easy bitflag manipulation ([#11](https://github.com/ratatui-org/ratatui/issues/11))
- *(calendar)* Add calendar widget ([#138](https://github.com/ratatui-org/ratatui/issues/138))
- *(color)* Add `FromStr` implementation for `Color` ([#180](https://github.com/ratatui-org/ratatui/issues/180))
- *(list)* Add len() to List ([#24](https://github.com/ratatui-org/ratatui/pull/24))
- *(paragraph)* Allow Lines to be individually aligned ([#149](https://github.com/ratatui-org/ratatui/issues/149))
- *(sparkline)* Finish #1 Sparkline directions PR ([#134](https://github.com/ratatui-org/ratatui/issues/134))
- *(terminal)* Add inline viewport ([#114](https://github.com/ratatui-org/ratatui/issues/114)) [**breaking**]
- *(test)* Expose test buffer ([#160](https://github.com/ratatui-org/ratatui/issues/160))
- *(text)* Add `Masked` to display secure data ([#168](https://github.com/ratatui-org/ratatui/issues/168)) [**breaking**]
- *(widget)* Add circle widget ([#159](https://github.com/ratatui-org/ratatui/issues/159))
- *(widget)* Add style methods to Span, Spans, Text ([#148](https://github.com/ratatui-org/ratatui/issues/148))
- *(widget)* Support adding padding to Block ([#20](https://github.com/ratatui-org/ratatui/issues/20))
- *(widget)* Add offset() and offset_mut() for table and list state ([#12](https://github.com/ratatui-org/ratatui/issues/12))
### Bug Fixes
- *(canvas)* Use full block for Marker::Block ([#133](https://github.com/tui-rs-revival/ratatui/issues/133)) [**breaking**]
- *(example)* Update input in examples to only use press events ([#129](https://github.com/tui-rs-revival/ratatui/issues/129))
- *(uncategorized)* Cleanup doc example ([#145](https://github.com/tui-rs-revival/ratatui/issues/145))
- *(reflow)* Remove debug macro call ([#198](https://github.com/tui-rs-revival/ratatui/issues/198))
- *(canvas)* Use full block for Marker::Block ([#133](https://github.com/ratatui-org/ratatui/issues/133)) [**breaking**]
- *(example)* Update input in examples to only use press events ([#129](https://github.com/ratatui-org/ratatui/issues/129))
- *(uncategorized)* Cleanup doc example ([#145](https://github.com/ratatui-org/ratatui/issues/145))
- *(reflow)* Remove debug macro call ([#198](https://github.com/ratatui-org/ratatui/issues/198))
### Refactor
- *(example)* Remove redundant `vec![]` in `user_input` example ([#26](https://github.com/tui-rs-revival/ratatui/issues/26))
- *(example)* Refactor paragraph example ([#152](https://github.com/tui-rs-revival/ratatui/issues/152))
- *(style)* Mark some Style fns const so they can be defined globally ([#115](https://github.com/tui-rs-revival/ratatui/issues/115))
- *(text)* Replace `Spans` with `Line` ([#178](https://github.com/tui-rs-revival/ratatui/issues/178))
- *(example)* Remove redundant `vec![]` in `user_input` example ([#26](https://github.com/ratatui-org/ratatui/issues/26))
- *(example)* Refactor paragraph example ([#152](https://github.com/ratatui-org/ratatui/issues/152))
- *(style)* Mark some Style fns const so they can be defined globally ([#115](https://github.com/ratatui-org/ratatui/issues/115))
- *(text)* Replace `Spans` with `Line` ([#178](https://github.com/ratatui-org/ratatui/issues/178))
### Documentation
- *(apps)* Fix rsadsb/adsb_deku radar link ([#140](https://github.com/tui-rs-revival/ratatui/issues/140))
- *(apps)* Add tenere ([#141](https://github.com/tui-rs-revival/ratatui/issues/141))
- *(apps)* Add twitch-tui ([#124](https://github.com/tui-rs-revival/ratatui/issues/124))
- *(apps)* Add oxycards ([#113](https://github.com/tui-rs-revival/ratatui/issues/113))
- *(apps)* Re-add trippy to APPS.md ([#117](https://github.com/tui-rs-revival/ratatui/issues/117))
- *(block)* Add example for block.inner ([#158](https://github.com/tui-rs-revival/ratatui/issues/158))
- *(changelog)* Update the empty profile link in contributors ([#112](https://github.com/tui-rs-revival/ratatui/issues/112))
- *(readme)* Fix small typo in readme ([#186](https://github.com/tui-rs-revival/ratatui/issues/186))
- *(readme)* Add termwiz demo to examples ([#183](https://github.com/tui-rs-revival/ratatui/issues/183))
- *(readme)* Add acknowledgement section ([#154](https://github.com/tui-rs-revival/ratatui/issues/154))
- *(readme)* Update project description ([#127](https://github.com/tui-rs-revival/ratatui/issues/127))
- *(uncategorized)* Scrape example code from examples/* ([#195](https://github.com/tui-rs-revival/ratatui/issues/195))
- *(apps)* Fix rsadsb/adsb_deku radar link ([#140](https://github.com/ratatui-org/ratatui/issues/140))
- *(apps)* Add tenere ([#141](https://github.com/ratatui-org/ratatui/issues/141))
- *(apps)* Add twitch-tui ([#124](https://github.com/ratatui-org/ratatui/issues/124))
- *(apps)* Add oxycards ([#113](https://github.com/ratatui-org/ratatui/issues/113))
- *(apps)* Re-add trippy to APPS.md ([#117](https://github.com/ratatui-org/ratatui/issues/117))
- *(block)* Add example for block.inner ([#158](https://github.com/ratatui-org/ratatui/issues/158))
- *(changelog)* Update the empty profile link in contributors ([#112](https://github.com/ratatui-org/ratatui/issues/112))
- *(readme)* Fix small typo in readme ([#186](https://github.com/ratatui-org/ratatui/issues/186))
- *(readme)* Add termwiz demo to examples ([#183](https://github.com/ratatui-org/ratatui/issues/183))
- *(readme)* Add acknowledgement section ([#154](https://github.com/ratatui-org/ratatui/issues/154))
- *(readme)* Update project description ([#127](https://github.com/ratatui-org/ratatui/issues/127))
- *(uncategorized)* Scrape example code from examples/* ([#195](https://github.com/ratatui-org/ratatui/issues/195))
### Styling
- *(apps)* Update the style of application list ([#184](https://github.com/tui-rs-revival/ratatui/issues/184))
- *(readme)* Update project introduction in README.md ([#153](https://github.com/tui-rs-revival/ratatui/issues/153))
- *(apps)* Update the style of application list ([#184](https://github.com/ratatui-org/ratatui/issues/184))
- *(readme)* Update project introduction in README.md ([#153](https://github.com/ratatui-org/ratatui/issues/153))
- *(uncategorized)* Clippy's variable inlining in format macros
### Testing
- *(buffer)* Add `assert_buffer_eq!` and Debug implementation ([#161](https://github.com/tui-rs-revival/ratatui/issues/161))
- *(list)* Add characterization tests for list ([#167](https://github.com/tui-rs-revival/ratatui/issues/167))
- *(widget)* Add unit tests for Paragraph ([#156](https://github.com/tui-rs-revival/ratatui/issues/156))
- *(buffer)* Add `assert_buffer_eq!` and Debug implementation ([#161](https://github.com/ratatui-org/ratatui/issues/161))
- *(list)* Add characterization tests for list ([#167](https://github.com/ratatui-org/ratatui/issues/167))
- *(widget)* Add unit tests for Paragraph ([#156](https://github.com/ratatui-org/ratatui/issues/156))
### Miscellaneous Tasks
- *(uncategorized)* Inline format args ([#190](https://github.com/tui-rs-revival/ratatui/issues/190))
- *(uncategorized)* Minor lints, making Clippy happier ([#189](https://github.com/tui-rs-revival/ratatui/issues/189))
- *(uncategorized)* Inline format args ([#190](https://github.com/ratatui-org/ratatui/issues/190))
- *(uncategorized)* Minor lints, making Clippy happier ([#189](https://github.com/ratatui-org/ratatui/issues/189))
### Build
- *(uncategorized)* Bump MSRV to 1.65.0 ([#171](https://github.com/tui-rs-revival/ratatui/issues/171))
- *(uncategorized)* Bump MSRV to 1.65.0 ([#171](https://github.com/ratatui-org/ratatui/issues/171))
### Continuous Integration
@@ -78,7 +188,7 @@
Thank you so much to everyone that contributed to this release!
Here is the list of contributors who has contributed to `ratatui` for the first time!
Here is the list of contributors who have contributed to `ratatui` for the first time!
- [@kpcyrd](https://github.com/kpcyrd)
- [@fujiapple852](https://github.com/fujiapple852)
@@ -100,12 +210,12 @@ Here is the list of contributors who has contributed to `ratatui` for the first
### Bug Fixes
- *(style)* Bold needs a bit ([#104](https://github.com/tui-rs-revival/ratatui/issues/104))
- *(style)* Bold needs a bit ([#104](https://github.com/ratatui-org/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))
- *(apps)* Add "logss" to apps ([#105](https://github.com/ratatui-org/ratatui/issues/105))
- *(uncategorized)* Fixup remaining tui references ([#106](https://github.com/ratatui-org/ratatui/issues/106))
### Contributors
@@ -125,67 +235,67 @@ 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))
- *(cd)* Add continuous deployment workflow ([#93](https://github.com/ratatui-org/ratatui/issues/93))
- *(ci)* Add MacOS to CI ([#60](https://github.com/ratatui-org/ratatui/issues/60))
- *(widget)* Add `offset()` to `TableState` ([#10](https://github.com/ratatui-org/ratatui/issues/10))
- *(widget)* Add `width()` to ListItem ([#17](https://github.com/ratatui-org/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))
- *(ci)* Test MSRV compatibility on CI ([#85](https://github.com/ratatui-org/ratatui/issues/85))
- *(ci)* Bump Rust version to 1.63.0 ([#80](https://github.com/ratatui-org/ratatui/issues/80))
- *(ci)* Use env for the cargo-make version ([#76](https://github.com/ratatui-org/ratatui/issues/76))
- *(ci)* Fix deprecation warnings on CI ([#58](https://github.com/ratatui-org/ratatui/issues/58))
- *(doc)* Add 3rd party libraries accidentally removed at #21 ([#61](https://github.com/ratatui-org/ratatui/issues/61))
- *(widget)* List should not ignore empty string items ([#42](https://github.com/ratatui-org/ratatui/issues/42)) [**breaking**]
- *(uncategorized)* Cassowary/layouts: add extra constraints for fixing Min(v)/Max(v) combination. ([#31](https://github.com/ratatui-org/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))
- *(uncategorized)* Fix typos ([#45](https://github.com/ratatui-org/ratatui/issues/45))
- *(uncategorized)* Fix typos ([#47](https://github.com/ratatui-org/ratatui/issues/47))
### Refactor
- *(style)* Make bitflags smaller ([#13](https://github.com/tui-rs-revival/ratatui/issues/13))
- *(style)* Make bitflags smaller ([#13](https://github.com/ratatui-org/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))
- *(apps)* Move 'apps using ratatui' to dedicated file ([#98](https://github.com/ratatui-org/ratatui/issues/98)) ([#99](https://github.com/ratatui-org/ratatui/issues/99))
- *(canvas)* Add documentation for x_bounds, y_bounds ([#35](https://github.com/ratatui-org/ratatui/issues/35))
- *(contributing)* Specify the use of unsafe for optimization ([#67](https://github.com/ratatui-org/ratatui/issues/67))
- *(github)* Remove pull request template ([#68](https://github.com/ratatui-org/ratatui/issues/68))
- *(readme)* Update crate status badge ([#102](https://github.com/ratatui-org/ratatui/issues/102))
- *(readme)* Small edits before first release ([#101](https://github.com/ratatui-org/ratatui/issues/101))
- *(readme)* Add install instruction and update title ([#100](https://github.com/ratatui-org/ratatui/issues/100))
- *(readme)* Add systeroid to application list ([#92](https://github.com/ratatui-org/ratatui/issues/92))
- *(readme)* Add glicol-cli to showcase list ([#95](https://github.com/ratatui-org/ratatui/issues/95))
- *(readme)* Add oxker to application list ([#74](https://github.com/ratatui-org/ratatui/issues/74))
- *(readme)* Add app kubectl-watch which uses tui ([#73](https://github.com/ratatui-org/ratatui/issues/73))
- *(readme)* Add poketex to 'apps using tui' in README ([#64](https://github.com/ratatui-org/ratatui/issues/64))
- *(readme)* Update README.md ([#39](https://github.com/ratatui-org/ratatui/issues/39))
- *(readme)* Update README.md ([#40](https://github.com/ratatui-org/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)* Fix: fix typos ([#90](https://github.com/ratatui-org/ratatui/issues/90))
- *(uncategorized)* Update to build more backends ([#81](https://github.com/ratatui-org/ratatui/issues/81))
- *(uncategorized)* Expand "Apps" and "Third-party" sections ([#21](https://github.com/ratatui-org/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))
- *(uncategorized)* Add hncli to list of applications made with tui-rs ([#41](https://github.com/ratatui-org/ratatui/issues/41))
- *(uncategorized)* Updated readme and contributing guide with updates about the fork ([#46](https://github.com/ratatui-org/ratatui/issues/46))
### Performance
- *(layout)* Better safe shared layout cache ([#62](https://github.com/tui-rs-revival/ratatui/issues/62))
- *(layout)* Better safe shared layout cache ([#62](https://github.com/ratatui-org/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))
- *(cargo)* Update project metadata ([#94](https://github.com/ratatui-org/ratatui/issues/94))
- *(ci)* Integrate `typos` for checking typos ([#91](https://github.com/ratatui-org/ratatui/issues/91))
- *(ci)* Change the target branch to main ([#79](https://github.com/ratatui-org/ratatui/issues/79))
- *(ci)* Re-enable clippy on CI ([#59](https://github.com/ratatui-org/ratatui/issues/59))
- *(uncategorized)* Integrate `committed` for checking conventional commits ([#77](https://github.com/ratatui-org/ratatui/issues/77))
- *(uncategorized)* Update `rust-version` to 1.59 in Cargo.toml ([#57](https://github.com/ratatui-org/ratatui/issues/57))
- *(uncategorized)* Update deps ([#51](https://github.com/ratatui-org/ratatui/issues/51))
- *(uncategorized)* Fix typo in layout.rs ([#619](https://github.com/ratatui-org/ratatui/issues/619))
- *(uncategorized)* Add apps using `tui`
### Contributors

View File

@@ -1,69 +1,157 @@
# Fork Status
# Contribution guidelines
## Pull Requests
First off, thank you for considering contributing to Ratatui.
**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.
If your contribution is not straightforward, please first discuss the change you wish to make by
creating a new issue before making the change, or starting a discussion on
[discord](https://discord.gg/pMCEU9hNEj).
## Issues
## Reporting 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 :
Before reporting an issue on the [issue tracker](https://github.com/ratatui-org/ratatui/issues),
please check that it has not already been reported by searching for some related keywords. Please
also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues
found.
- Recreate the issue
- Start by referencing the **original issue**: ```Referencing issue #[<issue number>](<original issue link>)```
- Then, paste the original issues **opening** text
## Pull requests
You can then resume the conversation by replying to this new issue you have created.
All contributions are obviously welcome. Please include as many details as possible in your PR
description to help the reviewer (follow the provided template). Make sure to highlight changes
which may need additional attention or you are uncertain about. Any idea with a large scale impact
on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
### Closing Issues
### Keep PRs small, intentional and focused
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.
Try to do one pull request per change. The time taken to review a PR grows exponential with the size
of the change. Small focused PRs will generally be much more faster to review. PRs that include both
refactoring (or reformatting) with actual changes are more difficult to review as every line of the
change becomes a place where a bug may have been introduced. Consider splitting refactoring /
reformatting changes into a separate PR from those that make a behavioral change, as the tests help
guarantee that the behavior is unchanged.
# Contributing
### Search `tui-rs` for similar work
The original fork of Ratatui, [`tui-rs`](https://github.com/fdehau/tui-rs/), has a large amount of
history of the project. Please search, read, link, and summarize any relevant
[issues](https://github.com/fdehau/tui-rs/issues/),
[discussions](https://github.com/fdehau/tui-rs/discussions/) and [pull
requests](https://github.com/fdehau/tui-rs/pulls).
### Use conventional commits
We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) and check for them as
a lint build step. To help adhere to the format, we recommend to install
[Commitizen](https://commitizen-tools.github.io/commitizen/). By using this tool you automatically
follow the configuration defined in [.cz.toml](.cz.toml). Your commit messages should have enough
information to help someone reading the [CHANGELOG](./CHANGELOG.md) understand what is new just from
the title. The summary helps expand on that to provide information that helps provide more context,
describes the nature of the problem that the commit is solving and any unintuitive effects of the
change. It's rare that code changes can easily communicate intent, so make sure this is clearly
documented.
### Clean up your commits
The final version of your PR that will be committed to the repository should be rebased and tested
against main. Every commit will end up as a line in the changelog, so please squash commits that are
only formatting or incremental fixes to things brought up as part of the PR review. Aim for a single
commit (unless there is a strong reason to stack the commits). See [Git Best Practices - On Sausage
Making](https://sethrobertson.github.io/GitBestPractices/#sausage) for more on this.
### Run CI tests before pushing a PR
We're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically run git hooks,
which will run `cargo make ci` before each push. To initialize the hook run `cargo test`. If
`cargo-make` is not installed, it will provide instructions to install it for you. This will ensure
that your code is formatted, compiles and passes all tests before you push. If you need to skip this
check, you can use `git push --no-verify`.
### Sign your commits
We use commit signature verification, which will block commits from being merged via the UI unless
they are signed. To set up your machine to sign commits, see [managing commit signature
verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification)
in GitHub docs.
## Implementation Guidelines
### Setup
Clone the repo and build it using [cargo-make](https://sagiegurari.github.io/cargo-make/)
Ratatui is an ordinary Rust project where common tasks are managed with
[cargo-make](https://github.com/sagiegurari/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`.
```shell
git clone https://github.com/ratatui-org/ratatui.git
cd ratatui
cargo make build
```
### Tests
The [test coverage](https://app.codecov.io/gh/ratatui-org/ratatui) of the crate is reasonably
good, but this can always be improved. Focus on keeping the tests simple and obvious and write unit
tests for all new or modified code. Beside the usual doc and unit tests, one of the most valuable
test you can write for Ratatui is a test against the `TestBackend`. It allows you to assert the
content of the output buffer that would have been flushed to the terminal after a given draw call.
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.
When writing tests, generally prefer to write unit tests and doc tests directly in the code file
being tested rather than integration tests in the `tests/` folder.
If an area that you're making a change in is not tested, write tests to characterize the existing
behavior before changing it. This helps ensure that we don't introduce bugs to existing software
using Ratatui (and helps make it easy to migrate apps still using `tui-rs`).
For coverage, we have two [bacon](https://dystroy.org/bacon/) jobs (one for all tests, and one for
unit tests, keyboard shortcuts `v` and `u` respectively) that run
[cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) to report the coverage. Several plugins
exist to show coverage directly in your editor. E.g.:
- <https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters>
- <https://github.com/alepez/vim-llvmcov>
### 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].
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`.
## :hammer_and_wrench: Pull requests
All contributions are obviously welcome.
Please include as many details as possible in your PR description to help the reviewer (follow the provided template).
Make sure to highlight changes which may need additional attention or you are uncertain about.
Any idea with a large scale impact on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
## Committing
To avoid any issues that may arrise with the CI/CD by not following the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) syntax, we recommend to install [Commitizen](https://commitizen-tools.github.io/commitizen/).\
By using this tool you automatically follow the configuration defined in [.cz.toml](.cz.toml).
Additionally, we're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically load pre-push hook, which will run `cargo make ci` before each push. It will load the hook automatically when you run `cargo test`. If `cargo-make` is not installed, it will install it for you.\
This will ensure that your code is formatted, compiles and passes all tests before you push. If you want to skip this check, you can use `git push --no-verify`.
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there
may be specific cases that this becomes necessary in order to avoid slowness. Please see [this
discussion](https://github.com/ratatui-org/ratatui/discussions/66) for more about the decision.
## Continuous Integration
We use Github Actions for the CI where we perform the following checks:
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
- The tests (docs, lib, tests and examples) should pass.
- The code should conform to the default format enforced by `rustfmt`.
- The code should not contain common style issues `clippy`.
You can also check most of those things yourself locally using `cargo make ci` which will offer you a shorter feedback loop.
You can also check most of those things yourself locally using `cargo make ci` which will offer you
a shorter feedback loop than pushing to github.
## Tests
## Relationship with `tui-rs`
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`.
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.
This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in February 2023, with the
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
([@fdehau](https://github.com/fdehau)).
The original repository contains all the issues, PRs and discussion that were raised originally, and
it is useful to refer to when contributing code, documentation, or issues with Ratatui.
We imported all the PRs from the original repository and implemented many of the smaller ones and
made notes on the leftovers. These are marked as draft PRs and labelled as [imported from
tui](https://github.com/ratatui-org/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
We have documented the current state of those PRs, and anyone is welcome to pick them up and
continue the work on them.
We have not imported 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.

View File

@@ -1,11 +1,11 @@
[package]
name = "ratatui"
version = "0.21.0"
authors = ["Florian Dehau <work@fdehau.com>"]
version = "0.22.0" # crate version
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
description = "A library to build rich terminal user interfaces or dashboards"
documentation = "https://docs.rs/ratatui/latest/ratatui/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/tui-rs-revival/ratatui"
repository = "https://github.com/ratatui-org/ratatui"
readme = "README.md"
license = "MIT"
exclude = [
@@ -40,6 +40,7 @@ bitflags = "2.3"
cassowary = "0.3"
crossterm = { version = "0.26", optional = true }
indoc = "2.0"
paste = "1.0.2"
serde = { version = "1", optional = true, features = ["derive"] }
termion = { version = "2.0", optional = true }
termwiz = { version = "0.20.0", optional = true }
@@ -53,6 +54,7 @@ argh = "0.1"
cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] }
criterion = { version = "0.5", features = ["html_reports"] }
fakeit = "1.1"
itertools = "0.10"
rand = "0.8"
[[bench]]

View File

@@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2016 Florian Dehau
Copyright (c) 2016-2022 Florian Dehau
Copyright (c) 2023 The Ratatui Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -3,33 +3,19 @@
[config]
skip_core_tasks = true
[env]
# all features except the backend ones
ALL_FEATURES = "all-widgets,macros,serde"
[tasks.default]
alias = "ci"
[tasks.ci]
run_task = [
{ name = "ci-unix", condition = { platforms = [
"linux",
"mac",
] } },
{ name = "ci-windows", condition = { platforms = [
"windows",
] } },
]
[tasks.ci-unix]
private = true
dependencies = [
"style-check",
"check-unix",
"test-unix",
"clippy-unix",
]
[tasks.ci-windows]
private = true
dependencies = [
"style-check",
"check-windows",
"test-windows",
"clippy-windows",
"clippy",
"check",
"test",
]
[tasks.style-check]
@@ -45,221 +31,133 @@ install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--vers
command = "typos"
[tasks.check]
run_task = [
{ name = "check-unix", condition = { platforms = [
"linux",
"mac",
] } },
{ name = "check-windows", condition = { platforms = [
"windows",
] } },
]
[tasks.check-unix]
private = true
dependencies = [
"check-crossterm",
"check-termion",
"check-termwiz",
]
[tasks.check-windows]
private = true
dependencies = [
"check-crossterm",
"check-termwiz",
]
[tasks.check-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "check-backend"
[tasks.check-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "check-backend"
[tasks.check-termwiz]
env = { TUI_FEATURES = "serde,termwiz" }
run_task = "check-backend"
[tasks.check-backend]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"check",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--all-targets",
"--all-features"
]
[tasks.check.windows]
args = [
"check",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
]
[tasks.build]
run_task = [
{ name = "build-unix", condition = { platforms = [
"linux",
"mac",
] } },
{ name = "build-windows", condition = { platforms = [
"windows",
] } },
]
[tasks.build-unix]
private = true
dependencies = [
"build-crossterm",
"build-termion",
"build-termwiz",
]
[tasks.build-windows]
private = true
dependencies = [
"build-crossterm",
"build-termwiz",
]
[tasks.build-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "build-backend"
[tasks.build-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "build-backend"
[tasks.build-termwiz]
env = { TUI_FEATURES = "serde,termwiz" }
run_task = "build-backend"
[tasks.build-backend]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"build",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--all-targets",
"--all-features",
]
[tasks.build.windows]
args = [
"build",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
]
[tasks.clippy]
run_task = [
{ name = "clippy-unix", condition = { platforms = [
"linux",
"mac",
] } },
{ name = "clippy-windows", condition = { platforms = [
"windows",
] } },
]
[tasks.clippy-unix]
private = true
dependencies = [
"clippy-crossterm",
"clippy-termion",
"clippy-termwiz",
]
[tasks.clippy-windows]
private = true
dependencies = [
"clippy-crossterm",
"clippy-termwiz",
]
[tasks.clippy-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "clippy-backend"
[tasks.clippy-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "clippy-backend"
[tasks.clippy-termwiz]
env = { TUI_FEATURES = "serde,termwiz" }
run_task = "clippy-backend"
[tasks.clippy-backend]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"clippy",
"--all-targets",
"--no-default-features",
"--tests",
"--benches",
"--features",
"${TUI_FEATURES}",
"--all-features",
"--",
"-D",
"warnings",
]
[tasks.clippy.windows]
args = [
"clippy",
"--all-targets",
"--tests",
"--benches",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz",
"--",
"-D",
"warnings",
]
[tasks.test]
run_task = [
{ name = "test-unix", condition = { platforms = [
"linux",
"mac",
] } },
{ name = "test-windows", condition = { platforms = [
"windows",
] } },
]
[tasks.test-unix]
private = true
dependencies = [
"test-crossterm",
"test-termion",
"test-termwiz",
"test-doc",
]
[tasks.test-windows]
private = true
dependencies = [
"test-crossterm",
"test-termwiz",
"test-doc",
]
[tasks.test-crossterm]
env = { TUI_FEATURES = "serde,crossterm,all-widgets,macros" }
run_task = "test-backend"
[tasks.test-termion]
env = { TUI_FEATURES = "serde,termion,all-widgets,macros" }
run_task = "test-backend"
[tasks.test-termwiz]
env = { TUI_FEATURES = "serde,termwiz,all-widgets,macros" }
run_task = "test-backend"
[tasks.test-backend]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"test",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--all-targets",
"--all-features",
]
[tasks.test-windows]
dependencies = [
"test-doc",
]
args = [
"test",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
]
[tasks.test-doc]
command = "cargo"
args = ["test", "--doc"]
args = [
"test", "--doc",
"--all-features",
]
[tasks.test-doc.windows]
args = [
"test", "--doc",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
]
[tasks.test-backend]
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
command = "cargo"
args = [
"test",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},${@}"
]
[tasks.coverage]
command = "cargo"
args = [
"llvm-cov",
"--lcov",
"--output-path", "target/lcov.info",
"--all-features",
]
[tasks.coverage.windows]
command = "cargo"
args = [
"llvm-cov",
"--lcov",
"--output-path", "target/lcov.info",
"--no-default-features",
"--features", "${ALL_FEATURES},crossterm,termwiz",
]
[tasks.run-example]
private = true
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
command = "cargo"
args = ["run", "--release", "--example", "${TUI_EXAMPLE_NAME}"]
args = ["run", "--release", "--example", "${TUI_EXAMPLE_NAME}", "--features", "all-widgets"]
[tasks.build-examples]
command = "cargo"
args = ["build", "--examples", "--release"]
args = ["build", "--examples", "--release", "--features", "all-widgets"]
[tasks.run-examples]
dependencies = ["build-examples"]

View File

@@ -6,17 +6,17 @@
dashboards. It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
project.
[![Crates.io](https://img.shields.io/crates/v/ratatui?logo=rust&style=for-the-badge)](https://crates.io/crates/ratatui)
[![License](https://img.shields.io/crates/l/ratatui?style=for-the-badge)](./LICENSE) [![GitHub CI
Status](https://img.shields.io/github/actions/workflow/status/tui-rs-revival/ratatui/ci.yml?style=for-the-badge&logo=github)](https://github.com/tui-rs-revival/ratatui/actions?query=workflow%3ACI+)
[![Docs.rs](https://img.shields.io/docsrs/ratatui?logo=rust&style=for-the-badge)](https://docs.rs/crate/ratatui/)
[![Crates.io](https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square)](https://crates.io/crates/ratatui)
[![License](https://img.shields.io/crates/l/ratatui?style=flat-square)](./LICENSE) [![GitHub CI
Status](https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github)](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
[![Docs.rs](https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square)](https://docs.rs/crate/ratatui/)
[![Dependency
Status](https://deps.rs/repo/github/tui-rs-revival/ratatui/status.svg?style=for-the-badge)](https://deps.rs/repo/github/tui-rs-revival/ratatui)
[![Codecov](https://img.shields.io/codecov/c/github/tui-rs-revival/ratatui?logo=codecov&style=for-the-badge&token=BAQ8SOKEST)](https://app.codecov.io/gh/tui-rs-revival/ratatui)
[![Discord](https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=for-the-badge)](https://discord.gg/pMCEU9hNEj)
Status](https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square)](https://deps.rs/repo/github/ratatui-org/ratatui)
[![Codecov](https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST)](https://app.codecov.io/gh/ratatui-org/ratatui)
[![Discord](https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square)](https://discord.gg/pMCEU9hNEj)
<!-- See RELEASE.md for instructions on creating the demo gif --->
![Demo of Ratatui](https://github.com/tui-rs-revival/ratatui/assets/24392180/93ab0e38-93e0-4ae0-a31b-91ae6c393185)
![Demo of Ratatui](https://github.com/ratatui-org/ratatui/assets/24392180/93ab0e38-93e0-4ae0-a31b-91ae6c393185)
<details>
<summary>Table of Contents</summary>
@@ -51,7 +51,7 @@ Or modify your `Cargo.toml`
```toml
[dependencies]
ratatui = { version = "0.21.0", features = ["all-widgets"]}
ratatui = { version = "0.22.0", features = ["all-widgets"]}
```
Ratatui is mostly backwards compatible with `tui-rs`. To migrate an existing project, it may be
@@ -60,7 +60,7 @@ E.g.:
```toml
[dependencies]
tui = { package = "ratatui", version = "0.21.0", features = ["all-widgets"]}
tui = { package = "ratatui", version = "0.22.0", features = ["all-widgets"]}
```
## Introduction
@@ -90,7 +90,7 @@ The following example demonstrates the minimal amount of code necessary to setup
render "Hello World!". The full code for this example which contains a little more detail is in
[hello_world.rs](./examples/hello_world.rs). For more guidance on how to create Ratatui apps, see
the [Docs](https://docs.rs/ratatui) and [Examples](#examples). There is also a starter template
available at [rust-tui-template](https://github.com/tui-rs-revival/rust-tui-template).
available at [rust-tui-template](https://github.com/ratatui-org/rust-tui-template).
```rust
fn main() -> Result<(), Box<dyn Error>> {
@@ -169,7 +169,7 @@ cargo run --example demo --no-default-features --features=termion
cargo run --example demo --no-default-features --features=termwiz
```
The UI code for the is in [examples/demo/ui.rs](./examples/demo/ui.rs) while the application state
The UI code for this is in [examples/demo/ui.rs](./examples/demo/ui.rs) while the application state
is in [examples/demo/app.rs](./examples/demo/app.rs).
If the user interface contains glyphs that are not displayed correctly by your terminal, you may
@@ -188,21 +188,23 @@ More examples are available in the [examples](./examples/) folder.
The library comes with the following
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
* [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
* [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
* [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
* [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
rendering [points, lines, shapes and a world
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
* [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
* [Block](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Block.html)
* [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
* [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
* [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
* [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
* [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
* [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
* [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
* [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
* [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
* [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
Each wiget has an associated example which can be found in the [examples](./examples/) folder. Run
Each widget has an associated example which can be found in the [examples](./examples/) folder. Run
each examples with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by
pressing `q`.
@@ -215,7 +217,7 @@ be installed with `cargo install cargo-make`).
`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-template](https://github.com/ratatui-org/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
@@ -241,7 +243,7 @@ be installed with `cargo install cargo-make`).
## Apps
Check out the list of more than 50 [Apps using
`Ratatui`](https://github.com/tui-rs-revival/ratatui/wiki/Apps-using-Ratatui)!
`Ratatui`](https://github.com/ratatui-org/ratatui/wiki/Apps-using-Ratatui)!
## Alternatives
@@ -251,12 +253,12 @@ to build text user interfaces in Rust.
## Contributors
[![GitHub
Contributors](https://contrib.rocks/image?repo=tui-rs-revival/ratatui)](https://github.com/tui-rs-revival/ratatui/graphs/contributors)
Contributors](https://contrib.rocks/image?repo=ratatui-org/ratatui)](https://github.com/ratatui-org/ratatui/graphs/contributors)
## Acknowledgments
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.
awesome logo** for the ratatui project and ratatui-org organization.
## License

View File

@@ -26,5 +26,5 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
1. Commit and push the changes.
1. Create a new tag: `git tag -a v[X.Y.Z]`
1. Push the tag: `git push --tags`
1. Wait for [Continuous Deployment](https://github.com/tui-rs-revival/ratatui/actions) workflow to
1. Wait for [Continuous Deployment](https://github.com/ratatui-org/ratatui/actions) workflow to
finish.

View File

@@ -15,6 +15,18 @@ need_stdout = false
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
need_stdout = false
[jobs.check-crossterm]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
need_stdout = false
[jobs.check-termion]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
need_stdout = false
[jobs.check-termwiz]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
need_stdout = false
[jobs.clippy]
command = [
"cargo", "clippy",
@@ -58,27 +70,22 @@ env.RUSTDOCFLAGS = "--cfg docsrs"
need_stdout = false
on_success = "job:doc" # so that we don't open the browser at each change
# You can run your application and have the result displayed in bacon,
# *if* it makes sense for this crate. You can run an example the same
# way. Don't forget the `--color always` part or the errors won't be
# properly parsed.
[jobs.run]
[jobs.coverage]
command = [
"cargo", "run",
"cargo", "llvm-cov",
"--lcov", "--output-path", "target/lcov.info",
"--all-features",
"--color", "always",
# put launch parameters for your program behind a `--` separator
]
need_stdout = true
allow_warnings = true
[jobs.check-crossterm]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
[jobs.check-termion]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
[jobs.check-termwiz]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
[jobs.coverage-unit-tests-only]
command = [
"cargo", "llvm-cov",
"--lcov", "--output-path", "target/lcov.info",
"--lib",
"--all-features",
"--color", "always",
]
# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
@@ -89,3 +96,5 @@ command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default
ctrl-c = "job:check-crossterm"
ctrl-t = "job:check-termion"
ctrl-w = "job:check-termwiz"
v = "job:coverage"
u = "job:coverage-unit-tests-only"

View File

@@ -46,7 +46,7 @@ filter_unconventional = true
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 = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/ratatui-org/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}" },

209
examples/README.md Normal file
View File

@@ -0,0 +1,209 @@
# Examples
These gifs were created using [Charm VHS](https://github.com/charmbracelet/vhs).
VHS has a problem rendering some background color transitions, which shows up in several examples
below. See <https://github.com/charmbracelet/vhs/issues/344> for more info. These problems don't
occur in a terminal.
## Barchart ([barchart.rs](./barchart.rs)
```shell
cargo run --example=barchart --features=crossterm
```
![Barchart][barchart.gif]
## Block ([block.rs](./block.rs))
```shell
cargo run --example=block --features=crossterm
```
![Block][block.gif]
## Calendar ([calendar.rs](./calendar.rs))
```shell
cargo run --example=calendar --features=crossterm widget-calendar
```
![Calendar][calendar.gif]
## Canvas ([canvas.rs](./canvas.rs))
```shell
cargo run --example=canvas --features=crossterm
```
![Canvas][canvas.gif]
## Chart ([chart.rs](./chart.rs))
```shell
cargo run --example=chart --features=crossterm
```
![Chart][chart.gif]
## Custom Widget ([custom_widget.rs](./custom_widget.rs))
```shell
cargo run --example=custom_widget --features=crossterm
```
This is not a particularly exciting example visually, but it demonstrates how to implement your own widget.
![Custom Widget][custom_widget.gif]
## Gauge ([gauge.rs](./gauge.rs))
Please note: the background renders poorly when we generate this example using VHS.
This problem doesn't generally happen during normal rendering in a terminal.
See <https://github.com/charmbracelet/vhs/issues/344> for more details
```shell
cargo run --example=gauge --features=crossterm
```
![Gauge][gauge.gif]
## Hello World ([hello_world.rs](./hello_world.rs))
```shell
cargo run --example=hello_world --features=crossterm
```
This is a pretty boring example, but it contains some good comments of documentation on some of the
standard approaches to writing tui apps.
![Hello World][hello_world.gif]
## Inline ([inline.rs](./inline.rs))
```shell
cargo run --example=inline --features=crossterm
```
![Inline][inline.gif]
## Layout ([layout.rs](./layout.rs))
```shell
cargo run --example=layout --features=crossterm
```
![Layout][layout.gif]
## List ([list.rs](./list.rs))
```shell
cargo run --example=list --features=crossterm
```
![List][list.gif]
## Panic ([panic.rs](./panic.rs))
```shell
cargo run --example=panic --features=crossterm
```
![Panic][panic.gif]
## Paragraph ([paragraph.rs](./paragraph.rs))
```shell
cargo run --example=paragraph --features=crossterm
```
![Paragraph][paragraph.gif]
## Popup ([popup.rs](./popup.rs))
```shell
cargo run --example=popup --features=crossterm
```
Please note: the background renders poorly when we generate this example using VHS.
This problem doesn't generally happen during normal rendering in a terminal.
See <https://github.com/charmbracelet/vhs/issues/344> for more details
![Popup][popup.gif]
## Scrollbar ([scrollbar.rs](./scrollbar.rs))
```shell
cargo run --example=scrollbar --features=crossterm
```
![Scrollbar][scrollbar.gif]
## Sparkline ([sparkline.rs](./sparkline.rs))
```shell
cargo run --example=sparkline --features=crossterm
```
![Sparkline][sparkline.gif]
## Table ([table.rs](./table.rs))
```shell
cargo run --example=table --features=crossterm
```
![Table][table.gif]
## Tabs ([tabs.rs](./tabs.rs))
```shell
cargo run --example=tabs --features=crossterm
```
![Tabs][tabs.gif]
## User Input ([user_input.rs](./user_input.rs))
```shell
cargo run --example=user_input --features=crossterm
```
![User Input][user_input.gif]
<!--
links to images to make it easier to update in bulk
These are generated with `vhs publish examples/xxx.gif`
To update these examples in bulk:
```shell
# build to ensure that running the examples doesn't have to wait so long
cargo build --examples --features=crossterm,all-widgets
for i in examples/*.tape
do
echo -n "[${i:s:examples/:::s:.tape:.gif:}]: "
vhs $i --publish --quiet
# may need to adjust this depending on if you see rate limiting from VHS
sleep 1
done
```
-->
[barchart.gif]: https://vhs.charm.sh/vhs-6ioxdeRBVkVpyXcjIEVaJU.gif
[block.gif]: https://vhs.charm.sh/vhs-1sEo9vVkHRwFtu95MOXrTj.gif
[calendar.gif]: https://vhs.charm.sh/vhs-1dBcpMSSP80WkBgm4lBhNo.gif
[canvas.gif]: https://vhs.charm.sh/vhs-4zeWEPF6bLEFSHuJrvaHlN.gif
[chart.gif]: https://vhs.charm.sh/vhs-zRzsE2AwRixQhcWMTAeF1.gif
[custom_widget.gif]: https://vhs.charm.sh/vhs-32mW1TpkrovTcm79QXmBSu.gif
[gauge.gif]: https://vhs.charm.sh/vhs-2rvSeP5r4lRkGTzNCKpm9a.gif
[hello_world.gif]: https://vhs.charm.sh/vhs-3CKUwxFuQi8oKQMS5zkPfQ.gif
[inline.gif]: https://vhs.charm.sh/vhs-miRl1mosKFoJV7LjjvF4T.gif
[layout.gif]: https://vhs.charm.sh/vhs-5R8O3LQGQ5pQVWwlPVrdbQ.gif
[list.gif]: https://vhs.charm.sh/vhs-4goo9reeUM9r0nYb54R7SP.gif
[panic.gif]: https://vhs.charm.sh/vhs-HrvKCHV4yeN69fb1EadTH.gif
[paragraph.gif]: https://vhs.charm.sh/vhs-2qIPDi79DUmtmeNDEeHVEF.gif
[popup.gif]: https://vhs.charm.sh/vhs-2QnC682AUeNYNXcjNlKTyp.gif
[scrollbar.gif]: https://vhs.charm.sh/vhs-2p13MMFreW7Gwt1xIonIWu.gif
[sparkline.gif]: https://vhs.charm.sh/vhs-4t59Vxw5Za33Rtvt9QrftA.gif
[table.gif]: https://vhs.charm.sh/vhs-6IrGHgT385DqA6xnwGF9oD.gif
[tabs.gif]: https://vhs.charm.sh/vhs-61WkbfhyDk0kbkjncErdHT.gif
[user_input.gif]: https://vhs.charm.sh/vhs-4fxUgkpEWcVyBRXuyYKODY.gif

View File

@@ -9,18 +9,22 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{BarChart, Block, Borders},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
struct Company<'a> {
revenue: [u64; 4],
label: &'a str,
bar_style: Style,
}
struct App<'a> {
data: Vec<(&'a str, u64)>,
months: [&'a str; 4],
companies: [Company<'a>; 3],
}
const TOTAL_REVENUE: &str = "Total Revenue";
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
@@ -50,6 +54,24 @@ impl<'a> App<'a> {
("B23", 3),
("B24", 5),
],
companies: [
Company {
label: "Comp.A",
revenue: [9500, 12500, 5300, 8500],
bar_style: Style::default().fg(Color::Green),
},
Company {
label: "Comp.B",
revenue: [1500, 2500, 3000, 4100],
bar_style: Style::default().fg(Color::Yellow),
},
Company {
label: "Comp.C",
revenue: [10500, 10600, 9000, 4200],
bar_style: Style::default().fg(Color::White),
},
],
months: ["Mars", "Apr", "May", "Jun"],
}
}
@@ -118,8 +140,16 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.constraints(
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]
.as_ref(),
)
.split(f.size());
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
@@ -128,35 +158,93 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
f.render_widget(barchart, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let barchart = BarChart::default()
.block(Block::default().title("Data2").borders(Borders::ALL))
.data(&app.data)
.bar_width(5)
.bar_gap(3)
.bar_style(Style::default().fg(Color::Green))
.value_style(
Style::default()
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
);
f.render_widget(barchart, chunks[0]);
let barchart = BarChart::default()
.block(Block::default().title("Data3").borders(Borders::ALL))
.data(&app.data)
.bar_style(Style::default().fg(Color::Red))
.bar_width(7)
.bar_gap(0)
.value_style(Style::default().bg(Color::Red))
.label_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::ITALIC),
);
f.render_widget(barchart, chunks[1]);
draw_bar_with_group_labels(f, app, chunks[1], false);
draw_bar_with_group_labels(f, app, chunks[2], true);
}
fn draw_bar_with_group_labels<B>(f: &mut Frame<B>, app: &App, area: Rect, bar_labels: bool)
where
B: Backend,
{
let groups: Vec<BarGroup> = app
.months
.iter()
.enumerate()
.map(|(i, &month)| {
let bars: Vec<Bar> = app
.companies
.iter()
.map(|c| {
let mut bar = Bar::default()
.value(c.revenue[i])
.style(c.bar_style)
.value_style(
Style::default()
.bg(c.bar_style.fg.unwrap())
.fg(Color::Black),
)
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.));
if bar_labels {
bar = bar.label(c.label.into());
}
bar
})
.collect();
BarGroup::default().label(month.into()).bars(&bars)
})
.collect();
let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.bar_width(7)
.group_gap(3);
for group in groups {
barchart = barchart.data(group)
}
f.render_widget(barchart, area);
const LEGEND_HEIGHT: u16 = 6;
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: TOTAL_REVENUE.len() as u16 + 2,
y: area.y,
x: area.x,
};
draw_legend(f, legend_area);
}
}
fn draw_legend<B>(f: &mut Frame<B>, area: Rect)
where
B: Backend,
{
let text = vec![
Line::from(Span::styled(
TOTAL_REVENUE,
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::White),
)),
Line::from(Span::styled(
"- Company A",
Style::default().fg(Color::Green),
)),
Line::from(Span::styled(
"- Company B",
Style::default().fg(Color::Yellow),
)),
Line::from(vec![Span::styled(
"- Company C",
Style::default().fg(Color::White),
)]),
];
let block = Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White));
let paragraph = Paragraph::new(text).block(block);
f.render_widget(paragraph, area);
}

11
examples/barchart.tape Normal file
View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/barchart.tape`
Output "target/barchart.gif"
Set Width 1200
Set Height 800
Hide
Type "cargo run --example=barchart"
Enter
Sleep 1s
Show
Sleep 5s

View File

@@ -1,17 +1,11 @@
use std::{error::Error, io};
use std::{error::Error, io, time::Duration};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Style, Stylize},
widgets::{block::title::Title, Block, BorderType, Borders, Padding, Paragraph},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
@@ -31,6 +25,7 @@ fn main() -> Result<(), Box<dyn Error>> {
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.clear()?;
terminal.show_cursor()?;
if let Err(err) = res {
@@ -44,9 +39,11 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
}
@@ -55,72 +52,72 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
fn ui<B: Backend>(f: &mut Frame<B>) {
// Wrapping block for a group
// Just draw the block and the group on the same area and build the group
// with at least a margin of 1
let size = f.size();
// Surrounding block
let block = Block::default()
let outer = f.size();
let outer_block = Block::default()
.borders(Borders::ALL)
.title(Title::from("Main block with round corners").alignment(Alignment::Center))
.title(block::Title::from("Main block with round corners").alignment(Alignment::Center))
.border_type(BorderType::Rounded);
f.render_widget(block, size);
let chunks = Layout::default()
let inner = outer_block.inner(outer);
let [top, bottom] = *Layout::default()
.direction(Direction::Vertical)
.margin(4)
.margin(1)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
// Top two inner blocks
let top_chunks = Layout::default()
.split(inner)
else {
return;
};
let [top_left, top_right] = *Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
.split(top)
else {
return;
};
let [bottom_left, bottom_right] = *Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(bottom)
else {
return;
};
// Top left inner block with green background
let block = Block::default()
.title(vec!["With".yellow(), " background".into()])
let top_left_block = Block::default()
.title("With Green Background")
.borders(Borders::all())
.on_green();
f.render_widget(block, top_chunks[0]);
// Top right inner block with styled title aligned to the right
let block = Block::default()
.title(Title::from("Styled title".white().on_red().bold()).alignment(Alignment::Right));
f.render_widget(block, top_chunks[1]);
// Bottom two inner blocks
let bottom_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.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]);
// Bottom right block with styled left and right border
let block = Block::default()
let top_right_block = Block::default()
.title(
block::Title::from("With styled title".white().on_red().bold())
.alignment(Alignment::Right),
)
.borders(Borders::ALL);
let bottom_left_block = Paragraph::new("Text inside padded block").block(
Block::default()
.title("With borders")
.borders(Borders::ALL)
.padding(Padding {
left: 4,
right: 4,
top: 2,
bottom: 2,
}),
);
let bottom_right_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()
let bottom_inner_block = Block::default()
.title("Block inside padded block")
.borders(Borders::ALL);
let inner_area = block.inner(bottom_chunks[1]);
f.render_widget(block, bottom_chunks[1]);
f.render_widget(inner_block, inner_area);
f.render_widget(outer_block, outer);
f.render_widget(Clear, top_left);
f.render_widget(top_left_block, top_left);
f.render_widget(top_right_block, top_right);
f.render_widget(bottom_left_block, bottom_left);
let bottom_right_inner = bottom_right_block.inner(bottom_right);
f.render_widget(bottom_right_block, bottom_right);
f.render_widget(bottom_inner_block, bottom_right_inner);
}

11
examples/block.tape Normal file
View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/block.tape`
Output "target/block.gif"
Set Width 1200
Set Height 800
Hide
Type "cargo run --example=block"
Enter
Sleep 1s
Show
Sleep 5s

View File

@@ -5,13 +5,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::calendar::*};
use time::{Date, Month, OffsetDateTime};
fn main() -> Result<(), Box<dyn Error>> {

11
examples/calendar.tape Normal file
View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/calendar.tape`
Output "target/calendar.gif"
Set Width 1200
Set Height 800
Hide
Type "cargo run --example=calendar --features=crossterm,widget-calendar"
Enter
Sleep 3s
Show
Sleep 5s

View File

@@ -10,15 +10,8 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Stylize},
symbols::Marker,
widgets::{
canvas::{Canvas, Map, MapResolution, Rectangle},
Block, Borders,
},
Frame, Terminal,
prelude::*,
widgets::{canvas::*, *},
};
struct App {

11
examples/canvas.tape Normal file
View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/canvas.tape`
Output "target/canvas.gif"
Set Width 1200
Set Height 800
Hide
Type "cargo run --example=canvas --features=crossterm"
Enter
Sleep 1s
Show
Sleep 5s

View File

@@ -9,15 +9,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style, Stylize},
symbols,
text::Span,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
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] = [

11
examples/chart.tape Normal file
View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/chart.tape`
Output "target/chart.gif"
Set Width 1200
Set Height 800
Hide
Type "cargo run --example=chart --features=crossterm"
Enter
Sleep 1s
Show
Sleep 5s

View File

@@ -5,14 +5,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
layout::Rect,
style::Style,
widgets::Widget,
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
#[derive(Default)]
struct Label<'a> {

View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/custom_widget.tape`
Output "target/custom_widget.gif"
Set Width 1200
Set Height 200
Hide
Type "cargo run --example=custom_widget --features=crossterm"
Enter
Sleep 1s
Show
Sleep 5s

View File

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

View File

@@ -9,10 +9,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
Terminal,
};
use ratatui::prelude::*;
use crate::{app::App, ui};

View File

@@ -1,9 +1,6 @@
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use ratatui::{
backend::{Backend, TermionBackend},
Terminal,
};
use ratatui::prelude::*;
use termion::{
event::Key,
input::{MouseTerminal, TermRead},

View File

@@ -4,7 +4,7 @@ use std::{
time::{Duration, Instant},
};
use ratatui::{backend::TermwizBackend, Terminal};
use ratatui::prelude::*;
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
use crate::{app::App, ui};

View File

@@ -1,15 +1,6 @@
use ratatui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Line, Span},
widgets::{
canvas::{Canvas, Circle, Line as CanvasLine, Map, MapResolution, Rectangle},
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
},
Frame,
prelude::*,
widgets::{canvas::*, *},
};
use crate::app::App;
@@ -22,7 +13,7 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.tabs
.titles
.iter()
.map(|t| Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title(app.title))
@@ -139,7 +130,7 @@ where
.tasks
.items
.iter()
.map(|i| ListItem::new(vec![Line::from(Span::raw(*i))]))
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks)
.block(Block::default().borders(Borders::ALL).title("List"))
@@ -163,7 +154,7 @@ where
"WARNING" => warning_style,
_ => info_style,
};
let content = vec![Line::from(vec![
let content = vec![text::Line::from(vec![
Span::styled(format!("{level:<9}"), s),
Span::raw(evt),
])];
@@ -263,9 +254,9 @@ where
B: Backend,
{
let text = vec![
Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
Line::from(""),
Line::from(vec![
text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
text::Line::from(""),
text::Line::from(vec![
Span::from("For example: "),
Span::styled("under", Style::default().fg(Color::Red)),
Span::raw(" "),
@@ -274,7 +265,7 @@ where
Span::styled("rainbow", Style::default().fg(Color::Blue)),
Span::raw("."),
]),
Line::from(vec![
text::Line::from(vec![
Span::raw("Oh and if you didn't "),
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
Span::raw(" you can "),
@@ -285,7 +276,7 @@ where
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
Span::raw(".")
]),
Line::from(
text::Line::from(
"One more thing is that it should display unicode characters: 10€"
),
];
@@ -356,7 +347,7 @@ where
});
for (i, s1) in app.servers.iter().enumerate() {
for s2 in &app.servers[i + 1..] {
ctx.draw(&CanvasLine {
ctx.draw(&canvas::Line {
x1: s1.coords.1,
y1: s1.coords.0,
y2: s2.coords.0,

View File

@@ -9,14 +9,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Gauge},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
struct App {
progress1: u16,
@@ -113,7 +106,6 @@ fn run_app<B: Backend>(
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Percentage(25),
@@ -155,7 +147,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let label = format!("{}/100", app.progress4);
let gauge = Gauge::default()
.block(Block::default().title("Gauge4"))
.block(Block::default().title("Gauge4").borders(Borders::ALL))
.gauge_style(
Style::default()
.fg(Color::Cyan)

11
examples/gauge.tape Normal file
View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/gauge.tape`
Output "target/gauge.gif"
Set Width 1200
Set Height 600
Hide
Type "cargo run --example=gauge --features=crossterm"
Enter
Sleep 1s
Show
Sleep 20s

View File

@@ -9,7 +9,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, widgets::Paragraph, Terminal};
use ratatui::{prelude::*, widgets::*};
/// This is a bare minimum example. There are many approaches to running an application loop, so
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and

11
examples/hello_world.tape Normal file
View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/hello_world.tape`
Output "target/hello_world.gif"
Set Width 1200
Set Height 200
Hide
Type "cargo run --example=hello_world --features=crossterm"
Enter
Sleep 1s
Show
Sleep 5s

View File

@@ -8,15 +8,7 @@ use std::{
};
use rand::distributions::{Distribution, Uniform};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Line, Span},
widgets::{block::title::Title, Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
Frame, Terminal, TerminalOptions, Viewport,
};
use ratatui::{prelude::*, widgets::*};
const NUM_DOWNLOADS: usize = 10;
@@ -227,7 +219,7 @@ fn run_app<B: Backend>(
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
let size = f.size();
let block = Block::default().title(Title::from("Progress").alignment(Alignment::Center));
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
f.render_widget(block, size);
let chunks = Layout::default()

8
examples/inline.tape Normal file
View File

@@ -0,0 +1,8 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/inline.tape`
Output "target/inline.gif"
Set Width 1200
Set Height 600
Type "cargo run --example=inline --features=crossterm"
Enter
Sleep 20s

View File

@@ -5,12 +5,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
@@ -51,21 +46,51 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
}
fn ui<B: Backend>(f: &mut Frame<B>) {
let chunks = Layout::default()
fn ui<B: Backend>(frame: &mut Frame<B>) {
let [top, mid, bottom] = *Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Percentage(80),
Constraint::Percentage(10),
Constraint::Length(4),
Constraint::Percentage(50),
Constraint::Min(4),
]
.as_ref(),
)
.split(f.size());
.split(frame.size())
else {
return;
};
let [left, right] = *Layout::default()
.direction(Direction::Horizontal)
.horizontal_margin(5)
.vertical_margin(2)
.constraints([Constraint::Ratio(2, 5), Constraint::Ratio(3, 5)].as_ref())
.split(mid)
else {
return;
};
frame.render_widget(
Paragraph::new("Constraint::Length(4)").block(Block::default().borders(Borders::ALL)),
top,
);
let block = Block::default().title("Block").borders(Borders::ALL);
f.render_widget(block, chunks[0]);
let block = Block::default().title("Block 2").borders(Borders::ALL);
f.render_widget(block, chunks[2]);
frame.render_widget(
Paragraph::new("Constraint::Percentage(50)").block(Block::default().borders(Borders::ALL)),
mid,
);
frame.render_widget(
Paragraph::new("Constraint::Ratio(2, 5)\nhorizontal_margin(5)\nvertical_margin(2)")
.block(Block::default().borders(Borders::ALL)),
left,
);
frame.render_widget(
Paragraph::new("Constraint::Ratio(3, 5)").block(Block::default().borders(Borders::ALL)),
right,
);
frame.render_widget(
Paragraph::new("Constraint::Min(4)").block(Block::default().borders(Borders::ALL)),
bottom,
);
}

11
examples/layout.tape Normal file
View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/layout.tape`
Output "target/layout.gif"
Set Width 1200
Set Height 600
Hide
Type "cargo run --example=layout --features=crossterm"
Enter
Sleep 1s
Show
Sleep 5s

View File

@@ -9,14 +9,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, ListState},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
struct StatefulList<T> {
state: ListState,

14
examples/list.tape Normal file
View File

@@ -0,0 +1,14 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/list.tape`
Output "target/list.gif"
Set Width 1200
Set Height 600
Hide
Type "cargo run --example=list --features=crossterm"
Enter
Sleep 1s
Show
Down@1s 4
Up@1s 2
Left@1s 1
Sleep 5s

View File

@@ -14,22 +14,13 @@
//! That's why this example is set up to show both situations, with and without
//! the chained panic hook, to see the difference.
#![deny(clippy::all)]
#![warn(clippy::pedantic, clippy::nursery)]
use std::{error::Error, io};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::Alignment,
text::Line,
widgets::{Block, Borders, Paragraph},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
type Result<T> = std::result::Result<T, Box<dyn Error>>;

19
examples/panic.tape Normal file
View File

@@ -0,0 +1,19 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/panic.tape`
Output "target/panic.gif"
Set Width 1200
Set Height 600
Type "cargo run --example=panic --features=crossterm"
Enter
Sleep 5s
Type p
Sleep 2s
Type reset
Enter
Type "cargo run --example=panic --features=crossterm"
Enter
Sleep 2s
Type e
Sleep 2s
Type p
Sleep 5s

View File

@@ -9,14 +9,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Masked, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
struct App {
scroll: u16,
@@ -101,7 +94,6 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Percentage(25),

11
examples/paragraph.tape Normal file
View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/paragraph.tape`
Output "target/paragraph.gif"
Set Width 1200
Set Height 1800
Hide
Type "cargo run --example=paragraph --features=crossterm"
Enter
Sleep 1s
Show
Sleep 5s

View File

@@ -5,13 +5,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::Stylize,
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
struct App {
show_popup: bool,

15
examples/popup.tape Normal file
View File

@@ -0,0 +1,15 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/popup.tape`
Output "target/popup.gif"
Set Width 1200
Set Height 600
Hide
Type "cargo run --example=popup --features=crossterm"
Enter
Sleep 1s
Show
Sleep 2s
Type p
Sleep 2s
Type p
Sleep 5s

View File

@@ -9,16 +9,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Margin},
style::{Color, Modifier, Style, Stylize},
text::{Line, Masked, Span},
widgets::{
scrollbar, Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
},
Frame, Terminal,
};
use ratatui::{prelude::*, symbols::scrollbar, widgets::*};
#[derive(Default)]
struct App {
@@ -120,7 +111,6 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Min(1),

11
examples/scrollbar.tape Normal file
View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/scrollbar.tape`
Output "target/scrollbar.gif"
Set Width 1200
Set Height 1200
Hide
Type "cargo run --example=scrollbar --features=crossterm"
Enter
Sleep 1s
Show
Sleep 5s

View File

@@ -13,13 +13,7 @@ use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, Sparkline},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
#[derive(Clone)]
pub struct RandomSignal {
@@ -135,7 +129,6 @@ fn run_app<B: Backend>(
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(3),

11
examples/sparkline.tape Normal file
View File

@@ -0,0 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/sparkline.tape`
Output "target/sparkline.gif"
Set Width 1200
Set Height 600
Hide
Type "cargo run --example=sparkline --features=crossterm"
Enter
Sleep 1s
Show
Sleep 5s

View File

@@ -5,13 +5,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Cell, Row, Table, TableState},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
struct App<'a> {
state: TableState,
@@ -122,7 +116,6 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let rects = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.margin(5)
.split(f.size());
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
@@ -151,7 +144,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.highlight_symbol(">> ")
.widths(&[
Constraint::Percentage(50),
Constraint::Length(30),
Constraint::Max(30),
Constraint::Min(10),
]);
f.render_stateful_widget(t, rects[0], &mut app.state);

15
examples/table.tape Normal file
View File

@@ -0,0 +1,15 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/table.tape`
Output "target/table.gif"
Set Width 1200
Set Height 600
Hide
Type "cargo run --example=table --features=crossterm"
Enter
Sleep 1s
Show
Down@1s 4
Up@1s 2
Down@1s 8
Up@1s 12
Sleep 5s

View File

@@ -5,14 +5,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style, Stylize},
text::Line,
widgets::{Block, Borders, Tabs},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
struct App<'a> {
pub titles: Vec<&'a str>,
@@ -89,7 +82,6 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(5)
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(size);

13
examples/tabs.tape Normal file
View File

@@ -0,0 +1,13 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/tabs.tape`
Output "target/tabs.gif"
Set Width 1200
Set Height 300
Hide
Type "cargo run --example=tabs --features=crossterm"
Enter
Sleep 1s
Show
Right@1s 4
Left@1s 2
Sleep 5s

View File

@@ -6,25 +6,20 @@ use std::{error::Error, io};
/// started.
///
/// This is a very simple example:
/// * A input box always focused. Every character you type is registered
/// here
/// * Pressing Backspace erases a character
/// * An input box always focused. Every character you type is registered
/// here.
/// * An entered character is inserted at the cursor position.
/// * Pressing Backspace erases the left character before the cursor position
/// * Pressing Enter pushes the current input in the history of previous
/// messages
/// messages.
/// **Note: ** as this is a relatively simple example unicode characters are unsupported and
/// their use will result in undefined behaviour.
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, Borders, List, ListItem, Paragraph},
Frame, Terminal,
};
use unicode_width::UnicodeWidthStr;
use ratatui::{prelude::*, widgets::*};
enum InputMode {
Normal,
@@ -35,6 +30,8 @@ enum InputMode {
struct App {
/// Current value of the input box
input: String,
/// Position of cursor in the editor area.
cursor_position: usize,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
@@ -47,10 +44,65 @@ impl Default for App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
cursor_position: 0,
}
}
}
impl App {
fn move_cursor_left(&mut self) {
let cursor_moved_left = self.cursor_position.saturating_sub(1);
self.cursor_position = self.clamp_cursor(cursor_moved_left);
}
fn move_cursor_right(&mut self) {
let cursor_moved_right = self.cursor_position.saturating_add(1);
self.cursor_position = self.clamp_cursor(cursor_moved_right);
}
fn enter_char(&mut self, new_char: char) {
self.input.insert(self.cursor_position, new_char);
self.move_cursor_right();
}
fn delete_char(&mut self) {
let is_not_cursor_leftmost = self.cursor_position != 0;
if is_not_cursor_leftmost {
// Method "remove" is not used on the saved text for deleting the selected char.
// Reason: Using remove on String works on bytes instead of the chars.
// Using remove would require special care because of char boundaries.
let current_index = self.cursor_position;
let from_left_to_current_index = current_index - 1;
// Getting all characters before the selected character.
let before_char_to_delete = self.input.chars().take(from_left_to_current_index);
// Getting all characters after selected character.
let after_char_to_delete = self.input.chars().skip(current_index);
// Put all characters together except the selected one.
// By leaving the selected one out, it is forgotten and therefore deleted.
self.input = before_char_to_delete.chain(after_char_to_delete).collect();
self.move_cursor_left();
}
}
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
new_cursor_pos.clamp(0, self.input.len())
}
fn reset_cursor(&mut self) {
self.cursor_position = 0;
}
fn submit_message(&mut self) {
self.messages.push(self.input.clone());
self.input.clear();
self.reset_cursor();
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
@@ -95,14 +147,18 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
_ => {}
},
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Enter => {
app.messages.push(app.input.drain(..).collect());
}
KeyCode::Char(c) => {
app.input.push(c);
KeyCode::Enter => app.submit_message(),
KeyCode::Char(to_insert) => {
app.enter_char(to_insert);
}
KeyCode::Backspace => {
app.input.pop();
app.delete_char();
}
KeyCode::Left => {
app.move_cursor_left();
}
KeyCode::Right => {
app.move_cursor_right();
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
@@ -118,7 +174,6 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(1),
@@ -134,7 +189,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
vec![
"Press ".into(),
"q".bold(),
" to exist, ".into(),
" to exit, ".into(),
"e".bold(),
" to start editing.".bold(),
],
@@ -172,8 +227,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
// rendering
f.set_cursor(
// Put cursor past the end of the input text
chunks[1].x + app.input.width() as u16 + 1,
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
chunks[1].x + app.cursor_position as u16 + 1,
// Move one line down, from the border to the input line
chunks[1].y + 1,
)

21
examples/user_input.tape Normal file
View File

@@ -0,0 +1,21 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/user_input.tape`
Output "target/user_input.gif"
Set Width 1200
Set Height 600
Hide
Type "cargo run --example=user_input --features=crossterm"
Enter
Sleep 1s
Show
Sleep 2s
Type e
Sleep 1s
Type "Hello, world!"
Enter
Sleep 2s
Backspace 13
Sleep 1s
Type "Goodbye, world!"
Enter
Sleep 5s

View File

@@ -12,7 +12,7 @@ use crossterm::{
execute, queue,
style::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
SetForegroundColor,
SetForegroundColor, SetUnderlineColor,
},
terminal::{self, Clear},
};
@@ -42,6 +42,7 @@ use crate::{
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Default, Clone)]
pub struct CrosstermBackend<W: Write> {
buffer: W,
}
@@ -81,6 +82,7 @@ where
{
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut underline_color = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
for (x, y, cell) in content {
@@ -107,6 +109,11 @@ where
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
bg = cell.bg;
}
if cell.underline_color != underline_color {
let color = CColor::from(cell.underline_color);
map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
underline_color = cell.underline_color;
}
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
}
@@ -115,6 +122,7 @@ where
self.buffer,
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetUnderlineColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
))
}
@@ -205,7 +213,7 @@ impl From<Color> for CColor {
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
#[derive(Debug)]
#[derive(Debug, Default, Clone, Copy)]
struct ModifierDiff {
pub from: Modifier,
pub to: Modifier,

View File

@@ -31,6 +31,7 @@ use crate::{
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Default, Clone)]
pub struct TermionBackend<W>
where
W: Write,
@@ -163,14 +164,16 @@ where
self.stdout.flush()
}
}
#[derive(Debug, Default, Clone, Copy)]
struct Fg(Color);
#[derive(Debug, Default, Clone, Copy)]
struct Bg(Color);
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
#[derive(Debug, Default, Clone, Copy)]
struct ModifierDiff {
from: Modifier,
to: Modifier,

View File

@@ -24,7 +24,7 @@ use crate::{
/// Termwiz backend implementation for the [`Backend`] trait.
/// # Example
///
/// ```rust
/// ```rust,no_run
/// use ratatui::backend::{Backend, TermwizBackend};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {

View File

@@ -28,7 +28,7 @@ use crate::{
/// # Ok(())
/// # }
/// ```
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct TestBackend {
width: u16,
buffer: Buffer,

View File

@@ -14,11 +14,13 @@ use crate::{
};
/// A buffer cell
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Cell {
pub symbol: String,
pub fg: Color,
pub bg: Color,
#[cfg(feature = "crossterm")]
pub underline_color: Color,
pub modifier: Modifier,
}
@@ -52,11 +54,25 @@ impl Cell {
if let Some(c) = style.bg {
self.bg = c;
}
#[cfg(feature = "crossterm")]
if let Some(c) = style.underline_color {
self.underline_color = c;
}
self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
self
}
#[cfg(feature = "crossterm")]
pub fn style(&self) -> Style {
Style::default()
.fg(self.fg)
.bg(self.bg)
.underline_color(self.underline_color)
.add_modifier(self.modifier)
}
#[cfg(not(feature = "crossterm"))]
pub fn style(&self) -> Style {
Style::default()
.fg(self.fg)
@@ -69,6 +85,10 @@ impl Cell {
self.symbol.push(' ');
self.fg = Color::Reset;
self.bg = Color::Reset;
#[cfg(feature = "crossterm")]
{
self.underline_color = Color::Reset;
}
self.modifier = Modifier::empty();
}
}
@@ -79,6 +99,8 @@ impl Default for Cell {
symbol: " ".into(),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "crossterm")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
}
}
@@ -106,12 +128,14 @@ impl Default for Cell {
/// symbol: String::from("r"),
/// fg: Color::Red,
/// bg: Color::White,
/// #[cfg(feature = "crossterm")]
/// underline_color: Color::Reset,
/// modifier: Modifier::empty()
/// });
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol, "x");
/// ```
#[derive(Clone, PartialEq, Eq, Default)]
#[derive(Default, Clone, Eq, PartialEq)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
@@ -559,10 +583,21 @@ impl Debug for Buffer {
overwritten.push((x, &c.symbol));
}
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
let style = (c.fg, c.bg, c.modifier);
if last_style != Some(style) {
last_style = Some(style);
styles.push((x, y, c.fg, c.bg, c.modifier));
#[cfg(feature = "crossterm")]
{
let style = (c.fg, c.bg, c.underline_color, c.modifier);
if last_style != Some(style) {
last_style = Some(style);
styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
}
}
#[cfg(not(feature = "crossterm"))]
{
let style = (c.fg, c.bg, c.modifier);
if last_style != Some(style) {
last_style = Some(style);
styles.push((x, y, c.fg, c.bg, c.modifier));
}
}
}
if !overwritten.is_empty() {
@@ -574,6 +609,12 @@ impl Debug for Buffer {
}
f.write_str(" ],\n styles: [\n")?;
for s in styles {
#[cfg(feature = "crossterm")]
f.write_fmt(format_args!(
" x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
s.0, s.1, s.2, s.3, s.4, s.5
))?;
#[cfg(not(feature = "crossterm"))]
f.write_fmt(format_args!(
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
s.0, s.1, s.2, s.3, s.4
@@ -607,6 +648,25 @@ mod tests {
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD),
);
#[cfg(feature = "crossterm")]
assert_eq!(
format!("{buf:?}"),
indoc::indoc!(
"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
\"Hello World!\",
\"G'day World!\",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
]
}"
)
);
#[cfg(not(feature = "crossterm"))]
assert_eq!(
format!("{buf:?}"),
indoc::indoc!(

View File

@@ -11,21 +11,23 @@ use cassowary::{
WeightedRelation::{EQ, GE, LE},
};
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Default, Hash, Clone, Copy, PartialEq, Eq)]
pub enum Corner {
#[default]
TopLeft,
TopRight,
BottomRight,
BottomLeft,
}
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Direction {
Horizontal,
#[default]
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Constraint {
Percentage(u16),
Ratio(u32, u32),
@@ -34,6 +36,12 @@ pub enum Constraint {
Min(u16),
}
impl Default for Constraint {
fn default() -> Self {
Constraint::Percentage(100)
}
}
impl Constraint {
pub fn apply(&self, length: u16) -> u16 {
match *self {
@@ -56,14 +64,15 @@ impl Constraint {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Margin {
pub vertical: u16,
pub horizontal: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Alignment {
#[default]
Left,
Center,
Right,
@@ -359,6 +368,7 @@ fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
}
/// A container used by the solver inside split
#[derive(Debug, Clone, Copy)]
struct Element {
x: Variable,
y: Variable,
@@ -395,7 +405,7 @@ impl Element {
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
/// area they are supposed to render to.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Rect {
pub x: u16,
pub y: u16,

View File

@@ -1,7 +1,9 @@
//! [ratatui](https://github.com/tui-rs-revival/ratatui) is a library used to build rich
#![forbid(unsafe_code)]
//! [ratatui](https://github.com/ratatui-org/ratatui) 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/ratatui-org/ratatui/master/assets/demo.gif)
//!
//! # Get started
//!
@@ -187,3 +189,5 @@ pub mod text;
pub mod widgets;
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};
pub mod prelude;

35
src/prelude.rs Normal file
View File

@@ -0,0 +1,35 @@
//! A prelude for conveniently writing applications using this library.
//!
//! ```rust,no_run
//! use ratatui::prelude::*;
//! ```
//!
//! Aside from the main types that are used in the library, this prelude also re-exports several
//! modules to make it easy to qualify types that would otherwise collide. E.g.:
//!
//! ```rust
//! use ratatui::{prelude::*, widgets::*};
//! use ratatui::widgets::{Block, Borders};
//!
//! #[derive(Debug, Default, PartialEq, Eq)]
//! struct Line;
//!
//! assert_eq!(Line::default(), Line);
//! assert_eq!(text::Line::default(), ratatui::text::Line::from(vec![]));
//! ```
#[cfg(feature = "crossterm")]
pub use crate::backend::CrosstermBackend;
#[cfg(feature = "termion")]
pub use crate::backend::TermionBackend;
#[cfg(feature = "termwiz")]
pub use crate::backend::TermwizBackend;
pub use crate::{
backend::{self, Backend},
buffer::{self, Buffer},
layout::{self, Alignment, Constraint, Corner, Direction, Layout, Margin, Rect},
style::{self, Color, Modifier, Style, Styled, Stylize},
symbols::{self, Marker},
terminal::{self, Frame, Terminal, TerminalOptions, Viewport},
text::{self, Line, Masked, Span, Text},
};

View File

@@ -15,20 +15,16 @@
//!
//! # Using style shorthands
//!
//! This is best for consise styling.
//! This is best for concise styling.
//! ## Example
//! ```
//! use ratatui::{
//! style::{Color, Modifier, Style, Styled, Stylize},
//! text::Span,
//! };
//! use ratatui::prelude::*;
//!
//! assert_eq!(
//! "hello".red().on_blue().bold(),
//! Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
//! )
//! ```
mod stylized;
use std::{
fmt::{self, Debug},
@@ -36,29 +32,109 @@ use std::{
};
use bitflags::bitflags;
pub use stylized::{Styled, Stylize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
mod stylize;
pub use stylize::{Styled, Stylize};
/// ANSI Color
///
/// All colors from the [ANSI color table](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
/// are supported (though some names are not exactly the same).
///
/// | Color Name | Color | Foreground | Background |
/// |----------------|-------------------------|------------|------------|
/// | `black` | [`Color::Black`] | 30 | 40 |
/// | `red` | [`Color::Red`] | 31 | 41 |
/// | `green` | [`Color::Green`] | 32 | 42 |
/// | `yellow` | [`Color::Yellow`] | 33 | 43 |
/// | `blue` | [`Color::Blue`] | 34 | 44 |
/// | `magenta` | [`Color::Magenta`] | 35 | 45 |
/// | `cyan` | [`Color::Cyan`] | 36 | 46 |
/// | `gray`* | [`Color::Gray`] | 37 | 47 |
/// | `darkgray`* | [`Color::DarkGray`] | 90 | 100 |
/// | `lightred` | [`Color::LightRed`] | 91 | 101 |
/// | `lightgreen` | [`Color::LightGreen`] | 92 | 102 |
/// | `lightyellow` | [`Color::LightYellow`] | 93 | 103 |
/// | `lightblue` | [`Color::LightBlue`] | 94 | 104 |
/// | `lightmagenta` | [`Color::LightMagenta`] | 95 | 105 |
/// | `lightcyan` | [`Color::LightCyan`] | 96 | 106 |
/// | `white`* | [`Color::White`] | 97 | 107 |
///
/// - `gray` is sometimes called `white` - this is not supported as we use `white` for bright white
/// - `gray` is sometimes called `silver` - this is supported
/// - `darkgray` is sometimes called `light black` or `bright black` (both are supported)
/// - `white` is sometimes called `light white` or `bright white` (both are supported)
/// - we support `bright` and `light` prefixes for all colors
/// - we support `-` and `_` and ` ` as separators for all colors
/// - we support both `gray` and `grey` spellings
///
/// # Example
///
/// ```
/// use ratatui::style::Color;
/// use std::str::FromStr;
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
/// assert_eq!("red".parse(), Ok(Color::Red));
/// assert_eq!("lightred".parse(), Ok(Color::LightRed));
/// assert_eq!("light red".parse(), Ok(Color::LightRed));
/// assert_eq!("light-red".parse(), Ok(Color::LightRed));
/// assert_eq!("light_red".parse(), Ok(Color::LightRed));
/// assert_eq!("lightRed".parse(), Ok(Color::LightRed));
/// assert_eq!("bright red".parse(), Ok(Color::LightRed));
/// assert_eq!("bright-red".parse(), Ok(Color::LightRed));
/// assert_eq!("silver".parse(), Ok(Color::Gray));
/// assert_eq!("dark-grey".parse(), Ok(Color::DarkGray));
/// assert_eq!("dark gray".parse(), Ok(Color::DarkGray));
/// assert_eq!("light-black".parse(), Ok(Color::DarkGray));
/// assert_eq!("white".parse(), Ok(Color::White));
/// assert_eq!("bright white".parse(), Ok(Color::White));
/// ```
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Color {
/// Resets the foreground or background color
#[default]
Reset,
/// ANSI Color: Black. Foreground: 30, Background: 40
Black,
/// ANSI Color: Red. Foreground: 31, Background: 41
Red,
/// ANSI Color: Green. Foreground: 32, Background: 42
Green,
/// ANSI Color: Yellow. Foreground: 33, Background: 43
Yellow,
/// ANSI Color: Blue. Foreground: 34, Background: 44
Blue,
/// ANSI Color: Magenta. Foreground: 35, Background: 45
Magenta,
/// ANSI Color: Cyan. Foreground: 36, Background: 46
Cyan,
/// ANSI Color: White. Foreground: 37, Background: 47
///
/// Note that this is sometimes called `silver` or `white` but we use `white` for bright white
Gray,
/// ANSI Color: Bright Black. Foreground: 90, Background: 100
///
/// Note that this is sometimes called `light black` or `bright black` but we use `dark gray`
DarkGray,
/// ANSI Color: Bright Red. Foreground: 91, Background: 101
LightRed,
/// ANSI Color: Bright Green. Foreground: 92, Background: 102
LightGreen,
/// ANSI Color: Bright Yellow. Foreground: 93, Background: 103
LightYellow,
/// ANSI Color: Bright Blue. Foreground: 94, Background: 104
LightBlue,
/// ANSI Color: Bright Magenta. Foreground: 95, Background: 105
LightMagenta,
/// ANSI Color: Bright Cyan. Foreground: 96, Background: 106
LightCyan,
/// ANSI Color: Bright White. Foreground: 97, Background: 107
/// Sometimes called `bright white` or `light white` in some terminals
White,
/// An RGB color
Rgb(u8, u8, u8),
/// An 8-bit 256 color. See <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>
Indexed(u8),
}
@@ -75,7 +151,7 @@ bitflags! {
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub struct Modifier: u16 {
const BOLD = 0b0000_0000_0001;
const DIM = 0b0000_0000_0010;
@@ -123,7 +199,9 @@ impl fmt::Debug for Modifier {
/// # use ratatui::layout::Rect;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::default().bg(Color::Red),
/// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED),
/// #[cfg(feature = "crossterm")]
/// Style::default().underline_color(Color::Green),
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
@@ -134,7 +212,9 @@ impl fmt::Debug for Modifier {
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Red),
/// add_modifier: Modifier::BOLD,
/// #[cfg(feature = "crossterm")]
/// underline_color: Some(Color::Green),
/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED,
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
@@ -160,17 +240,21 @@ impl fmt::Debug for Modifier {
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Reset),
/// #[cfg(feature = "crossterm")]
/// underline_color: Some(Color::Reset),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
/// );
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
#[cfg(feature = "crossterm")]
pub underline_color: Option<Color>,
pub add_modifier: Modifier,
pub sub_modifier: Modifier,
}
@@ -181,11 +265,24 @@ impl Default for Style {
}
}
impl Styled for Style {
type Item = Style;
fn style(&self) -> Style {
*self
}
fn set_style(self, style: Style) -> Self::Item {
self.patch(style)
}
}
impl Style {
pub const fn new() -> Style {
Style {
fg: None,
bg: None,
#[cfg(feature = "crossterm")]
underline_color: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
@@ -196,6 +293,8 @@ impl Style {
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
#[cfg(feature = "crossterm")]
underline_color: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::all(),
}
@@ -231,6 +330,27 @@ impl Style {
self
}
/// Changes the underline color. The text must be underlined with a modifier for this to work.
///
/// This uses a non-standard ANSI escape sequence. It is supported by most terminal emulators,
/// but is only implemented in the crossterm backend.
///
/// See [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters) code `58` and `59` for more information.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().underline_color(Color::Blue).add_modifier(Modifier::UNDERLINED);
/// let diff = Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED);
/// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED));
/// ```
#[cfg(feature = "crossterm")]
pub const fn underline_color(mut self, color: Color) -> Style {
self.underline_color = Some(color);
self
}
/// Changes the text emphasis.
///
/// When applied, it adds the given modifier to the `Style` modifiers.
@@ -245,9 +365,9 @@ impl Style {
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
/// assert_eq!(patched.sub_modifier, Modifier::empty());
/// ```
pub fn add_modifier(mut self, modifier: Modifier) -> Style {
self.sub_modifier.remove(modifier);
self.add_modifier.insert(modifier);
pub const fn add_modifier(mut self, modifier: Modifier) -> Style {
self.sub_modifier = self.sub_modifier.difference(modifier);
self.add_modifier = self.add_modifier.union(modifier);
self
}
@@ -265,9 +385,9 @@ impl Style {
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
/// ```
pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
self.add_modifier.remove(modifier);
self.sub_modifier.insert(modifier);
pub const fn remove_modifier(mut self, modifier: Modifier) -> Style {
self.add_modifier = self.add_modifier.difference(modifier);
self.sub_modifier = self.sub_modifier.union(modifier);
self
}
@@ -288,6 +408,11 @@ impl Style {
self.fg = other.fg.or(self.fg);
self.bg = other.bg.or(self.bg);
#[cfg(feature = "crossterm")]
{
self.underline_color = other.underline_color.or(self.underline_color);
}
self.add_modifier.remove(other.sub_modifier);
self.add_modifier.insert(other.add_modifier);
self.sub_modifier.remove(other.add_modifier);
@@ -298,7 +423,7 @@ impl Style {
}
/// Error type indicating a failure to parse a color string.
#[derive(Debug)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseColorError;
impl std::fmt::Display for ParseColorError {
@@ -311,9 +436,11 @@ impl std::error::Error for ParseColorError {}
/// Converts a string representation to a `Color` instance.
///
/// The `from_str` function attempts to parse the given string and convert it
/// to the corresponding `Color` variant. It supports named colors, RGB values,
/// and indexed colors. If the string cannot be parsed, a `ParseColorError` is returned.
/// The `from_str` function attempts to parse the given string and convert it to the corresponding
/// `Color` variant. It supports named colors, RGB values, and indexed colors. If the string cannot
/// be parsed, a `ParseColorError` is returned.
///
/// See the [`Color`](Color) documentation for more information on the supported color names.
///
/// # Examples
///
@@ -336,48 +463,64 @@ impl FromStr for Color {
type Err = ParseColorError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.to_lowercase().as_ref() {
"reset" => Self::Reset,
"black" => Self::Black,
"red" => Self::Red,
"green" => Self::Green,
"yellow" => Self::Yellow,
"blue" => Self::Blue,
"magenta" => Self::Magenta,
"cyan" => Self::Cyan,
"gray" => Self::Gray,
"darkgray" | "dark gray" => Self::DarkGray,
"lightred" | "light red" => Self::LightRed,
"lightgreen" | "light green" => Self::LightGreen,
"lightyellow" | "light yellow" => Self::LightYellow,
"lightblue" | "light blue" => Self::LightBlue,
"lightmagenta" | "light magenta" => Self::LightMagenta,
"lightcyan" | "light cyan" => Self::LightCyan,
"white" => Self::White,
_ => {
if let Ok(index) = s.parse::<u8>() {
Self::Indexed(index)
} else if let (Ok(r), Ok(g), Ok(b)) = {
if !s.starts_with('#') || s.len() != 7 {
Ok(
// There is a mix of different color names and formats in the wild.
// This is an attempt to support as many as possible.
match s
.to_lowercase()
.replace([' ', '-', '_'], "")
.replace("bright", "light")
.replace("grey", "gray")
.replace("silver", "gray")
.replace("lightblack", "darkgray")
.replace("lightwhite", "white")
.replace("lightgray", "white")
.as_ref()
{
"reset" => Self::Reset,
"black" => Self::Black,
"red" => Self::Red,
"green" => Self::Green,
"yellow" => Self::Yellow,
"blue" => Self::Blue,
"magenta" => Self::Magenta,
"cyan" => Self::Cyan,
"gray" => Self::Gray,
"darkgray" => Self::DarkGray,
"lightred" => Self::LightRed,
"lightgreen" => Self::LightGreen,
"lightyellow" => Self::LightYellow,
"lightblue" => Self::LightBlue,
"lightmagenta" => Self::LightMagenta,
"lightcyan" => Self::LightCyan,
"white" => Self::White,
_ => {
if let Ok(index) = s.parse::<u8>() {
Self::Indexed(index)
} else if let (Ok(r), Ok(g), Ok(b)) = {
if !s.starts_with('#') || s.len() != 7 {
return Err(ParseColorError);
}
(
u8::from_str_radix(&s[1..3], 16),
u8::from_str_radix(&s[3..5], 16),
u8::from_str_radix(&s[5..7], 16),
)
} {
Self::Rgb(r, g, b)
} else {
return Err(ParseColorError);
}
(
u8::from_str_radix(&s[1..3], 16),
u8::from_str_radix(&s[3..5], 16),
u8::from_str_radix(&s[5..7], 16),
)
} {
Self::Rgb(r, g, b)
} else {
return Err(ParseColorError);
}
}
})
},
)
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use super::*;
fn styles() -> Vec<Style> {
@@ -443,7 +586,7 @@ mod tests {
}
#[test]
fn test_modifier_debug() {
fn modifier_debug() {
assert_eq!(format!("{:?}", Modifier::empty()), "NONE");
assert_eq!(format!("{:?}", Modifier::BOLD), "BOLD");
assert_eq!(format!("{:?}", Modifier::DIM), "DIM");
@@ -465,32 +608,81 @@ mod tests {
}
#[test]
fn test_rgb_color() {
fn from_rgb_color() {
let color: Color = Color::from_str("#FF0000").unwrap();
assert_eq!(color, Color::Rgb(255, 0, 0));
}
#[test]
fn test_indexed_color() {
fn from_indexed_color() {
let color: Color = Color::from_str("10").unwrap();
assert_eq!(color, Color::Indexed(10));
}
#[test]
fn test_custom_color() {
let color: Color = Color::from_str("lightblue").unwrap();
assert_eq!(color, Color::LightBlue);
fn from_ansi_color() -> Result<(), Box<dyn Error>> {
assert_eq!(Color::from_str("reset")?, Color::Reset);
assert_eq!(Color::from_str("black")?, Color::Black);
assert_eq!(Color::from_str("red")?, Color::Red);
assert_eq!(Color::from_str("green")?, Color::Green);
assert_eq!(Color::from_str("yellow")?, Color::Yellow);
assert_eq!(Color::from_str("blue")?, Color::Blue);
assert_eq!(Color::from_str("magenta")?, Color::Magenta);
assert_eq!(Color::from_str("cyan")?, Color::Cyan);
assert_eq!(Color::from_str("gray")?, Color::Gray);
assert_eq!(Color::from_str("darkgray")?, Color::DarkGray);
assert_eq!(Color::from_str("lightred")?, Color::LightRed);
assert_eq!(Color::from_str("lightgreen")?, Color::LightGreen);
assert_eq!(Color::from_str("lightyellow")?, Color::LightYellow);
assert_eq!(Color::from_str("lightblue")?, Color::LightBlue);
assert_eq!(Color::from_str("lightmagenta")?, Color::LightMagenta);
assert_eq!(Color::from_str("lightcyan")?, Color::LightCyan);
assert_eq!(Color::from_str("white")?, Color::White);
// aliases
assert_eq!(Color::from_str("lightblack")?, Color::DarkGray);
assert_eq!(Color::from_str("lightwhite")?, Color::White);
assert_eq!(Color::from_str("lightgray")?, Color::White);
// silver = grey = gray
assert_eq!(Color::from_str("grey")?, Color::Gray);
assert_eq!(Color::from_str("silver")?, Color::Gray);
// spaces are ignored
assert_eq!(Color::from_str("light black")?, Color::DarkGray);
assert_eq!(Color::from_str("light white")?, Color::White);
assert_eq!(Color::from_str("light gray")?, Color::White);
// dashes are ignored
assert_eq!(Color::from_str("light-black")?, Color::DarkGray);
assert_eq!(Color::from_str("light-white")?, Color::White);
assert_eq!(Color::from_str("light-gray")?, Color::White);
// underscores are ignored
assert_eq!(Color::from_str("light_black")?, Color::DarkGray);
assert_eq!(Color::from_str("light_white")?, Color::White);
assert_eq!(Color::from_str("light_gray")?, Color::White);
// bright = light
assert_eq!(Color::from_str("bright-black")?, Color::DarkGray);
assert_eq!(Color::from_str("bright-white")?, Color::White);
// bright = light
assert_eq!(Color::from_str("brightblack")?, Color::DarkGray);
assert_eq!(Color::from_str("brightwhite")?, Color::White);
Ok(())
}
#[test]
fn test_invalid_colors() {
fn from_invalid_colors() {
let bad_colors = [
"invalid_color", // not a color string
"abcdef0", // 7 chars is not a color
" bcdefa", // doesn't start with a '#'
"blue ", // has space at end
" blue", // has space at start
"#abcdef00", // too many chars
"resett", // typo
"lightblackk", // typo
];
for bad_color in bad_colors {
@@ -500,9 +692,178 @@ mod tests {
);
}
}
#[test]
fn style_can_be_const() {
const _DEFAULT_STYLE: Style = Style::new().fg(Color::Red).bg(Color::Black);
const RED: Color = Color::Red;
const BLACK: Color = Color::Black;
const BOLD: Modifier = Modifier::BOLD;
const ITALIC: Modifier = Modifier::ITALIC;
const _RESET: Style = Style::reset();
const _RED_FG: Style = Style::new().fg(RED);
const _BLACK_BG: Style = Style::new().bg(BLACK);
const _ADD_BOLD: Style = Style::new().add_modifier(BOLD);
const _REMOVE_ITALIC: Style = Style::new().remove_modifier(ITALIC);
const ALL: Style = Style::new()
.fg(RED)
.bg(BLACK)
.add_modifier(BOLD)
.remove_modifier(ITALIC);
assert_eq!(
ALL,
Style::new()
.fg(Color::Red)
.bg(Color::Black)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
)
}
#[test]
fn style_can_be_stylized() {
// foreground colors
assert_eq!(Style::new().black(), Style::new().fg(Color::Black));
assert_eq!(Style::new().red(), Style::new().fg(Color::Red));
assert_eq!(Style::new().green(), Style::new().fg(Color::Green));
assert_eq!(Style::new().yellow(), Style::new().fg(Color::Yellow));
assert_eq!(Style::new().blue(), Style::new().fg(Color::Blue));
assert_eq!(Style::new().magenta(), Style::new().fg(Color::Magenta));
assert_eq!(Style::new().cyan(), Style::new().fg(Color::Cyan));
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
assert_eq!(Style::new().gray(), Style::new().fg(Color::Gray));
assert_eq!(Style::new().dark_gray(), Style::new().fg(Color::DarkGray));
assert_eq!(Style::new().light_red(), Style::new().fg(Color::LightRed));
assert_eq!(
Style::new().light_green(),
Style::new().fg(Color::LightGreen)
);
assert_eq!(
Style::new().light_yellow(),
Style::new().fg(Color::LightYellow)
);
assert_eq!(Style::new().light_blue(), Style::new().fg(Color::LightBlue));
assert_eq!(
Style::new().light_magenta(),
Style::new().fg(Color::LightMagenta)
);
assert_eq!(Style::new().light_cyan(), Style::new().fg(Color::LightCyan));
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
// Background colors
assert_eq!(Style::new().on_black(), Style::new().bg(Color::Black));
assert_eq!(Style::new().on_red(), Style::new().bg(Color::Red));
assert_eq!(Style::new().on_green(), Style::new().bg(Color::Green));
assert_eq!(Style::new().on_yellow(), Style::new().bg(Color::Yellow));
assert_eq!(Style::new().on_blue(), Style::new().bg(Color::Blue));
assert_eq!(Style::new().on_magenta(), Style::new().bg(Color::Magenta));
assert_eq!(Style::new().on_cyan(), Style::new().bg(Color::Cyan));
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
assert_eq!(Style::new().on_gray(), Style::new().bg(Color::Gray));
assert_eq!(
Style::new().on_dark_gray(),
Style::new().bg(Color::DarkGray)
);
assert_eq!(
Style::new().on_light_red(),
Style::new().bg(Color::LightRed)
);
assert_eq!(
Style::new().on_light_green(),
Style::new().bg(Color::LightGreen)
);
assert_eq!(
Style::new().on_light_yellow(),
Style::new().bg(Color::LightYellow)
);
assert_eq!(
Style::new().on_light_blue(),
Style::new().bg(Color::LightBlue)
);
assert_eq!(
Style::new().on_light_magenta(),
Style::new().bg(Color::LightMagenta)
);
assert_eq!(
Style::new().on_light_cyan(),
Style::new().bg(Color::LightCyan)
);
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
// Add Modifiers
assert_eq!(
Style::new().bold(),
Style::new().add_modifier(Modifier::BOLD)
);
assert_eq!(Style::new().dim(), Style::new().add_modifier(Modifier::DIM));
assert_eq!(
Style::new().italic(),
Style::new().add_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::new().underlined(),
Style::new().add_modifier(Modifier::UNDERLINED)
);
assert_eq!(
Style::new().slow_blink(),
Style::new().add_modifier(Modifier::SLOW_BLINK)
);
assert_eq!(
Style::new().rapid_blink(),
Style::new().add_modifier(Modifier::RAPID_BLINK)
);
assert_eq!(
Style::new().reversed(),
Style::new().add_modifier(Modifier::REVERSED)
);
assert_eq!(
Style::new().hidden(),
Style::new().add_modifier(Modifier::HIDDEN)
);
assert_eq!(
Style::new().crossed_out(),
Style::new().add_modifier(Modifier::CROSSED_OUT)
);
// Remove Modifiers
assert_eq!(
Style::new().not_bold(),
Style::new().remove_modifier(Modifier::BOLD)
);
assert_eq!(
Style::new().not_dim(),
Style::new().remove_modifier(Modifier::DIM)
);
assert_eq!(
Style::new().not_italic(),
Style::new().remove_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::new().not_underlined(),
Style::new().remove_modifier(Modifier::UNDERLINED)
);
assert_eq!(
Style::new().not_slow_blink(),
Style::new().remove_modifier(Modifier::SLOW_BLINK)
);
assert_eq!(
Style::new().not_rapid_blink(),
Style::new().remove_modifier(Modifier::RAPID_BLINK)
);
assert_eq!(
Style::new().not_reversed(),
Style::new().remove_modifier(Modifier::REVERSED)
);
assert_eq!(
Style::new().not_hidden(),
Style::new().remove_modifier(Modifier::HIDDEN)
);
assert_eq!(
Style::new().not_crossed_out(),
Style::new().remove_modifier(Modifier::CROSSED_OUT)
);
// reset
assert_eq!(Style::new().reset(), Style::reset());
}
}

260
src/style/stylize.rs Normal file
View File

@@ -0,0 +1,260 @@
use paste::paste;
use crate::{
style::{Color, Modifier, Style},
text::Span,
};
/// A trait for objects that have a `Style`.
///
/// This trait enables generic code to be written that can interact with any object that has a
/// `Style`. This is used by the `Stylize` trait to allow generic code to be written that can
/// interact with any object that can be styled.
pub trait Styled {
type Item;
fn style(&self) -> Style;
fn set_style(self, style: Style) -> Self::Item;
}
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
/// etc) and one for setting the background color (`on_red()`, `on_blue()`, etc.). Each method sets
/// the color of the style to the corresponding color.
///
/// ```rust,ignore
/// color!(black);
///
/// // generates
///
/// #[doc = "Sets the foreground color to [`black`](Color::Black)."]
/// fn black(self) -> T {
/// self.fg(Color::Black)
/// }
///
/// #[doc = "Sets the background color to [`black`](Color::Black)."]
/// fn on_black(self) -> T {
/// self.bg(Color::Black)
/// }
/// ```
macro_rules! color {
( $color:ident ) => {
paste! {
#[doc = "Sets the foreground color to [`" $color "`](Color::" $color:camel ")."]
fn $color(self) -> T {
self.fg(Color::[<$color:camel>])
}
#[doc = "Sets the background color to [`" $color "`](Color::" $color:camel ")."]
fn [<on_ $color>](self) -> T {
self.bg(Color::[<$color:camel>])
}
}
};
}
/// Generates a method for a modifier (`bold()`, `italic()`, etc.). Each method sets the modifier
/// of the style to the corresponding modifier.
///
/// # Examples
///
/// ```rust,ignore
/// modifier!(bold);
///
/// // generates
///
/// #[doc = "Adds the [`BOLD`](Modifier::BOLD) modifier."]
/// fn bold(self) -> T {
/// self.add_modifier(Modifier::BOLD)
/// }
///
/// #[doc = "Removes the [`BOLD`](Modifier::BOLD) modifier."]
/// fn not_bold(self) -> T {
/// self.remove_modifier(Modifier::BOLD)
/// }
/// ```
macro_rules! modifier {
( $modifier:ident ) => {
paste! {
#[doc = "Adds the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
fn [<$modifier>](self) -> T {
self.add_modifier(Modifier::[<$modifier:upper>])
}
}
paste! {
#[doc = "Removes the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
fn [<not_ $modifier>](self) -> T {
self.remove_modifier(Modifier::[<$modifier:upper>])
}
}
};
}
/// The trait that enables something to be have a style.
///
/// # Examples
/// ```
/// use ratatui::{
/// style::{Color, Modifier, Style, Styled, Stylize},
/// text::Span,
/// };
///
/// assert_eq!(
/// "hello".red().on_blue().bold(),
/// Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
/// )
pub trait Stylize<'a, T>: Sized {
fn bg(self, color: Color) -> T;
fn fg<S: Into<Color>>(self, color: S) -> T;
fn reset(self) -> T;
fn add_modifier(self, modifier: Modifier) -> T;
fn remove_modifier(self, modifier: Modifier) -> T;
color!(black);
color!(red);
color!(green);
color!(yellow);
color!(blue);
color!(magenta);
color!(cyan);
color!(gray);
color!(dark_gray);
color!(light_red);
color!(light_green);
color!(light_yellow);
color!(light_blue);
color!(light_magenta);
color!(light_cyan);
color!(white);
modifier!(bold);
modifier!(dim);
modifier!(italic);
modifier!(underlined);
modifier!(slow_blink);
modifier!(rapid_blink);
modifier!(reversed);
modifier!(hidden);
modifier!(crossed_out);
}
impl<'a, T, U> Stylize<'a, T> for U
where
U: Styled<Item = T>,
{
fn bg(self, color: Color) -> T {
let style = self.style().bg(color);
self.set_style(style)
}
fn fg<S: Into<Color>>(self, color: S) -> T {
let style = self.style().fg(color.into());
self.set_style(style)
}
fn add_modifier(self, modifier: Modifier) -> T {
let style = self.style().add_modifier(modifier);
self.set_style(style)
}
fn remove_modifier(self, modifier: Modifier) -> T {
let style = self.style().remove_modifier(modifier);
self.set_style(style)
}
fn reset(self) -> T {
self.set_style(Style::reset())
}
}
impl<'a> Styled for &'a str {
type Item = Span<'a>;
fn style(&self) -> Style {
Style::default()
}
fn set_style(self, style: Style) -> Self::Item {
Span::styled(self, style)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reset() {
assert_eq!(
"hello".on_cyan().light_red().bold().underlined().reset(),
Span::styled("hello", Style::reset())
)
}
#[test]
fn fg() {
let cyan_fg = Style::default().fg(Color::Cyan);
assert_eq!("hello".cyan(), Span::styled("hello", cyan_fg));
}
#[test]
fn bg() {
let cyan_bg = Style::default().bg(Color::Cyan);
assert_eq!("hello".on_cyan(), Span::styled("hello", cyan_bg));
}
#[test]
fn color_modifier() {
let cyan_bold = Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);
assert_eq!("hello".cyan().bold(), Span::styled("hello", cyan_bold))
}
#[test]
fn fg_bg() {
let cyan_fg_bg = Style::default().bg(Color::Cyan).fg(Color::Cyan);
assert_eq!("hello".cyan().on_cyan(), Span::styled("hello", cyan_fg_bg))
}
#[test]
fn repeated_attributes() {
let cyan_bg = Style::default().bg(Color::Cyan);
let cyan_fg = Style::default().fg(Color::Cyan);
// Behavior: the last one set is the definitive one
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", cyan_bg));
assert_eq!("hello".red().cyan(), Span::styled("hello", cyan_fg));
}
#[test]
fn all_chained() {
let all_modifier_black = Style::default()
.bg(Color::Black)
.fg(Color::Black)
.add_modifier(
Modifier::UNDERLINED
| Modifier::BOLD
| Modifier::DIM
| Modifier::SLOW_BLINK
| Modifier::REVERSED
| Modifier::CROSSED_OUT,
);
assert_eq!(
"hello"
.on_black()
.black()
.bold()
.underlined()
.dim()
.slow_blink()
.crossed_out()
.reversed(),
Span::styled("hello", all_modifier_black)
);
}
}

View File

@@ -1,228 +0,0 @@
use crate::{
style::{Color, Modifier, Style},
text::Span,
};
pub trait Styled {
type Item;
fn style(&self) -> Style;
fn set_style(self, style: Style) -> Self::Item;
}
// Otherwise rustfmt behaves weirdly for some reason
macro_rules! calculated_docs {
($(#[doc = $doc:expr] $item:item)*) => { $(#[doc = $doc] $item)* };
}
macro_rules! modifier_method {
($method_name:ident Modifier::$modifier:ident) => {
calculated_docs! {
#[doc = concat!(
"Applies the [`",
stringify!($modifier),
"`](crate::style::Modifier::",
stringify!($modifier),
") modifier.",
)]
fn $method_name(self) -> T {
self.modifier(Modifier::$modifier)
}
}
};
}
macro_rules! color_method {
($method_name_fg:ident, $method_name_bg:ident Color::$color:ident) => {
calculated_docs! {
#[doc = concat!(
"Sets the foreground color to [`",
stringify!($color),
"`](Color::",
stringify!($color),
")."
)]
fn $method_name_fg(self) -> T {
self.fg(Color::$color)
}
#[doc = concat!(
"Sets the background color to [`",
stringify!($color),
"`](Color::",
stringify!($color),
")."
)]
fn $method_name_bg(self) -> T {
self.bg(Color::$color)
}
}
};
}
/// The trait that enables something to be have a style.
/// # Examples
/// ```
/// use ratatui::{
/// style::{Color, Modifier, Style, Styled, Stylize},
/// text::Span,
/// };
///
/// assert_eq!(
/// "hello".red().on_blue().bold(),
/// Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
/// )
pub trait Stylize<'a, T>: Sized {
// Colors
fn fg<S: Into<Color>>(self, color: S) -> T;
fn bg(self, color: Color) -> T;
color_method!(black, on_black Color::Black);
color_method!(red, on_red Color::Red);
color_method!(green, on_green Color::Green);
color_method!(yellow, on_yellow Color::Yellow);
color_method!(blue, on_blue Color::Blue);
color_method!(magenta, on_magenta Color::Magenta);
color_method!(cyan, on_cyan Color::Cyan);
color_method!(gray, on_gray Color::Gray);
color_method!(dark_gray, on_dark_gray Color::DarkGray);
color_method!(light_red, on_light_red Color::LightRed);
color_method!(light_green, on_light_green Color::LightGreen);
color_method!(light_yellow, on_light_yellow Color::LightYellow);
color_method!(light_blue, on_light_blue Color::LightBlue);
color_method!(light_magenta, on_light_magenta Color::LightMagenta);
color_method!(light_cyan, on_light_cyan Color::LightCyan);
color_method!(white, on_white Color::White);
// Styles
fn reset(self) -> T;
// Modifiers
fn modifier(self, modifier: Modifier) -> T;
modifier_method!(bold Modifier::BOLD);
modifier_method!(dimmed Modifier::DIM);
modifier_method!(italic Modifier::ITALIC);
modifier_method!(underline Modifier::UNDERLINED);
modifier_method!(slow_blink Modifier::SLOW_BLINK);
modifier_method!(rapid_blink Modifier::RAPID_BLINK);
modifier_method!(reversed Modifier::REVERSED);
modifier_method!(hidden Modifier::HIDDEN);
modifier_method!(crossed_out Modifier::CROSSED_OUT);
}
impl<'a, T, U> Stylize<'a, T> for U
where
U: Styled<Item = T>,
{
fn fg<S: Into<Color>>(self, color: S) -> T {
let style = self.style().fg(color.into());
self.set_style(style)
}
fn modifier(self, modifier: Modifier) -> T {
let style = self.style().add_modifier(modifier);
self.set_style(style)
}
fn bg(self, color: Color) -> T {
let style = self.style().bg(color);
self.set_style(style)
}
fn reset(self) -> T {
self.set_style(Style::default())
}
}
impl<'a> Styled for &'a str {
type Item = Span<'a>;
fn style(&self) -> Style {
Style::default()
}
fn set_style(self, style: Style) -> Self::Item {
Span::styled(self, style)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reset() {
assert_eq!(
"hello".on_cyan().light_red().bold().underline().reset(),
Span::from("hello")
)
}
#[test]
fn fg() {
let cyan_fg = Style::default().fg(Color::Cyan);
assert_eq!("hello".cyan(), Span::styled("hello", cyan_fg));
}
#[test]
fn bg() {
let cyan_bg = Style::default().bg(Color::Cyan);
assert_eq!("hello".on_cyan(), Span::styled("hello", cyan_bg));
}
#[test]
fn color_modifier() {
let cyan_bold = Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);
assert_eq!("hello".cyan().bold(), Span::styled("hello", cyan_bold))
}
#[test]
fn fg_bg() {
let cyan_fg_bg = Style::default().bg(Color::Cyan).fg(Color::Cyan);
assert_eq!("hello".cyan().on_cyan(), Span::styled("hello", cyan_fg_bg))
}
#[test]
fn repeated_attributes() {
let cyan_bg = Style::default().bg(Color::Cyan);
let cyan_fg = Style::default().fg(Color::Cyan);
// Behavior: the last one set is the definitive one
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", cyan_bg));
assert_eq!("hello".red().cyan(), Span::styled("hello", cyan_fg));
}
#[test]
fn all_chained() {
let all_modifier_black = Style::default()
.bg(Color::Black)
.fg(Color::Black)
.add_modifier(
Modifier::UNDERLINED
| Modifier::BOLD
| Modifier::DIM
| Modifier::SLOW_BLINK
| Modifier::REVERSED
| Modifier::CROSSED_OUT,
);
assert_eq!(
"hello"
.on_black()
.black()
.bold()
.underline()
.dimmed()
.slow_blink()
.crossed_out()
.reversed(),
Span::styled("hello", all_modifier_black)
);
}
}

View File

@@ -21,6 +21,12 @@ pub mod block {
pub empty: &'static str,
}
impl Default for Set {
fn default() -> Self {
NINE_LEVELS
}
}
pub const THREE_LEVELS: Set = Set {
full: FULL,
seven_eighths: FULL,
@@ -69,6 +75,12 @@ pub mod bar {
pub empty: &'static str,
}
impl Default for Set {
fn default() -> Self {
NINE_LEVELS
}
}
pub const THREE_LEVELS: Set = Set {
full: FULL,
seven_eighths: FULL,
@@ -158,6 +170,12 @@ pub mod line {
pub cross: &'static str,
}
impl Default for Set {
fn default() -> Self {
NORMAL
}
}
pub const NORMAL: Set = Set {
vertical: VERTICAL,
horizontal: HORIZONTAL,
@@ -222,9 +240,10 @@ pub mod braille {
}
/// Marker to use when plotting data points
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy)]
pub enum Marker {
/// One point per cell in shape of dot
#[default]
Dot,
/// One point per cell in shape of a block
Block,
@@ -233,3 +252,52 @@ pub enum Marker {
/// Up to 8 points per cell
Braille,
}
pub mod scrollbar {
use super::{block, line};
/// Scrollbar Set
/// ```text
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ end
/// │ │ └──── track
/// │ └──────── thumb
/// └─────────── begin
/// ```
#[derive(Debug, Default, Clone)]
pub struct Set {
pub track: &'static str,
pub thumb: &'static str,
pub begin: &'static str,
pub end: &'static str,
}
pub const DOUBLE_VERTICAL: Set = Set {
track: line::DOUBLE_VERTICAL,
thumb: block::FULL,
begin: "",
end: "",
};
pub const DOUBLE_HORIZONTAL: Set = Set {
track: line::DOUBLE_HORIZONTAL,
thumb: block::FULL,
begin: "",
end: "",
};
pub const VERTICAL: Set = Set {
track: line::VERTICAL,
thumb: block::FULL,
begin: "",
end: "",
};
pub const HORIZONTAL: Set = Set {
track: line::HORIZONTAL,
thumb: block::FULL,
begin: "",
end: "",
};
}

View File

@@ -7,22 +7,23 @@ use crate::{
widgets::{StatefulWidget, Widget},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub enum Viewport {
#[default]
Fullscreen,
Inline(u16),
Fixed(Rect),
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// Options to pass to [`Terminal::with_options`]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct TerminalOptions {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
/// Interface to the terminal backed by Termion
#[derive(Debug)]
#[derive(Debug, Default, Clone)]
pub struct Terminal<B>
where
B: Backend,
@@ -46,6 +47,7 @@ where
}
/// Represents a consistent terminal interface for rendering.
#[derive(Debug)]
pub struct Frame<'a, B: 'a>
where
B: Backend,
@@ -137,6 +139,7 @@ where
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
/// [`Terminal::draw`].
#[derive(Debug, Clone)]
pub struct CompletedFrame<'a> {
pub buffer: &'a Buffer,
pub area: Rect,

View File

@@ -1,470 +0,0 @@
//! 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:
//! - 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 [`Line`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//!
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Line`].
//!
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations
//! so that you can start by using simple `String` or `&str` and then promote them to the previous
//! primitives when you need additional styling capabilities.
//!
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
//! its `title` property (which is a [`Line`] under the hood):
//!
//! ```rust
//! # use ratatui::widgets::Block;
//! # use ratatui::text::{Span, Line};
//! # use ratatui::style::{Color, Style};
//! // A simple string with no styling.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
//! // ])
//! let block = Block::default().title("My title");
//!
//! // A simple string with a unique style.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
//! // ])
//! let block = Block::default().title(
//! Span::styled("My title", Style::default().fg(Color::Yellow))
//! );
//!
//! // A string with multiple styles.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
//! // Span { content: Cow::Borrowed(" title"), .. }
//! // ])
//! let block = Block::default().title(vec![
//! Span::styled("My", Style::default().fg(Color::Yellow)),
//! Span::raw(" title"),
//! ]);
//! ```
use std::{borrow::Cow, fmt::Debug};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::style::{Style, Styled};
mod line;
mod masked;
mod spans;
#[allow(deprecated)]
pub use {line::Line, masked::Masked, spans::Spans};
/// A grapheme associated to a style.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StyledGrapheme<'a> {
pub symbol: &'a str,
pub style: Style,
}
/// A string where all graphemes have the same style.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span<'a> {
pub content: Cow<'a, str>,
pub style: Style,
}
impl<'a> Span<'a> {
/// Create a span with no style.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::Span;
/// Span::raw("My text");
/// Span::raw(String::from("My text"));
/// ```
pub fn raw<T>(content: T) -> Span<'a>
where
T: Into<Cow<'a, str>>,
{
Span {
content: content.into(),
style: Style::default(),
}
}
/// Create a span with a style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::text::Span;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Span::styled("My text", style);
/// Span::styled(String::from("My text"), style);
/// ```
pub fn styled<T>(content: T, style: Style) -> Span<'a>
where
T: Into<Cow<'a, str>>,
{
Span {
content: content.into(),
style,
}
}
/// Returns the width of the content held by this span.
pub fn width(&self) -> usize {
self.content.width()
}
/// Returns an iterator over the graphemes held by this span.
///
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
/// the resulting [`Style`].
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, StyledGrapheme};
/// # use ratatui::style::{Color, Modifier, Style};
/// # use std::iter::Iterator;
/// let style = Style::default().fg(Color::Yellow);
/// let span = Span::styled("Text", style);
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
/// let styled_graphemes = span.styled_graphemes(style);
/// assert_eq!(
/// vec![
/// StyledGrapheme {
/// symbol: "T",
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// },
/// StyledGrapheme {
/// symbol: "e",
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// },
/// StyledGrapheme {
/// symbol: "x",
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// },
/// StyledGrapheme {
/// symbol: "t",
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// },
/// ],
/// styled_graphemes.collect::<Vec<StyledGrapheme>>()
/// );
/// ```
pub fn styled_graphemes(
&'a self,
base_style: Style,
) -> impl Iterator<Item = StyledGrapheme<'a>> {
UnicodeSegmentation::graphemes(self.content.as_ref(), true)
.map(move |g| StyledGrapheme {
symbol: g,
style: base_style.patch(self.style),
})
.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> {
fn from(s: String) -> Span<'a> {
Span::raw(s)
}
}
impl<'a> From<&'a str> for Span<'a> {
fn from(s: &'a str) -> Span<'a> {
Span::raw(s)
}
}
impl<'a> Styled for Span<'a> {
type Item = Span<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self {
Self::styled(self.content, style)
}
}
/// A string split over multiple lines where each line is composed of several clusters, each with
/// their own style.
///
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
///
/// // An initial two lines of `Text` built from a `&str`
/// let mut text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
///
/// // Adding two more unstyled lines
/// text.extend(Text::raw("These are two\nmore lines!"));
/// assert_eq!(4, text.height());
///
/// // Adding a final two styled lines
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
/// assert_eq!(6, text.height());
/// ```
#[derive(Debug, Clone, PartialEq, Default, Eq)]
pub struct Text<'a> {
pub lines: Vec<Line<'a>>,
}
impl<'a> Text<'a> {
/// Create some text (potentially multiple lines) with no style.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// Text::raw("The first line\nThe second line");
/// Text::raw(String::from("The first line\nThe second line"));
/// ```
pub fn raw<T>(content: T) -> Text<'a>
where
T: Into<Cow<'a, str>>,
{
let lines: Vec<_> = match content.into() {
Cow::Borrowed("") => vec![Line::from("")],
Cow::Borrowed(s) => s.lines().map(Line::from).collect(),
Cow::Owned(s) if s.is_empty() => vec![Line::from("")],
Cow::Owned(s) => s.lines().map(|l| Line::from(l.to_owned())).collect(),
};
Text::from(lines)
}
/// Create some text (potentially multiple lines) with a style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style);
/// ```
pub fn styled<T>(content: T, style: Style) -> Text<'a>
where
T: Into<Cow<'a, str>>,
{
let mut text = Text::raw(content);
text.patch_style(style);
text
}
/// Returns the max width of all the lines.
///
/// ## Examples
///
/// ```rust
/// use ratatui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(15, text.width());
/// ```
pub fn width(&self) -> usize {
self.lines.iter().map(Line::width).max().unwrap_or_default()
}
/// Returns the height.
///
/// ## Examples
///
/// ```rust
/// use ratatui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
/// ```
pub fn height(&self) -> usize {
self.lines.len()
}
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_text = Text::raw("The first line\nThe second line");
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
/// assert_ne!(raw_text, styled_text);
///
/// raw_text.patch_style(style);
/// assert_eq!(raw_text, styled_text);
/// ```
pub fn patch_style(&mut self, style: Style) {
for line in &mut self.lines {
line.patch_style(style);
}
}
/// Resets the style of the Text.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Line, Text};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut text = Text::styled("The first line\nThe second line", style);
///
/// text.reset_style();
/// for line in &text.lines {
/// for span in &line.spans {
/// assert_eq!(Style::reset(), span.style);
/// }
/// }
/// ```
pub fn reset_style(&mut self) {
for line in &mut self.lines {
line.reset_style();
}
}
}
impl<'a> From<String> for Text<'a> {
fn from(s: String) -> Text<'a> {
Text::raw(s)
}
}
impl<'a> From<&'a str> for Text<'a> {
fn from(s: &'a str) -> Text<'a> {
Text::raw(s)
}
}
impl<'a> From<Cow<'a, str>> for Text<'a> {
fn from(s: Cow<'a, str>) -> Text<'a> {
Text::raw(s)
}
}
impl<'a> From<Span<'a>> for Text<'a> {
fn from(span: Span<'a>) -> Text<'a> {
Text {
lines: vec![Line::from(span)],
}
}
}
#[allow(deprecated)]
impl<'a> From<Spans<'a>> for Text<'a> {
fn from(spans: Spans<'a>) -> Text<'a> {
Text {
lines: vec![spans.into()],
}
}
}
impl<'a> From<Line<'a>> for Text<'a> {
fn from(line: Line<'a>) -> Text<'a> {
Text { lines: vec![line] }
}
}
#[allow(deprecated)]
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
Text {
lines: lines.into_iter().map(|l| l.0.into()).collect(),
}
}
}
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
Text { lines }
}
}
impl<'a> IntoIterator for Text<'a> {
type Item = Line<'a>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.lines.into_iter()
}
}
impl<'a, T> Extend<T> for Text<'a>
where
T: Into<Line<'a>>,
{
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
let lines = iter.into_iter().map(Into::into);
self.lines.extend(lines);
}
}

31
src/text/grapheme.rs Normal file
View File

@@ -0,0 +1,31 @@
use crate::style::{Style, Styled};
/// A grapheme associated to a style.
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
/// it actually is not a member of the text type hierarchy (`Text` -> `Line` -> `Span`).
/// It is a separate type used mostly for rendering purposes. A `Span` consists of components that
/// can be split into `StyledGrapheme`s, but it does not contain a collection of `StyledGrapheme`s.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct StyledGrapheme<'a> {
pub symbol: &'a str,
pub style: Style,
}
impl<'a> StyledGrapheme<'a> {
pub fn new(symbol: &'a str, style: Style) -> StyledGrapheme<'a> {
StyledGrapheme { symbol, style }
}
}
impl<'a> Styled for StyledGrapheme<'a> {
type Item = StyledGrapheme<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(mut self, style: Style) -> Self::Item {
self.style = style;
self
}
}

View File

@@ -1,14 +1,34 @@
#![allow(deprecated)]
use super::{Span, Spans, Style};
use std::borrow::Cow;
use super::{Span, Spans, Style, StyledGrapheme};
use crate::layout::Alignment;
#[derive(Debug, Clone, PartialEq, Default, Eq)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Line<'a> {
pub spans: Vec<Span<'a>>,
pub alignment: Option<Alignment>,
}
impl<'a> Line<'a> {
/// Create a line with a style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::text::Line;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Line::styled("My text", style);
/// Line::styled(String::from("My text"), style);
/// ```
pub fn styled<T>(content: T, style: Style) -> Line<'a>
where
T: Into<Cow<'a, str>>,
{
Line::from(Span::styled(content, style))
}
/// Returns the width of the underlying string.
///
/// ## Examples
@@ -26,6 +46,38 @@ impl<'a> Line<'a> {
self.spans.iter().map(Span::width).sum()
}
/// Returns an iterator over the graphemes held by this line.
///
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
/// the resulting [`Style`].
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Line, StyledGrapheme};
/// # use ratatui::style::{Color, Modifier, Style};
/// # use std::iter::Iterator;
/// let line = Line::styled("Text", Style::default().fg(Color::Yellow));
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
/// assert_eq!(
/// line.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
/// vec![
/// StyledGrapheme::new("T", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// StyledGrapheme::new("e", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// StyledGrapheme::new("x", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// StyledGrapheme::new("t", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// ]
/// );
/// ```
pub fn styled_graphemes(
&'a self,
base_style: Style,
) -> impl Iterator<Item = StyledGrapheme<'a>> {
self.spans
.iter()
.flat_map(move |span| span.styled_graphemes(base_style))
}
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
///
/// ## Examples
@@ -146,7 +198,7 @@ mod tests {
use crate::{
layout::Alignment,
style::{Color, Modifier, Style},
text::{Line, Span, Spans},
text::{Line, Span, Spans, StyledGrapheme},
};
#[test]
@@ -248,4 +300,34 @@ mod tests {
let line = Line::from("This is default");
assert_eq!(None, line.alignment);
}
#[test]
fn styled_graphemes() {
const RED: Style = Style::new().fg(Color::Red);
const GREEN: Style = Style::new().fg(Color::Green);
const BLUE: Style = Style::new().fg(Color::Blue);
const RED_ON_WHITE: Style = Style::new().fg(Color::Red).bg(Color::White);
const GREEN_ON_WHITE: Style = Style::new().fg(Color::Green).bg(Color::White);
const BLUE_ON_WHITE: Style = Style::new().fg(Color::Blue).bg(Color::White);
let line = Line::from(vec![
Span::styled("He", RED),
Span::styled("ll", GREEN),
Span::styled("o!", BLUE),
]);
let styled_graphemes = line
.styled_graphemes(Style::new().bg(Color::White))
.collect::<Vec<StyledGrapheme>>();
assert_eq!(
styled_graphemes,
vec![
StyledGrapheme::new("H", RED_ON_WHITE),
StyledGrapheme::new("e", RED_ON_WHITE),
StyledGrapheme::new("l", GREEN_ON_WHITE),
StyledGrapheme::new("l", GREEN_ON_WHITE),
StyledGrapheme::new("o", BLUE_ON_WHITE),
StyledGrapheme::new("!", BLUE_ON_WHITE),
],
);
}
}

View File

@@ -21,7 +21,7 @@ use super::Text;
/// Paragraph::new(password).render(buffer.area, &mut buffer);
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
/// ```
#[derive(Clone)]
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Masked<'a> {
inner: Cow<'a, str>,
mask_char: char,
@@ -86,43 +86,58 @@ impl<'a> From<Masked<'a>> for Text<'a> {
#[cfg(test)]
mod tests {
use std::borrow::Borrow;
use super::*;
use crate::text::Line;
#[test]
fn test_masked_value() {
fn new() {
let masked = Masked::new("12345", 'x');
assert_eq!(masked.inner, "12345");
assert_eq!(masked.mask_char, 'x');
}
#[test]
fn value() {
let masked = Masked::new("12345", 'x');
assert_eq!(masked.value(), "xxxxx");
}
#[test]
fn test_masked_debug() {
fn mask_char() {
let masked = Masked::new("12345", 'x');
assert_eq!(masked.mask_char(), 'x');
}
#[test]
fn debug() {
let masked = Masked::new("12345", 'x');
assert_eq!(format!("{masked:?}"), "12345");
}
#[test]
fn test_masked_display() {
fn display() {
let masked = Masked::new("12345", 'x');
assert_eq!(format!("{masked}"), "xxxxx");
}
#[test]
fn test_masked_conversions() {
fn into_text() {
let masked = Masked::new("12345", 'x');
let text: Text = masked.borrow().into();
let text: Text = (&masked).into();
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
let text: Text = masked.to_owned().into();
let text: Text = masked.into();
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
}
let cow: Cow<str> = masked.borrow().into();
#[test]
fn into_cow() {
let masked = Masked::new("12345", 'x');
let cow: Cow<str> = (&masked).into();
assert_eq!(cow, "xxxxx");
let cow: Cow<str> = masked.to_owned().into();
let cow: Cow<str> = masked.into();
assert_eq!(cow, "xxxxx");
}
}

71
src/text/mod.rs Normal file
View File

@@ -0,0 +1,71 @@
//! 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:
//! - 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 [`Line`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//!
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Line`].
//!
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations
//! so that you can start by using simple `String` or `&str` and then promote them to the previous
//! primitives when you need additional styling capabilities.
//!
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
//! its `title` property (which is a [`Line`] under the hood):
//!
//! ```rust
//! # use ratatui::widgets::Block;
//! # use ratatui::text::{Span, Line};
//! # use ratatui::style::{Color, Style};
//! // A simple string with no styling.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
//! // ])
//! let block = Block::default().title("My title");
//!
//! // A simple string with a unique style.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
//! // ])
//! let block = Block::default().title(
//! Span::styled("My title", Style::default().fg(Color::Yellow))
//! );
//!
//! // A string with multiple styles.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
//! // Span { content: Cow::Borrowed(" title"), .. }
//! // ])
//! let block = Block::default().title(vec![
//! Span::styled("My", Style::default().fg(Color::Yellow)),
//! Span::raw(" title"),
//! ]);
//! ```
use crate::style::Style;
mod grapheme;
pub use grapheme::StyledGrapheme;
mod line;
pub use line::Line;
mod masked;
pub use masked::Masked;
mod span;
pub use span::Span;
/// We keep this for backward compatibility.
mod spans;
#[allow(deprecated)]
pub use spans::Spans;
#[allow(clippy::module_inception)]
mod text;
pub use text::Text;

157
src/text/span.rs Normal file
View File

@@ -0,0 +1,157 @@
use std::{borrow::Cow, fmt::Debug};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::StyledGrapheme;
use crate::style::{Style, Styled};
/// A string where all graphemes have the same style.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Span<'a> {
pub content: Cow<'a, str>,
pub style: Style,
}
impl<'a> Span<'a> {
/// Create a span with no style.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::Span;
/// Span::raw("My text");
/// Span::raw(String::from("My text"));
/// ```
pub fn raw<T>(content: T) -> Span<'a>
where
T: Into<Cow<'a, str>>,
{
Span {
content: content.into(),
style: Style::default(),
}
}
/// Create a span with a style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::text::Span;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Span::styled("My text", style);
/// Span::styled(String::from("My text"), style);
/// ```
pub fn styled<T>(content: T, style: Style) -> Span<'a>
where
T: Into<Cow<'a, str>>,
{
Span {
content: content.into(),
style,
}
}
/// Returns the width of the content held by this span.
pub fn width(&self) -> usize {
self.content.width()
}
/// Returns an iterator over the graphemes held by this span.
///
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
/// the resulting [`Style`].
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, StyledGrapheme};
/// # use ratatui::style::{Color, Modifier, Style};
/// # use std::iter::Iterator;
/// let span = Span::styled("Text", Style::default().fg(Color::Yellow));
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
/// assert_eq!(
/// span.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
/// vec![
/// StyledGrapheme::new("T", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// StyledGrapheme::new("e", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// StyledGrapheme::new("x", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// StyledGrapheme::new("t", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// ],
/// );
/// ```
pub fn styled_graphemes(
&'a self,
base_style: Style,
) -> impl Iterator<Item = StyledGrapheme<'a>> {
UnicodeSegmentation::graphemes(self.content.as_ref(), true)
.map(move |g| StyledGrapheme {
symbol: g,
style: base_style.patch(self.style),
})
.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> {
fn from(s: String) -> Span<'a> {
Span::raw(s)
}
}
impl<'a> From<&'a str> for Span<'a> {
fn from(s: &'a str) -> Span<'a> {
Span::raw(s)
}
}
impl<'a> Styled for Span<'a> {
type Item = Span<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(mut self, style: Style) -> Self {
self.style = style;
self
}
}

View File

@@ -9,7 +9,7 @@ use crate::{layout::Alignment, text::Line};
/// future. All methods that accept Spans have been replaced with methods that
/// accept Into<Line<'a>> (which is implemented on `Spans`) to allow users of
/// this crate to gradually transition to Line.
#[derive(Debug, Clone, PartialEq, Default, Eq)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[deprecated(note = "Use `ratatui::text::Line` instead")]
pub struct Spans<'a>(pub Vec<Span<'a>>);

225
src/text/text.rs Normal file
View File

@@ -0,0 +1,225 @@
use std::borrow::Cow;
#[allow(deprecated)]
use super::{Line, Span, Spans};
use crate::style::Style;
/// A string split over multiple lines where each line is composed of several clusters, each with
/// their own style.
///
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
///
/// // An initial two lines of `Text` built from a `&str`
/// let mut text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
///
/// // Adding two more unstyled lines
/// text.extend(Text::raw("These are two\nmore lines!"));
/// assert_eq!(4, text.height());
///
/// // Adding a final two styled lines
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
/// assert_eq!(6, text.height());
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Text<'a> {
pub lines: Vec<Line<'a>>,
}
impl<'a> Text<'a> {
/// Create some text (potentially multiple lines) with no style.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// Text::raw("The first line\nThe second line");
/// Text::raw(String::from("The first line\nThe second line"));
/// ```
pub fn raw<T>(content: T) -> Text<'a>
where
T: Into<Cow<'a, str>>,
{
let lines: Vec<_> = match content.into() {
Cow::Borrowed("") => vec![Line::from("")],
Cow::Borrowed(s) => s.lines().map(Line::from).collect(),
Cow::Owned(s) if s.is_empty() => vec![Line::from("")],
Cow::Owned(s) => s.lines().map(|l| Line::from(l.to_owned())).collect(),
};
Text::from(lines)
}
/// Create some text (potentially multiple lines) with a style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style);
/// ```
pub fn styled<T>(content: T, style: Style) -> Text<'a>
where
T: Into<Cow<'a, str>>,
{
let mut text = Text::raw(content);
text.patch_style(style);
text
}
/// Returns the max width of all the lines.
///
/// ## Examples
///
/// ```rust
/// use ratatui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(15, text.width());
/// ```
pub fn width(&self) -> usize {
self.lines.iter().map(Line::width).max().unwrap_or_default()
}
/// Returns the height.
///
/// ## Examples
///
/// ```rust
/// use ratatui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
/// ```
pub fn height(&self) -> usize {
self.lines.len()
}
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_text = Text::raw("The first line\nThe second line");
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
/// assert_ne!(raw_text, styled_text);
///
/// raw_text.patch_style(style);
/// assert_eq!(raw_text, styled_text);
/// ```
pub fn patch_style(&mut self, style: Style) {
for line in &mut self.lines {
line.patch_style(style);
}
}
/// Resets the style of the Text.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Line, Text};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut text = Text::styled("The first line\nThe second line", style);
///
/// text.reset_style();
/// for line in &text.lines {
/// for span in &line.spans {
/// assert_eq!(Style::reset(), span.style);
/// }
/// }
/// ```
pub fn reset_style(&mut self) {
for line in &mut self.lines {
line.reset_style();
}
}
}
impl<'a> From<String> for Text<'a> {
fn from(s: String) -> Text<'a> {
Text::raw(s)
}
}
impl<'a> From<&'a str> for Text<'a> {
fn from(s: &'a str) -> Text<'a> {
Text::raw(s)
}
}
impl<'a> From<Cow<'a, str>> for Text<'a> {
fn from(s: Cow<'a, str>) -> Text<'a> {
Text::raw(s)
}
}
impl<'a> From<Span<'a>> for Text<'a> {
fn from(span: Span<'a>) -> Text<'a> {
Text {
lines: vec![Line::from(span)],
}
}
}
#[allow(deprecated)]
impl<'a> From<Spans<'a>> for Text<'a> {
fn from(spans: Spans<'a>) -> Text<'a> {
Text {
lines: vec![spans.into()],
}
}
}
impl<'a> From<Line<'a>> for Text<'a> {
fn from(line: Line<'a>) -> Text<'a> {
Text { lines: vec![line] }
}
}
#[allow(deprecated)]
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
Text {
lines: lines.into_iter().map(|l| l.0.into()).collect(),
}
}
}
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
Text { lines }
}
}
impl<'a> IntoIterator for Text<'a> {
type Item = Line<'a>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.lines.into_iter()
}
}
impl<'a, T> Extend<T> for Text<'a>
where
T: Into<Line<'a>>,
{
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
let lines = iter.into_iter().map(Into::into);
self.lines.extend(lines);
}
}

View File

@@ -1,6 +1,6 @@
use crate::{layout::Alignment, text::Line};
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Title<'a> {
pub content: Line<'a>,
/// Defaults to Left if unset
@@ -45,13 +45,3 @@ where
Self::default().content(value.into())
}
}
impl<'a> Default for Title<'a> {
fn default() -> Self {
Self {
content: Line::from(""),
alignment: Some(Alignment::Left),
position: Some(Position::Top),
}
}
}

View File

@@ -1,221 +0,0 @@
use std::cmp::min;
use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols,
widgets::{Block, Widget},
};
/// Display multiple bars in a single widgets
///
/// # Examples
///
/// ```
/// # use ratatui::widgets::{Block, Borders, BarChart};
/// # use ratatui::style::{Style, Color, Modifier};
/// BarChart::default()
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .bar_width(3)
/// .bar_gap(1)
/// .bar_style(Style::default().fg(Color::Yellow).bg(Color::Red))
/// .value_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
/// .label_style(Style::default().fg(Color::White))
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .max(4);
/// ```
#[derive(Debug, Clone)]
pub struct BarChart<'a> {
/// Block to wrap the widget in
block: Option<Block<'a>>,
/// The width of each bar
bar_width: u16,
/// The gap between each bar
bar_gap: u16,
/// Set of symbols used to display the data
bar_set: symbols::bar::Set,
/// Style of the bars
bar_style: Style,
/// Style of the values printed at the bottom of each bar
value_style: Style,
/// Style of the labels printed under each bar
label_style: Style,
/// Style for the widget
style: Style,
/// Slice of (label, value) pair to plot on the chart
data: &'a [(&'a str, u64)],
/// Value necessary for a bar to reach the maximum height (if no value is specified,
/// the maximum value in the data is taken as reference)
max: Option<u64>,
/// Values to display on the bar (computed when the data is passed to the widget)
values: Vec<String>,
}
impl<'a> Default for BarChart<'a> {
fn default() -> BarChart<'a> {
BarChart {
block: None,
max: None,
data: &[],
values: Vec::new(),
bar_style: Style::default(),
bar_width: 1,
bar_gap: 1,
bar_set: symbols::bar::NINE_LEVELS,
value_style: Style::default(),
label_style: Style::default(),
style: Style::default(),
}
}
}
impl<'a> BarChart<'a> {
pub fn data(mut self, data: &'a [(&'a str, u64)]) -> BarChart<'a> {
self.data = data;
self.values = Vec::with_capacity(self.data.len());
for &(_, v) in self.data {
self.values.push(format!("{v}"));
}
self
}
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
self.block = Some(block);
self
}
pub fn max(mut self, max: u64) -> BarChart<'a> {
self.max = Some(max);
self
}
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
self.bar_style = style;
self
}
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
self.bar_width = width;
self
}
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
self.bar_gap = gap;
self
}
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
self.bar_set = bar_set;
self
}
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
self.value_style = style;
self
}
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
self.label_style = style;
self
}
pub fn style(mut self, style: Style) -> BarChart<'a> {
self.style = style;
self
}
}
impl<'a> Widget for BarChart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let chart_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if chart_area.height < 2 {
return;
}
let max = self
.max
.unwrap_or_else(|| self.data.iter().map(|t| t.1).max().unwrap_or_default());
let max_index = min(
(chart_area.width / (self.bar_width + self.bar_gap)) as usize,
self.data.len(),
);
let mut data = self
.data
.iter()
.take(max_index)
.map(|&(l, v)| {
(
l,
v * u64::from(chart_area.height - 1) * 8 / std::cmp::max(max, 1),
)
})
.collect::<Vec<(&str, u64)>>();
for j in (0..chart_area.height - 1).rev() {
for (i, d) in data.iter_mut().enumerate() {
let symbol = match d.1 {
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
3 => self.bar_set.three_eighths,
4 => self.bar_set.half,
5 => self.bar_set.five_eighths,
6 => self.bar_set.three_quarters,
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
};
for x in 0..self.bar_width {
buf.get_mut(
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap) + x,
chart_area.top() + j,
)
.set_symbol(symbol)
.set_style(self.bar_style);
}
if d.1 > 8 {
d.1 -= 8;
} else {
d.1 = 0;
}
}
}
for (i, &(label, value)) in self.data.iter().take(max_index).enumerate() {
if value != 0 {
let value_label = &self.values[i];
let width = value_label.width() as u16;
if width < self.bar_width {
buf.set_string(
chart_area.left()
+ i as u16 * (self.bar_width + self.bar_gap)
+ (self.bar_width - width) / 2,
chart_area.bottom() - 2,
value_label,
self.value_style,
);
}
}
buf.set_stringn(
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap),
chart_area.bottom() - 1,
label,
self.bar_width as usize,
self.label_style,
);
}
}
}

View File

@@ -0,0 +1,98 @@
use crate::{buffer::Buffer, style::Style, text::Line};
/// represent a bar to be shown by the Barchart
///
/// # Examples
/// the following example creates a bar with the label "Bar 1", a value "10",
/// red background and a white value foreground
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Bar::default()
/// .label("Bar 1".into())
/// .value(10)
/// .style(Style::default().fg(Color::Red))
/// .value_style(Style::default().bg(Color::Red).fg(Color::White))
/// .text_value("10°C".to_string());
/// ```
#[derive(Debug, Default, Clone)]
pub struct Bar<'a> {
/// Value to display on the bar (computed when the data is passed to the widget)
pub(super) value: u64,
/// optional label to be printed under the bar
pub(super) label: Option<Line<'a>>,
/// style for the bar
pub(super) style: Style,
/// style of the value printed at the bottom of the bar.
pub(super) value_style: Style,
/// optional text_value to be shown on the bar instead of the actual value
pub(super) text_value: Option<String>,
}
impl<'a> Bar<'a> {
pub fn value(mut self, value: u64) -> Bar<'a> {
self.value = value;
self
}
pub fn label(mut self, label: Line<'a>) -> Bar<'a> {
self.label = Some(label);
self
}
pub fn style(mut self, style: Style) -> Bar<'a> {
self.style = style;
self
}
pub fn value_style(mut self, style: Style) -> Bar<'a> {
self.value_style = style;
self
}
/// set the text value printed in the bar. (By default self.value is printed)
pub fn text_value(mut self, text_value: String) -> Bar<'a> {
self.text_value = Some(text_value);
self
}
pub(super) fn render_label_and_value(
self,
buf: &mut Buffer,
max_width: u16,
x: u16,
y: u16,
default_value_style: Style,
default_label_style: Style,
) {
// render the value
if self.value != 0 {
let value_label = if let Some(text) = self.text_value {
text
} else {
self.value.to_string()
};
let width = value_label.len() as u16;
if width < max_width {
buf.set_string(
x + (max_width.saturating_sub(value_label.len() as u16) >> 1),
y,
value_label,
self.value_style.patch(default_value_style),
);
}
}
// render the label
if let Some(mut label) = self.label {
label.patch_style(default_label_style);
buf.set_line(
x + (max_width.saturating_sub(label.width() as u16) >> 1),
y + 1,
&label,
max_width,
);
}
}
}

View File

@@ -0,0 +1,63 @@
use super::Bar;
use crate::text::Line;
/// represent a group of bars to be shown by the Barchart
///
/// # Examples
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// BarGroup::default()
/// .label("Group 1".into())
/// .bars(&[Bar::default().value(200), Bar::default().value(150)]);
/// ```
#[derive(Debug, Default, Clone)]
pub struct BarGroup<'a> {
/// label of the group. It will be printed centered under this group of bars
pub(super) label: Option<Line<'a>>,
/// list of bars to be shown
pub(super) bars: Vec<Bar<'a>>,
}
impl<'a> BarGroup<'a> {
/// Set the group label
pub fn label(mut self, label: Line<'a>) -> BarGroup<'a> {
self.label = Some(label);
self
}
/// Set the bars of the group to be shown
pub fn bars(mut self, bars: &[Bar<'a>]) -> BarGroup<'a> {
self.bars = bars.to_vec();
self
}
/// return the maximum bar value of this group
pub(super) fn max(&self) -> Option<u64> {
self.bars.iter().max_by_key(|v| v.value).map(|v| v.value)
}
}
impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {
fn from(value: &[(&'a str, u64)]) -> BarGroup<'a> {
BarGroup {
label: None,
bars: value
.iter()
.map(|&(text, v)| Bar::default().value(v).label(text.into()))
.collect(),
}
}
}
impl<'a, const N: usize> From<&[(&'a str, u64); N]> for BarGroup<'a> {
fn from(value: &[(&'a str, u64); N]) -> BarGroup<'a> {
Self::from(value.as_ref())
}
}
impl<'a> From<&Vec<(&'a str, u64)>> for BarGroup<'a> {
fn from(value: &Vec<(&'a str, u64)>) -> BarGroup<'a> {
let array: &[(&str, u64)] = value;
Self::from(array)
}
}

759
src/widgets/barchart/mod.rs Normal file
View File

@@ -0,0 +1,759 @@
use crate::prelude::*;
mod bar;
mod bar_group;
pub use bar::Bar;
pub use bar_group::BarGroup;
use super::{Block, Widget};
/// Display multiple bars in a single widgets
///
/// # Examples
/// The following example creates a BarChart with two groups of bars.
/// The first group is added by an array slice (&[(&str, u64)]).
/// The second group is added by a slice of Groups (&[BarGroup]).
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// BarChart::default()
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .bar_width(3)
/// .bar_gap(1)
/// .group_gap(3)
/// .bar_style(Style::new().yellow().on_red())
/// .value_style(Style::new().red().bold())
/// .label_style(Style::new().white())
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]))
/// .max(4);
/// ```
#[derive(Debug, Clone)]
pub struct BarChart<'a> {
/// Block to wrap the widget in
block: Option<Block<'a>>,
/// The width of each bar
bar_width: u16,
/// The gap between each bar
bar_gap: u16,
/// The gap between each group
group_gap: u16,
/// Set of symbols used to display the data
bar_set: symbols::bar::Set,
/// Style of the bars
bar_style: Style,
/// Style of the values printed at the bottom of each bar
value_style: Style,
/// Style of the labels printed under each bar
label_style: Style,
/// Style for the widget
style: Style,
/// vector of groups containing bars
data: Vec<BarGroup<'a>>,
/// Value necessary for a bar to reach the maximum height (if no value is specified,
/// the maximum value in the data is taken as reference)
max: Option<u64>,
}
impl<'a> Default for BarChart<'a> {
fn default() -> BarChart<'a> {
BarChart {
block: None,
max: None,
data: Vec::new(),
bar_style: Style::default(),
bar_width: 1,
bar_gap: 1,
value_style: Style::default(),
label_style: Style::default(),
group_gap: 0,
bar_set: symbols::bar::NINE_LEVELS,
style: Style::default(),
}
}
}
impl<'a> BarChart<'a> {
/// Add group of bars to the BarChart
/// # Examples
/// The following example creates a BarChart with two groups of bars.
/// The first group is added by an array slice (&[(&str, u64)]).
/// The second group is added by a BarGroup instance.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
///
/// BarChart::default()
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]));
/// ```
pub fn data(mut self, data: impl Into<BarGroup<'a>>) -> BarChart<'a> {
let group: BarGroup = data.into();
if !group.bars.is_empty() {
self.data.push(group);
}
self
}
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
self.block = Some(block);
self
}
pub fn max(mut self, max: u64) -> BarChart<'a> {
self.max = Some(max);
self
}
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
self.bar_style = style;
self
}
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
self.bar_width = width;
self
}
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
self.bar_gap = gap;
self
}
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
self.bar_set = bar_set;
self
}
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
self.value_style = style;
self
}
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
self.label_style = style;
self
}
pub fn group_gap(mut self, gap: u16) -> BarChart<'a> {
self.group_gap = gap;
self
}
pub fn style(mut self, style: Style) -> BarChart<'a> {
self.style = style;
self
}
}
impl<'a> BarChart<'a> {
/// Check the bars, which fits inside the available space and removes
/// the bars and the groups, which are outside of the available space.
fn remove_invisible_groups_and_bars(&mut self, mut width: u16) {
for group_index in 0..self.data.len() {
let n_bars = self.data[group_index].bars.len() as u16;
let group_width = n_bars * self.bar_width + n_bars.saturating_sub(1) * self.bar_gap;
if width > group_width {
width = width.saturating_sub(group_width + self.group_gap + self.bar_gap);
} else {
let max_bars = (width + self.bar_gap) / (self.bar_width + self.bar_gap);
if max_bars == 0 {
self.data.truncate(group_index);
} else {
self.data[group_index].bars.truncate(max_bars as usize);
self.data.truncate(group_index + 1);
}
break;
}
}
}
/// Get the number of lines needed for the labels.
///
/// The number of lines depends on whether we need to print the bar labels and/or the group
/// labels.
/// - If there are no labels, return 0.
/// - If there are only bar labels, return 1.
/// - If there are only group labels, return 1.
/// - If there are both bar and group labels, return 2.
fn label_height(&self) -> u16 {
let has_group_labels = self.data.iter().any(|e| e.label.is_some());
let has_data_labels = self
.data
.iter()
.any(|e| e.bars.iter().any(|e| e.label.is_some()));
// convert true to 1 and false to 0 and add the two values
u16::from(has_group_labels) + u16::from(has_data_labels)
}
/// renders the block if there is one and updates the area to the inner area
fn render_block(&mut self, area: &mut Rect, buf: &mut Buffer) {
if let Some(block) = self.block.take() {
let inner_area = block.inner(*area);
block.render(*area, buf);
*area = inner_area
}
}
fn render_bars(&self, buf: &mut Buffer, bars_area: Rect, max: u64) {
// convert the bar values to ratatui::symbols::bar::Set
let mut groups: Vec<Vec<u64>> = self
.data
.iter()
.map(|group| {
group
.bars
.iter()
.map(|bar| bar.value * u64::from(bars_area.height) * 8 / max)
.collect()
})
.collect();
// print all visible bars (without labels and values)
for j in (0..bars_area.height).rev() {
let mut bar_x = bars_area.left();
for (group_data, group) in groups.iter_mut().zip(&self.data) {
for (d, bar) in group_data.iter_mut().zip(&group.bars) {
let symbol = match d {
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
3 => self.bar_set.three_eighths,
4 => self.bar_set.half,
5 => self.bar_set.five_eighths,
6 => self.bar_set.three_quarters,
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
};
let bar_style = bar.style.patch(self.bar_style);
for x in 0..self.bar_width {
buf.get_mut(bar_x + x, bars_area.top() + j)
.set_symbol(symbol)
.set_style(bar_style);
}
if *d > 8 {
*d -= 8;
} else {
*d = 0;
}
bar_x += self.bar_gap + self.bar_width;
}
bar_x += self.group_gap;
}
}
}
/// get the maximum data value. the returned value is always greater equal 1
fn maximum_data_value(&self) -> u64 {
self.max
.unwrap_or_else(|| {
self.data
.iter()
.map(|group| group.max().unwrap_or_default())
.max()
.unwrap_or_default()
})
.max(1u64)
}
fn render_labels_and_values(self, area: Rect, buf: &mut Buffer, label_height: u16) {
// print labels and values in one go
let mut bar_x = area.left();
let bar_y = area.bottom() - label_height - 1;
for group in self.data.into_iter() {
// print group labels under the bars or the previous labels
if let Some(mut label) = group.label {
label.patch_style(self.label_style);
let label_max_width = group.bars.len() as u16 * self.bar_width
+ (group.bars.len() as u16 - 1) * self.bar_gap;
buf.set_line(
bar_x + (label_max_width.saturating_sub(label.width() as u16) >> 1),
area.bottom() - 1,
&label,
label_max_width,
);
}
// print the bar values and numbers
for bar in group.bars.into_iter() {
bar.render_label_and_value(
buf,
self.bar_width,
bar_x,
bar_y,
self.value_style,
self.label_style,
);
bar_x += self.bar_gap + self.bar_width;
}
bar_x += self.group_gap;
}
}
}
impl<'a> Widget for BarChart<'a> {
fn render(mut self, mut area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
self.render_block(&mut area, buf);
if self.data.is_empty() {
return;
}
let label_height = self.label_height();
if area.height <= label_height {
return;
}
let max = self.maximum_data_value();
// remove invisible groups and bars, since we don't need to print them
self.remove_invisible_groups_and_bars(area.width);
let bars_area = Rect {
height: area.height - label_height,
..area
};
self.render_bars(buf, bars_area, max);
self.render_labels_and_values(area, buf, label_height);
}
}
impl<'a> Styled for BarChart<'a> {
type Item = BarChart<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use itertools::iproduct;
use super::*;
use crate::{
assert_buffer_eq,
widgets::{BorderType, Borders},
};
#[test]
fn default() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default();
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "; 3]));
}
#[test]
fn data() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default().data(&[("foo", 1), ("bar", 2)]);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
])
);
}
#[test]
fn block() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 5));
let block = Block::default()
.title("Block")
.border_type(BorderType::Double)
.borders(Borders::ALL);
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.block(block);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"╔Block════════╗",
"║ █ ║",
"║█ █ ║",
"║f b ║",
"╚═════════════╝",
])
);
}
#[test]
fn max() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let without_max = BarChart::default().data(&[("foo", 1), ("bar", 2), ("baz", 100)]);
without_max.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
"",
"f b b ",
])
);
let with_max = BarChart::default()
.data(&[("foo", 1), ("bar", 2), ("baz", 100)])
.max(2);
with_max.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" █ █ ",
"█ █ █ ",
"f b b ",
])
);
}
#[test]
fn bar_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_style(Style::new().red());
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
]);
for (x, y) in iproduct!([0, 2], [0, 1]) {
expected.get_mut(x, y).set_fg(Color::Red);
}
assert_buffer_eq!(buffer, expected);
}
#[test]
fn bar_width() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_width(3);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ███ ",
"█1█ █2█ ",
"foo bar ",
])
);
}
#[test]
fn bar_gap() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_gap(2);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
])
);
}
#[test]
fn bar_set() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 0), ("bar", 1), ("baz", 3)])
.bar_set(symbols::bar::THREE_LEVELS);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"",
" ▄ █ ",
"f b b ",
])
);
}
#[test]
fn bar_set_nine_levels() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 18, 3));
let widget = BarChart::default()
.data(&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
])
.bar_set(symbols::bar::NINE_LEVELS);
widget.render(Rect::new(0, 1, 18, 2), &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ",
"a b c d e f g h i ",
])
);
}
#[test]
fn value_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_width(3)
.value_style(Style::new().red());
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
" ███ ",
"█1█ █2█ ",
"foo bar ",
]);
expected.get_mut(1, 1).set_fg(Color::Red);
expected.get_mut(5, 1).set_fg(Color::Red);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn label_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.label_style(Style::new().red());
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
]);
expected.get_mut(0, 2).set_fg(Color::Red);
expected.get_mut(2, 2).set_fg(Color::Red);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.style(Style::new().red());
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"f b ",
]);
for (x, y) in iproduct!(0..15, 0..3) {
expected.get_mut(x, y).set_fg(Color::Red);
}
assert_buffer_eq!(buffer, expected);
}
#[test]
fn does_not_render_less_than_two_rows() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
let widget = BarChart::default().data(&[("foo", 1), ("bar", 2)]);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::empty(Rect::new(0, 0, 15, 1)));
}
fn create_test_barchart<'a>() -> BarChart<'a> {
BarChart::default()
.group_gap(2)
.data(BarGroup::default().label("G1".into()).bars(&[
Bar::default().value(2),
Bar::default().value(1),
Bar::default().value(2),
]))
.data(BarGroup::default().label("G2".into()).bars(&[
Bar::default().value(1),
Bar::default().value(2),
Bar::default().value(1),
]))
.data(BarGroup::default().label("G3".into()).bars(&[
Bar::default().value(1),
Bar::default().value(2),
Bar::default().value(1),
]))
}
#[test]
fn test_invisible_groups_and_bars_full() {
let chart = create_test_barchart();
// Check that the BarChart is shown in full
{
let mut c = chart.clone();
c.remove_invisible_groups_and_bars(21);
assert_eq!(c.data.len(), 3);
assert_eq!(c.data[2].bars.len(), 3);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 21, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
"█ █ █ █ ",
"█ █ █ █ █ █ █ █ █",
" G1 G2 G3 ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_missing_last_2_bars() {
// Last 2 bars of G3 should be out of screen. (screen width is 17)
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(17);
assert_eq!(w.data.len(), 3);
assert_eq!(w.data[2].bars.len(), 1);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
"█ █ █ ",
"█ █ █ █ █ █ █",
" G1 G2 G",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_missing_last_group() {
// G3 should be out of screen. (screen width is 16)
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(16);
assert_eq!(w.data.len(), 2);
assert_eq!(w.data[1].bars.len(), 3);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 16, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
"█ █ █ ",
"█ █ █ █ █ █ ",
" G1 G2 ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_show_only_1_bar() {
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(1);
assert_eq!(w.data.len(), 1);
assert_eq!(w.data[0].bars.len(), 1);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "", "G"]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_all_bars_outside_visible_area() {
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(0);
assert_eq!(w.data.len(), 0);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 3));
// Check if the render method panics
chart.render(buffer.area, &mut buffer);
}
#[test]
fn test_label_height() {
{
let barchart = BarChart::default().data(
BarGroup::default()
.label("Group Label".into())
.bars(&[Bar::default().value(2).label("Bar Label".into())]),
);
assert_eq!(barchart.label_height(), 2);
}
{
let barchart = BarChart::default().data(
BarGroup::default()
.label("Group Label".into())
.bars(&[Bar::default().value(2)]),
);
assert_eq!(barchart.label_height(), 1);
}
{
let barchart = BarChart::default().data(
BarGroup::default().bars(&[Bar::default().value(2).label("Bar Label".into())]),
);
assert_eq!(barchart.label_height(), 1);
}
{
let barchart =
BarChart::default().data(BarGroup::default().bars(&[Bar::default().value(2)]));
assert_eq!(barchart.label_height(), 0);
}
}
#[test]
fn can_be_stylized() {
assert_eq!(
BarChart::default().black().on_white().bold().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
)
}
#[test]
fn test_empty_group() {
let chart = BarChart::default()
.data(BarGroup::default().label("invisible".into()))
.data(
BarGroup::default()
.label("G".into())
.bars(&[Bar::default().value(1), Bar::default().value(2)]),
);
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "█ █", " G "]);
assert_buffer_eq!(buffer, expected);
}
}

View File

@@ -1,7 +1,7 @@
#[path = "../title.rs"]
pub mod title;
use self::title::{Position, Title};
pub use self::title::{Position, Title};
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
@@ -10,8 +10,9 @@ use crate::{
widgets::{Borders, Widget},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum BorderType {
#[default]
Plain,
Rounded,
Double,
@@ -29,7 +30,7 @@ impl BorderType {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Padding {
pub left: u16,
pub right: u16,
@@ -112,7 +113,7 @@ impl Padding {
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Block<'a> {
/// List of titles
titles: Vec<Title<'a>>,
@@ -137,12 +138,6 @@ pub struct Block<'a> {
padding: Padding,
}
impl<'a> Default for Block<'a> {
fn default() -> Block<'a> {
Block::new()
}
}
impl<'a> Block<'a> {
pub const fn new() -> Self {
Self {
@@ -514,7 +509,11 @@ impl<'a> Styled for Block<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Rect;
use crate::{
assert_buffer_eq,
layout::Rect,
style::{Color, Modifier, Stylize},
};
#[test]
fn inner_takes_into_account_the_borders() {
@@ -872,4 +871,50 @@ mod tests {
.style(_DEFAULT_STYLE)
.padding(_DEFAULT_PADDING);
}
#[test]
fn can_be_stylized() {
assert_eq!(
Block::default().black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
#[test]
fn title_alignment() {
let tests = vec![
(Alignment::Left, "test "),
(Alignment::Center, " test "),
(Alignment::Right, " test"),
];
for (alignment, expected) in tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
Block::default()
.title("test")
.title_alignment(alignment)
.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![expected]));
}
}
#[test]
fn title_alignment_overrides_block_title_alignment() {
let tests = vec![
(Alignment::Right, Alignment::Left, "test "),
(Alignment::Left, Alignment::Center, " test "),
(Alignment::Center, Alignment::Right, " test"),
];
for (block_title_alignment, alignment, expected) in tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
Block::default()
.title(Title::from("test").alignment(alignment))
.title_alignment(block_title_alignment)
.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![expected]));
}
}
}

View File

@@ -21,6 +21,7 @@ use crate::{
};
/// Display a month calendar for the month containing `display_date`
#[derive(Debug, Clone)]
pub struct Monthly<'a, S: DateStyler> {
display_date: Date,
events: S,
@@ -172,6 +173,7 @@ pub trait DateStyler {
}
/// A simple `DateStyler` based on a [`HashMap`]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct CalendarEventStore(pub HashMap<Date, Style>);
impl CalendarEventStore {

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// Shape to draw a circle with a given center and radius and with the given color
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Circle {
pub x: f64,
pub y: f64,

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Line {
pub x1: f64,
pub y1: f64,

View File

@@ -6,8 +6,9 @@ use crate::{
},
};
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy)]
pub enum MapResolution {
#[default]
Low,
High,
}
@@ -22,21 +23,12 @@ impl MapResolution {
}
/// Shape to draw a world map with the given resolution and color
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Map {
pub resolution: MapResolution,
pub color: Color,
}
impl Default for Map {
fn default() -> Map {
Map {
resolution: MapResolution::Low,
color: Color::Reset,
}
}
}
impl Shape for Map {
fn draw(&self, painter: &mut Painter) {
for (x, y) in self.resolution.data() {

View File

@@ -29,14 +29,14 @@ pub trait Shape {
}
/// Label to draw some text on the canvas
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Label<'a> {
x: f64,
y: f64,
line: TextLine<'a>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
struct Layer {
string: String,
colors: Vec<Color>,
@@ -51,7 +51,7 @@ trait Grid: Debug {
fn reset(&mut self);
}
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
struct BrailleGrid {
width: u16,
height: u16,
@@ -114,7 +114,7 @@ impl Grid for BrailleGrid {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
struct CharGrid {
width: u16,
height: u16,
@@ -355,6 +355,7 @@ impl<'a> Context<'a> {
/// });
/// });
/// ```
#[derive(Debug, Clone)]
pub struct Canvas<'a, F>
where
F: Fn(&mut Context),

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// A shape to draw a group of points with the given color
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Points<'a> {
pub coords: &'a [(f64, f64)],
pub color: Color,
@@ -19,12 +19,3 @@ impl<'a> Shape for Points<'a> {
}
}
}
impl<'a> Default for Points<'a> {
fn default() -> Points<'a> {
Points {
coords: &[],
color: Color::Reset,
}
}
}

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// Shape to draw a rectangle from a `Rect` with the given color
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Rectangle {
pub x: f64,
pub y: f64,

View File

@@ -5,7 +5,7 @@ use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::{Alignment, Constraint, Rect},
style::{Color, Style},
style::{Color, Style, Styled},
symbols,
text::{Line as TextLine, Span},
widgets::{
@@ -15,7 +15,7 @@ use crate::{
};
/// An X or Y axis for the chart widget
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Axis<'a> {
/// Title displayed next to axis end
title: Option<TextLine<'a>>,
@@ -29,18 +29,6 @@ pub struct Axis<'a> {
labels_alignment: Alignment,
}
impl<'a> Default for Axis<'a> {
fn default() -> Axis<'a> {
Axis {
title: None,
bounds: [0.0, 0.0],
labels: None,
style: Style::default(),
labels_alignment: Alignment::Left,
}
}
}
impl<'a> Axis<'a> {
pub fn title<T>(mut self, title: T) -> Axis<'a>
where
@@ -88,16 +76,17 @@ impl<'a> Axis<'a> {
}
/// Used to determine which style of graphing to use
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy)]
pub enum GraphType {
/// Draw each point
#[default]
Scatter,
/// Draw each point and lines between each point using the same marker
Line,
}
/// A group of data points
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Dataset<'a> {
/// Name of the dataset (used in the legend if shown)
name: Cow<'a, str>,
@@ -111,18 +100,6 @@ pub struct Dataset<'a> {
style: Style,
}
impl<'a> Default for Dataset<'a> {
fn default() -> Dataset<'a> {
Dataset {
name: Cow::from(""),
data: &[],
marker: symbols::Marker::Dot,
graph_type: GraphType::Scatter,
style: Style::default(),
}
}
}
impl<'a> Dataset<'a> {
pub fn name<S>(mut self, name: S) -> Dataset<'a>
where
@@ -155,7 +132,7 @@ impl<'a> Dataset<'a> {
/// A container that holds all the infos about where to display each elements of the chart (axis,
/// labels, legend, ...).
#[derive(Debug, Clone, PartialEq, Default)]
#[derive(Debug, Default, Clone, PartialEq)]
struct ChartLayout {
/// Location of the title of the x axis
title_x: Option<(u16, u16)>,
@@ -211,7 +188,7 @@ struct ChartLayout {
/// .bounds([0.0, 10.0])
/// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()));
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Chart<'a> {
/// A block to display around the widget eventually
block: Option<Block<'a>>,
@@ -612,9 +589,46 @@ impl<'a> Widget for Chart<'a> {
}
}
impl<'a> Styled for Axis<'a> {
type Item = Axis<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
impl<'a> Styled for Dataset<'a> {
type Item = Dataset<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
impl<'a> Styled for Chart<'a> {
type Item = Chart<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::{Modifier, Stylize};
struct LegendTestCase {
chart_area: Rect,
@@ -652,4 +666,40 @@ mod tests {
assert_eq!(layout.legend_area, case.legend_area);
}
}
#[test]
fn axis_can_be_stylized() {
assert_eq!(
Axis::default().black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
#[test]
fn dataset_can_be_stylized() {
assert_eq!(
Dataset::default().black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
#[test]
fn chart_can_be_stylized() {
assert_eq!(
Chart::new(vec![]).black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
}

View File

@@ -23,7 +23,7 @@ use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
///
/// For a more complete example how to utilize `Clear` to realize popups see
/// the example `examples/popup.rs`
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Clear;
impl Widget for Clear {

View File

@@ -1,7 +1,7 @@
use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
style::{Color, Style, Styled},
symbols,
text::{Line, Span},
widgets::{Block, Widget},
@@ -179,6 +179,7 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
/// .line_set(symbols::line::THICK)
/// .ratio(0.4);
/// ```
#[derive(Debug, Default, Clone)]
pub struct LineGauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
@@ -188,19 +189,6 @@ pub struct LineGauge<'a> {
gauge_style: Style,
}
impl<'a> Default for LineGauge<'a> {
fn default() -> Self {
Self {
block: None,
ratio: 0.0,
label: None,
style: Style::default(),
line_set: symbols::line::NORMAL,
gauge_style: Style::default(),
}
}
}
impl<'a> LineGauge<'a> {
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
@@ -279,6 +267,8 @@ impl<'a> Widget for LineGauge<'a> {
.set_style(Style {
fg: self.gauge_style.fg,
bg: None,
#[cfg(feature = "crossterm")]
underline_color: self.gauge_style.underline_color,
add_modifier: self.gauge_style.add_modifier,
sub_modifier: self.gauge_style.sub_modifier,
});
@@ -289,6 +279,8 @@ impl<'a> Widget for LineGauge<'a> {
.set_style(Style {
fg: self.gauge_style.bg,
bg: None,
#[cfg(feature = "crossterm")]
underline_color: self.gauge_style.underline_color,
add_modifier: self.gauge_style.add_modifier,
sub_modifier: self.gauge_style.sub_modifier,
});
@@ -296,9 +288,34 @@ impl<'a> Widget for LineGauge<'a> {
}
}
impl<'a> Styled for Gauge<'a> {
type Item = Gauge<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
impl<'a> Styled for LineGauge<'a> {
type Item = LineGauge<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::{Modifier, Stylize};
#[test]
#[should_panic]
@@ -317,4 +334,54 @@ mod tests {
fn gauge_invalid_ratio_lower_bound() {
Gauge::default().ratio(-0.5);
}
#[test]
fn gauge_can_be_stylized() {
assert_eq!(
Gauge::default().black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
#[test]
fn line_gauge_can_be_stylized() {
assert_eq!(
LineGauge::default()
.black()
.on_white()
.bold()
.not_dim()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
#[test]
fn line_gauge_default() {
// TODO: replace to `assert_eq!(LineGauge::default(), LineGauge::default())`
// when `Eq` or `PartialEq` is implemented for `LineGauge`.
assert_eq!(
format!("{:?}", LineGauge::default()),
format!(
"{:?}",
LineGauge {
block: None,
ratio: 0.0,
label: None,
style: Style::default(),
line_set: symbols::line::NORMAL,
gauge_style: Style::default(),
}
),
"LineGauge::default() should have correct default values."
);
}
}

View File

@@ -3,12 +3,12 @@ use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::{Corner, Rect},
style::Style,
style::{Style, Styled},
text::Text,
widgets::{Block, StatefulWidget, Widget},
};
#[derive(Debug, Clone, Default)]
#[derive(Debug, Default, Clone)]
pub struct ListState {
offset: usize,
selected: Option<usize>,
@@ -291,6 +291,30 @@ impl<'a> Widget for List<'a> {
}
}
impl<'a> Styled for List<'a> {
type Item = List<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
impl<'a> Styled for ListItem<'a> {
type Item = ListItem<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
@@ -298,7 +322,7 @@ mod tests {
use super::*;
use crate::{
assert_buffer_eq,
style::Color,
style::{Color, Modifier, Stylize},
text::{Line, Span},
widgets::{Borders, StatefulWidget, Widget},
};
@@ -880,4 +904,28 @@ mod tests {
"did not scroll the selected item into view"
);
}
#[test]
fn list_can_be_stylized() {
assert_eq!(
List::new(vec![]).black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
#[test]
fn list_item_can_be_stylized() {
assert_eq!(
ListItem::new("").black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
}

View File

@@ -27,7 +27,7 @@ mod gauge;
mod list;
mod paragraph;
mod reflow;
pub mod scrollbar;
mod scrollbar;
mod sparkline;
mod table;
mod tabs;
@@ -37,7 +37,7 @@ use std::fmt::{self, Debug};
use bitflags::bitflags;
pub use self::{
barchart::BarChart,
barchart::{Bar, BarChart, BarGroup},
block::{Block, BorderType, Padding},
chart::{Axis, Chart, Dataset, GraphType},
clear::Clear,
@@ -53,7 +53,7 @@ use crate::{buffer::Buffer, layout::Rect};
bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
#[derive(Clone, Copy, Default, PartialEq, Eq)]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub struct Borders: u8 {
/// Show no border (default)
const NONE = 0b0000;

View File

@@ -42,7 +42,7 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// .alignment(Alignment::Center)
/// .wrap(Wrap { trim: true });
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Paragraph<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
@@ -85,7 +85,7 @@ pub struct Paragraph<'a> {
/// // - Here is another point
/// // that is long enough to wrap
/// ```
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy)]
pub struct Wrap {
/// Should leading whitespace be trimmed
pub trim: bool,
@@ -214,7 +214,7 @@ mod test {
use super::*;
use crate::{
backend::TestBackend,
style::Color,
style::{Color, Modifier, Stylize},
text::{Line, Span},
widgets::Borders,
Terminal,
@@ -702,4 +702,16 @@ mod test {
Buffer::with_lines(vec!["こんにちは, ", "世界! 😃 "]),
);
}
#[test]
fn can_be_stylized() {
assert_eq!(
Paragraph::new("").black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
}

View File

@@ -15,6 +15,7 @@ pub trait LineComposer<'a> {
}
/// A state machine that wraps lines on word boundaries.
#[derive(Debug, Default, Clone)]
pub struct WordWrapper<'a, O, I>
where
// Outer iterator providing the individual lines
@@ -207,6 +208,7 @@ where
}
/// A state machine that truncates overhanging lines.
#[derive(Debug, Default, Clone)]
pub struct LineTruncator<'a, O, I>
where
// Outer iterator providing the individual lines

View File

@@ -3,56 +3,11 @@ use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols::{block::FULL, line},
};
/// Scrollbar Set
/// ```text
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ end
/// │ │ └──── track
/// │ └──────── thumb
/// └─────────── begin
/// ```
#[derive(Debug, Clone)]
pub struct Set {
pub track: &'static str,
pub thumb: &'static str,
pub begin: &'static str,
pub end: &'static str,
}
pub const DOUBLE_VERTICAL: Set = Set {
track: line::DOUBLE_VERTICAL,
thumb: FULL,
begin: "",
end: "",
};
pub const DOUBLE_HORIZONTAL: Set = Set {
track: line::DOUBLE_HORIZONTAL,
thumb: FULL,
begin: "",
end: "",
};
pub const VERTICAL: Set = Set {
track: line::VERTICAL,
thumb: FULL,
begin: "",
end: "",
};
pub const HORIZONTAL: Set = Set {
track: line::HORIZONTAL,
thumb: FULL,
begin: "",
end: "",
symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
};
/// An enum representing the direction of scrolling in a Scrollbar widget.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
pub enum ScrollDirection {
/// Forward scroll direction, usually corresponds to scrolling downwards or rightwards.
#[default]
@@ -80,7 +35,7 @@ pub enum ScrollDirection {
///
/// If you don't have multi-line content, you can leave the `viewport_content_length` set to the
/// default of 0 and it'll use the track size as a `viewport_content_length`.
#[derive(Clone, Copy, Debug, Default)]
#[derive(Debug, Default, Clone, Copy)]
pub struct ScrollbarState {
// The current position within the scrollable content.
position: u16,
@@ -146,7 +101,7 @@ impl ScrollbarState {
}
/// Scrollbar Orientation
#[derive(Default, Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub enum ScrollbarOrientation {
#[default]
VerticalRight,
@@ -155,9 +110,8 @@ pub enum ScrollbarOrientation {
HorizontalTop,
}
/// Scrollbar widget for tui-rs library.
/// A widget to display a scrollbar
///
/// This widget can be used to display a scrollbar in a terminal user interface.
/// The following components of the scrollbar are customizable in symbol and style.
///
/// ```text
@@ -502,7 +456,10 @@ impl<'a> StatefulWidget for Scrollbar<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::assert_buffer_eq;
use crate::{
assert_buffer_eq,
symbols::scrollbar::{HORIZONTAL, VERTICAL},
};
#[test]
fn test_no_render_when_area_zero() {

View File

@@ -3,7 +3,7 @@ use std::cmp::min;
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
style::{Style, Styled},
symbols,
widgets::{Block, Widget},
};
@@ -38,8 +38,9 @@ pub struct Sparkline<'a> {
direction: RenderDirection,
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy)]
pub enum RenderDirection {
#[default]
LeftToRight,
RightToLeft,
}
@@ -89,6 +90,18 @@ impl<'a> Sparkline<'a> {
}
}
impl<'a> Styled for Sparkline<'a> {
type Item = Sparkline<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
impl<'a> Widget for Sparkline<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let spark_area = match self.block.take() {
@@ -155,7 +168,11 @@ impl<'a> Widget for Sparkline<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_buffer_eq, buffer::Cell};
use crate::{
assert_buffer_eq,
buffer::Cell,
style::{Color, Modifier, Stylize},
};
// 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
@@ -206,4 +223,21 @@ mod tests {
let buffer = render(widget, 12);
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "]));
}
#[test]
fn can_be_stylized() {
assert_eq!(
Sparkline::default()
.black()
.on_white()
.bold()
.not_dim()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
}

Some files were not shown because too many files have changed in this diff Show More