Compare commits

..

1 Commits

Author SHA1 Message Date
Florian Dehau
02213a25a8 feat!(terminal): inline viewport 2021-12-26 13:41:17 +01:00
128 changed files with 2566 additions and 12772 deletions

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
if !(command cargo-make >/dev/null 2>&1); then # Check if cargo-make is installed
echo Attempting to run cargo-make as part of the pre-push hook but it\'s not installed.
echo Please install it by running the following command:
echo
echo " cargo install --force cargo-make"
echo
echo If you don\'t want to run cargo-make as part of the pre-push hook, you can run
echo the following command instead of git push:
echo
echo " git push --no-verify"
exit 1
fi
cargo make ci

View File

@@ -1,78 +0,0 @@
# configuration for https://github.com/commitizen/cz-cli
[tool.commitizen]
name = "cz_customize"
tag_format = "$version"
version_type = "semver"
version_provider = "cargo"
update_changelog_on_bump = true
major_version_zero = true
use_shortcuts = true
[tool.commitizen.customize]
message_template = """{{change_type}}({{scope}}): {{subject}}
{% if body %}\
{{body}}\
{% endif %}
{%if is_breaking_change %}\
BREAKING_CHANGE: \
{% endif %}\
{{footer}}\
"""
example = "feature: this feature enable customize through config file"
schema = "<type>(<scope>): <subject>\n\n<body>\n\n<footer>"
schema_pattern = "(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)\\(\\w+\\):\\s(?P<subject>.*)(\\n\\n(?P<body>.*))?(\\n\\n(?P<footer>.*))?"
# The order needs to be preserved, as it influences the order when executing cz commit/cz c
# Change types
[[tool.commitizen.customize.questions]]
type = "list"
name = "change_type"
choices = [
{ value = "build", name = "build: Changes that affect the build system or external dependencies (example scopes: pip, docker, npm)", key = "b" },
{ value = "chore", name = "chore: A modification that generally does not fall into any other category", key = "c" },
{ value = "ci", name = "ci: Changes to our CI configuration files and scripts (example scopes: GitLabCI)", key = "i" },
{ value = "docs", name = "docs: Documentation only changes", key = "d" },
{ value = "feat", name = "feat: A new feature.", key = "f" },
{ value = "fix", name = "fix: A bug fix.", key = "x" },
{ value = "perf", name = "perf: A code change that improves performance", key = "p" },
{ value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature", key = "r" },
{ value = "revert", name = "revert: Revert previous commits", key = "v" },
{ value = "style", name = "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)", key = "s" },
{ value = "test", name = "test: Adding missing or correcting existing tests", key = "t" },
]
message = "Select the type of change you are committing"
# The scope of the change, can be a file, class name or other context
[[tool.commitizen.customize.questions]]
type = "input"
name = "scope"
message = "What is the scope of this change? (class or file name): (press [enter] to skip)\n"
# Summary of the changes
[[tool.commitizen.customize.questions]]
"type" = "input"
"name" = "subject"
"message" = "Write a short and imperative summary of the code changes: (lower case and no period)\n"
# The commit body, elaborate the changes if need be.
[[tool.commitizen.customize.questions]]
type = "input"
name = "body"
message = "Provide additional contextual information about the code changes: (press [enter] to skip)\n"
# Specify if the changes are breaking
[[tool.commitizen.customize.questions]]
type = "confirm"
name = "is_breaking_change"
message = "Is this a BREAKING CHANGE?"
default = false
# Reference closing issues and share other
[[tool.commitizen.customize.questions]]
type = "input"
name = "footer"
message = "Footer. Information about Breaking Changes and reference issues that this commit closes: (press [enter] to skip)"

View File

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

8
.github/CODEOWNERS vendored
View File

@@ -1,8 +0,0 @@
# See https://help.github.com/articles/about-codeowners/
# for more info about CODEOWNERS file
# It uses the same pattern rule for gitignore file
# https://git-scm.com/docs/gitignore#_pattern_format
# Maintainers
* @orhun @mindoodoo @sayanarijit @sophacles @joshka @kdheepak

View File

@@ -7,7 +7,7 @@ assignees: ''
---
<!--
Hi there, sorry `ratatui` is not working as expected.
Hi there, sorry `tui` is not working as expected.
Please fill this bug report conscientiously.
A detailed and complete issue is more likely to be processed quickly.
-->

View File

@@ -1 +1,17 @@
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
## Description
<!--
A clear and concise description of what this PR changes.
-->
## Testing guidelines
<!--
A clear and concise description of how the changes can be tested.
For example, you can include a command to run the relevant tests or examples.
You can also include screenshots of the expected behavior.
-->
## Checklist
* [ ] I have read the [contributing guidelines](../CONTRIBUTING.md).
* [ ] I have added relevant tests.
* [ ] I have documented all new additions.

View File

@@ -1,80 +0,0 @@
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-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
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: --dry-run --allow-dirty
- name: Generate a changelog
uses: orhun/git-cliff-action@v2
with:
config: cliff.toml
args: --unreleased --tag ${{ env.NEXT_TAG }} --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,152 +1,73 @@
name: CI
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
push:
branches:
- main
- master
pull_request:
branches:
- main
merge_group:
- master
# 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
CI_CARGO_MAKE_VERSION: 0.35.6
# 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:
lint:
linux:
name: Linux
runs-on: ubuntu-latest
steps:
- name: Checkout
if: github.event_name != 'pull_request'
uses: actions/checkout@v3
- name: Checkout
if: github.event_name == 'pull_request'
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Check conventional commits
uses: crate-ci/committed@master
with:
args: "-vv"
commits: HEAD
- name: Check typos
uses: crate-ci/typos@master
- name: Lint dependencies
uses: EmbarkStudios/cargo-deny-action@v1
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- 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
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools
- 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 }}
rust: ["1.52.1", "stable"]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Rust {{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
- name: "Install dependencies"
run: sudo apt-get install libncurses5-dev
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
with:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Run cargo make check
run: cargo make check
rust-version: ${{ matrix.rust }}
components: rustfmt,clippy
- uses: actions/checkout@v1
- name: "Get cargo bin directory"
id: cargo-bin-dir
run: echo "::set-output name=dir::$HOME/.cargo/bin"
- name: "Cache cargo make"
id: cache-cargo-make
uses: actions/cache@v2
with:
path: ${{ steps.cargo-bin-dir.outputs.dir }}/cargo-make
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Install cargo-make"
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Format / Build / Test"
run: cargo make ci
env:
RUST_BACKTRACE: full
test-doc:
windows:
name: Windows
runs-on: windows-latest
strategy:
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
runs-on: ${{ matrix.os }}
rust: ["1.52.1", "stable"]
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
- uses: actions/checkout@v1
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
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 }}
rust-version: ${{ matrix.rust }}
components: rustfmt,clippy
- uses: actions/checkout@v1
- name: "Get cargo bin directory"
id: cargo-bin-dir
run: echo "::set-output name=dir::$HOME\.cargo\bin"
- name: "Cache cargo make"
id: cache-cargo-make
uses: actions/cache@v2
with:
path: ${{ steps.cargo-bin-dir.outputs.dir }}\cargo-make.exe
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Install cargo-make"
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Format / Build / Test"
run: cargo make ci
env:
RUST_BACKTRACE: full

View File

@@ -1,9 +0,0 @@
# configuration for https://github.com/DavidAnson/markdownlint
no-inline-html:
allowed_elements:
- img
- details
- summary
line-length:
line_length: 100

View File

@@ -1,374 +1,10 @@
# Changelog
## v0.22.0 - 2023-07-17
## To be released
### 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/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/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/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/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/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/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/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/ratatui-org/ratatui/issues/171))
### Continuous Integration
- *(uncategorized)* Add ci, build, and revert to allowed commit types
### Contributors
Thank you so much to everyone that contributed to this release!
Here is the list of contributors who have contributed to `ratatui` for the first time!
- [@kpcyrd](https://github.com/kpcyrd)
- [@fujiapple852](https://github.com/fujiapple852)
- [@BrookJeynes](https://github.com/BrookJeynes)
- [@Ziqi-Yang](https://github.com/Ziqi-Yang)
- [@Xithrius](https://github.com/Xithrius)
- [@lesleyrs](https://github.com/lesleyrs)
- [@pythops](https://github.com/pythops)
- [@wcampbell0x2a](https://github.com/wcampbell0x2a)
- [@sophacles](https://github.com/sophacles)
- [@Eyesonjune18](https://github.com/Eyesonjune18)
- [@a-kenji](https://github.com/a-kenji)
- [@TimerErTim](https://github.com/TimerErTim)
- [@Mehrbod2002](https://github.com/Mehrbod2002)
- [@thomas-mauran](https://github.com/thomas-mauran)
- [@nyurik](https://github.com/nyurik)
## v0.20.1 - 2023-03-19
### Bug Fixes
- *(style)* Bold needs a bit ([#104](https://github.com/ratatui-org/ratatui/issues/104))
### Documentation
- *(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
Thank you so much to everyone that contributed to this release!
- [@joshka](https://github.com/joshka)
- [@todoesverso](https://github.com/todoesverso)
- [@UncleScientist](https://github.com/UncleScientist)
## v0.20.0 - 2023-03-19
This marks the first release of `ratatui`, a community-maintained fork of [tui](https://github.com/fdehau/tui-rs).
The purpose of this release is to include **bug fixes** and **small changes** into the repository thus **no new features** are added. We have transferred all the pull requests from the original repository and worked on the low hanging ones to incorporate them in this "maintenance" release.
Here is a list of changes:
### Features
- *(cd)* Add continuous deployment workflow ([#93](https://github.com/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/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/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/ratatui-org/ratatui/issues/13))
### Documentation
- *(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/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/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/ratatui-org/ratatui/issues/62))
### Miscellaneous Tasks
- *(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
Thank you so much to everyone that contributed to this release!
- [@orhun](https://github.com/orhun)
- [@mindoodoo](https://github.com/mindoodoo)
- [@sayanarijit](https://github.com/sayanarijit)
- [@Owletti](https://github.com/Owletti)
- [@UncleScientist](https://github.com/UncleScientist)
- [@rhysd](https://github.com/rhysd)
- [@ckaznable](https://github.com/ckaznable)
- [@imuxin](https://github.com/imuxin)
- [@mrjackwills](https://github.com/mrjackwills)
- [@conradludgate](https://github.com/conradludgate)
- [@kianmeng](https://github.com/kianmeng)
- [@chaosprint](https://github.com/chaosprint)
And most importantly, special thanks to [Florian Dehau](https://github.com/fdehau) for creating this awesome library 💖 We look forward to building on the strong foundations that the original crate laid out.
## v0.19.0 - 2022-08-14
### Features
* Bump `crossterm` to `0.25`
## v0.18.0 - 2022-04-24
### Features
* Update `crossterm` to `0.23`
## v0.17.0 - 2022-01-22
### Features
* Add option to `widgets::List` to repeat the highlight symbol for each line of multi-line items (#533).
* Add option to control the alignment of `Axis` labels in the `Chart` widget (#568).
### Breaking changes
* The minimum supported rust version is now `1.56.1`.
#### New default backend and consolidated backend options (#553)
* `crossterm` is now the default backend.
If you are already using the `crossterm` backend, you can simplify your dependency specification in `Cargo.toml`:
```diff
- tui = { version = "0.16", default-features = false, features = ["crossterm"] }
+ tui = "0.17"
```
If you are using the `termion` backend, your `Cargo` is now a bit more verbose:
```diff
- tui = "0.16"
+ tui = { version = "0.17", default-features = false, features = ["termion"] }
```
`crossterm` has also been bumped to version `0.22`.
Because of their apparent low usage, `curses` and `rustbox` backends have been removed.
If you are using one of them, you can import their last implementation in your own project:
* [curses](https://github.com/fdehau/tui-rs/blob/v0.16.0/src/backend/curses.rs)
* [rustbox](https://github.com/fdehau/tui-rs/blob/v0.16.0/src/backend/rustbox.rs)
#### Canvas labels (#543)
* Labels of the `Canvas` widget are now `text::Spans`.
The signature of `widgets::canvas::Context::print` has thus been updated:
```diff
- ctx.print(x, y, "Some text", Color::Yellow);
+ ctx.print(x, y, Span::styled("Some text", Style::default().fg(Color::Yellow)))
```
* Add option to `widgets::List` to repeat the hightlight symbol for each line of multi-line items (#533).
## v0.16.0 - 2021-08-01
@@ -591,7 +227,7 @@ In this new release, you may now write this as:
```rust
Block::default()
.style(Style::default().bg(Color::Green))
// The style is not overridden anymore, we simply add new style rule for the title.
// The style is not overidden anymore, we simply add new style rule for the title.
.title(Span::styled("My title", Style::default().add_modifier(Modifier::BOLD)))
```
@@ -599,9 +235,9 @@ In addition, the crate now provides a method `patch` to combine two styles into
rules:
```rust
let style = Style::default().modifier(Modifier::BOLD);
let style = Style::default().modifer(Modifier::BOLD);
let style = style.patch(Style::default().add_modifier(Modifier::ITALIC));
// style.modifier == Modifier::BOLD | Modifier::ITALIC, the modifier has been enriched not overridden
// style.modifer == Modifier::BOLD | Modifier::ITALIC, the modifier has been enriched not overidden
```
- `Style::modifier` has been removed in favor of `Style::add_modifier` and `Style::remove_modifier`.
@@ -919,7 +555,7 @@ let style = Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD);
### Bug Fixes
* Ensure correct behavior of the alternate screens with the `Crossterm` backend.
* Ensure correct behavoir of the alternate screens with the `Crossterm` backend.
* Fix out of bounds panic when two `Buffer` are merged.
## v0.4.0 - 2019-02-03
@@ -988,7 +624,7 @@ additional `termion` features.
* Replace `Item` by a generic and flexible `Text` that can be used in both
`Paragraph` and `List` widgets.
* Remove unnecessary borrows on `Style`.
* Remove unecessary borrows on `Style`.
## v0.3.0-beta.0 - 2018-09-04
@@ -1011,7 +647,7 @@ widgets on the given `Frame`
* All widgets use the consumable builder pattern
* `SelectableList` can have no selected item and the highlight symbol is hidden
in this case
* Remove markup language inside `Paragraph`. `Paragraph` now expects an iterator
* Remove markup langage inside `Paragraph`. `Paragraph` now expects an iterator
of `Text` items
## v0.2.3 - 2018-06-09
@@ -1019,7 +655,7 @@ of `Text` items
### Features
* Add `start_corner` option for `List`
* Add more text alignment options for `Paragraph`
* Add more text aligment options for `Paragraph`
## v0.2.2 - 2018-05-06

View File

@@ -1,157 +1,33 @@
# Contribution guidelines
# Contributing
First off, thank you for considering contributing to Ratatui.
## Building
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).
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
## Reporting issues
`tui` is an ordinary Rust project where common tasks are managed with [cargo-make].
It wraps common `cargo` commands with sane defaults depending on your platform of choice.
Building the project should be as easy as running `cargo make build`.
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.
## :hammer_and_wrench: Pull requests
## 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.
### Keep PRs small, intentional and focused
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.
### 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
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.
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.
## 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 than pushing to github.
You can also check most of those things yourself locally using `cargo make ci` which will offer you a shorter feedback loop.
## Relationship with `tui-rs`
## Tests
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.
The test coverage of the crate is far from being ideal but we already have a fair amount of tests in place.
Beside the usual doc and unit tests, one of the most valuable test you can write for `tui` is a test again the `TestBackend`.
It allows you to assert the content of the output buffer that would have been flushed to the terminal after a given draw call.
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.

View File

@@ -1,162 +1,95 @@
[package]
name = "ratatui"
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/"
name = "tui"
version = "0.16.0"
authors = ["Florian Dehau <work@fdehau.com>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
documentation = "https://docs.rs/tui/0.16.0/tui/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/ratatui-org/ratatui"
repository = "https://github.com/fdehau/tui-rs"
readme = "README.md"
license = "MIT"
exclude = [
"assets/*",
".github",
"Makefile.toml",
"CONTRIBUTING.md",
"*.log",
"tags",
]
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
autoexamples = true
edition = "2021"
rust-version = "1.65.0"
edition = "2018"
[badges]
[features]
default = ["crossterm"]
all-widgets = ["widget-calendar"]
widget-calendar = ["time"]
macros = []
serde = ["dep:serde", "bitflags/serde"]
[package.metadata.docs.rs]
all-features = true
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
bitflags = "2.3"
tracing = "0.1"
bitflags = "1.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 }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-segmentation = "1.2"
unicode-width = "0.1"
termion = { version = "1.5", optional = true }
crossterm = { version = "0.22", optional = true }
serde = { version = "1", optional = true, features = ["derive"]}
[dev-dependencies]
anyhow = "1.0.71"
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"
tracing-subscriber = "0.2"
rand = "0.8"
[[bench]]
name = "paragraph"
harness = false
argh = "0.1"
[[example]]
name = "barchart"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "block"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "canvas"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "calendar"
required-features = ["crossterm", "widget-calendar"]
doc-scrape-examples = true
[[example]]
name = "chart"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "custom_widget"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "demo"
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
doc-scrape-examples = false
[[example]]
name = "gauge"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "hello_world"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "layout"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "list"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "panic"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "paragraph"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "popup"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "scrollbar"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "sparkline"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "table"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "tabs"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "user_input"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "inline"
required-features = ["crossterm"]
doc-scrape-examples = true

View File

@@ -1,7 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016-2022 Florian Dehau
Copyright (c) 2023 The Ratatui Developers
Copyright (c) 2016 Florian Dehau
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

@@ -1,163 +1,150 @@
# configuration for https://github.com/sagiegurari/cargo-make
[config]
skip_core_tasks = true
[env]
# all features except the backend ones
ALL_FEATURES = "all-widgets,macros,serde"
[tasks.default]
alias = "ci"
[tasks.ci]
dependencies = [
"style-check",
"clippy",
"check",
"test",
run_task = [
{ name = "ci-unix", condition = { platforms = ["linux", "mac"] } },
{ name = "ci-windows", condition = { platforms = ["windows"] } },
]
[tasks.style-check]
dependencies = ["fmt", "typos"]
[tasks.ci-unix]
private = true
dependencies = [
"fmt",
"check-crossterm",
"check-termion",
"test-crossterm",
"test-termion",
"clippy-crossterm",
"clippy-termion",
"test-doc",
]
[tasks.ci-windows]
private = true
dependencies = [
"fmt",
"check-crossterm",
"test-crossterm",
"clippy-crossterm",
"test-doc",
]
[tasks.fmt]
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all", "--check"]
args = [
"fmt",
"--all",
"--",
"--check",
]
[tasks.typos]
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
command = "typos"
[tasks.check-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "check"
[tasks.check-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "check"
[tasks.check]
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-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "build"
[tasks.build-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "build"
[tasks.build]
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-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "clippy"
[tasks.clippy-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "clippy"
[tasks.clippy]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"clippy",
"--all-targets",
"--tests",
"--benches",
"--all-features",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--",
"-D",
"warnings",
]
[tasks.clippy.windows]
args = [
"clippy",
"--all-targets",
"--tests",
"--benches",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz",
"--",
"-D",
"warnings",
]
[tasks.test-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "test"
[tasks.test-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "test"
[tasks.test]
dependencies = [
"test-doc",
]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"test",
"--all-targets",
"--all-features",
]
[tasks.test-windows]
dependencies = [
"test-doc",
]
args = [
"test",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--lib",
"--tests",
"--examples",
]
[tasks.test-doc]
command = "cargo"
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",
"--doc",
]
[tasks.run-example]
private = true
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
command = "cargo"
args = ["run", "--release", "--example", "${TUI_EXAMPLE_NAME}", "--features", "all-widgets"]
args = [
"run",
"--release",
"--example",
"${TUI_EXAMPLE_NAME}"
]
[tasks.build-examples]
command = "cargo"
args = ["build", "--examples", "--release", "--features", "all-widgets"]
args = [
"build",
"--examples",
"--release"
]
[tasks.run-examples]
dependencies = ["build-examples"]

320
README.md
View File

@@ -1,265 +1,119 @@
# Ratatui
# tui-rs
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
[![Build Status](https://github.com/fdehau/tui-rs/workflows/CI/badge.svg)](https://github.com/fdehau/tui-rs/actions?query=workflow%3ACI+)
[![Crate Status](https://img.shields.io/crates/v/tui.svg)](https://crates.io/crates/tui)
[![Docs Status](https://docs.rs/tui/badge.svg)](https://docs.rs/crate/tui/)
`ratatui` is a [Rust](https://www.rust-lang.org) library to build rich terminal user interfaces and
dashboards. It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
project.
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
[![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/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)
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
user interfaces and dashboards. It is heavily inspired by the `Javascript`
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
`Go` library [termui](https://github.com/gizak/termui).
<!-- See RELEASE.md for instructions on creating the demo gif --->
![Demo of Ratatui](https://github.com/ratatui-org/ratatui/assets/24392180/93ab0e38-93e0-4ae0-a31b-91ae6c393185)
The library supports multiple backends:
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
- [termion](https://github.com/ticki/termion)
<details>
<summary>Table of Contents</summary>
The library is based on the principle of immediate rendering with intermediate
buffers. This means that at each new frame you should build all widgets that are
supposed to be part of the UI. While providing a great flexibility for rich and
interactive UI, this may introduce overhead for highly dynamic content. So, the
implementation try to minimize the number of ansi escapes sequences generated to
draw the updated UI. In practice, given the speed of `Rust` the overhead rather
comes from the terminal emulator than the library itself.
* [Ratatui](#ratatui)
* [Installation](#installation)
* [Introduction](#introduction)
* [Quickstart](#quickstart)
* [Status of this fork](#status-of-this-fork)
* [Rust version requirements](#rust-version-requirements)
* [Documentation](#documentation)
* [Examples](#examples)
* [Widgets](#widgets)
* [Built in](#built-in)
* [Third\-party libraries, bootstrapping templates and
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
* [Apps](#apps)
* [Alternatives](#alternatives)
* [Contributors](#contributors)
* [Acknowledgments](#acknowledgments)
* [License](#license)
Moreover, the library does not provide any input handling nor any event system and
you may rely on the previously cited libraries to achieve such features.
</details>
### Rust version requirements
## Installation
Since version 0.17.0, `tui` requires **rustc version 1.52.1 or greater**.
### [Documentation](https://docs.rs/tui)
### Demo
The demo shown in the gif can be run with all available backends.
```shell
cargo add ratatui --features all-widgets
```
Or modify your `Cargo.toml`
```toml
[dependencies]
ratatui = { version = "0.22.0", features = ["all-widgets"]}
```
Ratatui is mostly backwards compatible with `tui-rs`. To migrate an existing project, it may be
easier to rename the ratatui dependency to `tui` rather than updating every usage of the crate.
E.g.:
```toml
[dependencies]
tui = { package = "ratatui", version = "0.22.0", features = ["all-widgets"]}
```
## Introduction
`ratatui` is a terminal UI library that supports multiple backends:
* [crossterm](https://github.com/crossterm-rs/crossterm) [default]
* [termion](https://github.com/ticki/termion)
* [termwiz](https://github.com/wez/wezterm/tree/master/termwiz)
The library is based on the principle of immediate rendering with intermediate buffers. This means
that at each new frame you should build all widgets that are supposed to be part of the UI. While
providing a great flexibility for rich and interactive UI, this may introduce overhead for highly
dynamic content. So, the implementation try to minimize the number of ansi escapes sequences
generated to draw the updated UI. In practice, given the speed of `Rust` the overhead rather comes
from the terminal emulator than the library itself.
Moreover, the library does not provide any input handling nor any event system and you may rely on
the previously cited libraries to achieve such features.
We keep a [CHANGELOG](./CHANGELOG.md) generated by [git-cliff](https://github.com/orhun/git-cliff)
utilizing [Conventional Commits](https://www.conventionalcommits.org/).
## Quickstart
The following example demonstrates the minimal amount of code necessary to setup a terminal and
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/ratatui-org/rust-tui-template).
```rust
fn main() -> Result<(), Box<dyn Error>> {
let mut terminal = setup_terminal()?;
run(&mut terminal)?;
restore_terminal(&mut terminal)?;
Ok(())
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>, Box<dyn Error>> {
let mut stdout = io::stdout();
enable_raw_mode()?;
execute!(stdout, EnterAlternateScreen)?;
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
}
fn restore_terminal(
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
) -> Result<(), Box<dyn Error>> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen,)?;
Ok(terminal.show_cursor()?)
}
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<(), Box<dyn Error>> {
Ok(loop {
terminal.draw(|frame| {
let greeting = Paragraph::new("Hello World!");
frame.render_widget(greeting, frame.size());
})?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if KeyCode::Char('q') == key.code {
break;
}
}
}
})
}
```
## Status of this fork
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
regarding the [future of `tui-rs`](https://github.com/fdehau/tui-rs/issues/654), several members of
the community forked the project and created this crate. We look forward to continuing the work
started by Florian 🚀
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
feel free to join and come chat! There are also plans to implement a [Matrix](https://matrix.org/)
bridge in the near future. **Discord is not a MUST to contribute**. We follow a pretty standard
github centered open source workflow keeping the most important conversations on GitHub, open an
issue or PR and it will be addressed. 😄
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
you are interested in working on a PR or issue opened in the previous repository.
## Rust version requirements
Since version 0.21.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.65.0.
## Documentation
The documentation can be found on [docs.rs.](https://docs.rs/ratatui)
## Examples
The demo shown in the gif above is available on all available backends.
```shell
# crossterm
cargo run --example demo
cargo run --example demo --release -- --tick-rate 200
# termion
cargo run --example demo --no-default-features --features=termion
# termwiz
cargo run --example demo --no-default-features --features=termwiz
cargo run --example demo --no-default-features --features=termion --release -- --tick-rate 200
```
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).
where `tick-rate` is the UI refresh rate in ms.
If the user interface contains glyphs that are not displayed correctly by your terminal, you may
want to run the demo without those symbols:
The UI code is in [examples/demo/ui.rs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/demo/ui.rs) while the
application state is in [examples/demo/app.rs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/demo/app.rs).
```shell
If the user interface contains glyphs that are not displayed correctly by your terminal, you may want to run
the demo without those symbols:
```
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
```
More examples are available in the [examples](./examples/) folder.
### Widgets
## Widgets
The library comes with the following list of widgets:
### Built in
* [Block](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/block.rs)
* [Gauge](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/gauge.rs)
* [Sparkline](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/sparkline.rs)
* [Chart](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/chart.rs)
* [BarChart](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/barchart.rs)
* [List](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/list.rs)
* [Table](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/table.rs)
* [Paragraph](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/paragraph.rs)
* [Canvas (with line, point cloud, map)](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/canvas.rs)
* [Tabs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/tabs.rs)
The library comes with the following
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
Click on each item to see the source of the example. Run the examples with with
cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
* [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)
* [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)
You can run all examples by running `cargo make run-examples` (require
`cargo-make` that can be installed with `cargo install cargo-make`).
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`.
### Third-party widgets
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
be installed with `cargo install cargo-make`).
* [tui-logger](https://github.com/gin66/tui-logger)
### Third-party libraries, bootstrapping templates and widgets
### Apps using tui
* [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
`tui::text::Text`
* [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`tui::style::Color`
* [rust-tui-template](https://github.com/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
Tui-rs + Crossterm apps
* [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
* [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
* [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
* [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications
with a React/Elm inspired approach
* [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
Tui-realm
* [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget): Widget for tree data
structures.
* [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
windows and their rendering
* [tui-textarea](https://github.com/rhysd/tui-textarea): Simple yet powerful multi-line text editor
widget supporting several key shortcuts, undo/redo, text search, etc.
* [tui-input](https://github.com/sayanarijit/tui-input): TUI input library supporting multiple
backends and tui-rs.
* [tui-term](https://github.com/a-kenji/tui-term): A pseudoterminal widget library
that enables the rendering of terminal applications as ratatui widgets.
* [spotify-tui](https://github.com/Rigellute/spotify-tui)
* [bandwhich](https://github.com/imsnif/bandwhich)
* [kmon](https://github.com/orhun/kmon)
* [gpg-tui](https://github.com/orhun/gpg-tui)
* [ytop](https://github.com/cjbassi/ytop)
* [zenith](https://github.com/bvaisvil/zenith)
* [bottom](https://github.com/ClementTsang/bottom)
* [oha](https://github.com/hatoo/oha)
* [gitui](https://github.com/extrawurst/gitui)
* [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli)
* [desed](https://github.com/SoptikHa2/desed)
* [diskonaut](https://github.com/imsnif/diskonaut)
* [tickrs](https://github.com/tarkah/tickrs)
* [rusty-krab-manager](https://github.com/aryakaul/rusty-krab-manager)
* [termchat](https://github.com/lemunozm/termchat)
* [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui)
* [gping](https://github.com/orf/gping/)
* [Vector](https://vector.dev)
* [KDash](https://github.com/kdash-rs/kdash)
* [xplr](https://github.com/sayanarijit/xplr)
* [minesweep](https://github.com/cpcloud/minesweep-rs)
* [Battleship.rs](https://github.com/deepu105/battleship-rs)
* [termscp](https://github.com/veeso/termscp)
* [joshuto](https://github.com/kamiyaa/joshuto)
* [adsb_deku/radar](https://github.com/wcampbell0x2a/adsb_deku#radar-tui)
* [hoard](https://github.com/Hyde46/hoard)
## Apps
### Alternatives
Check out the list of more than 50 [Apps using
`Ratatui`](https://github.com/ratatui-org/ratatui/wiki/Apps-using-Ratatui)!
## Alternatives
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
to build text user interfaces in Rust.
## Contributors
[![GitHub
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 ratatui-org organization.
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an
alternative solution to build text user interfaces in Rust.
## License
[MIT](./LICENSE)
[MIT](LICENSE)

View File

@@ -1,30 +0,0 @@
# Creating a Release
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub
actions](.github/workflows/cd.yml) and triggered by pushing a tag.
1. Record a new demo gif. The preferred tool for this is [ttyrec](http://0xcc.net/ttyrec/) and
[ttygif](https://github.com/icholy/ttygif). [Asciinema](https://asciinema.org/) handles block
character height poorly, [termanilizer](https://www.terminalizer.com/) takes forever to render,
[vhs](https://github.com/charmbracelet/vhs) handles braille
characters poorly (though if <https://github.com/charmbracelet/vhs/issues/322> is fixed, then
it's probably the best option).
```shell
cargo build --example demo
ttyrec -e 'cargo --quiet run --release --example demo -- --tick-rate 100' demo.rec
ttygif demo.rec
```
Then upload it somewhere (e.g. use `vhs publish tty.gif` to publish it or upload it to a GitHub
wiki page as an attachment). Avoid adding the gif to the git repo as binary files tend to bloat
repositories.
1. Bump the version in [Cargo.toml](Cargo.toml).
1. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff)
can be used for generating the entries.
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/ratatui-org/ratatui/actions) workflow to
finish.

BIN
assets/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -1,100 +0,0 @@
# This is a configuration file for the bacon tool
#
# Bacon repository: https://github.com/Canop/bacon
# Complete help on configuration: https://dystroy.org/bacon/config/
# You can also check bacon's own bacon.toml file
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
default_job = "check"
[jobs.check]
command = ["cargo", "check", "--all-features", "--color", "always"]
need_stdout = false
[jobs.check-all]
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",
"--all-targets",
"--color", "always",
]
need_stdout = false
[jobs.test]
command = [
"cargo", "test",
"--all-features",
"--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.doc]
command = [
"cargo", "+nightly", "doc",
"-Zunstable-options", "-Zrustdoc-scrape-examples",
"--all-features",
"--color", "always",
"--no-deps",
]
env.RUSTDOCFLAGS = "--cfg docsrs"
need_stdout = false
# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = [
"cargo", "+nightly", "doc",
"-Zunstable-options", "-Zrustdoc-scrape-examples",
"--all-features",
"--color", "always",
"--no-deps",
"--open",
]
env.RUSTDOCFLAGS = "--cfg docsrs"
need_stdout = false
on_success = "job:doc" # so that we don't open the browser at each change
[jobs.coverage]
command = [
"cargo", "llvm-cov",
"--lcov", "--output-path", "target/lcov.info",
"--all-features",
"--color", "always",
]
[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.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"
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

@@ -1,89 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{Paragraph, Widget, Wrap},
};
/// because the scroll offset is a u16, the maximum number of lines that can be scrolled is 65535.
/// This is a limitation of the current implementation and may be fixed by changing the type of the
/// scroll offset to a u32.
const MAX_SCROLL_OFFSET: u16 = u16::MAX;
const NO_WRAP_WIDTH: u16 = 200;
const WRAP_WIDTH: u16 = 100;
/// Benchmark for rendering a paragraph with a given number of lines. The design of this benchmark
/// allows comparison of the performance of rendering a paragraph with different numbers of lines.
/// as well as comparing with the various settings on the scroll and wrap features.
pub fn paragraph(c: &mut Criterion) {
let mut group = c.benchmark_group("paragraph");
for &line_count in [64, 2048, MAX_SCROLL_OFFSET].iter() {
let lines = random_lines(line_count);
let lines = lines.as_str();
// benchmark that measures the overhead of creating a paragraph separately from rendering
group.bench_with_input(BenchmarkId::new("new", line_count), lines, |b, lines| {
b.iter(|| Paragraph::new(black_box(lines)))
});
// render the paragraph with no scroll
group.bench_with_input(
BenchmarkId::new("render", line_count),
&Paragraph::new(lines),
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
);
// scroll the paragraph by half the number of lines and render
group.bench_with_input(
BenchmarkId::new("render_scroll_half", line_count),
&Paragraph::new(lines).scroll((0u16, line_count / 2)),
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
);
// scroll the paragraph by the full number of lines and render
group.bench_with_input(
BenchmarkId::new("render_scroll_full", line_count),
&Paragraph::new(lines).scroll((0u16, line_count)),
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
);
// render the paragraph wrapped to 100 characters
group.bench_with_input(
BenchmarkId::new("render_wrap", line_count),
&Paragraph::new(lines).wrap(Wrap { trim: false }),
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
);
// scroll the paragraph by the full number of lines and render wrapped to 100 characters
group.bench_with_input(
BenchmarkId::new("render_wrap_scroll_full", line_count),
&Paragraph::new(lines)
.wrap(Wrap { trim: false })
.scroll((0u16, line_count)),
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
);
}
group.finish();
}
/// render the paragraph into a buffer with the given width
fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
let mut buffer = Buffer::empty(Rect::new(0, 0, width, 50));
bencher.iter(|| {
paragraph.clone().render(buffer.area, &mut buffer);
})
}
/// Create a string with the given number of lines filled with nonsense words
///
/// English language has about 5.1 average characters per word so including the space between words
/// this should emit around 200 characters per paragraph on average.
fn random_lines(count: u16) -> String {
let count = count as i64;
let sentence_count = 3;
let word_count = 11;
fakeit::words::paragraph(count, sentence_count, word_count, "\n".into())
}
criterion_group!(benches, paragraph);
criterion_main!(benches);

View File

@@ -1,86 +0,0 @@
# configuration for https://github.com/orhun/git-cliff
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
- *({{commit.scope}})* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
{%- endfor -%}
{% raw %}\n{% endraw %}\
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
- *(uncategorized)* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
{% endif -%}
{% endfor -%}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/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}" },
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 00 -->Features" },
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes" },
{ message = "^refactor", group = "<!-- 02 -->Refactor" },
{ message = "^doc", group = "<!-- 03 -->Documentation" },
{ message = "^perf", group = "<!-- 04 -->Performance" },
{ message = "^style", group = "<!-- 05 -->Styling" },
{ message = "^test", group = "<!-- 06 -->Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore", group = "<!-- 07 -->Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 08 -->Security" },
{ message = "^build", group = "<!-- 09 -->Build" },
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-rc.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

View File

@@ -1,30 +0,0 @@
# configuration for https://github.com/crate-ci/committed
# https://www.conventionalcommits.org
style = "conventional"
# disallow merge commits
merge_commit = false
# subject is not required to be capitalized
subject_capitalized = false
# subject should start with an imperative verb
imperative_subject = true
# subject should not end with a punctuation
subject_not_punctuated = true
# disable line length
line_length = 0
# disable subject length
subject_length = 0
# default allowed_types [ "chore", "docs", "feat", "fix", "perf", "refactor", "style", "test" ]
allowed_types = [
"build",
"chore",
"ci",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test",
]

View File

@@ -1,28 +0,0 @@
# configuration for https://github.com/EmbarkStudios/cargo-deny
[licenses]
default = "deny"
unlicensed = "deny"
copyleft = "deny"
confidence-threshold = 0.8
allow = [
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"MIT",
"Unicode-DFS-2016",
"WTFPL",
]
[advisories]
unmaintained = "deny"
yanked = "deny"
[bans]
multiple-versions = "allow"
[sources]
unknown-registry = "deny"
unknown-git = "warn"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]

View File

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

@@ -1,30 +1,25 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
struct Company<'a> {
revenue: [u64; 4],
label: &'a str,
bar_style: Style,
}
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{BarChart, Block, Borders},
Frame, Terminal,
};
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 {
@@ -54,24 +49,6 @@ 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"],
}
}
@@ -104,7 +81,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -140,16 +117,8 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]
.as_ref(),
)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
@@ -158,93 +127,35 @@ 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]);
draw_bar_with_group_labels(f, app, chunks[1], false);
draw_bar_with_group_labels(f, app, chunks[2], true);
}
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
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,
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()
.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),
)]),
];
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
);
f.render_widget(barchart, chunks[0]);
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);
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]);
}

View File

@@ -1,11 +0,0 @@
# 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,11 +1,17 @@
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::{prelude::*, widgets::*};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, BorderType, Borders},
Frame, Terminal,
};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
@@ -25,11 +31,10 @@ fn main() -> Result<(), Box<dyn Error>> {
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.clear()?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -39,11 +44,9 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(ui)?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
@@ -52,72 +55,65 @@ 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
let outer = f.size();
let outer_block = Block::default()
.borders(Borders::ALL)
.title(block::Title::from("Main block with round corners").alignment(Alignment::Center))
.border_type(BorderType::Rounded);
let inner = outer_block.inner(outer);
let [top, bottom] = *Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(inner)
else {
return;
};
let [top_left, top_right] = *Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.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;
};
// with at least a margin of 1
let size = f.size();
let top_left_block = Block::default()
.title("With Green Background")
.borders(Borders::all())
.on_green();
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()
// Surrounding block
let block = Block::default()
.borders(Borders::ALL)
.title("Main block with round corners")
.title_alignment(Alignment::Center)
.border_type(BorderType::Rounded);
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(4)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
// Top two inner blocks
let top_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
// Top left inner block with green background
let block = Block::default()
.title(vec![
Span::styled("With", Style::default().fg(Color::Yellow)),
Span::from(" background"),
])
.style(Style::default().bg(Color::Green));
f.render_widget(block, top_chunks[0]);
// Top right inner block with styled title aligned to the right
let block = Block::default()
.title(Span::styled(
"Styled title",
Style::default()
.fg(Color::White)
.bg(Color::Red)
.add_modifier(Modifier::BOLD),
))
.title_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);
f.render_widget(block, bottom_chunks[0]);
// Bottom right block with styled left and right border
let block = Block::default()
.title("With styled borders and doubled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Double)
.padding(Padding::uniform(1));
let bottom_inner_block = Block::default()
.title("Block inside padded block")
.borders(Borders::ALL);
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);
.border_type(BorderType::Double);
f.render_widget(block, bottom_chunks[1]);
}

View File

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

@@ -1,277 +0,0 @@
use std::{error::Error, io, rc::Rc};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::calendar::*};
use time::{Date, Month, OffsetDateTime};
fn main() -> Result<(), Box<dyn Error>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
loop {
let _ = terminal.draw(|f| draw(f));
if let Event::Key(key) = event::read()? {
#[allow(clippy::single_match)]
match key.code {
KeyCode::Char(_) => {
break;
}
_ => {}
};
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}
fn draw<B: Backend>(f: &mut Frame<B>) {
let app_area = f.size();
let calarea = Rect {
x: app_area.x + 1,
y: app_area.y + 1,
height: app_area.height - 1,
width: app_area.width - 1,
};
let mut start = OffsetDateTime::now_local()
.unwrap()
.date()
.replace_month(Month::January)
.unwrap()
.replace_day(1)
.unwrap();
let list = make_dates(start.year());
for chunk in split_rows(&calarea)
.iter()
.flat_map(|row| split_cols(row).to_vec())
{
let cal = cals::get_cal(start.month(), start.year(), &list);
f.render_widget(cal, chunk);
start = start.replace_month(start.month().next()).unwrap();
}
}
fn split_rows(area: &Rect) -> Rc<[Rect]> {
let list_layout = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints(
[
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(33),
]
.as_ref(),
);
list_layout.split(*area)
}
fn split_cols(area: &Rect) -> Rc<[Rect]> {
let list_layout = Layout::default()
.direction(Direction::Horizontal)
.margin(0)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
);
list_layout.split(*area)
}
fn make_dates(current_year: i32) -> CalendarEventStore {
let mut list = CalendarEventStore::today(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Blue),
);
// Holidays
let holiday_style = Style::default()
.fg(Color::Red)
.add_modifier(Modifier::UNDERLINED);
// new year's
list.add(
Date::from_calendar_date(current_year, Month::January, 1).unwrap(),
holiday_style,
);
// next new_year's for December "show surrounding"
list.add(
Date::from_calendar_date(current_year + 1, Month::January, 1).unwrap(),
holiday_style,
);
// groundhog day
list.add(
Date::from_calendar_date(current_year, Month::February, 2).unwrap(),
holiday_style,
);
// april fool's
list.add(
Date::from_calendar_date(current_year, Month::April, 1).unwrap(),
holiday_style,
);
// earth day
list.add(
Date::from_calendar_date(current_year, Month::April, 22).unwrap(),
holiday_style,
);
// star wars day
list.add(
Date::from_calendar_date(current_year, Month::May, 4).unwrap(),
holiday_style,
);
// festivus
list.add(
Date::from_calendar_date(current_year, Month::December, 23).unwrap(),
holiday_style,
);
// new year's eve
list.add(
Date::from_calendar_date(current_year, Month::December, 31).unwrap(),
holiday_style,
);
// seasons
let season_style = Style::default()
.fg(Color::White)
.bg(Color::Yellow)
.add_modifier(Modifier::UNDERLINED);
// spring equinox
list.add(
Date::from_calendar_date(current_year, Month::March, 22).unwrap(),
season_style,
);
// summer solstice
list.add(
Date::from_calendar_date(current_year, Month::June, 21).unwrap(),
season_style,
);
// fall equinox
list.add(
Date::from_calendar_date(current_year, Month::September, 22).unwrap(),
season_style,
);
list.add(
Date::from_calendar_date(current_year, Month::December, 21).unwrap(),
season_style,
);
list
}
mod cals {
use super::*;
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
use Month::*;
match m {
May => example1(m, y, es),
June => example2(m, y, es),
July => example3(m, y, es),
December => example3(m, y, es),
February => example4(m, y, es),
November => example5(m, y, es),
_ => default(m, y, es),
}
}
fn default<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_month_header(Style::default())
.default_style(default_style)
}
fn example1<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_surrounding(default_style)
.default_style(default_style)
.show_month_header(Style::default())
}
fn example2<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::DIM)
.fg(Color::LightYellow);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_weekdays_header(header_style)
.default_style(default_style)
.show_month_header(Style::default())
}
fn example3<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_surrounding(Style::default().add_modifier(Modifier::DIM))
.show_weekdays_header(header_style)
.default_style(default_style)
.show_month_header(Style::default())
}
fn example4<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_weekdays_header(header_style)
.default_style(default_style)
}
fn example5<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_month_header(header_style)
.default_style(default_style)
}
}

View File

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

@@ -1,17 +1,23 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::*,
widgets::{canvas::*, *},
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::Span,
widgets::{
canvas::{Canvas, Map, MapResolution, Rectangle},
Block, Borders,
},
Frame, Terminal,
};
struct App {
@@ -23,8 +29,6 @@ struct App {
vy: f64,
dir_x: bool,
dir_y: bool,
tick_count: u64,
marker: Marker,
}
impl App {
@@ -44,22 +48,10 @@ impl App {
vy: 1.0,
dir_x: true,
dir_y: true,
tick_count: 0,
marker: Marker::Dot,
}
}
fn on_tick(&mut self) {
self.tick_count += 1;
// only change marker every 4 ticks (1s) to avoid stroboscopic effect
if (self.tick_count % 4) == 0 {
self.marker = match self.marker {
Marker::Dot => Marker::Block,
Marker::Block => Marker::Bar,
Marker::Bar => Marker::Braille,
Marker::Braille => Marker::Dot,
};
}
if self.ball.x < self.playground.left() as f64
|| self.ball.x + self.ball.width > self.playground.right() as f64
{
@@ -108,7 +100,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -163,20 +155,22 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.split(f.size());
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.marker(app.marker)
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
resolution: MapResolution::High,
});
ctx.print(app.x, -app.y, "You are here".yellow());
ctx.print(
app.x,
-app.y,
Span::styled("You are here", Style::default().fg(Color::Yellow)),
);
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]);
f.render_widget(canvas, chunks[0]);
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.marker(app.marker)
.paint(|ctx| {
ctx.draw(&app.ball);
})

View File

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

@@ -1,15 +1,22 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
symbols,
text::Span,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
Frame, Terminal,
};
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] = [
@@ -110,7 +117,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -182,7 +189,12 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 1".cyan().bold())
.title(Span::styled(
"Chart 1",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
@@ -196,7 +208,11 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
.labels(vec![
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("0"),
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
])
.bounds([-20.0, 20.0]),
);
f.render_widget(chart, chunks[0]);
@@ -210,7 +226,12 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 2".cyan().bold())
.title(Span::styled(
"Chart 2",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
@@ -218,14 +239,22 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[1]);
@@ -238,7 +267,12 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 3".cyan().bold())
.title(Span::styled(
"Chart 3",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
@@ -246,14 +280,22 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 50.0])
.labels(vec!["0".bold(), "25".into(), "50".bold()]),
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("25"),
Span::styled("50", Style::default().add_modifier(Modifier::BOLD)),
]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5".bold()]),
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5", Style::default().add_modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[2]);
}

View File

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

@@ -1,11 +1,17 @@
use std::{error::Error, io};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
layout::Rect,
style::Style,
widgets::Widget,
Frame, Terminal,
};
#[derive(Default)]
struct Label<'a> {
@@ -46,7 +52,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())

View File

@@ -1,11 +0,0 @@
# 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::*;
use tui::widgets::ListState;
const TASKS: [&str; 24] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",

View File

@@ -1,17 +1,18 @@
use crate::{app::App, ui};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
use tui::{
backend::{Backend, CrosstermBackend},
Terminal,
};
use ratatui::prelude::*;
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
@@ -35,7 +36,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -55,15 +56,13 @@ fn run_app<B: Backend>(
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char(c) => app.on_key(c),
KeyCode::Left => app.on_left(),
KeyCode::Up => app.on_up(),
KeyCode::Right => app.on_right(),
KeyCode::Down => app.on_down(),
_ => {}
}
match key.code {
KeyCode::Char(c) => app.on_key(c),
KeyCode::Left => app.on_left(),
KeyCode::Up => app.on_up(),
KeyCode::Right => app.on_right(),
KeyCode::Down => app.on_down(),
_ => {}
}
}
}

View File

@@ -1,17 +1,17 @@
use std::{error::Error, time::Duration};
use argh::FromArgs;
mod app;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termwiz")]
mod termwiz;
mod ui;
#[cfg(feature = "crossterm")]
use crate::crossterm::run;
#[cfg(feature = "termion")]
use crate::termion::run;
use argh::FromArgs;
use std::{error::Error, time::Duration};
/// Demo
#[derive(Debug, FromArgs)]
struct Cli {
@@ -26,11 +26,6 @@ struct Cli {
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let tick_rate = Duration::from_millis(cli.tick_rate);
#[cfg(feature = "crossterm")]
crate::crossterm::run(tick_rate, cli.enhanced_graphics)?;
#[cfg(feature = "termion")]
crate::termion::run(tick_rate, cli.enhanced_graphics)?;
#[cfg(feature = "termwiz")]
crate::termwiz::run(tick_rate, cli.enhanced_graphics)?;
run(tick_rate, cli.enhanced_graphics)?;
Ok(())
}

View File

@@ -1,23 +1,21 @@
use crate::{app::App, ui};
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use ratatui::prelude::*;
use termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::IntoAlternateScreen,
screen::AlternateScreen,
};
use tui::{
backend::{Backend, TermionBackend},
Terminal,
};
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
let stdout = io::stdout()
.into_raw_mode()
.unwrap()
.into_alternate_screen()
.unwrap();
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
@@ -66,14 +64,14 @@ fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
let stdin = io::stdin();
for key in stdin.keys().flatten() {
if let Err(err) = keys_tx.send(Event::Input(key)) {
eprintln!("{err}");
eprintln!("{}", err);
return;
}
}
});
thread::spawn(move || loop {
if let Err(err) = tx.send(Event::Tick) {
eprintln!("{err}");
eprintln!("{}", err);
break;
}
thread::sleep(tick_rate);

View File

@@ -1,76 +0,0 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use ratatui::prelude::*;
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
let backend = TermwizBackend::new()?;
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// create app and run it
let app = App::new("Termwiz Demo", enhanced_graphics);
let res = run_app(&mut terminal, app, tick_rate);
terminal.show_cursor()?;
terminal.flush()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app(
terminal: &mut Terminal<TermwizBackend>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if let Ok(Some(input)) = terminal
.backend_mut()
.buffered_terminal_mut()
.terminal()
.poll_input(Some(timeout))
{
match input {
InputEvent::Key(key_code) => match key_code.key {
KeyCode::UpArrow => app.on_up(),
KeyCode::DownArrow => app.on_down(),
KeyCode::LeftArrow => app.on_left(),
KeyCode::RightArrow => app.on_right(),
KeyCode::Char(c) => app.on_key(c),
_ => {}
},
InputEvent::Resized { cols, rows } => {
terminal
.backend_mut()
.buffered_terminal_mut()
.resize(cols, rows);
}
_ => {}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
return Ok(());
}
}
}

View File

@@ -1,9 +1,17 @@
use ratatui::{
prelude::*,
widgets::{canvas::*, *},
};
use crate::app::App;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans},
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
widgets::{
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
},
Frame,
};
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
@@ -13,7 +21,7 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.tabs
.titles
.iter()
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.map(|t| Spans::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title(app.title))
@@ -74,7 +82,6 @@ where
.bg(Color::Black)
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
)
.use_unicode(app.enhanced_graphics)
.label(label)
.ratio(app.progress);
f.render_widget(gauge, chunks[0]);
@@ -130,7 +137,7 @@ where
.tasks
.items
.iter()
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
.map(|i| ListItem::new(vec![Spans::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks)
.block(Block::default().borders(Borders::ALL).title("List"))
@@ -154,8 +161,8 @@ where
"WARNING" => warning_style,
_ => info_style,
};
let content = vec![text::Line::from(vec![
Span::styled(format!("{level:<9}"), s),
let content = vec![Spans::from(vec![
Span::styled(format!("{:<9}", level), s),
Span::raw(evt),
])];
ListItem::new(content)
@@ -254,9 +261,9 @@ where
B: Backend,
{
let text = 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![
Spans::from("This is a paragraph with several lines. You can change style your text the way you want"),
Spans::from(""),
Spans::from(vec![
Span::from("For example: "),
Span::styled("under", Style::default().fg(Color::Red)),
Span::raw(" "),
@@ -265,7 +272,7 @@ where
Span::styled("rainbow", Style::default().fg(Color::Blue)),
Span::raw("."),
]),
text::Line::from(vec![
Spans::from(vec![
Span::raw("Oh and if you didn't "),
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
Span::raw(" you can "),
@@ -276,7 +283,7 @@ where
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
Span::raw(".")
]),
text::Line::from(
Spans::from(
"One more thing is that it should display unicode characters: 10€"
),
];
@@ -339,15 +346,9 @@ where
height: 10.0,
color: Color::Yellow,
});
ctx.draw(&Circle {
x: app.servers[2].coords.1,
y: app.servers[2].coords.0,
radius: 10.0,
color: Color::Green,
});
for (i, s1) in app.servers.iter().enumerate() {
for s2 in &app.servers[i + 1..] {
ctx.draw(&canvas::Line {
ctx.draw(&Line {
x1: s1.coords.1,
y1: s1.coords.0,
y2: s2.coords.0,
@@ -410,7 +411,7 @@ where
.iter()
.map(|c| {
let cells = vec![
Cell::from(Span::raw(format!("{c:?}: "))),
Cell::from(Span::raw(format!("{:?}: ", c))),
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
Cell::from(Span::styled("Background", Style::default().bg(*c))),
];

View File

@@ -1,15 +1,21 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Gauge},
Frame, Terminal,
};
struct App {
progress1: u16,
@@ -71,7 +77,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -106,6 +112,7 @@ 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),
@@ -145,9 +152,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.use_unicode(true);
f.render_widget(gauge, chunks[2]);
let label = format!("{}/100", app.progress4);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge4").borders(Borders::ALL))
.block(Block::default().title("Gauge4"))
.gauge_style(
Style::default()
.fg(Color::Cyan)

View File

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

@@ -1,80 +0,0 @@
use std::{
io::{self, Stdout},
time::Duration,
};
use anyhow::{Context, Result};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
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
/// teardown of a terminal application.
///
/// A more robust application would probably want to handle errors and ensure that the terminal is
/// restored to a sane state before exiting. This example does not do that. It also does not handle
/// events or update the application state. It just draws a greeting and exits when the user
/// presses 'q'.
fn main() -> Result<()> {
let mut terminal = setup_terminal().context("setup failed")?;
run(&mut terminal).context("app loop failed")?;
restore_terminal(&mut terminal).context("restore terminal failed")?;
Ok(())
}
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and
/// hide the cursor. This example does not handle errors. A more robust application would probably
/// want to handle errors and ensure that the terminal is restored to a sane state before exiting.
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
let mut stdout = io::stdout();
enable_raw_mode().context("failed to enable raw mode")?;
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
}
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
/// the cursor.
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode().context("failed to disable raw mode")?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)
.context("unable to switch to main screen")?;
terminal.show_cursor().context("unable to show cursor")
}
/// Run the application loop. This is where you would handle events and update the application
/// state. This example exits when the user presses 'q'. Other styles of application loops are
/// possible, for example, you could have multiple application states and switch between them based
/// on events, or you could have a single application state and update it based on events.
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
loop {
terminal.draw(crate::render_app)?;
if should_quit()? {
break;
}
}
Ok(())
}
/// Render the application. This is where you would draw the application UI. This example just
/// draws a greeting.
fn render_app(frame: &mut ratatui::Frame<CrosstermBackend<Stdout>>) {
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
frame.render_widget(greeting, frame.size());
}
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
/// events. There is a 250ms timeout on the event poll so that the application can exit in a timely
/// manner, and to ensure that the terminal is rendered at least once every 250ms.
fn should_quit() -> Result<bool> {
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
if let Event::Key(key) = event::read().context("event read failed")? {
return Ok(KeyCode::Char('q') == key.code);
}
}
Ok(false)
}

View File

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

@@ -1,3 +1,4 @@
use rand::distributions::{Distribution, Uniform};
use std::{
collections::{BTreeMap, VecDeque},
error::Error,
@@ -6,9 +7,16 @@ use std::{
thread,
time::{Duration, Instant},
};
use rand::distributions::{Distribution, Uniform};
use ratatui::{prelude::*, widgets::*};
use tracing::{event, span, Level};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans},
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
Frame, Terminal, TerminalOptions, ViewportVariant,
};
const NUM_DOWNLOADS: usize = 10;
@@ -64,13 +72,18 @@ struct Worker {
}
fn main() -> Result<(), Box<dyn Error>> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_writer(io::stderr)
.init();
crossterm::terminal::enable_raw_mode()?;
let stdout = io::stdout();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(8),
viewport: ViewportVariant::Inline(8),
},
)?;
@@ -168,6 +181,8 @@ fn run_app<B: Backend>(
}
redraw = true;
let span = span!(Level::INFO, "recv");
let _guard = span.enter();
match rx.recv()? {
Event::Input(event) => {
if event.code == crossterm::event::KeyCode::Char('q') {
@@ -175,21 +190,32 @@ fn run_app<B: Backend>(
}
}
Event::Resize => {
terminal.autoresize()?;
event!(Level::INFO, "resize");
terminal.resize()?;
}
Event::Tick => {}
Event::DownloadUpdate(worker_id, _download_id, progress) => {
Event::Tick => {
event!(Level::INFO, "tick");
}
Event::DownloadUpdate(worker_id, download_id, progress) => {
event!(
Level::INFO,
worker_id,
download_id,
progress,
"download update"
);
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
download.progress = progress;
redraw = false
}
Event::DownloadDone(worker_id, download_id) => {
event!(Level::INFO, worker_id, download_id, "download done");
let download = downloads.in_progress.remove(&worker_id).unwrap();
terminal.insert_before(1, |buf| {
Paragraph::new(Line::from(vec![
Paragraph::new(Spans::from(vec![
Span::from("Finished "),
Span::styled(
format!("download {download_id}"),
format!("download {}", download_id),
Style::default().add_modifier(Modifier::BOLD),
),
Span::from(format!(
@@ -219,7 +245,9 @@ fn run_app<B: Backend>(
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
let size = f.size();
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
let block = Block::default()
.title("Progress")
.title_alignment(Alignment::Center);
f.render_widget(block, size);
let chunks = Layout::default()
@@ -231,7 +259,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
let progress = LineGauge::default()
.gauge_style(Style::default().fg(Color::Blue))
.label(format!("{done}/{NUM_DOWNLOADS}"))
.label(format!("{}/{}", done, NUM_DOWNLOADS))
.ratio(done as f64 / NUM_DOWNLOADS as f64);
f.render_widget(progress, chunks[0]);
@@ -243,9 +271,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
// in progress downloads
let items: Vec<ListItem> = downloads
.in_progress
.values()
.map(|download| {
ListItem::new(Line::from(vec![
.iter()
.map(|(_worker_id, download)| {
ListItem::new(Spans::from(vec![
Span::raw(symbols::DOT),
Span::styled(
format!(" download {:>2}", download.id),

View File

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

@@ -1,11 +1,15 @@
use std::{error::Error, io};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders},
Frame, Terminal,
};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
@@ -28,7 +32,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -46,51 +50,21 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
let [top, mid, bottom] = *Layout::default()
fn ui<B: Backend>(f: &mut Frame<B>) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(4),
Constraint::Percentage(50),
Constraint::Min(4),
Constraint::Percentage(10),
Constraint::Percentage(80),
Constraint::Percentage(10),
]
.as_ref(),
)
.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,
);
.split(f.size());
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,
);
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]);
}

View File

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

@@ -1,15 +1,21 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, List, ListItem, ListState},
Frame, Terminal,
};
use ratatui::{prelude::*, widgets::*};
struct StatefulList<T> {
state: ListState,
@@ -57,9 +63,9 @@ impl<T> StatefulList<T> {
}
}
/// This struct holds the current state of the app. In particular, it has the `items` field which is
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
/// widget with its state and have access to features such as natural scrolling.
/// This struct holds the current state of the app. In particular, it has the `items` field which is a wrapper
/// around `ListState`. Keeping track of the items state let us render the associated widget with its state
/// and have access to features such as natural scrolling.
///
/// Check the event handling at the bottom to see how to change the state on incoming events.
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
@@ -160,7 +166,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -180,14 +186,12 @@ fn run_app<B: Backend>(
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Left => app.items.unselect(),
KeyCode::Down => app.items.next(),
KeyCode::Up => app.items.previous(),
_ => {}
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Left => app.items.unselect(),
KeyCode::Down => app.items.next(),
KeyCode::Up => app.items.previous(),
_ => {}
}
}
}
@@ -211,13 +215,12 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.items
.iter()
.map(|i| {
let mut lines = vec![Line::from(i.0)];
let mut lines = vec![Spans::from(i.0)];
for _ in 0..i.1 {
lines.push(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
.italic()
.into(),
);
lines.push(Spans::from(Span::styled(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
Style::default().add_modifier(Modifier::ITALIC),
)));
}
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
})
@@ -252,13 +255,16 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
_ => Style::default(),
};
// Add a example datetime and apply proper spacing between them
let header = Line::from(vec![
Span::styled(format!("{level:<9}"), s),
" ".into(),
"2020-01-01 10:00:00".italic(),
let header = Spans::from(vec![
Span::styled(format!("{:<9}", level), s),
Span::raw(" "),
Span::styled(
"2020-01-01 10:00:00",
Style::default().add_modifier(Modifier::ITALIC),
),
]);
// The event gets its own line
let log = Line::from(vec![event.into()]);
let log = Spans::from(vec![Span::raw(event)]);
// Here several things happen:
// 1. Add a `---` spacing line above the final list entry
@@ -266,9 +272,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
// 3. Add a spacer line
// 4. Add the actual event
ListItem::new(vec![
Line::from("-".repeat(chunks[1].width as usize)),
Spans::from("-".repeat(chunks[1].width as usize)),
header,
Line::from(""),
Spans::from(""),
log,
])
})

View File

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

@@ -1,134 +0,0 @@
//! How to use a panic hook to reset the terminal before printing the panic to
//! the terminal.
//!
//! When exiting normally or when handling `Result::Err`, we can reset the
//! terminal manually at the end of `main` just before we print the error.
//!
//! Because a panic interrupts the normal control flow, manually resetting the
//! terminal at the end of `main` won't do us any good. Instead, we need to
//! make sure to set up a panic hook that first resets the terminal before
//! handling the panic. This both reuses the standard panic hook to ensure a
//! consistent panic handling UX and properly resets the terminal to not
//! distort the output.
//!
//! That's why this example is set up to show both situations, with and without
//! the chained panic hook, to see the difference.
use std::{error::Error, io};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
#[derive(Default)]
struct App {
hook_enabled: bool,
}
impl App {
fn chain_hook(&mut self) {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic| {
reset_terminal().unwrap();
original_hook(panic);
}));
self.hook_enabled = true;
}
}
fn main() -> Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::default();
let res = run_tui(&mut terminal, &mut app);
reset_terminal()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
/// Initializes the terminal.
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
enable_raw_mode()?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
/// Resets the terminal.
fn reset_terminal() -> Result<()> {
disable_raw_mode()?;
crossterm::execute!(io::stdout(), LeaveAlternateScreen)?;
Ok(())
}
/// Runs the TUI loop.
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, app))?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('p') => {
panic!("intentional demo panic");
}
KeyCode::Char('e') => {
app.chain_hook();
}
_ => {
return Ok(());
}
}
}
}
}
/// Render the TUI.
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let text = vec![
if app.hook_enabled {
Line::from("HOOK IS CURRENTLY **ENABLED**")
} else {
Line::from("HOOK IS CURRENTLY **DISABLED**")
},
Line::from(""),
Line::from("press `p` to panic"),
Line::from("press `e` to enable the terminal-resetting panic hook"),
Line::from("press any other key to quit without panic"),
Line::from(""),
Line::from("when you panic without the chained hook,"),
Line::from("you will likely have to reset your terminal afterwards"),
Line::from("with the `reset` command"),
Line::from(""),
Line::from("with the chained panic hook enabled,"),
Line::from("you should see the panic report as you would without ratatui"),
Line::from(""),
Line::from("try first without the panic handler to see the difference"),
];
let b = Block::default()
.title("Panic Handler Demo")
.borders(Borders::ALL);
let p = Paragraph::new(text).block(b).alignment(Alignment::Center);
f.render_widget(p, f.size());
}

View File

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

@@ -1,15 +1,21 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, Paragraph, Wrap},
Frame, Terminal,
};
struct App {
scroll: u16,
@@ -49,7 +55,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -89,11 +95,12 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let block = Block::default().black();
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(5)
.constraints(
[
Constraint::Percentage(25),
@@ -106,54 +113,59 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.split(size);
let text = vec![
Line::from("This is a line "),
Line::from("This is a line ".red()),
Line::from("This is a line".on_blue()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.on_green()),
Line::from("This is a line".green().italic()),
Line::from(vec![
"Masked text: ".into(),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
]),
Spans::from("This is a line "),
Spans::from(Span::styled(
"This is a line ",
Style::default().fg(Color::Red),
)),
Spans::from(Span::styled(
"This is a line",
Style::default().bg(Color::Blue),
)),
Spans::from(Span::styled(
"This is a longer line",
Style::default().add_modifier(Modifier::CROSSED_OUT),
)),
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
Spans::from(Span::styled(
"This is a line",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::ITALIC),
)),
];
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::Gray))
.style(Style::default().bg(Color::White).fg(Color::Black))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), no wrap"));
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Left, no wrap"))
.alignment(Alignment::Left);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Left, wrap"))
.alignment(Alignment::Left)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Right alignment, with wrap"))
.alignment(Alignment::Right)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::Gray))
.block(create_block("Center alignment, with wrap, with scroll"))
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Center, wrap"))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true })
.scroll((app.scroll, 0));
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Right, wrap"))
.alignment(Alignment::Right)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[3]);
}

View File

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

@@ -1,11 +1,18 @@
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame, Terminal,
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
struct App {
show_popup: bool,
@@ -39,7 +46,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -50,12 +57,10 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
terminal.draw(|f| ui(f, &app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('p') => app.show_popup = !app.show_popup,
_ => {}
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('p') => app.show_popup = !app.show_popup,
_ => {}
}
}
}
@@ -73,15 +78,18 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
} else {
"Press p to show the popup"
};
let paragraph = Paragraph::new(text.slow_blink())
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
let paragraph = Paragraph::new(Span::styled(
text,
Style::default().add_modifier(Modifier::SLOW_BLINK),
))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[0]);
let block = Block::default()
.title("Content")
.borders(Borders::ALL)
.on_blue();
.style(Style::default().bg(Color::Blue));
f.render_widget(block, chunks[1]);
if app.show_popup {

View File

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

@@ -1,245 +0,0 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, symbols::scrollbar, widgets::*};
#[derive(Default)]
struct App {
pub vertical_scroll_state: ScrollbarState,
pub horizontal_scroll_state: ScrollbarState,
pub vertical_scroll: usize,
pub horizontal_scroll: usize,
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::default();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('j') => {
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
app.vertical_scroll_state = app
.vertical_scroll_state
.position(app.vertical_scroll as u16);
}
KeyCode::Char('k') => {
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
app.vertical_scroll_state = app
.vertical_scroll_state
.position(app.vertical_scroll as u16);
}
KeyCode::Char('h') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.position(app.horizontal_scroll as u16);
}
KeyCode::Char('l') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.position(app.horizontal_scroll as u16);
}
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let block = Block::default().black();
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(size);
let text = vec![
Line::from("This is a line "),
Line::from("This is a line ".red()),
Line::from("This is a line".on_dark_gray()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.reset()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
]),
Line::from("This is a line "),
Line::from("This is a line ".red()),
Line::from("This is a line".on_dark_gray()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.reset()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
]),
];
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len() as u16);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.content_length(long_line.len() as u16);
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.gray()
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let title = Block::default()
.title("Use h j k l to scroll ◄ ▲ ▼ ►")
.title_alignment(Alignment::Center);
f.render_widget(title, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block("Vertical scrollbar with arrows"))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[1]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some("")),
chunks[1],
&mut app.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Vertical scrollbar without arrows and mirrored",
))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[2]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalLeft)
.symbols(scrollbar::VERTICAL)
.begin_symbol(None)
.end_symbol(None),
chunks[2].inner(&Margin {
vertical: 1,
horizontal: 0,
}),
&mut app.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
))
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[3]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("🬋")
.end_symbol(None),
chunks[3].inner(&Margin {
vertical: 0,
horizontal: 1,
}),
&mut app.horizontal_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Horizontal scrollbar without arrows & custom thumb and track symbol",
))
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[4]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("")
.track_symbol(""),
chunks[4].inner(&Margin {
vertical: 0,
horizontal: 1,
}),
&mut app.horizontal_scroll_state,
);
}

View File

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

@@ -1,9 +1,3 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
@@ -13,7 +7,18 @@ use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::{prelude::*, widgets::*};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, Sparkline},
Frame, Terminal,
};
#[derive(Clone)]
pub struct RandomSignal {
@@ -94,7 +99,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -129,6 +134,7 @@ 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),

View File

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

@@ -1,11 +1,16 @@
use std::{error::Error, io};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Cell, Row, Table, TableState},
Frame, Terminal,
};
struct App<'a> {
state: TableState,
@@ -90,7 +95,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -101,13 +106,11 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
terminal.draw(|f| ui(f, &mut app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
_ => {}
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
_ => {}
}
}
}
@@ -116,6 +119,7 @@ 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);
@@ -144,7 +148,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.highlight_symbol(">> ")
.widths(&[
Constraint::Percentage(50),
Constraint::Max(30),
Constraint::Length(30),
Constraint::Min(10),
]);
f.render_stateful_widget(t, rects[0], &mut app.state);

View File

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

@@ -1,11 +1,17 @@
use std::{error::Error, io};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, Tabs},
Frame, Terminal,
};
struct App<'a> {
pub titles: Vec<&'a str>,
@@ -55,7 +61,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -66,13 +72,11 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
terminal.draw(|f| ui(f, &app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Right => app.next(),
KeyCode::Left => app.previous(),
_ => {}
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Right => app.next(),
KeyCode::Left => app.previous(),
_ => {}
}
}
}
@@ -82,17 +86,21 @@ 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);
let block = Block::default().on_white().black();
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
f.render_widget(block, size);
let titles = app
.titles
.iter()
.map(|t| {
let (first, rest) = t.split_at(1);
Line::from(vec![first.yellow(), rest.green()])
Spans::from(vec![
Span::styled(first, Style::default().fg(Color::Yellow)),
Span::styled(rest, Style::default().fg(Color::Green)),
])
})
.collect();
let tabs = Tabs::new(titles)

View File

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

@@ -1,25 +1,29 @@
use std::{error::Error, io};
/// A simple example demonstrating how to handle user input. This is
/// a bit out of the scope of the library as it does not provide any
/// input handling out of the box. However, it may helps some to get
/// started.
///
/// This is a very simple example:
/// * 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
/// * A input box always focused. Every character you type is registered
/// here
/// * Pressing Backspace erases a character
/// * Pressing Enter pushes the current input in the history of previous
/// messages.
/// **Note: ** as this is a relatively simple example unicode characters are unsupported and
/// their use will result in undefined behaviour.
/// messages
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, Borders, List, ListItem, Paragraph},
Frame, Terminal,
};
use unicode_width::UnicodeWidthStr;
enum InputMode {
Normal,
@@ -30,8 +34,6 @@ 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
@@ -44,65 +46,10 @@ 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()?;
@@ -125,7 +72,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
println!("{:?}", err)
}
Ok(())
@@ -146,26 +93,21 @@ 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.submit_message(),
KeyCode::Char(to_insert) => {
app.enter_char(to_insert);
InputMode::Editing => match key.code {
KeyCode::Enter => {
app.messages.push(app.input.drain(..).collect());
}
KeyCode::Char(c) => {
app.input.push(c);
}
KeyCode::Backspace => {
app.delete_char();
}
KeyCode::Left => {
app.move_cursor_left();
}
KeyCode::Right => {
app.move_cursor_right();
app.input.pop();
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
_ => {}
},
_ => {}
}
}
}
@@ -174,6 +116,7 @@ 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),
@@ -187,31 +130,31 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let (msg, style) = match app.input_mode {
InputMode::Normal => (
vec![
"Press ".into(),
"q".bold(),
" to exit, ".into(),
"e".bold(),
" to start editing.".bold(),
Span::raw("Press "),
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit, "),
Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to start editing."),
],
Style::default().add_modifier(Modifier::RAPID_BLINK),
),
InputMode::Editing => (
vec![
"Press ".into(),
"Esc".bold(),
" to stop editing, ".into(),
"Enter".bold(),
" to record the message".into(),
Span::raw("Press "),
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to stop editing, "),
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to record the message"),
],
Style::default(),
),
};
let mut text = Text::from(Line::from(msg));
let mut text = Text::from(Spans::from(msg));
text.patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[0]);
let input = Paragraph::new(app.input.as_str())
let input = Paragraph::new(app.input.as_ref())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Yellow),
@@ -224,12 +167,10 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
{}
InputMode::Editing => {
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
// rendering
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
f.set_cursor(
// 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,
// Put cursor past the end of the input text
chunks[1].x + app.input.width() as u16 + 1,
// Move one line down, from the border to the input line
chunks[1].y + 1,
)
@@ -241,7 +182,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.iter()
.enumerate()
.map(|(i, m)| {
let content = Line::from(Span::raw(format!("{i}: {m}")));
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
ListItem::new(content)
})
.collect();

View File

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

@@ -1,5 +0,0 @@
# configuration for https://rust-lang.github.io/rustfmt/
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
wrap_comments = true
comment_width = 100

View File

@@ -1,48 +1,20 @@
//! This module provides the `CrosstermBackend` implementation for the `Backend` trait.
//! It uses the `crossterm` crate to interact with the terminal.
//!
//!
//! [`Backend`]: trait.Backend.html
//! [`CrosstermBackend`]: struct.CrosstermBackend.html
use std::io::{self, Write};
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
style::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
SetForegroundColor, SetUnderlineColor,
},
terminal::{self, Clear},
};
use crate::{
backend::{Backend, ClearType},
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
};
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
style::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
SetForegroundColor,
},
terminal::{self, Clear},
};
use std::io::{self, Write};
/// A backend implementation using the `crossterm` crate.
///
/// The `CrosstermBackend` struct is a wrapper around a type implementing `Write`, which
/// is used to send commands to the terminal. It provides methods for drawing content,
/// manipulating the cursor, and clearing the terminal screen.
///
/// # Example
///
/// ```rust
/// use ratatui::backend::{Backend, CrosstermBackend};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let buffer = std::io::stdout();
/// let mut backend = CrosstermBackend::new(buffer);
/// backend.clear()?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Default, Clone)]
pub struct CrosstermBackend<W: Write> {
buffer: W,
}
@@ -51,7 +23,6 @@ impl<W> CrosstermBackend<W>
where
W: Write,
{
/// Creates a new `CrosstermBackend` with the given buffer.
pub fn new(buffer: W) -> CrosstermBackend<W> {
CrosstermBackend { buffer }
}
@@ -61,12 +32,10 @@ impl<W> Write for CrosstermBackend<W>
where
W: Write,
{
/// Writes a buffer of bytes to the underlying buffer.
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buffer.write(buf)
}
/// Flushes the underlying buffer.
fn flush(&mut self) -> io::Result<()> {
self.buffer.flush()
}
@@ -82,13 +51,12 @@ 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 {
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
map_error(queue!(self.buffer, MoveTo(x, y)))?;
queue!(self.buffer, MoveTo(x, y))?;
}
last_pos = Some((x, y));
if cell.modifier != modifier {
@@ -101,55 +69,44 @@ where
}
if cell.fg != fg {
let color = CColor::from(cell.fg);
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
queue!(self.buffer, SetForegroundColor(color))?;
fg = cell.fg;
}
if cell.bg != bg {
let color = CColor::from(cell.bg);
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
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)))?;
queue!(self.buffer, Print(&cell.symbol))?;
}
map_error(queue!(
queue!(
self.buffer,
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetUnderlineColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
))
)
}
fn hide_cursor(&mut self) -> io::Result<()> {
map_error(execute!(self.buffer, Hide))
execute!(self.buffer, Hide)
}
fn show_cursor(&mut self) -> io::Result<()> {
map_error(execute!(self.buffer, Show))
execute!(self.buffer, Show)
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
crossterm::cursor::position()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
map_error(execute!(self.buffer, MoveTo(x, y)))
execute!(self.buffer, MoveTo(x, y))
}
fn clear(&mut self) -> io::Result<()> {
self.clear_region(ClearType::All)
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
map_error(execute!(
fn clear(&mut self, clear_type: ClearType) -> io::Result<()> {
execute!(
self.buffer,
Clear(match clear_type {
ClearType::All => crossterm::terminal::ClearType::All,
@@ -158,12 +115,12 @@ where
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
})
))
)
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
for _ in 0..n {
map_error(queue!(self.buffer, Print("\n")))?;
queue!(self.buffer, Print("\n"))?;
}
self.buffer.flush()
}
@@ -180,10 +137,6 @@ where
}
}
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
impl From<Color> for CColor {
fn from(color: Color) -> Self {
match color {
@@ -210,10 +163,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, Default, Clone, Copy)]
#[derive(Debug)]
struct ModifierDiff {
pub from: Modifier,
pub to: Modifier,
@@ -227,54 +177,54 @@ impl ModifierDiff {
//use crossterm::Attribute;
let removed = self.from - self.to;
if removed.contains(Modifier::REVERSED) {
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
queue!(w, SetAttribute(CAttribute::NoReverse))?;
}
if removed.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
if self.to.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
queue!(w, SetAttribute(CAttribute::Dim))?;
}
}
if removed.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
queue!(w, SetAttribute(CAttribute::NoItalic))?;
}
if removed.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
queue!(w, SetAttribute(CAttribute::NoUnderline))?;
}
if removed.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
}
if removed.contains(Modifier::CROSSED_OUT) {
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
}
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
queue!(w, SetAttribute(CAttribute::NoBlink))?;
}
let added = self.to - self.from;
if added.contains(Modifier::REVERSED) {
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
queue!(w, SetAttribute(CAttribute::Reverse))?;
}
if added.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
queue!(w, SetAttribute(CAttribute::Bold))?;
}
if added.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
queue!(w, SetAttribute(CAttribute::Italic))?;
}
if added.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
queue!(w, SetAttribute(CAttribute::Underlined))?;
}
if added.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
queue!(w, SetAttribute(CAttribute::Dim))?;
}
if added.contains(Modifier::CROSSED_OUT) {
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
queue!(w, SetAttribute(CAttribute::CrossedOut))?;
}
if added.contains(Modifier::SLOW_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
queue!(w, SetAttribute(CAttribute::SlowBlink))?;
}
if added.contains(Modifier::RAPID_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
queue!(w, SetAttribute(CAttribute::RapidBlink))?;
}
Ok(())

View File

@@ -1,33 +1,7 @@
//! This module provides the backend implementations for different terminal libraries.
//! It defines the [`Backend`] trait which is used to abstract over the specific
//! terminal library being used.
//!
//! The following terminal libraries are supported:
//! - Crossterm (with the `crossterm` feature)
//! - Termion (with the `termion` feature)
//! - Termwiz (with the `termwiz` feature)
//!
//! Additionally, a [`TestBackend`] is provided for testing purposes.
//!
//! # Example
//!
//! ```rust
//! use ratatui::backend::{Backend, CrosstermBackend};
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let buffer = std::io::stdout();
//! let mut backend = CrosstermBackend::new(buffer);
//! backend.clear()?;
//! # Ok(())
//! # }
//! ```
//!
//! [`Backend`]: trait.Backend.html
//! [`TestBackend`]: struct.TestBackend.html
use std::io;
use crate::{buffer::Cell, layout::Rect};
use crate::buffer::Cell;
use crate::layout::Rect;
#[cfg(feature = "termion")]
mod termion;
@@ -39,17 +13,10 @@ mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "termwiz")]
mod termwiz;
#[cfg(feature = "termwiz")]
pub use self::termwiz::TermwizBackend;
mod test;
pub use self::test::TestBackend;
/// Enum representing the different types of clearing operations that can be performed
/// on the terminal screen.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ClearType {
All,
AfterCursor,
@@ -58,60 +25,16 @@ pub enum ClearType {
UntilNewLine,
}
/// The `Backend` trait provides an abstraction over different terminal libraries.
/// It defines the methods required to draw content, manipulate the cursor, and
/// clear the terminal screen.
pub trait Backend {
/// Draw the given content to the terminal screen.
///
/// The content is provided as an iterator over `(u16, u16, &Cell)` tuples,
/// where the first two elements represent the x and y coordinates, and the
/// third element is a reference to the [`Cell`] to be drawn.
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>;
/// Insert `n` line breaks to the terminal screen.
///
/// This method is optional and may not be implemented by all backends.
fn append_lines(&mut self, _n: u16) -> io::Result<()> {
Ok(())
}
/// Hide the cursor on the terminal screen.
fn append_lines(&mut self, n: u16) -> io::Result<()>;
fn hide_cursor(&mut self) -> Result<(), io::Error>;
/// Show the cursor on the terminal screen.
fn show_cursor(&mut self) -> Result<(), io::Error>;
/// Get the current cursor position on the terminal screen.
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
/// Set the cursor position on the terminal screen to the given x and y coordinates.
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
/// Clears the whole terminal screen
fn clear(&mut self) -> Result<(), io::Error>;
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
///
/// This method is optional and may not be implemented by all backends.
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), io::Error> {
match clear_type {
ClearType::All => self.clear(),
ClearType::AfterCursor
| ClearType::BeforeCursor
| ClearType::CurrentLine
| ClearType::UntilNewLine => Err(io::Error::new(
io::ErrorKind::Other,
format!("clear_type [{clear_type:?}] not supported with this backend"),
)),
}
}
/// Get the size of the terminal screen as a [`Rect`].
fn clear(&mut self, clear_type: ClearType) -> Result<(), io::Error>;
fn size(&self) -> Result<Rect, io::Error>;
/// Flush any buffered content to the terminal screen.
fn flush(&mut self) -> Result<(), io::Error>;
}

View File

@@ -1,37 +1,14 @@
//! This module provides the `TermionBackend` implementation for the [`Backend`] trait.
//! It uses the Termion crate to interact with the terminal.
//!
//! [`Backend`]: crate::backend::Backend
//! [`TermionBackend`]: crate::backend::TermionBackend
use std::{
fmt,
io::{self, Write},
};
use crate::{
backend::{Backend, ClearType},
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
};
use std::{
fmt,
io::{self, Write},
};
/// A backend that uses the Termion library to draw content, manipulate the cursor,
/// and clear the terminal screen.
///
/// # Example
///
/// ```rust
/// use ratatui::backend::{Backend, TermionBackend};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let stdout = std::io::stdout();
/// let mut backend = TermionBackend::new(stdout);
/// backend.clear()?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Default, Clone)]
pub struct TermionBackend<W>
where
W: Write,
@@ -43,7 +20,6 @@ impl<W> TermionBackend<W>
where
W: Write,
{
/// Creates a new Termion backend with the given output.
pub fn new(stdout: W) -> TermionBackend<W> {
TermionBackend { stdout }
}
@@ -66,11 +42,7 @@ impl<W> Backend for TermionBackend<W>
where
W: Write,
{
fn clear(&mut self) -> io::Result<()> {
self.clear_region(ClearType::All)
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
fn clear(&mut self, clear_type: ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => write!(self.stdout, "{}", termion::clear::All)?,
ClearType::AfterCursor => write!(self.stdout, "{}", termion::clear::AfterCursor)?,
@@ -88,20 +60,24 @@ where
self.stdout.flush()
}
/// Hides cursor
fn hide_cursor(&mut self) -> io::Result<()> {
write!(self.stdout, "{}", termion::cursor::Hide)?;
self.stdout.flush()
}
/// Shows cursor
fn show_cursor(&mut self) -> io::Result<()> {
write!(self.stdout, "{}", termion::cursor::Show)?;
self.stdout.flush()
}
/// Gets cursor position (0-based index)
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
termion::cursor::DetectCursorPos::cursor_pos(&mut self.stdout).map(|(x, y)| (x - 1, y - 1))
}
/// Sets cursor position (0-based index)
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
write!(self.stdout, "{}", termion::cursor::Goto(x + 1, y + 1))?;
self.stdout.flush()
@@ -148,13 +124,15 @@ where
}
write!(
self.stdout,
"{string}{}{}{}",
"{}{}{}{}",
string,
Fg(Color::Reset),
Bg(Color::Reset),
termion::style::Reset,
)
}
/// Return the size of the terminal
fn size(&self) -> io::Result<Rect> {
let terminal = termion::terminal_size()?;
Ok(Rect::new(0, 0, terminal.0, terminal.1))
@@ -164,16 +142,11 @@ 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

@@ -1,223 +0,0 @@
//! This module provides the `TermwizBackend` implementation for the [`Backend`] trait.
//! It uses the `termwiz` crate to interact with the terminal.
//!
//! [`Backend`]: trait.Backend.html
//! [`TermwizBackend`]: crate::backend::TermionBackend
use std::{error::Error, io};
use termwiz::{
caps::Capabilities,
cell::{AttributeChange, Blink, Intensity, Underline},
color::{AnsiColor, ColorAttribute, SrgbaTuple},
surface::{Change, CursorVisibility, Position},
terminal::{buffered::BufferedTerminal, SystemTerminal, Terminal},
};
use crate::{
backend::Backend,
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
};
/// Termwiz backend implementation for the [`Backend`] trait.
/// # Example
///
/// ```rust,no_run
/// use ratatui::backend::{Backend, TermwizBackend};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut backend = TermwizBackend::new()?;
/// backend.clear()?;
/// # Ok(())
/// # }
/// ```
pub struct TermwizBackend {
buffered_terminal: BufferedTerminal<SystemTerminal>,
}
impl TermwizBackend {
/// Creates a new Termwiz backend instance.
pub fn new() -> Result<TermwizBackend, Box<dyn Error>> {
let mut buffered_terminal =
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
buffered_terminal.terminal().set_raw_mode()?;
buffered_terminal.terminal().enter_alternate_screen()?;
Ok(TermwizBackend { buffered_terminal })
}
/// Creates a new Termwiz backend instance with the given buffered terminal.
pub fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> TermwizBackend {
TermwizBackend {
buffered_terminal: instance,
}
}
/// Returns a reference to the buffered terminal used by the backend.
pub fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
&self.buffered_terminal
}
/// Returns a mutable reference to the buffered terminal used by the backend.
pub fn buffered_terminal_mut(&mut self) -> &mut BufferedTerminal<SystemTerminal> {
&mut self.buffered_terminal
}
}
impl Backend for TermwizBackend {
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, cell) in content {
self.buffered_terminal.add_changes(vec![
Change::CursorPosition {
x: Position::Absolute(x as usize),
y: Position::Absolute(y as usize),
},
Change::Attribute(AttributeChange::Foreground(cell.fg.into())),
Change::Attribute(AttributeChange::Background(cell.bg.into())),
]);
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Intensity(
if cell.modifier.contains(Modifier::BOLD) {
Intensity::Bold
} else if cell.modifier.contains(Modifier::DIM) {
Intensity::Half
} else {
Intensity::Normal
},
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Italic(
cell.modifier.contains(Modifier::ITALIC),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Underline(
if cell.modifier.contains(Modifier::UNDERLINED) {
Underline::Single
} else {
Underline::None
},
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Reverse(
cell.modifier.contains(Modifier::REVERSED),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Invisible(
cell.modifier.contains(Modifier::HIDDEN),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::StrikeThrough(
cell.modifier.contains(Modifier::CROSSED_OUT),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Blink(
if cell.modifier.contains(Modifier::SLOW_BLINK) {
Blink::Slow
} else if cell.modifier.contains(Modifier::RAPID_BLINK) {
Blink::Rapid
} else {
Blink::None
},
)));
self.buffered_terminal.add_change(&cell.symbol);
}
Ok(())
}
fn hide_cursor(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
Ok(())
}
fn show_cursor(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
Ok(())
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
let (x, y) = self.buffered_terminal.cursor_position();
Ok((x as u16, y as u16))
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.buffered_terminal.add_change(Change::CursorPosition {
x: Position::Absolute(x as usize),
y: Position::Absolute(y as usize),
});
Ok(())
}
fn clear(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
Ok(())
}
fn size(&self) -> Result<Rect, io::Error> {
let (term_width, term_height) = self.buffered_terminal.dimensions();
let max = u16::max_value();
Ok(Rect::new(
0,
0,
if term_width > usize::from(max) {
max
} else {
term_width as u16
},
if term_height > usize::from(max) {
max
} else {
term_height as u16
},
))
}
fn flush(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.flush()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok(())
}
}
impl From<Color> for ColorAttribute {
fn from(color: Color) -> ColorAttribute {
match color {
Color::Reset => ColorAttribute::Default,
Color::Black => AnsiColor::Black.into(),
Color::Gray | Color::DarkGray => AnsiColor::Grey.into(),
Color::Red => AnsiColor::Maroon.into(),
Color::LightRed => AnsiColor::Red.into(),
Color::Green => AnsiColor::Green.into(),
Color::LightGreen => AnsiColor::Lime.into(),
Color::Yellow => AnsiColor::Olive.into(),
Color::LightYellow => AnsiColor::Yellow.into(),
Color::Magenta => AnsiColor::Purple.into(),
Color::LightMagenta => AnsiColor::Fuchsia.into(),
Color::Cyan => AnsiColor::Teal.into(),
Color::LightCyan => AnsiColor::Aqua.into(),
Color::White => AnsiColor::White.into(),
Color::Blue => AnsiColor::Navy.into(),
Color::LightBlue => AnsiColor::Blue.into(),
Color::Indexed(i) => ColorAttribute::PaletteIndex(i),
Color::Rgb(r, g, b) => {
ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b)))
}
}
}
}

View File

@@ -1,34 +1,13 @@
//! This module provides the `TestBackend` implementation for the [`Backend`] trait.
//! It is used in the integration tests to verify the correctness of the library.
use std::{
fmt::{Display, Write},
io,
};
use unicode_width::UnicodeWidthStr;
use crate::{
backend::Backend,
backend::{Backend, ClearType},
buffer::{Buffer, Cell},
layout::Rect,
};
use std::{fmt::Write, io};
use unicode_width::UnicodeWidthStr;
/// A backend used for the integration tests.
///
/// # Example
///
/// ```rust
/// use ratatui::{backend::{Backend, TestBackend}, buffer::Buffer};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut backend = TestBackend::new(10, 2);
/// backend.clear()?;
/// backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct TestBackend {
width: u16,
buffer: Buffer,
@@ -38,11 +17,6 @@ pub struct TestBackend {
}
/// Returns a string representation of the given buffer for debugging purpose.
///
/// This function is used to visualize the buffer content in a human-readable format.
/// It iterates through the buffer content and appends each cell's symbol to the view string.
/// If a cell is hidden by a multi-width symbol, it is added to the overwritten vector and
/// displayed at the end of the line.
fn buffer_view(buffer: &Buffer) -> String {
let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
for cells in buffer.content.chunks(buffer.area.width as usize) {
@@ -53,13 +27,18 @@ fn buffer_view(buffer: &Buffer) -> String {
if skip == 0 {
view.push_str(&c.symbol);
} else {
overwritten.push((x, &c.symbol));
overwritten.push((x, &c.symbol))
}
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
}
view.push('"');
if !overwritten.is_empty() {
write!(&mut view, " Hidden by multi-width symbols: {overwritten:?}").unwrap();
write!(
&mut view,
" Hidden by multi-width symbols: {:?}",
overwritten
)
.unwrap();
}
view.push('\n');
}
@@ -67,7 +46,6 @@ fn buffer_view(buffer: &Buffer) -> String {
}
impl TestBackend {
/// Creates a new TestBackend with the specified width and height.
pub fn new(width: u16, height: u16) -> TestBackend {
TestBackend {
width,
@@ -78,21 +56,16 @@ impl TestBackend {
}
}
/// Returns a reference to the internal buffer of the TestBackend.
pub fn buffer(&self) -> &Buffer {
&self.buffer
}
/// Resizes the TestBackend to the specified width and height.
pub fn resize(&mut self, width: u16, height: u16) {
self.buffer.resize(Rect::new(0, 0, width, height));
self.width = width;
self.height = height;
}
/// Asserts that the TestBackend's buffer is equal to the expected buffer.
/// If the buffers are not equal, a panic occurs with a detailed error message
/// showing the differences between the expected and actual buffers.
pub fn assert_buffer(&self, expected: &Buffer) {
assert_eq!(expected.area, self.buffer.area);
let diff = expected.diff(&self.buffer);
@@ -120,20 +93,15 @@ impl TestBackend {
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = expected.get(*x, *y);
format!("{i}: at ({x}, {y}) expected {expected_cell:?} got {cell:?}")
format!(
"{}: at ({}, {}) expected {:?} got {:?}",
i, x, y, expected_cell, cell
)
})
.collect::<Vec<String>>()
.join("\n");
debug_info.push_str(&nice_diff);
panic!("{debug_info}");
}
}
impl Display for TestBackend {
/// Formats the TestBackend for display by calling the buffer_view function
/// on its internal buffer.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", buffer_view(&self.buffer))
panic!("{}", debug_info);
}
}
@@ -149,6 +117,10 @@ impl Backend for TestBackend {
Ok(())
}
fn append_lines(&mut self, _n: u16) -> Result<(), io::Error> {
Ok(())
}
fn hide_cursor(&mut self) -> Result<(), io::Error> {
self.cursor = false;
Ok(())
@@ -168,7 +140,7 @@ impl Backend for TestBackend {
Ok(())
}
fn clear(&mut self) -> Result<(), io::Error> {
fn clear(&mut self, _clear_type: ClearType) -> Result<(), io::Error> {
self.buffer.reset();
Ok(())
}

View File

@@ -1,26 +1,18 @@
use std::{
cmp::min,
fmt::{Debug, Formatter, Result},
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
#[allow(deprecated)]
use crate::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span, Spans},
text::{Span, Spans},
};
use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Cell {
pub symbol: String,
pub fg: Color,
pub bg: Color,
#[cfg(feature = "crossterm")]
pub underline_color: Color,
pub modifier: Modifier,
}
@@ -54,25 +46,11 @@ 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)
@@ -85,10 +63,6 @@ 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();
}
}
@@ -99,8 +73,6 @@ impl Default for Cell {
symbol: " ".into(),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "crossterm")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
}
}
@@ -116,9 +88,9 @@ impl Default for Cell {
/// # Examples:
///
/// ```
/// use ratatui::buffer::{Buffer, Cell};
/// use ratatui::layout::Rect;
/// use ratatui::style::{Color, Style, Modifier};
/// use tui::buffer::{Buffer, Cell};
/// use tui::layout::Rect;
/// use tui::style::{Color, Style, Modifier};
///
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf.get_mut(0, 2).set_symbol("x");
@@ -128,14 +100,12 @@ 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(Default, Clone, Eq, PartialEq)]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
@@ -147,7 +117,7 @@ pub struct Buffer {
impl Buffer {
/// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer {
let cell = Cell::default();
let cell: Cell = Default::default();
Buffer::filled(area, &cell)
}
@@ -206,15 +176,15 @@ impl Buffer {
&mut self.content[i]
}
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
/// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
///
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
///
/// # Examples
///
/// ```
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Global coordinates to the top corner of this buffer's area
@@ -226,8 +196,8 @@ impl Buffer {
/// Panics when given an coordinate that is outside of this Buffer's area.
///
/// ```should_panic
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
@@ -240,7 +210,9 @@ impl Buffer {
&& x < self.area.right()
&& y >= self.area.top()
&& y < self.area.bottom(),
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
"Trying to access position outside the buffer: x={}, y={}, area={:?}",
x,
y,
self.area
);
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
@@ -253,8 +225,8 @@ impl Buffer {
/// # Examples
///
/// ```
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// assert_eq!(buffer.pos_of(0), (200, 100));
@@ -266,8 +238,8 @@ impl Buffer {
/// Panics when given an index that is outside the Buffer's content.
///
/// ```should_panic
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
/// let buffer = Buffer::empty(rect);
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
@@ -276,12 +248,13 @@ impl Buffer {
pub fn pos_of(&self, i: usize) -> (u16, u16) {
debug_assert!(
i < self.content.len(),
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
"Trying to get the coords of a cell outside the buffer: i={} len={}",
i,
self.content.len()
);
(
self.area.x + (i as u16) % self.area.width,
self.area.y + (i as u16) / self.area.width,
self.area.x + i as u16 % self.area.width,
self.area.y + i as u16 / self.area.width,
)
}
@@ -316,7 +289,7 @@ impl Buffer {
continue;
}
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
// change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
if width > max_offset.saturating_sub(x_offset) {
break;
}
@@ -333,9 +306,7 @@ impl Buffer {
(x_offset as u16, y)
}
#[allow(deprecated)]
#[deprecated(note = "Use `Buffer::set_line` instead")]
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) {
pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &spans.0 {
@@ -356,28 +327,7 @@ impl Buffer {
(x, y)
}
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &line.spans {
if remaining_width == 0 {
break;
}
let pos = self.set_stringn(
x,
y,
span.content.as_ref(),
remaining_width as usize,
span.style,
);
let w = pos.0.saturating_sub(x);
x = pos.0;
remaining_width = remaining_width.saturating_sub(w);
}
(x, y)
}
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
pub fn set_span<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) {
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
}
@@ -408,7 +358,7 @@ impl Buffer {
if self.content.len() > length {
self.content.truncate(length);
} else {
self.content.resize(length, Cell::default());
self.content.resize(length, Default::default());
}
self.area = area;
}
@@ -423,7 +373,7 @@ impl Buffer {
/// Merge an other buffer into this one
pub fn merge(&mut self, other: &Buffer) {
let area = self.area.union(other.area);
let cell = Cell::default();
let cell: Cell = Default::default();
self.content.resize(area.area() as usize, cell.clone());
// Move original content to the appropriate space
@@ -483,10 +433,10 @@ impl Buffer {
let next_buffer = &other.content;
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
// Cells invalidated by drawing/replacing preceding multi-width characters:
// Cells invalidated by drawing/replacing preceeding multi-width characters:
let mut invalidated: usize = 0;
// Cells from the current buffer to skip due to preceding multi-width characters taking
// their place (the skipped cells should be blank anyway):
// Cells from the current buffer to skip due to preceeding multi-width characters taking their
// place (the skipped cells should be blank anyway):
let mut to_skip: usize = 0;
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
if (current != previous || invalidated > 0) && to_skip == 0 {
@@ -503,128 +453,6 @@ impl Buffer {
}
}
/// Assert that two buffers are equal by comparing their areas and content.
///
/// On panic, displays the areas or the content and a diff of the contents.
#[macro_export]
macro_rules! assert_buffer_eq {
($actual_expr:expr, $expected_expr:expr) => {
match (&$actual_expr, &$expected_expr) {
(actual, expected) => {
if actual.area != expected.area {
panic!(
indoc::indoc!(
"
buffer areas not equal
expected: {:?}
actual: {:?}"
),
expected, actual
);
}
let diff = expected.diff(&actual);
if !diff.is_empty() {
let nice_diff = diff
.iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = expected.get(*x, *y);
indoc::formatdoc! {"
{i}: at ({x}, {y})
expected: {expected_cell:?}
actual: {cell:?}
"}
})
.collect::<Vec<String>>()
.join("\n");
panic!(
indoc::indoc!(
"
buffer contents not equal
expected: {:?}
actual: {:?}
diff:
{}"
),
expected, actual, nice_diff
);
}
// shouldn't get here, but this guards against future behavior
// that changes equality but not area or content
assert_eq!(actual, expected, "buffers not equal");
}
}
};
}
impl Debug for Buffer {
/// Writes a debug representation of the buffer to the given formatter.
///
/// The format is like a pretty printed struct, with the following fields:
/// * `area`: displayed as `Rect { x: 1, y: 2, width: 3, height: 4 }`
/// * `content`: displayed as a list of strings representing the content of the buffer
/// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
/// modifier: Modifier::BOLD }` only showing a value when there is a change in style.
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"Buffer {{\n area: {:?},\n content: [\n",
&self.area
))?;
let mut last_style = None;
let mut styles = vec![];
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
let mut overwritten = vec![];
let mut skip: usize = 0;
f.write_str(" \"")?;
for (x, c) in line.iter().enumerate() {
if skip == 0 {
f.write_str(&c.symbol)?;
} else {
overwritten.push((x, &c.symbol));
}
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
#[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() {
f.write_fmt(format_args!(
"// hidden by multi-width symbols: {overwritten:?}"
))?;
}
f.write_str("\",\n")?;
}
f.write_str(" ],\n styles: [\n")?;
for s in styles {
#[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
))?;
}
f.write_str(" ]\n}")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -635,81 +463,6 @@ mod tests {
cell
}
#[test]
fn it_implements_debug() {
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
buf.set_string(0, 0, "Hello World!", Style::default());
buf.set_string(
0,
1,
"G'day World!",
Style::default()
.fg(Color::Green)
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD),
);
#[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!(
"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
\"Hello World!\",
\"G'day World!\",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
]
}"
)
);
}
#[test]
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic]
#[test]
fn assert_buffer_eq_panics_on_unequal_area() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic]
#[test]
fn assert_buffer_eq_panics_on_unequal_style() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
assert_buffer_eq!(buffer, other_buffer);
}
#[test]
fn it_translates_to_and_from_coordinates() {
let rect = Rect::new(200, 100, 50, 80);
@@ -751,38 +504,21 @@ mod tests {
// Zero-width
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
assert_eq!(buffer, Buffer::with_lines(vec![" "]));
buffer.set_string(0, 0, "aaa", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
assert_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
// Width limit:
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
buffer.set_string(0, 0, "12345", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// Width truncation:
buffer.set_string(0, 0, "123456", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// multi-line
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
buffer.set_string(0, 0, "12345", Style::default());
buffer.set_string(0, 1, "67890", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
}
#[test]
fn buffer_set_string_multi_width_overwrite() {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
// multi-width overwrite
buffer.set_string(0, 0, "aaaaa", Style::default());
buffer.set_string(0, 0, "称号", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
}
#[test]
@@ -793,12 +529,12 @@ mod tests {
// Leading grapheme with zero width
let s = "\u{1}a";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
// Trailing grapheme with zero with
let s = "a\u{1}";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
}
#[test]
@@ -806,11 +542,11 @@ mod tests {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
buffer.set_string(0, 0, "コン", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
// Only 1 space left.
buffer.set_string(0, 0, "コンピ", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
}
#[test]
@@ -935,7 +671,7 @@ mod tests {
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
}
#[test]
@@ -959,7 +695,7 @@ mod tests {
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_buffer_eq!(
assert_eq!(
one,
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
);
@@ -993,6 +729,6 @@ mod tests {
width: 4,
height: 4,
};
assert_buffer_eq!(one, merged);
assert_eq!(one, merged);
}
}

View File

@@ -1,34 +1,28 @@
use std::{
cell::RefCell,
cmp::{max, min},
collections::HashMap,
rc::Rc,
};
use std::cell::RefCell;
use std::cmp::{max, min};
use std::collections::HashMap;
use cassowary::{
strength::{MEDIUM, REQUIRED, WEAK},
Constraint as CassowaryConstraint, Expression, Solver, Variable,
WeightedRelation::{EQ, GE, LE},
};
use cassowary::strength::{REQUIRED, WEAK};
use cassowary::WeightedRelation::*;
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
#[derive(Debug, Default, Hash, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
pub enum Corner {
#[default]
TopLeft,
TopRight,
BottomRight,
BottomLeft,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
pub enum Direction {
Horizontal,
#[default]
Vertical,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Constraint {
// TODO: enforce range 0 - 100
Percentage(u16),
Ratio(u32, u32),
Length(u16),
@@ -36,26 +30,13 @@ 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 {
Constraint::Percentage(p) => {
let p = p as f32 / 100.0;
let length = length as f32;
(p * length).min(length) as u16
}
Constraint::Ratio(numerator, denominator) => {
// avoid division by zero by using 1 when denominator is 0
// this results in 0/0 -> 0 and x/0 -> x for x != 0
let percentage = numerator as f32 / denominator.max(1) as f32;
let length = length as f32;
(percentage * length).min(length) as u16
Constraint::Percentage(p) => length * p / 100,
Constraint::Ratio(num, den) => {
let r = num * u32::from(length) / den;
r as u16
}
Constraint::Length(l) => length.min(l),
Constraint::Max(m) => length.min(m),
@@ -64,15 +45,14 @@ impl Constraint {
}
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Margin {
pub vertical: u16,
pub horizontal: u16,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alignment {
#[default]
Left,
Center,
Right,
@@ -88,19 +68,12 @@ pub struct Layout {
expand_to_fill: bool,
}
type Cache = HashMap<(Rect, Layout), Rc<[Rect]>>;
thread_local! {
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(HashMap::new());
static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
}
impl Default for Layout {
fn default() -> Layout {
Layout::new()
}
}
impl Layout {
pub const fn new() -> Layout {
Layout {
direction: Direction::Vertical,
margin: Margin {
@@ -111,7 +84,9 @@ impl Layout {
expand_to_fill: true,
}
}
}
impl Layout {
pub fn constraints<C>(mut self, constraints: C) -> Layout
where
C: Into<Vec<Constraint>>,
@@ -120,7 +95,7 @@ impl Layout {
self
}
pub const fn margin(mut self, margin: u16) -> Layout {
pub fn margin(mut self, margin: u16) -> Layout {
self.margin = Margin {
horizontal: margin,
vertical: margin,
@@ -128,22 +103,22 @@ impl Layout {
self
}
pub const fn horizontal_margin(mut self, horizontal: u16) -> Layout {
pub fn horizontal_margin(mut self, horizontal: u16) -> Layout {
self.margin.horizontal = horizontal;
self
}
pub const fn vertical_margin(mut self, vertical: u16) -> Layout {
pub fn vertical_margin(mut self, vertical: u16) -> Layout {
self.margin.vertical = vertical;
self
}
pub const fn direction(mut self, direction: Direction) -> Layout {
pub fn direction(mut self, direction: Direction) -> Layout {
self.direction = direction;
self
}
pub(crate) const fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
pub(crate) fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
self.expand_to_fill = expand_to_fill;
self
}
@@ -153,7 +128,7 @@ impl Layout {
///
/// # Examples
/// ```
/// # use ratatui::layout::{Rect, Constraint, Direction, Layout};
/// # use tui::layout::{Rect, Constraint, Direction, Layout};
/// let chunks = Layout::default()
/// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
@@ -164,8 +139,8 @@ impl Layout {
/// height: 10,
/// });
/// assert_eq!(
/// chunks[..],
/// [
/// chunks,
/// vec![
/// Rect {
/// x: 2,
/// y: 2,
@@ -191,8 +166,8 @@ impl Layout {
/// height: 2,
/// });
/// assert_eq!(
/// chunks[..],
/// [
/// chunks,
/// vec![
/// Rect {
/// x: 0,
/// y: 0,
@@ -208,7 +183,7 @@ impl Layout {
/// ]
/// );
/// ```
pub fn split(&self, area: Rect) -> Rc<[Rect]> {
pub fn split(&self, area: Rect) -> Vec<Rect> {
// TODO: Maybe use a fixed size cache ?
LAYOUT_CACHE.with(|c| {
c.borrow_mut()
@@ -219,7 +194,7 @@ impl Layout {
}
}
fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
let mut solver = Solver::new();
let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
let elements = layout
@@ -227,13 +202,11 @@ fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
.iter()
.map(|_| Element::new())
.collect::<Vec<Element>>();
let mut res = layout
let mut results = layout
.constraints
.iter()
.map(|_| Rect::default())
.collect::<Rc<[Rect]>>();
let results = Rc::get_mut(&mut res).expect("newly created Rc should have no shared refs");
.collect::<Vec<Rect>>();
let dest_area = area.inner(&layout.margin);
for (i, e) in elements.iter().enumerate() {
@@ -275,25 +248,18 @@ fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
ccs.push(match *size {
Constraint::Length(v) => elements[i].width | EQ(MEDIUM) | f64::from(v),
Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].width | EQ(MEDIUM) | (f64::from(v * dest_area.width) / 100.0)
elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
}
Constraint::Ratio(n, d) => {
elements[i].width
| EQ(MEDIUM)
| EQ(WEAK)
| (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => elements[i].width | GE(MEDIUM) | f64::from(v),
Constraint::Max(v) => elements[i].width | LE(MEDIUM) | f64::from(v),
Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
});
match *size {
Constraint::Min(v) | Constraint::Max(v) => {
ccs.push(elements[i].width | EQ(WEAK) | f64::from(v));
}
_ => {}
}
}
}
Direction::Vertical => {
@@ -304,25 +270,18 @@ fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
ccs.push(match *size {
Constraint::Length(v) => elements[i].height | EQ(MEDIUM) | f64::from(v),
Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].height | EQ(MEDIUM) | (f64::from(v * dest_area.height) / 100.0)
elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
}
Constraint::Ratio(n, d) => {
elements[i].height
| EQ(MEDIUM)
| EQ(WEAK)
| (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => elements[i].height | GE(MEDIUM) | f64::from(v),
Constraint::Max(v) => elements[i].height | LE(MEDIUM) | f64::from(v),
Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
});
match *size {
Constraint::Min(v) | Constraint::Max(v) => {
ccs.push(elements[i].height | EQ(WEAK) | f64::from(v));
}
_ => {}
}
}
}
}
@@ -364,11 +323,10 @@ fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
}
}
}
res
results
}
/// A container used by the solver inside split
#[derive(Debug, Clone, Copy)]
struct Element {
x: Variable,
y: Variable,
@@ -403,9 +361,9 @@ impl Element {
}
}
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
/// area they are supposed to render to.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
pub struct Rect {
pub x: u16,
pub y: u16,
@@ -436,23 +394,23 @@ impl Rect {
}
}
pub const fn area(self) -> u16 {
pub fn area(self) -> u16 {
self.width * self.height
}
pub const fn left(self) -> u16 {
pub fn left(self) -> u16 {
self.x
}
pub const fn right(self) -> u16 {
pub fn right(self) -> u16 {
self.x.saturating_add(self.width)
}
pub const fn top(self) -> u16 {
pub fn top(self) -> u16 {
self.y
}
pub const fn bottom(self) -> u16 {
pub fn bottom(self) -> u16 {
self.y.saturating_add(self.height)
}
@@ -495,7 +453,7 @@ impl Rect {
}
}
pub const fn intersects(self, other: Rect) -> bool {
pub fn intersects(self, other: Rect) -> bool {
self.x < other.x + other.width
&& self.x + self.width > other.x
&& self.y < other.y + other.height
@@ -546,7 +504,7 @@ mod tests {
- f64::from(width) / f64::from(height))
.abs()
< 1.0
);
)
}
}
@@ -575,68 +533,4 @@ mod tests {
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
}
#[test]
fn test_constraint_apply() {
assert_eq!(Constraint::Percentage(0).apply(100), 0);
assert_eq!(Constraint::Percentage(50).apply(100), 50);
assert_eq!(Constraint::Percentage(100).apply(100), 100);
assert_eq!(Constraint::Percentage(200).apply(100), 100);
assert_eq!(Constraint::Percentage(u16::MAX).apply(100), 100);
// 0/0 intentionally avoids a panic by returning 0.
assert_eq!(Constraint::Ratio(0, 0).apply(100), 0);
// 1/0 intentionally avoids a panic by returning 100% of the length.
assert_eq!(Constraint::Ratio(1, 0).apply(100), 100);
assert_eq!(Constraint::Ratio(0, 1).apply(100), 0);
assert_eq!(Constraint::Ratio(1, 2).apply(100), 50);
assert_eq!(Constraint::Ratio(2, 2).apply(100), 100);
assert_eq!(Constraint::Ratio(3, 2).apply(100), 100);
assert_eq!(Constraint::Ratio(u32::MAX, 2).apply(100), 100);
assert_eq!(Constraint::Length(0).apply(100), 0);
assert_eq!(Constraint::Length(50).apply(100), 50);
assert_eq!(Constraint::Length(100).apply(100), 100);
assert_eq!(Constraint::Length(200).apply(100), 100);
assert_eq!(Constraint::Length(u16::MAX).apply(100), 100);
assert_eq!(Constraint::Max(0).apply(100), 0);
assert_eq!(Constraint::Max(50).apply(100), 50);
assert_eq!(Constraint::Max(100).apply(100), 100);
assert_eq!(Constraint::Max(200).apply(100), 100);
assert_eq!(Constraint::Max(u16::MAX).apply(100), 100);
assert_eq!(Constraint::Min(0).apply(100), 100);
assert_eq!(Constraint::Min(50).apply(100), 100);
assert_eq!(Constraint::Min(100).apply(100), 100);
assert_eq!(Constraint::Min(200).apply(100), 200);
assert_eq!(Constraint::Min(u16::MAX).apply(100), u16::MAX);
}
#[test]
fn rect_can_be_const() {
const RECT: Rect = Rect {
x: 0,
y: 0,
width: 10,
height: 10,
};
const _AREA: u16 = RECT.area();
const _LEFT: u16 = RECT.left();
const _RIGHT: u16 = RECT.right();
const _TOP: u16 = RECT.top();
const _BOTTOM: u16 = RECT.bottom();
assert!(RECT.intersects(RECT));
}
#[test]
fn layout_can_be_const() {
const _LAYOUT: Layout = Layout::new();
const _DEFAULT_LAYOUT: Layout = Layout::new()
.direction(Direction::Horizontal)
.margin(1)
.expand_to_fill(false);
const _HORIZONTAL_LAYOUT: Layout = Layout::new().horizontal_margin(1);
const _VERTICAL_LAYOUT: Layout = Layout::new().vertical_margin(1);
}
}

View File

@@ -1,19 +1,16 @@
#![forbid(unsafe_code)]
//! [ratatui](https://github.com/ratatui-org/ratatui) is a library used to build rich
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
//! terminal users interfaces and dashboards.
//!
//! ![](https://raw.githubusercontent.com/ratatui-org/ratatui/master/assets/demo.gif)
//! ![](https://raw.githubusercontent.com/fdehau/tui-rs/master/assets/demo.gif)
//!
//! # Get started
//!
//! ## Adding `ratatui` as a dependency
//! ## Adding `tui` as a dependency
//!
//! Add the following to your `Cargo.toml`:
//! ```toml
//! [dependencies]
//! crossterm = "0.26"
//! ratatui = "0.20"
//! tui = "0.16"
//! crossterm = "0.22"
//! ```
//!
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
@@ -23,28 +20,20 @@
//! ```toml
//! [dependencies]
//! termion = "1.5"
//! ratatui = { version = "0.20", default-features = false, features = ['termion'] }
//! tui = { version = "0.16", default-features = false, features = ['termion'] }
//! ```
//!
//! The same logic applies for all other available backends.
//!
//! ### Features
//!
//! Widgets which add dependencies are gated behind feature flags to prevent unused transitive
//! dependencies. The available features are:
//!
//! * `widget-calendar` - enables [`widgets::calendar`] and adds a dependency on the [time
//! crate](https://crates.io/crates/time).
//!
//! ## Creating a `Terminal`
//!
//! Every application using `ratatui` should start by instantiating a `Terminal`. It is a light
//! Every application using `tui` should start by instantiating a `Terminal`. It is a light
//! abstraction over available backends that provides basic functionalities such as clearing the
//! screen, hiding the cursor, etc.
//!
//! ```rust,no_run
//! use std::io;
//! use ratatui::{backend::CrosstermBackend, Terminal};
//! use tui::{backend::CrosstermBackend, Terminal};
//!
//! fn main() -> Result<(), io::Error> {
//! let stdout = io::stdout();
@@ -59,7 +48,7 @@
//!
//! ```rust,ignore
//! use std::io;
//! use ratatui::{backend::TermionBackend, Terminal};
//! use tui::{backend::TermionBackend, Terminal};
//! use termion::raw::IntoRawMode;
//!
//! fn main() -> Result<(), io::Error> {
@@ -87,13 +76,14 @@
//!
//! ```rust,no_run
//! use std::{io, thread, time::Duration};
//! use ratatui::{
//! use tui::{
//! backend::CrosstermBackend,
//! widgets::{Block, Borders},
//! widgets::{Widget, Block, Borders},
//! layout::{Layout, Constraint, Direction},
//! Terminal
//! };
//! use crossterm::{
//! event::{self, DisableMouseCapture, EnableMouseCapture},
//! event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
//! execute,
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
//! };
@@ -114,12 +104,6 @@
//! f.render_widget(block, size);
//! })?;
//!
//! // Start a thread to discard any input events. Without handling events, the
//! // stdin buffer will fill up, and be read into the shell when the program exits.
//! thread::spawn(|| loop {
//! event::read();
//! });
//!
//! thread::sleep(Duration::from_millis(5000));
//!
//! // restore terminal
@@ -142,7 +126,7 @@
//! full customization. And `Layout` is no exception:
//!
//! ```rust,no_run
//! use ratatui::{
//! use tui::{
//! backend::Backend,
//! layout::{Constraint, Direction, Layout},
//! widgets::{Block, Borders},
@@ -176,9 +160,6 @@
//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
//! corresponding area.
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub mod backend;
pub mod buffer;
pub mod layout;
@@ -188,6 +169,4 @@ pub mod terminal;
pub mod text;
pub mod widgets;
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};
pub mod prelude;
pub use self::terminal::{Frame, Terminal, TerminalOptions, ViewportVariant};

View File

@@ -1,35 +0,0 @@
//! 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

@@ -1,140 +1,28 @@
//! `style` contains the primitives used to control how your user interface will look.
//!
//! # Using the `Style` struct
//!
//! This is useful when creating style variables.
//! ## Example
//! ```
//! use ratatui::style::{Color, Modifier, Style};
//!
//! Style::default()
//! .fg(Color::Black)
//! .bg(Color::Green)
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
//! ```
//!
//! # Using style shorthands
//!
//! This is best for concise styling.
//! ## Example
//! ```
//! 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))
//! )
//! ```
use std::{
fmt::{self, Debug},
str::FromStr,
};
use bitflags::bitflags;
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)]
#[derive(Debug, Clone, Copy, 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),
}
@@ -146,12 +34,11 @@ bitflags! {
/// ## Examples
///
/// ```rust
/// # use ratatui::style::Modifier;
/// # use tui::style::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub struct Modifier: u16 {
const BOLD = 0b0000_0000_0001;
const DIM = 0b0000_0000_0010;
@@ -165,24 +52,10 @@ bitflags! {
}
}
/// Implement the `Debug` trait for `Modifier` manually.
///
/// This will avoid printing the empty modifier as 'Borders(0x0)' and instead print it as 'NONE'.
impl fmt::Debug for Modifier {
/// Format the modifier as `NONE` if the modifier is empty or as a list of flags separated by
/// `|` otherwise.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.is_empty() {
return write!(f, "NONE");
}
fmt::Debug::fmt(&self.0, f)
}
}
/// Style let you control the main characteristics of the displayed elements.
///
/// ```rust
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::style::{Color, Modifier, Style};
/// Style::default()
/// .fg(Color::Black)
/// .bg(Color::Green)
@@ -194,14 +67,12 @@ impl fmt::Debug for Modifier {
/// just S3.
///
/// ```rust
/// # use ratatui::style::{Color, Modifier, Style};
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::style::{Color, Modifier, Style};
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED),
/// #[cfg(feature = "crossterm")]
/// Style::default().underline_color(Color::Green),
/// Style::default().bg(Color::Red),
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
@@ -212,9 +83,7 @@ impl fmt::Debug for Modifier {
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Red),
/// #[cfg(feature = "crossterm")]
/// underline_color: Some(Color::Green),
/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED,
/// add_modifier: Modifier::BOLD,
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
@@ -225,9 +94,9 @@ impl fmt::Debug for Modifier {
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// # use ratatui::style::{Color, Modifier, Style};
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// # use tui::style::{Color, Modifier, Style};
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::reset().fg(Color::Yellow),
@@ -240,61 +109,38 @@ 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, Eq, PartialEq)]
#[derive(Debug, Clone, Copy, 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,
}
impl Default for Style {
fn default() -> Style {
Style::new()
}
}
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(),
}
}
}
impl Style {
/// Returns a `Style` resetting all properties.
pub const fn reset() -> Style {
pub fn reset() -> Style {
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
#[cfg(feature = "crossterm")]
underline_color: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::all(),
}
@@ -305,12 +151,12 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::style::{Color, Style};
/// # use tui::style::{Color, Style};
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
/// ```
pub const fn fg(mut self, color: Color) -> Style {
pub fn fg(mut self, color: Color) -> Style {
self.fg = Some(color);
self
}
@@ -320,37 +166,16 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::style::{Color, Style};
/// # use tui::style::{Color, Style};
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
/// ```
pub const fn bg(mut self, color: Color) -> Style {
pub fn bg(mut self, color: Color) -> Style {
self.bg = Some(color);
self
}
/// 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.
@@ -358,16 +183,16 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD);
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
/// assert_eq!(patched.sub_modifier, Modifier::empty());
/// ```
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);
pub fn add_modifier(mut self, modifier: Modifier) -> Style {
self.sub_modifier.remove(modifier);
self.add_modifier.insert(modifier);
self
}
@@ -378,16 +203,16 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
/// ```
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);
pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
self.add_modifier.remove(modifier);
self.sub_modifier.insert(modifier);
self
}
@@ -396,7 +221,7 @@ impl Style {
///
/// ## Examples
/// ```
/// # use ratatui::style::{Color, Modifier, Style};
/// # use tui::style::{Color, Modifier, Style};
/// let style_1 = Style::default().fg(Color::Yellow);
/// let style_2 = Style::default().bg(Color::Red);
/// let combined = style_1.patch(style_2);
@@ -408,11 +233,6 @@ 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);
@@ -422,105 +242,8 @@ impl Style {
}
}
/// Error type indicating a failure to parse a color string.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseColorError;
impl std::fmt::Display for ParseColorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Failed to parse Colors")
}
}
impl std::error::Error for ParseColorError {}
/// Converts a string representation to a `Color` instance.
///
/// The `from_str` function attempts to parse the given string and convert it to the corresponding
/// `Color` variant. It supports named colors, RGB values, and indexed colors. If the string cannot
/// be parsed, a `ParseColorError` is returned.
///
/// See the [`Color`](Color) documentation for more information on the supported color names.
///
/// # Examples
///
/// ```
/// # use std::str::FromStr;
/// # use ratatui::style::Color;
/// let color: Color = Color::from_str("blue").unwrap();
/// assert_eq!(color, Color::Blue);
///
/// let color: Color = Color::from_str("#FF0000").unwrap();
/// assert_eq!(color, Color::Rgb(255, 0, 0));
///
/// let color: Color = Color::from_str("10").unwrap();
/// assert_eq!(color, Color::Indexed(10));
///
/// let color: Result<Color, _> = Color::from_str("invalid_color");
/// assert!(color.is_err());
/// ```
impl FromStr for Color {
type Err = ParseColorError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(
// 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);
}
}
},
)
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use super::*;
fn styles() -> Vec<Style> {
@@ -555,315 +278,4 @@ mod tests {
}
}
}
#[test]
fn combine_individual_modifiers() {
use crate::{buffer::Buffer, layout::Rect};
let mods = vec![
Modifier::BOLD,
Modifier::DIM,
Modifier::ITALIC,
Modifier::UNDERLINED,
Modifier::SLOW_BLINK,
Modifier::RAPID_BLINK,
Modifier::REVERSED,
Modifier::HIDDEN,
Modifier::CROSSED_OUT,
];
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
for m in &mods {
buffer.get_mut(0, 0).set_style(Style::reset());
buffer
.get_mut(0, 0)
.set_style(Style::default().add_modifier(*m));
let style = buffer.get(0, 0).style();
assert!(style.add_modifier.contains(*m));
assert!(!style.sub_modifier.contains(*m));
}
}
#[test]
fn modifier_debug() {
assert_eq!(format!("{:?}", Modifier::empty()), "NONE");
assert_eq!(format!("{:?}", Modifier::BOLD), "BOLD");
assert_eq!(format!("{:?}", Modifier::DIM), "DIM");
assert_eq!(format!("{:?}", Modifier::ITALIC), "ITALIC");
assert_eq!(format!("{:?}", Modifier::UNDERLINED), "UNDERLINED");
assert_eq!(format!("{:?}", Modifier::SLOW_BLINK), "SLOW_BLINK");
assert_eq!(format!("{:?}", Modifier::RAPID_BLINK), "RAPID_BLINK");
assert_eq!(format!("{:?}", Modifier::REVERSED), "REVERSED");
assert_eq!(format!("{:?}", Modifier::HIDDEN), "HIDDEN");
assert_eq!(format!("{:?}", Modifier::CROSSED_OUT), "CROSSED_OUT");
assert_eq!(
format!("{:?}", Modifier::BOLD | Modifier::DIM),
"BOLD | DIM"
);
assert_eq!(
format!("{:?}", Modifier::all()),
"BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT"
);
}
#[test]
fn from_rgb_color() {
let color: Color = Color::from_str("#FF0000").unwrap();
assert_eq!(color, Color::Rgb(255, 0, 0));
}
#[test]
fn from_indexed_color() {
let color: Color = Color::from_str("10").unwrap();
assert_eq!(color, Color::Indexed(10));
}
#[test]
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 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 '#'
"#abcdef00", // too many chars
"resett", // typo
"lightblackk", // typo
];
for bad_color in bad_colors {
assert!(
Color::from_str(bad_color).is_err(),
"bad color: '{bad_color}'"
);
}
}
#[test]
fn style_can_be_const() {
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());
}
}

View File

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

@@ -21,12 +21,6 @@ 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,
@@ -75,12 +69,6 @@ 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,
@@ -170,12 +158,6 @@ pub mod line {
pub cross: &'static str,
}
impl Default for Set {
fn default() -> Self {
NORMAL
}
}
pub const NORMAL: Set = Set {
vertical: VERTICAL,
horizontal: HORIZONTAL,
@@ -240,64 +222,12 @@ pub mod braille {
}
/// Marker to use when plotting data points
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, 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,
/// One point per cell in the shape of a bar
Bar,
/// 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

@@ -1,29 +1,34 @@
use std::io;
use crate::{
backend::{Backend, ClearType},
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
};
use std::io;
use tracing::{event, span, Level};
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub enum Viewport {
#[default]
#[derive(Debug, Clone, PartialEq)]
pub enum ViewportVariant {
Fullscreen,
Inline(u16),
Fixed(Rect),
}
#[derive(Debug, Clone, PartialEq)]
/// 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,
pub viewport: ViewportVariant,
}
#[derive(Debug, Clone, PartialEq)]
struct Viewport {
variant: ViewportVariant,
area: Rect,
}
/// Interface to the terminal backed by Termion
#[derive(Debug, Default, Clone)]
#[derive(Debug)]
pub struct Terminal<B>
where
B: Backend,
@@ -38,7 +43,6 @@ where
hidden_cursor: bool,
/// Viewport
viewport: Viewport,
viewport_area: Rect,
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
last_known_size: Rect,
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
@@ -47,7 +51,6 @@ where
}
/// Represents a consistent terminal interface for rendering.
#[derive(Debug)]
pub struct Frame<'a, B: 'a>
where
B: Backend,
@@ -67,7 +70,7 @@ where
{
/// Frame size, guaranteed not to change when rendering.
pub fn size(&self) -> Rect {
self.terminal.viewport_area
self.terminal.viewport.area
}
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
@@ -75,10 +78,10 @@ where
/// # Examples
///
/// ```rust
/// # use ratatui::Terminal;
/// # use ratatui::backend::TestBackend;
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::Block;
/// # use tui::Terminal;
/// # use tui::backend::TestBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::Block;
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let block = Block::default();
@@ -101,10 +104,10 @@ where
/// # Examples
///
/// ```rust
/// # use ratatui::Terminal;
/// # use ratatui::backend::TestBackend;
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::{List, ListItem, ListState};
/// # use tui::Terminal;
/// # use tui::backend::TestBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::{List, ListItem, ListState};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let mut state = ListState::default();
@@ -136,10 +139,9 @@ where
}
}
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
/// 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,
@@ -153,7 +155,7 @@ where
// Attempt to restore the cursor state
if self.hidden_cursor {
if let Err(err) = self.show_cursor() {
eprintln!("Failed to show the cursor: {err}");
eprintln!("Failed to show the cursor: {}", err);
}
}
}
@@ -169,28 +171,45 @@ where
Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
viewport: ViewportVariant::Fullscreen,
},
)
}
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
let size = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
Viewport::Fixed(area) => area,
};
let size = backend.size()?;
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (size, (0, 0)),
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
Viewport::Fixed(area) => (area, (area.left(), area.top())),
ViewportVariant::Fullscreen => (size, (0, 0)),
ViewportVariant::Inline(height) => {
let pos = backend.get_cursor()?;
let mut row = pos.1;
let max_height = size.height.min(height);
backend.append_lines(max_height.saturating_sub(1))?;
let missing_lines = row.saturating_add(max_height).saturating_sub(size.height);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
(
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
)
}
ViewportVariant::Fixed(area) => (area, (area.left(), area.top())),
};
Ok(Terminal {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
viewport: Viewport {
variant: options.viewport,
area: viewport_area,
},
last_known_size: size,
last_known_cursor_pos: cursor_pos,
})
@@ -228,20 +247,42 @@ where
self.backend.draw(updates.into_iter())
}
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
/// be saved so the size can remain consistent when rendering.
/// This leads to a full clear of the screen.
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
let next_area = match self.viewport {
Viewport::Fullscreen => size,
Viewport::Inline(height) => {
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn resize(&mut self) -> io::Result<()> {
let size = self.size()?;
if self.last_known_size == size {
return Ok(());
}
event!(Level::DEBUG, last_known_size = ?self.last_known_size, ?size, "terminal size changed");
let next_area = match self.viewport.variant {
ViewportVariant::Fullscreen => size,
ViewportVariant::Inline(height) => {
let (_, mut row) = self.get_cursor()?;
let offset_in_previous_viewport = self
.last_known_cursor_pos
.1
.saturating_sub(self.viewport_area.top());
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
.saturating_sub(self.viewport.area.top());
let max_height = height.min(size.height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
self.backend.append_lines(lines_after_cursor)?;
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
}
}
Viewport::Fixed(area) => area,
ViewportVariant::Fixed(area) => area,
};
self.set_viewport_area(next_area);
self.clear()?;
@@ -251,21 +292,10 @@ where
}
fn set_viewport_area(&mut self, area: Rect) {
self.viewport.area = area;
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport_area = area;
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
// fixed viewports do not get autoresized
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let size = self.size()?;
if size != self.last_known_size {
self.resize(size)?;
}
};
Ok(())
event!(Level::DEBUG, area = ?area, "viewport changed");
}
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
@@ -274,9 +304,12 @@ where
where
F: FnOnce(&mut Frame<B>),
{
let span = span!(Level::DEBUG, "draw");
let _guard = span.enter();
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;
self.resize()?;
let mut frame = self.get_frame();
f(&mut frame);
@@ -296,11 +329,15 @@ where
}
}
self.swap_buffers();
// Swap buffers
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
// Flush
self.backend.flush()?;
event!(Level::DEBUG, "completed frame");
Ok(CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_size,
@@ -331,17 +368,18 @@ where
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
event!(Level::DEBUG, "clear");
match self.viewport.variant {
ViewportVariant::Fullscreen => self.backend.clear(ClearType::All)?,
ViewportVariant::Inline(_) => {
self.backend
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
self.backend.clear_region(ClearType::AfterCursor)?;
.set_cursor(self.viewport.area.left(), self.viewport.area.top())?;
self.backend.clear(ClearType::AfterCursor)?;
}
Viewport::Fixed(area) => {
ViewportVariant::Fixed(area) => {
for row in area.top()..area.bottom() {
self.backend.set_cursor(0, row)?;
self.backend.clear_region(ClearType::AfterCursor)?;
self.backend.clear(ClearType::AfterCursor)?;
}
}
}
@@ -350,12 +388,6 @@ where
Ok(())
}
/// Clears the inactive buffer and swaps it with the current buffer
pub fn swap_buffers(&mut self) {
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
}
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Rect> {
self.backend.size()
@@ -364,8 +396,8 @@ where
/// Insert some content before the current inline viewport. This has no effect when the
/// viewport is fullscreen.
///
/// This function scrolls down the current viewport by the given height. The newly freed space
/// is then made available to the `draw_fn` closure through a writable `Buffer`.
/// This function scrolls down the current viewport by the given height. The newly freed space is
/// then made available to the `draw_fn` closure through a writable `Buffer`.
///
/// Before:
/// ```ignore
@@ -393,15 +425,15 @@ where
/// ## Insert a single line before the current viewport
///
/// ```rust
/// # use ratatui::widgets::{Paragraph, Widget};
/// # use ratatui::text::{Line, Span};
/// # use ratatui::style::{Color, Style};
/// # use ratatui::{Terminal};
/// # use ratatui::backend::TestBackend;
/// # use tui::widgets::{Paragraph, Widget};
/// # use tui::text::{Spans, Span};
/// # use tui::style::{Color, Style};
/// # use tui::{Terminal};
/// # use tui::backend::TestBackend;
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
/// Paragraph::new(Line::from(vec![
/// Paragraph::new(Spans::from(vec![
/// Span::raw("This line will be added "),
/// Span::styled("before", Style::default().fg(Color::Blue)),
/// Span::raw(" the current viewport")
@@ -412,7 +444,9 @@ where
where
F: FnOnce(&mut Buffer),
{
if !matches!(self.viewport, Viewport::Inline(_)) {
let span = span!(Level::DEBUG, "insert_before");
let _guard = span.enter();
if !matches!(self.viewport.variant, ViewportVariant::Inline(_)) {
return Ok(());
}
@@ -420,11 +454,11 @@ where
let height = height.min(self.last_known_size.height);
self.backend.append_lines(height)?;
let missing_lines =
height.saturating_sub(self.last_known_size.bottom() - self.viewport_area.top());
height.saturating_sub(self.last_known_size.bottom() - self.viewport.area.top());
let area = Rect {
x: self.viewport_area.left(),
y: self.viewport_area.top().saturating_sub(missing_lines),
width: self.viewport_area.width,
x: self.viewport.area.left(),
y: self.viewport.area.top().saturating_sub(missing_lines),
width: self.viewport.area.width,
height,
};
let mut buffer = Buffer::empty(area);
@@ -439,51 +473,16 @@ where
self.backend.flush()?;
let remaining_lines = self.last_known_size.height - area.bottom();
let missing_lines = self.viewport_area.height.saturating_sub(remaining_lines);
self.backend.append_lines(self.viewport_area.height)?;
let missing_lines = self.viewport.area.height.saturating_sub(remaining_lines);
self.backend.append_lines(self.viewport.area.height)?;
self.set_viewport_area(Rect {
x: area.left(),
y: area.bottom().saturating_sub(missing_lines),
width: area.width,
height: self.viewport_area.height,
height: self.viewport.area.height,
});
Ok(())
}
}
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Rect,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, (u16, u16))> {
let pos = backend.get_cursor()?;
let mut row = pos.1;
let max_height = size.height.min(height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
backend.append_lines(lines_after_cursor)?;
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Ok((
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
))
}

428
src/text.rs Normal file
View File

@@ -0,0 +1,428 @@
//! Primitives for styled text.
//!
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
//! those strings may be associated to a set of styles. `tui` has three ways to represent them:
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
//! - A single line string where each grapheme may have its own style is represented by [`Spans`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//!
//! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Spans`].
//!
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
//! supported for their properties. Moreover, `tui` provides convenient `From` implementations so
//! that you can start by using simple `String` or `&str` and then promote them to the previous
//! primitives when you need additional styling capabilities.
//!
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
//! its `title` property (which is a [`Spans`] under the hood):
//!
//! ```rust
//! # use tui::widgets::Block;
//! # use tui::text::{Span, Spans};
//! # use tui::style::{Color, Style};
//! // A simple string with no styling.
//! // Converted to Spans(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
//! // ])
//! let block = Block::default().title("My title");
//!
//! // A simple string with a unique style.
//! // Converted to Spans(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 Spans(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;
use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
/// A grapheme associated to a style.
#[derive(Debug, Clone, PartialEq)]
pub struct StyledGrapheme<'a> {
pub symbol: &'a str,
pub style: Style,
}
/// A string where all graphemes have the same style.
#[derive(Debug, Clone, PartialEq)]
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 tui::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 tui::text::Span;
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Span::styled("My text", style);
/// Span::styled(String::from("My text"), style);
/// ```
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 tui::text::{Span, StyledGrapheme};
/// # use tui::style::{Color, Modifier, Style};
/// # use std::iter::Iterator;
/// let style = Style::default().fg(Color::Yellow);
/// let span = Span::styled("Text", style);
/// 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")
}
}
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)
}
}
/// A string composed of clusters of graphemes, each with their own style.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Spans<'a>(pub Vec<Span<'a>>);
impl<'a> Spans<'a> {
/// Returns the width of the underlying string.
///
/// ## Examples
///
/// ```rust
/// # use tui::text::{Span, Spans};
/// # use tui::style::{Color, Style};
/// let spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
/// ]);
/// assert_eq!(7, spans.width());
/// ```
pub fn width(&self) -> usize {
self.0.iter().map(Span::width).sum()
}
}
impl<'a> From<String> for Spans<'a> {
fn from(s: String) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<&'a str> for Spans<'a> {
fn from(s: &'a str) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
Spans(spans)
}
}
impl<'a> From<Span<'a>> for Spans<'a> {
fn from(span: Span<'a>) -> Spans<'a> {
Spans(vec![span])
}
}
impl<'a> From<Spans<'a>> for String {
fn from(line: Spans<'a>) -> String {
line.0.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s.content.as_ref());
acc
})
}
}
/// A string split over multiple lines where each line is composed of several clusters, each with
/// their own style.
///
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
///
/// ```rust
/// # use tui::text::Text;
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
///
/// // An initial two lines of `Text` built from a `&str`
/// let mut text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
///
/// // Adding two more unstyled lines
/// text.extend(Text::raw("These are two\nmore lines!"));
/// assert_eq!(4, text.height());
///
/// // Adding a final two styled lines
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
/// assert_eq!(6, text.height());
/// ```
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Text<'a> {
pub lines: Vec<Spans<'a>>,
}
impl<'a> Text<'a> {
/// Create some text (potentially multiple lines) with no style.
///
/// ## Examples
///
/// ```rust
/// # use tui::text::Text;
/// Text::raw("The first line\nThe second line");
/// Text::raw(String::from("The first line\nThe second line"));
/// ```
pub fn raw<T>(content: T) -> Text<'a>
where
T: Into<Cow<'a, str>>,
{
Text {
lines: match content.into() {
Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
},
}
}
/// Create some text (potentially multiple lines) with a style.
///
/// # Examples
///
/// ```rust
/// # use tui::text::Text;
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style);
/// ```
pub fn styled<T>(content: T, style: Style) -> Text<'a>
where
T: Into<Cow<'a, str>>,
{
let mut text = Text::raw(content);
text.patch_style(style);
text
}
/// Returns the max width of all the lines.
///
/// ## Examples
///
/// ```rust
/// use tui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(15, text.width());
/// ```
pub fn width(&self) -> usize {
self.lines
.iter()
.map(Spans::width)
.max()
.unwrap_or_default()
}
/// Returns the height.
///
/// ## Examples
///
/// ```rust
/// use tui::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()
}
/// Apply a new style to existing text.
///
/// # Examples
///
/// ```rust
/// # use tui::text::Text;
/// # use tui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_text = Text::raw("The first line\nThe second line");
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
/// assert_ne!(raw_text, styled_text);
///
/// raw_text.patch_style(style);
/// assert_eq!(raw_text, styled_text);
/// ```
pub fn patch_style(&mut self, style: Style) {
for line in &mut self.lines {
for span in &mut line.0 {
span.style = span.style.patch(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![Spans::from(span)],
}
}
}
impl<'a> From<Spans<'a>> for Text<'a> {
fn from(spans: Spans<'a>) -> Text<'a> {
Text { lines: vec![spans] }
}
}
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
Text { lines }
}
}
impl<'a> IntoIterator for Text<'a> {
type Item = Spans<'a>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.lines.into_iter()
}
}
impl<'a> Extend<Spans<'a>> for Text<'a> {
fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
self.lines.extend(iter);
}
}

View File

@@ -1,31 +0,0 @@
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,333 +0,0 @@
#![allow(deprecated)]
use std::borrow::Cow;
use super::{Span, Spans, Style, StyledGrapheme};
use crate::layout::Alignment;
#[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
///
/// ```rust
/// # use ratatui::text::{Span, Line};
/// # use ratatui::style::{Color, Style};
/// let line = Line::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
/// ]);
/// assert_eq!(7, line.width());
/// ```
pub fn width(&self) -> usize {
self.spans.iter().map(Span::width).sum()
}
/// 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
///
/// ```rust
/// # use ratatui::text::{Span, Line};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_line = Line::from(vec![
/// Span::raw("My"),
/// Span::raw(" text"),
/// ]);
/// let mut styled_line = Line::from(vec![
/// Span::styled("My", style),
/// Span::styled(" text", style),
/// ]);
///
/// assert_ne!(raw_line, styled_line);
///
/// raw_line.patch_style(style);
/// assert_eq!(raw_line, styled_line);
/// ```
pub fn patch_style(&mut self, style: Style) {
for span in &mut self.spans {
span.patch_style(style);
}
}
/// Resets the style of each Span in the Line.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Line};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut line = Line::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
/// ]);
///
/// line.reset_style();
/// assert_eq!(Style::reset(), line.spans[0].style);
/// assert_eq!(Style::reset(), line.spans[1].style);
/// ```
pub fn reset_style(&mut self) {
for span in &mut self.spans {
span.reset_style();
}
}
/// Sets the target alignment for this line of text.
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
///
/// ## Examples
///
/// ```rust
/// # use std::borrow::Cow;
/// # use ratatui::layout::Alignment;
/// # use ratatui::text::{Span, Line};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut line = Line::from("Hi, what's up?");
/// assert_eq!(None, line.alignment);
/// assert_eq!(Some(Alignment::Right), line.alignment(Alignment::Right).alignment)
/// ```
pub fn alignment(self, alignment: Alignment) -> Self {
Self {
alignment: Some(alignment),
..self
}
}
}
impl<'a> From<String> for Line<'a> {
fn from(s: String) -> Self {
Self::from(vec![Span::from(s)])
}
}
impl<'a> From<&'a str> for Line<'a> {
fn from(s: &'a str) -> Self {
Self::from(vec![Span::from(s)])
}
}
impl<'a> From<Vec<Span<'a>>> for Line<'a> {
fn from(spans: Vec<Span<'a>>) -> Self {
Self {
spans,
..Default::default()
}
}
}
impl<'a> From<Span<'a>> for Line<'a> {
fn from(span: Span<'a>) -> Self {
Self::from(vec![span])
}
}
impl<'a> From<Line<'a>> for String {
fn from(line: Line<'a>) -> String {
line.spans.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s.content.as_ref());
acc
})
}
}
impl<'a> From<Spans<'a>> for Line<'a> {
fn from(value: Spans<'a>) -> Self {
Self::from(value.0)
}
}
#[cfg(test)]
mod tests {
use crate::{
layout::Alignment,
style::{Color, Modifier, Style},
text::{Line, Span, Spans, StyledGrapheme},
};
#[test]
fn test_width() {
let line = Line::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::raw(" text"),
]);
assert_eq!(7, line.width());
let empty_line = Line::default();
assert_eq!(0, empty_line.width());
}
#[test]
fn test_patch_style() {
let style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::ITALIC);
let mut raw_line = Line::from(vec![Span::raw("My"), Span::raw(" text")]);
let styled_line = Line::from(vec![
Span::styled("My", style),
Span::styled(" text", style),
]);
assert_ne!(raw_line, styled_line);
raw_line.patch_style(style);
assert_eq!(raw_line, styled_line);
}
#[test]
fn test_reset_style() {
let mut line = Line::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
]);
line.reset_style();
assert_eq!(Style::reset(), line.spans[0].style);
assert_eq!(Style::reset(), line.spans[1].style);
}
#[test]
fn test_from_string() {
let s = String::from("Hello, world!");
let line = Line::from(s);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
}
#[test]
fn test_from_str() {
let s = "Hello, world!";
let line = Line::from(s);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
}
#[test]
fn test_from_vec() {
let spans = vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
];
let line = Line::from(spans.clone());
assert_eq!(spans, line.spans);
}
#[test]
fn test_from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
let line = Line::from(span.clone());
assert_eq!(vec![span], line.spans);
}
#[test]
fn test_from_spans() {
let spans = vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
];
assert_eq!(Line::from(Spans::from(spans.clone())), Line::from(spans));
}
#[test]
fn test_into_string() {
let line = Line::from(vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
]);
let s: String = line.into();
assert_eq!("Hello, world!", s);
}
#[test]
fn test_alignment() {
let line = Line::from("This is left").alignment(Alignment::Left);
assert_eq!(Some(Alignment::Left), line.alignment);
let line = Line::from("This is default");
assert_eq!(None, line.alignment);
}
#[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

@@ -1,143 +0,0 @@
use std::{
borrow::Cow,
fmt::{self, Debug, Display},
};
use super::Text;
/// A wrapper around a string that is masked when displayed.
///
/// The masked string is displayed as a series of the same character.
/// This might be used to display a password field or similar secure data.
///
/// # Examples
///
/// ```rust
/// use ratatui::{buffer::Buffer, layout::Rect, text::Masked, widgets::{Paragraph, Widget}};
///
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
/// let password = Masked::new("12345", 'x');
///
/// Paragraph::new(password).render(buffer.area, &mut buffer);
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
/// ```
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Masked<'a> {
inner: Cow<'a, str>,
mask_char: char,
}
impl<'a> Masked<'a> {
pub fn new(s: impl Into<Cow<'a, str>>, mask_char: char) -> Self {
Self {
inner: s.into(),
mask_char,
}
}
/// The character to use for masking.
pub fn mask_char(&self) -> char {
self.mask_char
}
/// The underlying string, with all characters masked.
pub fn value(&self) -> Cow<'a, str> {
self.inner.chars().map(|_| self.mask_char).collect()
}
}
impl Debug for Masked<'_> {
/// Debug representation of a masked string is the underlying string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.inner).map_err(|_| fmt::Error)
}
}
impl Display for Masked<'_> {
/// Display representation of a masked string is the masked string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.value()).map_err(|_| fmt::Error)
}
}
impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
fn from(masked: &'a Masked) -> Cow<'a, str> {
masked.value()
}
}
impl<'a> From<Masked<'a>> for Cow<'a, str> {
fn from(masked: Masked<'a>) -> Cow<'a, str> {
masked.value()
}
}
impl<'a> From<&'a Masked<'_>> for Text<'a> {
fn from(masked: &'a Masked) -> Text<'a> {
Text::raw(masked.value())
}
}
impl<'a> From<Masked<'a>> for Text<'a> {
fn from(masked: Masked<'a>) -> Text<'a> {
Text::raw(masked.value())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::text::Line;
#[test]
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 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 display() {
let masked = Masked::new("12345", 'x');
assert_eq!(format!("{masked}"), "xxxxx");
}
#[test]
fn into_text() {
let masked = Masked::new("12345", 'x');
let text: Text = (&masked).into();
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
let text: Text = masked.into();
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
}
#[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.into();
assert_eq!(cow, "xxxxx");
}
}

View File

@@ -1,71 +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 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;

View File

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

@@ -1,225 +0,0 @@
#![allow(deprecated)]
use super::{Span, Style};
use crate::{layout::Alignment, text::Line};
/// A string composed of clusters of graphemes, each with their own style.
///
/// `Spans` has been deprecated in favor of `Line`, and will be removed in the
/// future. All methods that accept Spans have been replaced with methods that
/// accept Into<Line<'a>> (which is implemented on `Spans`) to allow users of
/// this crate to gradually transition to Line.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[deprecated(note = "Use `ratatui::text::Line` instead")]
pub struct Spans<'a>(pub Vec<Span<'a>>);
impl<'a> Spans<'a> {
/// Returns the width of the underlying string.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style};
/// let spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
/// ]);
/// assert_eq!(7, spans.width());
/// ```
pub fn width(&self) -> usize {
self.0.iter().map(Span::width).sum()
}
/// Patches the style of each Span in an existing Spans, adding modifiers from the given style.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_spans = Spans::from(vec![
/// Span::raw("My"),
/// Span::raw(" text"),
/// ]);
/// let mut styled_spans = Spans::from(vec![
/// Span::styled("My", style),
/// Span::styled(" text", style),
/// ]);
///
/// assert_ne!(raw_spans, styled_spans);
///
/// raw_spans.patch_style(style);
/// assert_eq!(raw_spans, styled_spans);
/// ```
pub fn patch_style(&mut self, style: Style) {
for span in &mut self.0 {
span.patch_style(style);
}
}
/// Resets the style of each Span in the Spans.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
/// ]);
///
/// spans.reset_style();
/// assert_eq!(Style::reset(), spans.0[0].style);
/// assert_eq!(Style::reset(), spans.0[1].style);
/// ```
pub fn reset_style(&mut self) {
for span in &mut self.0 {
span.reset_style();
}
}
/// Sets the target alignment for this line of text.
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
///
/// ## Examples
///
/// ```rust
/// # use std::borrow::Cow;
/// # use ratatui::layout::Alignment;
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut line = Spans::from("Hi, what's up?").alignment(Alignment::Right);
/// assert_eq!(Some(Alignment::Right), line.alignment)
/// ```
pub fn alignment(self, alignment: Alignment) -> Line<'a> {
let line = Line::from(self);
line.alignment(alignment)
}
}
impl<'a> From<String> for Spans<'a> {
fn from(s: String) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<&'a str> for Spans<'a> {
fn from(s: &'a str) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
Spans(spans)
}
}
impl<'a> From<Span<'a>> for Spans<'a> {
fn from(span: Span<'a>) -> Spans<'a> {
Spans(vec![span])
}
}
impl<'a> From<Spans<'a>> for String {
fn from(line: Spans<'a>) -> String {
line.0.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s.content.as_ref());
acc
})
}
}
#[cfg(test)]
mod tests {
use crate::{
style::{Color, Modifier, Style},
text::{Span, Spans},
};
#[test]
fn test_width() {
let spans = Spans::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::raw(" text"),
]);
assert_eq!(7, spans.width());
let empty_spans = Spans::default();
assert_eq!(0, empty_spans.width());
}
#[test]
fn test_patch_style() {
let style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::ITALIC);
let mut raw_spans = Spans::from(vec![Span::raw("My"), Span::raw(" text")]);
let styled_spans = Spans::from(vec![
Span::styled("My", style),
Span::styled(" text", style),
]);
assert_ne!(raw_spans, styled_spans);
raw_spans.patch_style(style);
assert_eq!(raw_spans, styled_spans);
}
#[test]
fn test_reset_style() {
let mut spans = Spans::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
]);
spans.reset_style();
assert_eq!(Style::reset(), spans.0[0].style);
assert_eq!(Style::reset(), spans.0[1].style);
}
#[test]
fn test_from_string() {
let s = String::from("Hello, world!");
let spans = Spans::from(s);
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
}
#[test]
fn test_from_str() {
let s = "Hello, world!";
let spans = Spans::from(s);
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
}
#[test]
fn test_from_vec() {
let spans_vec = vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
];
let spans = Spans::from(spans_vec.clone());
assert_eq!(spans_vec, spans.0);
}
#[test]
fn test_from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
let spans = Spans::from(span.clone());
assert_eq!(vec![span], spans.0);
}
#[test]
fn test_into_string() {
let spans = Spans::from(vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
]);
let s: String = spans.into();
assert_eq!("Hello, world!", s);
}
}

View File

@@ -1,225 +0,0 @@
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,47 +0,0 @@
use crate::{layout::Alignment, text::Line};
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Title<'a> {
pub content: Line<'a>,
/// Defaults to Left if unset
pub alignment: Option<Alignment>,
/// Defaults to Top if unset
pub position: Option<Position>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Position {
#[default]
Top,
Bottom,
}
impl<'a> Title<'a> {
pub fn content<T>(mut self, content: T) -> Title<'a>
where
T: Into<Line<'a>>,
{
self.content = content.into();
self
}
pub fn alignment(mut self, alignment: Alignment) -> Title<'a> {
self.alignment = Some(alignment);
self
}
pub fn position(mut self, position: Position) -> Title<'a> {
self.position = Some(position);
self
}
}
impl<'a, T> From<T> for Title<'a>
where
T: Into<Line<'a>>,
{
fn from(value: T) -> Self {
Self::default().content(value.into())
}
}

219
src/widgets/barchart.rs Normal file
View File

@@ -0,0 +1,219 @@
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols,
widgets::{Block, Widget},
};
use std::cmp::min;
use unicode_width::UnicodeWidthStr;
/// Display multiple bars in a single widgets
///
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, BarChart};
/// # use tui::style::{Style, Color, Modifier};
/// BarChart::default()
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .bar_width(3)
/// .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: Default::default(),
label_style: Default::default(),
style: Default::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

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

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

View File

@@ -1,759 +0,0 @@
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,18 +1,14 @@
#[path = "../title.rs"]
pub mod title;
pub use self::title::{Position, Title};
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Style, Styled},
style::Style,
symbols::line,
text::{Span, Spans},
widgets::{Borders, Widget},
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BorderType {
#[default]
Plain,
Rounded,
Double,
@@ -20,7 +16,7 @@ pub enum BorderType {
}
impl BorderType {
pub const fn line_symbols(border_type: BorderType) -> line::Set {
pub fn line_symbols(border_type: BorderType) -> line::Set {
match border_type {
BorderType::Plain => line::NORMAL,
BorderType::Rounded => line::ROUNDED,
@@ -30,69 +26,14 @@ impl BorderType {
}
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Padding {
pub left: u16,
pub right: u16,
pub top: u16,
pub bottom: u16,
}
impl Padding {
pub const fn new(left: u16, right: u16, top: u16, bottom: u16) -> Self {
Padding {
left,
right,
top,
bottom,
}
}
pub const fn zero() -> Self {
Padding {
left: 0,
right: 0,
top: 0,
bottom: 0,
}
}
pub const fn horizontal(value: u16) -> Self {
Padding {
left: value,
right: value,
top: 0,
bottom: 0,
}
}
pub const fn vertical(value: u16) -> Self {
Padding {
left: 0,
right: 0,
top: value,
bottom: value,
}
}
pub const fn uniform(value: u16) -> Self {
Padding {
left: value,
right: value,
top: value,
bottom: value,
}
}
}
/// Base widget to be used with all upper level ones. It may be used to display a box border around
/// the widget and/or add a title.
///
/// # Examples
///
/// ```
/// # use ratatui::widgets::{Block, BorderType, Borders};
/// # use ratatui::style::{Style, Color};
/// # use tui::widgets::{Block, BorderType, Borders};
/// # use tui::style::{Style, Color};
/// Block::default()
/// .title("Block")
/// .borders(Borders::LEFT | Borders::RIGHT)
@@ -100,198 +41,91 @@ impl Padding {
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// ```
///
/// You may also use multiple titles like in the following:
/// ```
/// # use ratatui::widgets::{Block, BorderType, Borders, block::title::{Position, Title}};
/// # use ratatui::style::{Style, Color};
/// Block::default()
/// .title("Title 1")
/// .title(Title::from("Title 2").position(Position::Bottom))
/// .borders(Borders::LEFT | Borders::RIGHT)
/// .border_style(Style::default().fg(Color::White))
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Block<'a> {
/// List of titles
titles: Vec<Title<'a>>,
/// The style to be patched to all titles of the block
titles_style: Style,
/// The default alignment of the titles that don't have one
titles_alignment: Alignment,
/// The default position of the titles that don't have one
titles_position: Position,
/// Optional title place on the upper left of the block
title: Option<Spans<'a>>,
/// Title alignment. The default is top left of the block, but one can choose to place
/// title in the top middle, or top right of the block
title_alignment: Alignment,
/// Visible borders
borders: Borders,
/// Border style
border_style: Style,
/// Type of the border. The default is plain lines but one can choose to have rounded or
/// doubled lines instead.
/// Type of the border. The default is plain lines but one can choose to have rounded corners
/// or doubled lines instead.
border_type: BorderType,
/// Widget style
style: Style,
/// Block padding
padding: Padding,
}
impl<'a> Default for Block<'a> {
fn default() -> Block<'a> {
Block {
title: None,
title_alignment: Alignment::Left,
borders: Borders::NONE,
border_style: Default::default(),
border_type: BorderType::Plain,
style: Default::default(),
}
}
}
impl<'a> Block<'a> {
pub const fn new() -> Self {
Self {
titles: Vec::new(),
titles_style: Style::new(),
titles_alignment: Alignment::Left,
titles_position: Position::Top,
borders: Borders::NONE,
border_style: Style::new(),
border_type: BorderType::Plain,
style: Style::new(),
padding: Padding::zero(),
}
}
/// # Example
/// ```
/// # use ratatui::widgets::{Block, block::title::Title};
/// # use ratatui::layout::Alignment;
/// Block::default()
/// .title("Title") // By default in the top right corner
/// .title(Title::from("Left").alignment(Alignment::Left))
/// .title(
/// Title::from("Center")
/// .alignment(Alignment::Center),
/// );
/// ```
/// Adds a title to the block.
///
/// The `title` function allows you to add a title to the block. You can call this function
/// multiple times to add multiple titles.
///
/// Each title will be rendered with a single space separating titles that are in the same
/// position or alignment. When both centered and non-centered titles are rendered, the centered
/// space is calculated based on the full width of the block, rather than the leftover width.
///
/// You can provide various types as the title, including strings, string slices, borrowed
/// strings (`Cow<str>`), spans, or vectors of spans (`Vec<Span>`).
///
/// By default, the titles will avoid being rendered in the corners of the block but will align
/// against the left or right edge of the block if there is no border on that edge.
///
/// Note: If the block is too small and multiple titles overlap, the border might get cut off at
/// a corner.
pub fn title<T>(mut self, title: T) -> Block<'a>
where
T: Into<Title<'a>>,
T: Into<Spans<'a>>,
{
self.titles.push(title.into());
self.title = Some(title.into());
self
}
/// Applies the style to all titles. If a title already has a style, it will add on top of it.
pub const fn title_style(mut self, style: Style) -> Block<'a> {
self.titles_style = style;
#[deprecated(
since = "0.10.0",
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
)]
pub fn title_style(mut self, style: Style) -> Block<'a> {
if let Some(t) = self.title {
let title = String::from(t);
self.title = Some(Spans::from(Span::styled(title, style)));
}
self
}
/// Aligns all elements that don't have an alignment
/// # Example
/// This example aligns all titles in the center except "right" title
/// ```
/// # use ratatui::widgets::{Block, block::title::Title};
/// # use ratatui::layout::Alignment;
/// Block::default()
/// // This title won't be aligned in the center
/// .title(Title::from("right").alignment(Alignment::Right))
/// .title("foo")
/// .title("bar")
/// .title_alignment(Alignment::Center);
/// ```
pub const fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
self.titles_alignment = alignment;
pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
self.title_alignment = alignment;
self
}
#[deprecated(since = "0.22.0", note = "You should use a `title_position` instead.")]
/// This method just calls `title_position` with Position::Bottom
pub fn title_on_bottom(self) -> Block<'a> {
self.title_position(Position::Bottom)
}
/// Positions all titles that don't have a position
/// # Example
/// This example position all titles on the bottom except "top" title
/// ```
/// # use ratatui::widgets::{Block, BorderType, Borders, block::title::{Position, Title}};
/// Block::default()
/// // This title won't be aligned in the center
/// .title(Title::from("top").position(Position::Top))
/// .title("foo")
/// .title("bar")
/// .title_position(Position::Bottom);
/// ```
pub const fn title_position(mut self, position: Position) -> Block<'a> {
self.titles_position = position;
self
}
pub const fn border_style(mut self, style: Style) -> Block<'a> {
pub fn border_style(mut self, style: Style) -> Block<'a> {
self.border_style = style;
self
}
pub const fn style(mut self, style: Style) -> Block<'a> {
pub fn style(mut self, style: Style) -> Block<'a> {
self.style = style;
self
}
pub const fn borders(mut self, flag: Borders) -> Block<'a> {
pub fn borders(mut self, flag: Borders) -> Block<'a> {
self.borders = flag;
self
}
pub const fn border_type(mut self, border_type: BorderType) -> Block<'a> {
pub fn border_type(mut self, border_type: BorderType) -> Block<'a> {
self.border_type = border_type;
self
}
/// Compute the inner area of a block based on its border visibility rules.
///
/// # Examples
///
/// ```
/// // Draw a block nested within another block
/// use ratatui::{backend::TestBackend, buffer::Buffer, terminal::Terminal, widgets::{Block, Borders}};
/// let backend = TestBackend::new(15, 5);
/// let mut terminal = Terminal::new(backend).unwrap();
/// let outer_block = Block::default()
/// .title("Outer Block")
/// .borders(Borders::ALL);
/// let inner_block = Block::default()
/// .title("Inner Block")
/// .borders(Borders::ALL);
/// terminal.draw(|f| {
/// let inner_area = outer_block.inner(f.size());
/// f.render_widget(outer_block, f.size());
/// f.render_widget(inner_block, inner_area);
/// });
/// let expected = Buffer::with_lines(vec![
/// "┌Outer Block──┐",
/// "│┌Inner Block┐│",
/// "││ ││",
/// "│└───────────┘│",
/// "└─────────────┘",
/// ]);
/// terminal.backend().assert_buffer(&expected);
/// ```
pub fn inner(&self, area: Rect) -> Rect {
let mut inner = area;
if self.borders.intersects(Borders::LEFT) {
inner.x = inner.x.saturating_add(1).min(inner.right());
inner.width = inner.width.saturating_sub(1);
}
if self.borders.intersects(Borders::TOP) || !self.titles.is_empty() {
if self.borders.intersects(Borders::TOP) || self.title.is_some() {
inner.y = inner.y.saturating_add(1).min(inner.bottom());
inner.height = inner.height.saturating_sub(1);
}
@@ -301,26 +135,15 @@ impl<'a> Block<'a> {
if self.borders.intersects(Borders::BOTTOM) {
inner.height = inner.height.saturating_sub(1);
}
inner.x = inner.x.saturating_add(self.padding.left);
inner.y = inner.y.saturating_add(self.padding.top);
inner.width = inner
.width
.saturating_sub(self.padding.left + self.padding.right);
inner.height = inner
.height
.saturating_sub(self.padding.top + self.padding.bottom);
inner
}
}
pub const fn padding(mut self, padding: Padding) -> Block<'a> {
self.padding = padding;
self
}
fn render_borders(&self, area: Rect, buf: &mut Buffer) {
impl<'a> Widget for Block<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.area() == 0 {
return;
}
buf.set_style(area, self.style);
let symbols = BorderType::line_symbols(self.border_type);
@@ -377,143 +200,47 @@ impl<'a> Block<'a> {
.set_symbol(symbols.top_left)
.set_style(self.border_style);
}
}
/* Titles Rendering */
fn get_title_y(&self, position: Position, area: Rect) -> u16 {
match position {
Position::Bottom => area.bottom() - 1,
Position::Top => area.top(),
// Title
if let Some(title) = self.title {
let left_border_dx = if self.borders.intersects(Borders::LEFT) {
1
} else {
0
};
let right_border_dx = if self.borders.intersects(Borders::RIGHT) {
1
} else {
0
};
let title_area_width = area
.width
.saturating_sub(left_border_dx)
.saturating_sub(right_border_dx);
let title_dx = match self.title_alignment {
Alignment::Left => left_border_dx,
Alignment::Center => area.width.saturating_sub(title.width() as u16) / 2,
Alignment::Right => area
.width
.saturating_sub(title.width() as u16)
.saturating_sub(right_border_dx),
};
let title_x = area.left() + title_dx;
let title_y = area.top();
buf.set_spans(title_x, title_y, &title, title_area_width);
}
}
fn title_filter(&self, title: &Title, alignment: Alignment, position: Position) -> bool {
title.alignment.unwrap_or(self.titles_alignment) == alignment
&& title.position.unwrap_or(self.titles_position) == position
}
fn calculate_title_area_offsets(&self, area: Rect) -> (u16, u16, u16) {
let left_border_dx = u16::from(self.borders.intersects(Borders::LEFT));
let right_border_dx = u16::from(self.borders.intersects(Borders::RIGHT));
let title_area_width = area
.width
.saturating_sub(left_border_dx)
.saturating_sub(right_border_dx);
(left_border_dx, right_border_dx, title_area_width)
}
fn render_left_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
let (left_border_dx, _, title_area_width) = self.calculate_title_area_offsets(area);
let mut current_offset = left_border_dx;
self.titles
.iter()
.filter(|title| self.title_filter(title, Alignment::Left, position))
.for_each(|title| {
let title_x = current_offset;
current_offset += title.content.width() as u16 + 1;
buf.set_line(
title_x + area.left(),
self.get_title_y(position, area),
&title.content,
title_area_width,
);
});
}
fn render_center_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
let (_, _, title_area_width) = self.calculate_title_area_offsets(area);
let titles = self
.titles
.iter()
.filter(|title| self.title_filter(title, Alignment::Center, position));
let titles_sum = titles
.clone()
.fold(-1, |acc, f| acc + f.content.width() as i16 + 1); // First element isn't spaced
let mut current_offset = area.width.saturating_sub(titles_sum as u16) / 2;
titles.for_each(|title| {
let title_x = current_offset;
current_offset += title.content.width() as u16 + 1;
buf.set_line(
title_x + area.left(),
self.get_title_y(position, area),
&title.content,
title_area_width,
);
});
}
fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
let (_, right_border_dx, title_area_width) = self.calculate_title_area_offsets(area);
let mut current_offset = right_border_dx;
self.titles
.iter()
.filter(|title| self.title_filter(title, Alignment::Right, position))
.rev() // so that the titles appear in the order they have been set
.for_each(|title| {
current_offset += title.content.width() as u16 + 1;
let title_x = current_offset - 1; // First element isn't spaced
buf.set_line(
area.width.saturating_sub(title_x) + area.left(),
self.get_title_y(position, area),
&title.content,
title_area_width,
);
});
}
fn render_title_position(&self, position: Position, area: Rect, buf: &mut Buffer) {
// Note: the order in which these functions are called define the overlapping behavior
self.render_right_titles(position, area, buf);
self.render_center_titles(position, area, buf);
self.render_left_titles(position, area, buf);
}
fn render_titles(&self, area: Rect, buf: &mut Buffer) {
self.render_title_position(Position::Top, area, buf);
self.render_title_position(Position::Bottom, area, buf);
}
}
impl<'a> Widget for Block<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.area() == 0 {
return;
}
self.render_borders(area, buf);
self.render_titles(area, buf);
}
}
impl<'a> Styled for Block<'a> {
type Item = Block<'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::{
assert_buffer_eq,
layout::Rect,
style::{Color, Modifier, Stylize},
};
use crate::layout::Rect;
#[test]
fn inner_takes_into_account_the_borders() {
@@ -810,7 +537,8 @@ mod tests {
);
assert_eq!(
Block::default()
.title(Title::from("Test").alignment(Alignment::Center))
.title("Test")
.title_alignment(Alignment::Center)
.inner(Rect {
x: 0,
y: 0,
@@ -826,7 +554,8 @@ mod tests {
);
assert_eq!(
Block::default()
.title(Title::from("Test").alignment(Alignment::Right))
.title("Test")
.title_alignment(Alignment::Right)
.inner(Rect {
x: 0,
y: 0,
@@ -841,80 +570,4 @@ mod tests {
},
);
}
#[test]
fn border_type_can_be_const() {
const _PLAIN: line::Set = BorderType::line_symbols(BorderType::Plain);
}
#[test]
fn padding_can_be_const() {
const PADDING: Padding = Padding::new(1, 1, 1, 1);
const UNI_PADDING: Padding = Padding::uniform(1);
assert_eq!(PADDING, UNI_PADDING);
const _NO_PADDING: Padding = Padding::zero();
const _HORIZONTAL: Padding = Padding::horizontal(1);
const _VERTICAL: Padding = Padding::vertical(1);
}
#[test]
fn block_can_be_const() {
const _DEFAULT_STYLE: Style = Style::new();
const _DEFAULT_PADDING: Padding = Padding::uniform(1);
const _DEFAULT_BLOCK: Block = Block::new()
.title_style(_DEFAULT_STYLE)
.title_alignment(Alignment::Left)
.title_position(Position::Top)
.borders(Borders::ALL)
.border_style(_DEFAULT_STYLE)
.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

@@ -1,248 +0,0 @@
//! A simple calendar widget. `(feature: widget-calendar)`
//!
//!
//!
//! The [`Monthly`] widget will display a calendar for the monh provided in `display_date`. Days are
//! styled using the default style unless:
//! * `show_surrounding` is set, then days not in the `display_date` month will use that style.
//! * a style is returned by the [`DateStyler`] for the day
//!
//! [`Monthly`] has several controls for what should be displayed
use std::collections::HashMap;
use time::{Date, Duration, OffsetDateTime};
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
text::Span,
widgets::{Block, Widget},
};
/// Display a month calendar for the month containing `display_date`
#[derive(Debug, Clone)]
pub struct Monthly<'a, S: DateStyler> {
display_date: Date,
events: S,
show_surrounding: Option<Style>,
show_weekday: Option<Style>,
show_month: Option<Style>,
default_style: Style,
block: Option<Block<'a>>,
}
impl<'a, S: DateStyler> Monthly<'a, S> {
/// Construct a calendar for the `display_date` and highlight the `events`
pub fn new(display_date: Date, events: S) -> Self {
Self {
display_date,
events,
show_surrounding: None,
show_weekday: None,
show_month: None,
default_style: Style::default(),
block: None,
}
}
/// Fill the calendar slots for days not in the current month also, this causes each line to be
/// completely filled. If there is an event style for a date, this style will be patched with
/// the event's style
pub fn show_surrounding(mut self, style: Style) -> Self {
self.show_surrounding = Some(style);
self
}
/// Display a header containing weekday abbreviations
pub fn show_weekdays_header(mut self, style: Style) -> Self {
self.show_weekday = Some(style);
self
}
/// Display a header containing the month and year
pub fn show_month_header(mut self, style: Style) -> Self {
self.show_month = Some(style);
self
}
/// How to render otherwise unstyled dates
pub fn default_style(mut self, s: Style) -> Self {
self.default_style = s;
self
}
/// Render the calendar within a [Block](crate::widgets::Block)
pub fn block(mut self, b: Block<'a>) -> Self {
self.block = Some(b);
self
}
/// Return a style with only the background from the default style
fn default_bg(&self) -> Style {
match self.default_style.bg {
None => Style::default(),
Some(c) => Style::default().bg(c),
}
}
/// All logic to style a date goes here.
fn format_date(&self, date: Date) -> Span {
if date.month() != self.display_date.month() {
match self.show_surrounding {
None => Span::styled(" ", self.default_bg()),
Some(s) => {
let style = self
.default_style
.patch(s)
.patch(self.events.get_style(date));
Span::styled(format!("{:2?}", date.day()), style)
}
}
} else {
Span::styled(
format!("{:2?}", date.day()),
self.default_style.patch(self.events.get_style(date)),
)
}
}
}
impl<'a, S: DateStyler> Widget for Monthly<'a, S> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
// Block is used for borders and such
// Draw that first, and use the blank area inside the block for our own purposes
let mut area = match self.block.take() {
None => area,
Some(b) => {
let inner = b.inner(area);
b.render(area, buf);
inner
}
};
// Draw the month name and year
if let Some(style) = self.show_month {
let line = Span::styled(
format!("{} {}", self.display_date.month(), self.display_date.year()),
style,
);
// cal is 21 cells wide, so hard code the 11
let x_off = 11_u16.saturating_sub(line.width() as u16 / 2);
buf.set_line(area.x + x_off, area.y, &line.into(), area.width);
area.y += 1
}
// Draw days of week
if let Some(style) = self.show_weekday {
let days = String::from(" Su Mo Tu We Th Fr Sa");
buf.set_string(area.x, area.y, days, style);
area.y += 1;
}
// Set the start of the calendar to the Sunday before the 1st (or the sunday of the first)
let first_of_month = self.display_date.replace_day(1).unwrap();
let offset = Duration::days(first_of_month.weekday().number_days_from_sunday().into());
let mut curr_day = first_of_month - offset;
// go through all the weeks containing a day in the target month.
while curr_day.month() as u8 != self.display_date.month().next() as u8 {
let mut spans = Vec::with_capacity(14);
for i in 0..7 {
// Draw the gutter. Do it here so we can avoid worrying about
// styling the ' ' in the format_date method
if i == 0 {
spans.push(Span::styled(" ", Style::default()));
} else {
spans.push(Span::styled(" ", self.default_bg()));
}
spans.push(self.format_date(curr_day));
curr_day += Duration::DAY;
}
buf.set_line(area.x, area.y, &spans.into(), area.width);
area.y += 1;
}
}
}
/// Provides a method for styling a given date. [Monthly] is generic on this trait, so any type
/// that implements this trait can be used.
pub trait DateStyler {
/// Given a date, return a style for that date
fn get_style(&self, date: Date) -> Style;
}
/// A simple `DateStyler` based on a [`HashMap`]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct CalendarEventStore(pub HashMap<Date, Style>);
impl CalendarEventStore {
/// Construct a store that has the current date styled.
pub fn today(style: Style) -> Self {
let mut res = Self::default();
res.add(OffsetDateTime::now_local().unwrap().date(), style);
res
}
/// Add a date and style to the store
pub fn add(&mut self, date: Date, style: Style) {
// to simplify style nonsense, last write wins
let _ = self.0.insert(date, style);
}
/// Helper for trait impls
fn lookup_style(&self, date: Date) -> Style {
self.0.get(&date).copied().unwrap_or_default()
}
}
impl DateStyler for CalendarEventStore {
fn get_style(&self, date: Date) -> Style {
self.lookup_style(date)
}
}
impl DateStyler for &CalendarEventStore {
fn get_style(&self, date: Date) -> Style {
self.lookup_style(date)
}
}
impl Default for CalendarEventStore {
fn default() -> Self {
Self(HashMap::with_capacity(4))
}
}
#[cfg(test)]
mod tests {
use time::Month;
use super::*;
use crate::style::Color;
#[test]
fn event_store() {
let a = (
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
Style::default(),
);
let b = (
Date::from_calendar_date(2023, Month::January, 2).unwrap(),
Style::default().bg(Color::Red).fg(Color::Blue),
);
let mut s = CalendarEventStore::default();
s.add(b.0, b.1);
assert_eq!(
s.get_style(a.0),
a.1,
"Date not added to the styler should look up as Style::default()"
);
assert_eq!(
s.get_style(b.0),
b.1,
"Date added to styler should return the provided style"
);
}
}

View File

@@ -1,66 +0,0 @@
use crate::{
style::Color,
widgets::canvas::{Painter, Shape},
};
/// Shape to draw a circle with a given center and radius and with the given color
#[derive(Debug, Default, Clone)]
pub struct Circle {
pub x: f64,
pub y: f64,
pub radius: f64,
pub color: Color,
}
impl Shape for Circle {
fn draw(&self, painter: &mut Painter<'_, '_>) {
for angle in 0..360 {
let radians = f64::from(angle).to_radians();
let circle_x = self.radius.mul_add(radians.cos(), self.x);
let circle_y = self.radius.mul_add(radians.sin(), self.y);
if let Some((x, y)) = painter.get_point(circle_x, circle_y) {
painter.paint(x, y, self.color);
}
}
}
}
#[cfg(test)]
mod tests {
use crate::{
buffer::Buffer,
layout::Rect,
style::Color,
symbols::Marker,
widgets::{
canvas::{Canvas, Circle},
Widget,
},
};
#[test]
fn test_it_draws_a_circle() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
let canvas = Canvas::default()
.paint(|ctx| {
ctx.draw(&Circle {
x: 5.0,
y: 2.0,
radius: 5.0,
color: Color::Reset,
});
})
.marker(Marker::Braille)
.x_bounds([-10.0, 10.0])
.y_bounds([-10.0, 10.0]);
canvas.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
" ⢀⣠⢤⣀ ",
" ⢰⠋ ⠈⣇",
" ⠘⣆⡀ ⣠⠇",
" ⠉⠉⠁ ",
" ",
]);
assert_eq!(buffer, expected);
}
}

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, Default, Clone)]
#[derive(Debug, Clone)]
pub struct Line {
pub x1: f64,
pub y1: f64,
@@ -15,11 +15,13 @@ pub struct Line {
impl Shape for Line {
fn draw(&self, painter: &mut Painter) {
let Some((x1, y1)) = painter.get_point(self.x1, self.y1) else {
return;
let (x1, y1) = match painter.get_point(self.x1, self.y1) {
Some(c) => c,
None => return,
};
let Some((x2, y2)) = painter.get_point(self.x2, self.y2) else {
return;
let (x2, y2) = match painter.get_point(self.x2, self.y2) {
Some(c) => c,
None => return,
};
let (dx, x_range) = if x2 >= x1 {
(x2 - x1, x1..=x2)

View File

@@ -6,9 +6,8 @@ use crate::{
},
};
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Clone, Copy)]
pub enum MapResolution {
#[default]
Low,
High,
}
@@ -23,12 +22,21 @@ impl MapResolution {
}
/// Shape to draw a world map with the given resolution and color
#[derive(Debug, Default, Clone)]
#[derive(Debug, 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

@@ -1,27 +1,23 @@
mod circle;
mod line;
mod map;
mod points;
mod rectangle;
mod world;
use std::fmt::Debug;
pub use self::line::Line;
pub use self::map::{Map, MapResolution};
pub use self::points::Points;
pub use self::rectangle::Rectangle;
pub use self::{
circle::Circle,
line::Line,
map::{Map, MapResolution},
points::Points,
rectangle::Rectangle,
};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols,
text::Line as TextLine,
text::Spans,
widgets::{Block, Widget},
};
use std::fmt::Debug;
/// Interface for all shapes that may be drawn on a Canvas widget.
pub trait Shape {
@@ -29,14 +25,14 @@ pub trait Shape {
}
/// Label to draw some text on the canvas
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone)]
pub struct Label<'a> {
x: f64,
y: f64,
line: TextLine<'a>,
spans: Spans<'a>,
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone)]
struct Layer {
string: String,
colors: Vec<Color>,
@@ -51,7 +47,7 @@ trait Grid: Debug {
fn reset(&mut self);
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone)]
struct BrailleGrid {
width: u16,
height: u16,
@@ -114,7 +110,7 @@ impl Grid for BrailleGrid {
}
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone)]
struct CharGrid {
width: u16,
height: u16,
@@ -187,7 +183,7 @@ impl<'a, 'b> Painter<'a, 'b> {
///
/// # Examples:
/// ```
/// use ratatui::{symbols, widgets::canvas::{Painter, Context}};
/// use tui::{symbols, widgets::canvas::{Painter, Context}};
///
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
@@ -224,7 +220,7 @@ impl<'a, 'b> Painter<'a, 'b> {
///
/// # Examples:
/// ```
/// use ratatui::{style::Color, symbols, widgets::canvas::{Painter, Context}};
/// use tui::{style::Color, symbols, widgets::canvas::{Painter, Context}};
///
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
@@ -264,13 +260,9 @@ impl<'a> Context<'a> {
y_bounds: [f64; 2],
marker: symbols::Marker,
) -> Context<'a> {
let dot = symbols::DOT.chars().next().unwrap();
let block = symbols::block::FULL.chars().next().unwrap();
let bar = symbols::bar::HALF.chars().next().unwrap();
let grid: Box<dyn Grid> = match marker {
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
symbols::Marker::Block => Box::new(CharGrid::new(width, height, block)),
symbols::Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, '•')),
symbols::Marker::Block => Box::new(CharGrid::new(width, height, '▄')),
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
};
Context {
@@ -301,21 +293,21 @@ impl<'a> Context<'a> {
}
/// Print a string on the canvas at the given position
pub fn print<T>(&mut self, x: f64, y: f64, line: T)
pub fn print<T>(&mut self, x: f64, y: f64, spans: T)
where
T: Into<TextLine<'a>>,
T: Into<Spans<'a>>,
{
self.labels.push(Label {
x,
y,
line: line.into(),
spans: spans.into(),
});
}
/// Push the last layer if necessary
fn finish(&mut self) {
if self.dirty {
self.layer();
self.layer()
}
}
}
@@ -325,10 +317,10 @@ impl<'a> Context<'a> {
/// # Examples
///
/// ```
/// # use ratatui::widgets::{Block, Borders};
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
/// # use ratatui::style::Color;
/// # use tui::widgets::{Block, Borders};
/// # use tui::layout::Rect;
/// # use tui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
/// # use tui::style::Color;
/// Canvas::default()
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
/// .x_bounds([-180.0, 180.0])
@@ -355,7 +347,6 @@ impl<'a> Context<'a> {
/// });
/// });
/// ```
#[derive(Debug, Clone)]
pub struct Canvas<'a, F>
where
F: Fn(&mut Context),
@@ -393,18 +384,11 @@ where
self
}
/// Define the viewport of the canvas.
/// If you were to "zoom" to a certain part of the world you may want to choose different
/// bounds.
pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
self.x_bounds = bounds;
self
}
/// Define the viewport of the canvas.
///
/// If you were to "zoom" to a certain part of the world you may want to choose different
/// bounds.
pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
self.y_bounds = bounds;
self
@@ -428,8 +412,8 @@ where
/// # Examples
///
/// ```
/// # use ratatui::widgets::canvas::Canvas;
/// # use ratatui::symbols;
/// # use tui::widgets::canvas::Canvas;
/// # use tui::symbols;
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
///
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
@@ -460,8 +444,9 @@ where
let width = canvas_area.width as usize;
let Some(ref painter) = self.painter else {
return;
let painter = match self.painter {
Some(ref p) => p,
None => return,
};
// Create a blank context that match the size of the canvas
@@ -476,7 +461,7 @@ where
painter(&mut ctx);
ctx.finish();
// Retrieve painted points for each layer
// Retreive painted points for each layer
for layer in ctx.layers {
for (i, (ch, color)) in layer
.string
@@ -512,108 +497,7 @@ where
{
let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
buf.set_line(x, y, &label.line, canvas_area.right() - x);
buf.set_spans(x, y, &label.spans, canvas_area.right() - x);
}
}
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::*;
use crate::{buffer::Cell, symbols::Marker};
// helper to test the canvas checks that drawing a vertical and horizontal line
// results in the expected output
fn test_marker(marker: Marker, expected: &str) {
let area = Rect::new(0, 0, 5, 5);
let mut cell = Cell::default();
cell.set_char('x');
let mut buf = Buffer::filled(area, &cell);
let horizontal_line = Line {
x1: 0.0,
y1: 0.0,
x2: 10.0,
y2: 0.0,
color: Color::Reset,
};
let vertical_line = Line {
x1: 0.0,
y1: 0.0,
x2: 0.0,
y2: 10.0,
color: Color::Reset,
};
Canvas::default()
.marker(marker)
.paint(|ctx| {
ctx.draw(&vertical_line);
ctx.draw(&horizontal_line);
})
.x_bounds([0.0, 10.0])
.y_bounds([0.0, 10.0])
.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(expected.lines().collect()));
}
#[test]
fn test_bar_marker() {
test_marker(
Marker::Bar,
indoc!(
"
▄xxxx
▄xxxx
▄xxxx
▄xxxx
▄▄▄▄▄"
),
);
}
#[test]
fn test_block_marker() {
test_marker(
Marker::Block,
indoc!(
"
█xxxx
█xxxx
█xxxx
█xxxx
█████"
),
);
}
#[test]
fn test_braille_marker() {
test_marker(
Marker::Braille,
indoc!(
"
⡇xxxx
⡇xxxx
⡇xxxx
⡇xxxx
⣇⣀⣀⣀⣀"
),
);
}
#[test]
fn test_dot_marker() {
test_marker(
Marker::Dot,
indoc!(
"
•xxxx
•xxxx
•xxxx
•xxxx
•••••"
),
);
}
}

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