Compare commits

..

3 Commits

Author SHA1 Message Date
Josh McKinney
72a912820a style: fix formatting
There are a couple of small formatting changes in the current nightly
2023-07-02 00:55:00 -07:00
Eyesonjune18
ec411bb1cd feat(text): expose graphemes on Line
- add `Line::styled()`
- add `Line::styled_graphemes()`
- add `StyledGrapheme::new()`
2023-07-01 23:43:30 -07:00
Eyesonjune18
47f8c815ce refactor(text): split text::* into separate files 2023-07-01 22:37:16 -07:00
298 changed files with 18545 additions and 57745 deletions

16
.cargo-husky/hooks/pre-push Executable file
View File

@@ -0,0 +1,16 @@
#!/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,17 +0,0 @@
# configuration for https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.rs]
indent_style = space
indent_size = 4
[*.yml]
indent_style = space
indent_size = 2

9
.github/CODEOWNERS vendored
View File

@@ -1,11 +1,8 @@
# See <https://help.github.com/articles/about-codeowners/>
# 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>
# https://git-scm.com/docs/gitignore#_pattern_format
# Maintainers
* @ratatui/maintainers
* @orhun @mindoodoo @sayanarijit @sophacles @joshka

View File

@@ -2,7 +2,7 @@
name: Bug report
about: Create an issue about a bug you encountered
title: ''
labels: 'Type: Bug'
labels: bug
assignees: ''
---
@@ -17,22 +17,26 @@ A detailed and complete issue is more likely to be processed quickly.
A clear and concise description of what the bug is.
-->
## To Reproduce
<!--
Try to reduce the issue to a simple code sample exhibiting the problem.
Ideally, fork the project and add a test or an example.
-->
## Expected behavior
<!--
A clear and concise description of what you expected to happen.
-->
## Screenshots
<!--
If applicable, add screenshots, gifs or videos to help explain your problem.
-->
## Environment
<!--
Add a description of the systems where you are observing the issue. For example:

View File

@@ -1,14 +1 @@
blank_issues_enabled: false
contact_links:
- name: Frequently Asked Questions
url: https://ratatui.rs/faq/
about: Check the website FAQ section to see if your question has already been answered
- name: Ratatui Forum
url: https://forum.ratatui.rs
about: Ask questions about ratatui on our Forum
- name: Discord Chat
url: https://discord.gg/pMCEU9hNEj
about: Ask questions about ratatui on Discord
- name: Matrix Chat
url: https://matrix.to/#/#ratatui:matrix.org
about: Ask questions about ratatui on Matrix

View File

@@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'Type: Enhancement'
labels: enhancement
assignees: ''
---

View File

@@ -1,18 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Maintain dependencies for Cargo
- package-ecosystem: "cargo"
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
# Maintain dependencies for GitHub Actions
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10

View File

@@ -1,25 +0,0 @@
name: Run Benchmarks
on:
push:
branches:
- main
jobs:
benchmark_base_branch:
name: Continuous Benchmarking with Bencher
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: bencherdev/bencher@main
- name: Track base branch benchmarks with Bencher
run: |
bencher run \
--project ratatui-org \
--token '${{ secrets.BENCHER_API_TOKEN }}' \
--branch main \
--testbed ubuntu-latest \
--adapter rust_criterion \
--err \
cargo bench

View File

@@ -1,25 +0,0 @@
name: Run and Cache Benchmarks
on:
pull_request:
types: [opened, reopened, edited, synchronize]
jobs:
benchmark_fork_pr_branch:
name: Run Fork PR Benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run Benchmarks
run: cargo bench > benchmark_results.txt
- name: Upload Benchmark Results
uses: actions/upload-artifact@v4
with:
name: benchmark_results.txt
path: ./benchmark_results.txt
- name: Upload GitHub Pull Request Event
uses: actions/upload-artifact@v4
with:
name: event.json
path: ${{ github.event_path }}

View File

@@ -1,75 +0,0 @@
name: Track Benchmarks with Bencher
on:
workflow_run:
workflows: [Run and Cache Benchmarks]
types: [completed]
permissions:
contents: read
pull-requests: write
jobs:
track_fork_pr_branch:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
env:
BENCHMARK_RESULTS: benchmark_results.txt
PR_EVENT: event.json
steps:
- name: Download Benchmark Results
uses: actions/github-script@v7
with:
script: |
async function downloadArtifact(artifactName) {
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == artifactName
})[0];
if (!matchArtifact) {
core.setFailed(`Failed to find artifact: ${artifactName}`);
}
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/${artifactName}.zip`, Buffer.from(download.data));
}
await downloadArtifact(process.env.BENCHMARK_RESULTS);
await downloadArtifact(process.env.PR_EVENT);
- name: Unzip Benchmark Results
run: |
unzip $BENCHMARK_RESULTS.zip
unzip $PR_EVENT.zip
- name: Export PR Event Data
uses: actions/github-script@v7
with:
script: |
let fs = require('fs');
let prEvent = JSON.parse(fs.readFileSync(process.env.PR_EVENT, {encoding: 'utf8'}));
core.exportVariable("PR_HEAD", `${prEvent.number}/merge`);
core.exportVariable("PR_BASE", prEvent.pull_request.base.ref);
core.exportVariable("PR_BASE_SHA", prEvent.pull_request.base.sha);
core.exportVariable("PR_NUMBER", prEvent.number);
- uses: bencherdev/bencher@main
- name: Track Benchmarks with Bencher
run: |
bencher run \
--project ratatui-org \
--token '${{ secrets.BENCHER_API_TOKEN }}' \
--branch '${{ env.PR_HEAD }}' \
--branch-start-point '${{ env.PR_BASE }}' \
--branch-start-point-hash '${{ env.PR_BASE_SHA }}' \
--testbed ubuntu-latest \
--adapter rust_criterion \
--err \
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
--ci-number '${{ env.PR_NUMBER }}' \
--file "$BENCHMARK_RESULTS"

View File

@@ -1,52 +0,0 @@
#!/bin/bash
# Exit on error. Append "|| true" if you expect an error.
set -o errexit
# Exit on error inside any functions or subshells.
set -o errtrace
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
set -o nounset
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
set -o pipefail
# Turn on traces, useful while debugging but commented out by default
# set -o xtrace
last_release="$(git tag --sort=committerdate | grep -P "v0+\.\d+\.\d+$" | tail -1)"
echo "🐭 Last release: ${last_release}"
# detect breaking changes
if [ -n "$(git log --oneline ${last_release}..HEAD | grep '!:')" ]; then
echo "🐭 Breaking changes detected since ${last_release}"
git log --oneline ${last_release}..HEAD | grep '!:'
# increment the minor version
minor="${last_release##v0.}"
minor="${minor%.*}"
next_minor="$((minor + 1))"
next_release="v0.${next_minor}.0"
else
# increment the patch version
patch="${last_release##*.}"
next_patch="$((patch + 1))"
next_release="${last_release/%${patch}/${next_patch}}"
fi
echo "🐭 Next release: ${next_release}"
suffix="alpha"
last_tag="$(git tag --sort=committerdate | tail -1)"
if [[ "${last_tag}" = "${next_release}-${suffix}"* ]]; then
echo "🐭 Last alpha release: ${last_tag}"
# increment the alpha version
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
alpha="${last_tag##*-${suffix}.}"
next_alpha="$((alpha + 1))"
next_tag="${last_tag/%${alpha}/${next_alpha}}"
else
# increment the patch and start the alpha version from 0
# e.g. v0.22.0 -> v0.22.1-alpha.0
next_tag="${next_release}-${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 alpha release: ${next_tag}"

19
.github/workflows/cd.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Continuous Deployment
on:
push:
tags:
- "v*.*.*"
jobs:
publish:
name: Publish on crates.io
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Publish
uses: actions-rs/cargo@v1
with:
command: publish
args: --token ${{ secrets.CARGO_TOKEN }}

View File

@@ -1,86 +0,0 @@
name: Check Pull Requests
on:
pull_request_target:
types:
- opened
- edited
- synchronize
- labeled
- unlabeled
merge_group:
permissions:
pull-requests: write
jobs:
check-title:
runs-on: ubuntu-latest
steps:
- name: Check PR title
if: github.event_name == 'pull_request_target'
uses: amannn/action-semantic-pull-request@v5
id: check_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Add comment indicating we require pull request titles to follow conventional commits specification
- uses: marocchino/sticky-pull-request-comment@v2
if: always() && (steps.check_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Thank you for opening this pull request!
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
Details:
> ${{ steps.check_pr_title.outputs.error_message }}
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.check_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true
check-breaking-change-label:
runs-on: ubuntu-latest
env:
# use an environment variable to pass untrusted input to the script
# see https://securitylab.github.com/research/github-actions-untrusted-input/
PR_TITLE: ${{ github.event.pull_request.title }}
steps:
- name: Check breaking change label
id: check_breaking_change
run: |
pattern='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(\w+\))?!:'
# Check if pattern matches
if echo "${PR_TITLE}" | grep -qE "$pattern"; then
echo "breaking_change=true" >> $GITHUB_OUTPUT
else
echo "breaking_change=false" >> $GITHUB_OUTPUT
fi
- name: Add label
if: steps.check_breaking_change.outputs.breaking_change == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['Type: Breaking Change']
})
do-not-merge:
if: ${{ contains(github.event.*.labels.*.name, 'do not merge') }}
name: Prevent Merging
runs-on: ubuntu-latest
steps:
- name: Check for label
run: |
echo "Pull request is labeled as 'do not merge'"
echo "This workflow fails so that the pull request cannot be merged"
exit 1

View File

@@ -1,16 +0,0 @@
name: Check Semver
on:
pull_request:
branches:
- main
jobs:
check-semver:
name: Check semver
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Check semver
uses: obi1kenobi/cargo-semver-checks-action@v2

View File

@@ -1,160 +1,121 @@
name: Continuous Integration
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
push:
branches:
- feat-wrapping
- main
pull_request:
branches:
- main
- feat-wrapping
merge_group:
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
# lint, clippy and coverage 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.
name: CI
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- run: cargo +nightly fmt --all --check
typos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@master
dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
cargo-machete:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: bnjbvr/cargo-machete@v0.6.2
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make clippy
markdownlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v17
with:
globs: |
'**/*.md'
'!target'
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools
- uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov,cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make coverage
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
check:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
toolchain: ["1.74.0", "stable"]
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.65.0", "stable" ]
runs-on: ${{ matrix.os }}
needs: lint
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
- uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make check
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: "Check"
run: cargo make check
env:
RUST_BACKTRACE: full
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
lint-docs:
clippy:
name: Clippy
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -Dwarnings
needs: lint
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/install@cargo-docs-rs
- uses: Swatinem/rust-cache@v2
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account
# the package.metadata.docs.rs configured in Cargo.toml.
# https://github.com/dtolnay/cargo-docs-rs
- run: cargo +nightly docs-rs
test-doc:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make test-doc
- uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: "Clippy"
run: cargo make clippy
env:
RUST_BACKTRACE: full
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
toolchain: ["1.74.0", "stable"]
backend: [crossterm, termion, termwiz]
exclude:
# termion is not supported on windows
- os: windows-latest
backend: termion
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.65.0", "stable" ]
runs-on: ${{ matrix.os }}
needs: lint
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
- uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- uses: taiki-e/install-action@v2
with:
tool: cargo-make,nextest
- uses: Swatinem/rust-cache@v2
- run: cargo make test-backend ${{ matrix.backend }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: "Test"
run: cargo make test
env:
RUST_BACKTRACE: full
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
lint:
runs-on: ubuntu-latest
steps:
- 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
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: "Formatting"
run: cargo fmt --all --check
coverage:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools
- name: cargo install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: cargo llvm-cov
run: cargo llvm-cov --all-features --lcov --output-path lcov.info
env:
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
with:
fail_ci_if_error: true

View File

@@ -1,48 +0,0 @@
name: Release alpha version
on:
workflow_dispatch:
schedule:
# At 00:00 on Saturday
# https://crontab.guru/#0_0_*_*_6
- cron: "0 0 * * 6"
defaults:
run:
shell: bash
jobs:
publish-alpha:
name: Create an alpha release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Calculate the next release
run: .github/workflows/calculate-alpha-release.bash
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Publish
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
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

View File

@@ -1,45 +0,0 @@
name: Release stable version
on:
push:
tags:
- "v*.*.*"
jobs:
publish-stable:
name: Create an stable release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --latest --strip header
env:
OUTPUT: BODY.md
- name: Publish on GitHub
uses: ncipollo/release-action@v1
with:
prerelease: false
bodyFile: BODY.md
publish-crate:
name: Publish crate
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Publish
run: cargo publish --token ${{ secrets.CARGO_TOKEN }}

View File

@@ -1,15 +1,9 @@
# configuration for https://github.com/DavidAnson/markdownlint
first-line-heading: false
no-inline-html:
allowed_elements:
- img
- details
- summary
- div
- br
line-length:
line_length: 100
# to support repeated headers in the changelog
no-duplicate-heading: false

View File

@@ -1,699 +0,0 @@
# Breaking Changes
This document contains a list of breaking changes in each version and some notes to help migrate
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
GitHub with a [breaking change] label.
[breaking change]: (https://github.com/ratatui/ratatui/issues?q=label%3A%22breaking+change%22)
## Summary
This is a quick summary of the sections below:
- [v0.28.0](#v0280)
`Backend::size` returns `Size` instead of `Rect`
- `Backend` trait migrates to `get/set_cursor_position`
- Ratatui now requires Crossterm 0.28.0
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
- `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize`
- `ratatui::terminal` module is now private
- `ToText` no longer has a lifetime
- `Frame::size` is deprecated and renamed to `Frame::area`
- [v0.27.0](#v0270)
- List no clamps the selected index to list
- Prelude items added / removed
- 'termion' updated to 4.0
- `Rect::inner` takes `Margin` directly instead of reference
- `Buffer::filled` takes `Cell` directly instead of reference
- `Stylize::bg()` now accepts `Into<Color>`
- Removed deprecated `List::start_corner`
- `LineGauge::gauge_style` is deprecated
- [v0.26.0](#v0260)
- `Flex::Start` is the new default flex mode for `Layout`
- `patch_style` & `reset_style` now consume and return `Self`
- Removed deprecated `Block::title_on_bottom`
- `Line` now has an extra `style` field which applies the style to the entire line
- `Block` style methods cannot be created in a const context
- `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>`
- `Table::new` now accepts `IntoIterator<Item: Into<Row<'a>>>`.
- [v0.25.0](#v0250)
- Removed `Axis::title_style` and `Buffer::set_background`
- `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>`
- `Table::new()` now requires specifying the widths
- `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>`
- Layout::new() now accepts direction and constraint parameters
- The default `Tabs::highlight_style` is now `Style::new().reversed()`
- [v0.24.0](#v0240)
- MSRV is now 1.70.0
- `ScrollbarState`: `position`, `content_length`, and `viewport_content_length` are now `usize`
- `BorderType`: `line_symbols` is now `border_symbols` and returns `symbols::border::set`
- `Frame<'a, B: Backend>` is now `Frame<'a>`
- `Stylize` shorthands for `String` now consume the value and return `Span<'static>`
- `Spans` is removed
- [v0.23.0](#v0230)
- `Scrollbar`: `track_symbol` now takes `Option<&str>`
- `Scrollbar`: symbols moved to `symbols` module
- MSRV is now 1.67.0
- [v0.22.0](#v0220)
- `serde` representation of `Borders` and `Modifiers` has changed
- [v0.21.0](#v0210)
- MSRV is now 1.65.0
- `terminal::ViewPort` is now an enum
- `"".as_ref()` must be annotated to implement `Into<Text<'a>>`
- `Marker::Block` renders as a block char instead of a bar char
- [v0.20.0](#v0200)
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## v0.28.0
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
[#1254]: https://github.com/ratatui/ratatui/pull/1254
The `Backend::size` method returns a `Size` instead of a `Rect`.
There is no need for the position here as it was always 0,0.
### `Backend` trait migrates to `get/set_cursor_position` ([#1284])
[#1284]: https://github.com/ratatui/ratatui/pull/1284
If you just use the types implementing the `Backend` trait, you will see deprecation hints but
nothing is a breaking change for you.
If you implement the Backend trait yourself, you need to update the implementation and add the
`get/set_cursor_position` method. You can remove the `get/set_cursor` methods as they are deprecated
and a default implementation for them exists.
### Ratatui now requires Crossterm 0.28.0 ([#1278])
[#1278]: https://github.com/ratatui/ratatui/pull/1278
Crossterm is updated to version 0.28.0, which is a semver incompatible version with the previous
version (0.27.0). Ratatui re-exports the version of crossterm that it is compatible with under
`ratatui::crossterm`, which can be used to avoid incompatible versions in your dependency list.
### `Axis::labels()` now accepts `IntoIterator<Into<Line>>` ([#1273] and [#1283])
[#1273]: https://github.com/ratatui/ratatui/pull/1173
[#1283]: https://github.com/ratatui/ratatui/pull/1283
Previously Axis::labels accepted `Vec<Span>`. Any code that uses conversion methods that infer the
type will need to be rewritten as the compiler cannot infer the correct type.
```diff
- Axis::default().labels(vec!["a".into(), "b".into()])
+ Axis::default().labels(["a", "b"])
```
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1245])
[#1245]: https://github.com/ratatui/ratatui/pull/1245
```diff
- let is_initialized = Layout::init_cache(100);
+ Layout::init_cache(NonZeroUsize::new(100).unwrap());
```
### `ratatui::terminal` module is now private ([#1160])
[#1160]: https://github.com/ratatui/ratatui/pull/1160
The `terminal` module is now private and can not be used directly. The types under this module are
exported from the root of the crate. This reduces clashes with other modules in the backends that
are also named terminal, and confusion about module exports for newer Rust users.
```diff
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
```
### `ToText` no longer has a lifetime ([#1234])
[#1234]: https://github.com/ratatui/ratatui/pull/1234
This change simplifies the trait and makes it easier to implement.
### `Frame::size` is deprecated and renamed to `Frame::area`
[#1293]: https://github.com/ratatui/ratatui/pull/1293
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
## [v0.27.0](https://github.com/ratatui/ratatui/releases/tag/v0.27.0)
### List no clamps the selected index to list ([#1159])
[#1149]: https://github.com/ratatui/ratatui/pull/1149
The `List` widget now clamps the selected index to the bounds of the list when navigating with
`first`, `last`, `previous`, and `next`, as well as when setting the index directly with `select`.
Previously selecting an index past the end of the list would show treat the list as having a
selection which was not visible. Now the last item in the list will be selected instead.
### Prelude items added / removed ([#1149])
The following items have been removed from the prelude:
- `style::Styled` - this trait is useful for widgets that want to
support the Stylize trait, but it adds complexity as widgets have two
`style` methods and a `set_style` method.
- `symbols::Marker` - this item is used by code that needs to draw to
the `Canvas` widget, but it's not a common item that would be used by
most users of the library.
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
are rarely used by code that needs to interact with the terminal, and
they're generally only ever used once in any app.
The following items have been added to the prelude:
- `layout::{Position, Size}` - these items are used by code that needs
to interact with the layout system. These are newer items that were
added in the last few releases, which should be used more liberally.
This may cause conflicts for types defined elsewhere with a similar
name.
To update your app:
```diff
// if your app uses Styled::style() or Styled::set_style():
-use ratatui::prelude::*;
+use ratatui::{prelude::*, style::Styled};
// if your app uses symbols::Marker:
-use ratatui::prelude::*;
+use ratatui::{prelude::*, symbols::Marker}
// if your app uses terminal::{CompletedFrame, TerminalOptions, Viewport}
-use ratatui::prelude::*;
+use ratatui::{prelude::*, terminal::{CompletedFrame, TerminalOptions, Viewport}};
// to disambiguate existing types named Position or Size:
- use some_crate::{Position, Size};
- let size: Size = ...;
- let position: Position = ...;
+ let size: some_crate::Size = ...;
+ let position: some_crate::Position = ...;
```
### Termion is updated to 4.0 [#1106]
Changelog: <https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
A change is only necessary if you were matching on all variants of the `MouseEvent` enum without a
wildcard. In this case, you need to either handle the two new variants, `MouseLeft` and
`MouseRight`, or add a wildcard.
[#1106]: https://github.com/ratatui/ratatui/pull/1106
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
[#1008]: https://github.com/ratatui/ratatui/pull/1008
`Margin` needs to be passed without reference now.
```diff
-let area = area.inner(&Margin {
+let area = area.inner(Margin {
vertical: 0,
horizontal: 2,
});
```
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
[#1148]: https://github.com/ratatui/ratatui/pull/1148
`Buffer::filled` moves the `Cell` instead of taking a reference.
```diff
-Buffer::filled(area, &Cell::new("X"));
+Buffer::filled(area, Cell::new("X"));
```
### `Stylize::bg()` now accepts `Into<Color>` ([#1103])
[#1103]: https://github.com/ratatui/ratatui/pull/1103
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
flexible types from calling scopes, though it can break some type inference in the calling scope.
### Remove deprecated `List::start_corner` and `layout::Corner` ([#759])
[#759]: https://github.com/ratatui/ratatui/pull/759
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
```diff
- list.start_corner(Corner::TopLeft);
- list.start_corner(Corner::TopRight);
// This is not an error, BottomRight rendered top to bottom previously
- list.start_corner(Corner::BottomRight);
// all becomes
+ list.direction(ListDirection::TopToBottom);
```
```diff
- list.start_corner(Corner::BottomLeft);
// becomes
+ list.direction(ListDirection::BottomToTop);
```
`layout::Corner` was removed entirely.
### `LineGauge::gauge_style` is deprecated ([#565])
[#565]: https://github.com/ratatui/ratatui/pull/1148
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
```diff
let gauge = LineGauge::default()
- .gauge_style(Style::default().fg(Color::Red).bg(Color::Blue)
+ .filled_style(Style::default().fg(Color::Green))
+ .unfilled_style(Style::default().fg(Color::White));
```
## [v0.26.0](https://github.com/ratatui/ratatui/releases/tag/v0.26.0)
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
[#881]: https://github.com/ratatui/ratatui/pull/881
Previously, constraints would stretch to fill all available space, violating constraints if
necessary.
With v0.26.0, `Flex` modes are introduced, and the default is `Flex::Start`, which will align
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
more easily.
With v0.26.0, users will most likely not need to change what constraints they use to create
existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Legacy`.
```diff
- let rects = Layout::horizontal([Length(1), Length(2)]).split(area);
// becomes
+ let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::Legacy).split(area);
```
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
[#774]: https://github.com/ratatui/ratatui/pull/774
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
can some break type inference in the calling scope for empty containers.
This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new()`), or by using
`Table::default()`.
```diff
- let table = Table::new(vec![], widths);
// becomes
+ let table = Table::default().widths(widths);
```
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
[#776]: https://github.com/ratatui/ratatui/pull/776
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
types from calling scopes, though it can break some type inference in the calling scope.
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
by removing the call to `.collect()`.
```diff
- let tabs = Tabs::new((0.3).map(|i| format!("{i}")).collect());
// becomes
+ let tabs = Tabs::new((0.3).map(|i| format!("{i}")));
```
### Table::default() now sets segment_size to None and column_spacing to ([#751])
[#751]: https://github.com/ratatui/ratatui/pull/751
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
field to `SegmentSize::None`. This will affect the rendering of a small amount of apps.
To use the previous default values, call `table.segment_size(Default::default())` and
`table.column_spacing(0)`.
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
[#754]: https://github.com/ratatui/ratatui/pull/754
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable
reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent
setters, these now take ownership of `Self` and return it.
The following example shows how to migrate for `Line`, but the same applies for `Text` and `Span`.
```diff
- let mut line = Line::from("foobar");
- line.patch_style(style);
// becomes
+ let line = Line::new("foobar").patch_style(style);
```
### Remove deprecated `Block::title_on_bottom` ([#757])
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
```diff
- block.title("foobar").title_on_bottom();
+ block.title(Title::from("foobar").position(Position::Bottom));
```
### `Block` style methods cannot be used in a const context ([#720])
[#720]: https://github.com/ratatui/ratatui/pull/720
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
longer can be called from a constant context.
### `Line` now has a `style` field that applies to the entire line ([#708])
[#708]: https://github.com/ratatui/ratatui/pull/708
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).
Each `Span` contained within the line will no longer have the style that is applied to the line in
the `Span::style` field.
```diff
let line = Line {
spans: vec!["".into()],
alignment: Alignment::Left,
+ ..Default::default()
};
// or
let line = Line::raw(vec!["".into()])
.alignment(Alignment::Left);
```
## [v0.25.0](https://github.com/ratatui/ratatui/releases/tag/v0.25.0)
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
[#691]: https://github.com/ratatui/ratatui/pull/691
These items were deprecated since 0.10.
- You should use styling capabilities of [`text::Line`] given as argument of [`Axis::title`]
instead of `Axis::title_style`
- You should use styling capabilities of [`Buffer::set_style`] instead of `Buffer::set_background`
[`text::Line`]: https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html
[`Axis::title`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.Axis.html#method.title
[`Buffer::set_style`]: https://docs.rs/ratatui/latest/ratatui/buffer/struct.Buffer.html#method.set_style
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
[#672]: https://github.com/ratatui/ratatui/pull/672
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
E.g.
```diff
- let list = List::new(vec![]);
// becomes
+ let list = List::default();
```
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
[#635]: https://github.com/ratatui/ratatui/pull/635
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
widget in the default configuration would not show any indication of the selected tab.
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
widget in the default configuration would not show any indication of the selected tab.
### `Table::new()` now requires specifying the widths of the columns ([#664])
[#664]: https://github.com/ratatui/ratatui/pull/664
Previously `Table`s could be constructed without `widths`. In almost all cases this is an error.
A new `widths` parameter is now mandatory on `Table::new()`. Existing code of the form:
```diff
- Table::new(rows).widths(widths)
```
Should be updated to:
```diff
+ Table::new(rows, widths)
```
For ease of automated replacement in cases where the amount of code broken by this change is large
or complex, it may be convenient to replace `Table::new` with `Table::default().rows`.
```diff
- Table::new(rows).block(block).widths(widths);
// becomes
+ Table::default().rows(rows).widths(widths)
```
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
[#663]: https://github.com/ratatui/ratatui/pull/663
Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy
`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove
the `&`.
E.g.
```diff
- let table = Table::new(rows).widths(&[Constraint::Length(1)]);
// becomes
+ let table = Table::new(rows, [Constraint::Length(1)]);
```
### Layout::new() now accepts direction and constraint parameters ([#557])
[#557]: https://github.com/ratatui/ratatui/pull/557
Previously layout new took no parameters. Existing code should either use `Layout::default()` or
the new constructor.
```rust
let layout = layout::new()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Max(2)]);
// becomes either
let layout = layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Max(2)]);
// or
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
```
## [v0.24.0](https://github.com/ratatui/ratatui/releases/tag/v0.24.0)
### `ScrollbarState` field type changed from `u16` to `usize` ([#456])
[#456]: https://github.com/ratatui/ratatui/pull/456
In order to support larger content lengths, the `position`, `content_length` and
`viewport_content_length` methods on `ScrollbarState` now take `usize` instead of `u16`
### `BorderType::line_symbols` renamed to `border_symbols` ([#529])
[#529]: https://github.com/ratatui/ratatui/issues/529
Applications can now set custom borders on a `Block` by calling `border_set()`. The
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
`symbols::border::Set`. E.g.:
```diff
- let line_set: symbols::line::Set = BorderType::line_symbols(BorderType::Plain);
// becomes
+ let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Plain);
```
### Generic `Backend` parameter removed from `Frame` ([#530])
[#530]: https://github.com/ratatui/ratatui/issues/530
`Frame` is no longer generic over Backend. Code that accepted `Frame<Backend>` will now need to
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
instance of a Frame. E.g.:
```diff
- fn ui<B: Backend>(frame: &mut Frame<B>) { ... }
// becomes
+ fn ui(frame: Frame) { ... }
```
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
[#466]: https://github.com/ratatui/ratatui/issues/466
In order to support using `Stylize` shorthands (e.g. `"foo".red()`) on temporary `String` values, a
new implementation of `Stylize` was added that returns a `Span<'static>`. This causes the value to
be consumed rather than borrowed. Existing code that expects to use the string after a call will no
longer compile. E.g.
```diff
- let s = String::new("foo");
- let span1 = s.red();
- let span2 = s.blue(); // will no longer compile as s is consumed by the previous line
// becomes
+ let span1 = s.clone().red();
+ let span2 = s.blue();
```
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
[#426]: https://github.com/ratatui/ratatui/issues/426
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
`Buffer::set_line`.
```diff
- let spans = Spans::from(some_string_str_span_or_vec_span);
- buffer.set_spans(0, 0, spans, 10);
// becomes
+ let line - Line::from(some_string_str_span_or_vec_span);
+ buffer.set_line(0, 0, line, 10);
```
## [v0.23.0](https://github.com/ratatui/ratatui/releases/tag/v0.23.0)
### `Scrollbar::track_symbol()` now takes an `Option<&str>` instead of `&str` ([#360])
[#360]: https://github.com/ratatui/ratatui/issues/360
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
```diff
- let scrollbar = Scrollbar::default().track_symbol("|");
// becomes
+ let scrollbar = Scrollbar::default().track_symbol(Some("|"));
```
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
[#330]: https://github.com/ratatui/ratatui/issues/330
The symbols for defining scrollbars have been moved to the `symbols` module from the
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the
new module locations. E.g.:
```diff
- use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
// becomes
+ use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
```
### MSRV updated to 1.67 ([#361])
[#361]: https://github.com/ratatui/ratatui/issues/361
The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
## [v0.22.0](https://github.com/ratatui/ratatui/releases/tag/v0.22.0)
### `bitflags` updated to 2.3 ([#205])
[#205]: https://github.com/ratatui/ratatui/issues/205
The `serde` representation of `bitflags` has changed. Any existing serialized types that have
Borders or Modifiers will need to be re-serialized. This is documented in the [`bitflags`
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
## [v0.21.0](https://github.com/ratatui/ratatui/releases/tag/v0.21.0)
### MSRV is 1.65.0 ([#171])
[#171]: https://github.com/ratatui/ratatui/issues/171
The minimum supported rust version is now 1.65.0.
### `Terminal::with_options()` stabilized to allow configuring the viewport ([#114])
[#114]: https://github.com/ratatui/ratatui/issues/114
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
and `ViewPort` was changed from a struct to an enum.
```diff
let terminal = Terminal::with_options(backend, TerminalOptions {
- viewport: Viewport::fixed(area),
});
// becomes
let terminal = Terminal::with_options(backend, TerminalOptions {
+ viewport: Viewport::Fixed(area),
});
```
### Code that binds `Into<Text<'a>>` now requires type annotations ([#168])
[#168]: https://github.com/ratatui/ratatui/issues/168
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
`to_string()` / `to_owned()` / `as_str()` on the value. E.g.:
```diff
- let paragraph = Paragraph::new("".as_ref());
// becomes
+ let paragraph = Paragraph::new("".as_str());
```
### `Marker::Block` now renders as a block rather than a bar character ([#133])
[#133]: https://github.com/ratatui/ratatui/issues/133
Code using the `Block` marker that previously rendered using a half block character (`'▀'``) now
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
the existing code.
```diff
- let canvas = Canvas::default().marker(Marker::Block);
// becomes
+ let canvas = Canvas::default().marker(Marker::Bar);
```
## [v0.20.0](https://github.com/ratatui/ratatui/releases/tag/v0.20.0)
v0.20.0 was the first release of Ratatui - versions prior to this were release as tui-rs. See the
[Changelog](./CHANGELOG.md) for more details.
### MSRV is update to 1.63.0 ([#80])
[#80]: https://github.com/ratatui/ratatui/issues/80
The minimum supported rust version is 1.63.0
### List no longer ignores empty string in items ([#42])
[#42]: https://github.com/ratatui/ratatui/issues/42
The following code now renders 3 items instead of 2. Code which relies on the previous behavior will
need to manually filter empty items prior to display.
```rust
let items = vec![
ListItem::new("line one"),
ListItem::new(""),
ListItem::new("line four"),
];
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
https://forum.ratatui.rs/ or https://discord.gg/pMCEU9hNEj.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,211 +1,69 @@
# Contribution guidelines
# Fork Status
First off, thank you for considering contributing to Ratatui.
## Pull Requests
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).
**All** pull requests opened on the original repository have been imported. We'll be going through any open PRs in a timely manner, starting with the **smallest bug fixes and README updates**. If you have an open PR make sure to let us know about it on our [discord](https://discord.gg/pMCEU9hNEj) as it helps to know you are still active.
## Reporting issues
## Issues
Before reporting an issue on the [issue tracker](https://github.com/ratatui/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.
We have been unsuccessful in importing all issues opened on the previous repository.
For that reason, anyone wanting to **work on or discuss** an issue will have to follow the following workflow :
## Pull requests
- Recreate the issue
- Start by referencing the **original issue**: ```Referencing issue #[<issue number>](<original issue link>)```
- Then, paste the original issues **opening** text
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.
You can then resume the conversation by replying to this new issue you have created.
### Keep PRs small, intentional, and focused
### Closing Issues
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.
If you close an issue that you have "imported" to this fork, please make sure that you add the issue to the **CLOSED_ISSUES.md**. This will enable us to keep track of which issues have been closed from the original repo, in case we are able to have the original repository transferred.
### Code formatting
Run `cargo make format` before committing to ensure that code is consistently formatted with
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml).
### 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.
### Run CI tests before pushing a PR
Running `cargo make ci` before pushing will perform the same checks that we do in the CI process.
It's not mandatory to do this before pushing, however it may save you time to do so instead of
waiting for GitHub to run the checks.
### 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.
# Contributing
## 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/ratatui.git
cd ratatui
cargo make build
```
### Tests
The [test coverage](https://app.codecov.io/gh/ratatui/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>
### Documentation
Here are some guidelines for writing documentation in Ratatui.
Every public API **must** be documented.
Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust
concepts.
#### Content
The main doc comment should talk about the general features that the widget supports and introduce
the concepts pointing to the various methods. Focus on interaction with various features and giving
enough information that helps understand why you might want something.
Examples should help users understand a particular usage, not test a feature. They should be as
simple as possible. Prefer hiding imports and using wildcards to keep things concise. Some imports
may still be shown to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style
methods). Speaking of `Stylize`, you should use it over the more verbose style setters:
```rust
let style = Style::new().red().bold();
// not
let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
```
#### Format
- First line is summary, second is blank, third onward is more detail
```rust
/// Summary
///
/// A detailed description
/// with examples.
fn foo() {}
```
- Max line length is 100 characters
See [VS Code rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
- Doc comments are above macros
i.e.
```rust
/// doc comment
#[derive(Debug)]
struct Foo {}
```
- Code items should be between backticks
i.e. ``[`Block`]``, **NOT** ``[Block]``
### Deprecation notice
We generally want to wait at least two versions before removing deprecated items, so users have
time to update. However, if a deprecation is blocking for us to implement a new feature we may
*consider* removing it in a one version notice.
### 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/ratatui/discussions/66) for more about the decision.
**Do not** use unsafe to achieve better performances. This is subject to change, [see.](https://github.com/tui-rs-revival/tui-rs-revival/discussions/66)
The only exception to this rule is if it's to fix **reproducible slowness.**
## Building
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
`ratatui` is an ordinary Rust project where common tasks are managed with [cargo-make].
It wraps common `cargo` commands with sane defaults depending on your platform of choice.
Building the project should be as easy as running `cargo make build`.
## :hammer_and_wrench: Pull requests
All contributions are obviously welcome.
Please include as many details as possible in your PR description to help the reviewer (follow the provided template).
Make sure to highlight changes which may need additional attention or you are uncertain about.
Any idea with a large scale impact on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
## Committing
To avoid any issues that may arrise with the CI/CD by not following the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) syntax, we recommend to install [Commitizen](https://commitizen-tools.github.io/commitizen/).\
By using this tool you automatically follow the configuration defined in [.cz.toml](.cz.toml).
Additionally, we're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically load pre-push hook, which will run `cargo make ci` before each push. It will load the hook automatically when you run `cargo test`. If `cargo-make` is not installed, it will install it for you.\
This will ensure that your code is formatted, compiles and passes all tests before you push. If you want to skip this check, you can use `git push --no-verify`.
## Continuous Integration
We use GitHub Actions for the CI where we perform the following checks:
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, 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/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 `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.

View File

@@ -1,57 +1,159 @@
[workspace]
resolver = "2"
members = ["ratatui*"]
# disabled for now because of the orphan rule on conversions
# <https://github.com/ratatui/ratatui/issues/1388#issuecomment-2379895747>
exclude = ["ratatui-termion", "ratatui-termwiz"]
[package]
name = "ratatui"
version = "0.21.0"
authors = ["Florian Dehau <work@fdehau.com>"]
description = "A library to build rich terminal user interfaces or dashboards"
documentation = "https://docs.rs/ratatui/latest/ratatui/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/tui-rs-revival/ratatui"
readme = "README.md"
license = "MIT"
exclude = [
"assets/*",
".github",
"Makefile.toml",
"CONTRIBUTING.md",
"*.log",
"tags",
]
autoexamples = true
edition = "2021"
rust-version = "1.65.0"
[workspace.dependencies]
ratatui = { path = "ratatui" }
ratatui-core = { path = "ratatui-core" }
ratatui-crossterm = { path = "ratatui-crossterm" }
ratatui-termion = { path = "ratatui-termion" }
ratatui-termwiz = { path = "ratatui-termwiz" }
ratatui-widgets = { path = "ratatui-widgets" }
[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"]
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
compact_str = "0.8.0"
crossterm = { version = "0.28.1" }
document-features = { version = "0.2.7" }
instability = "0.3.1"
itertools = "0.13"
lru = "0.12.0"
paste = "1.0.2"
palette = { version = "0.7.6" }
serde = { version = "1", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
termion = { version = "4.0.0" }
termwiz = { version = "0.22.0" }
time = { version = "0.3.11", features = ["local-offset"] }
crossterm = { version = "0.26", optional = true }
indoc = "2.0"
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-truncate = "1"
unicode-width = "=0.1.13"
unicode-width = "0.1"
# dev-dependencies
argh = "0.1.12"
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
[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"
font8x8 = "0.3.1"
futures = "0.3.30"
indoc = "2"
octocrab = "0.40.0"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.22.0"
serde_json = "1.0.109"
tokio = { version = "1.39.2", features = [
"rt",
"macros",
"time",
"rt-multi-thread",
] }
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
rand = "0.8"
[[bench]]
name = "paragraph"
harness = false
[[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 +0,0 @@
{
"drips": {
"ethereum": {
"ownedBy": "0x6053C8984f4F214Ad12c4653F28514E1E09213B5"
}
}
}

View File

@@ -1,7 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016-2022 Florian Dehau
Copyright (c) 2023-2024 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,15 +0,0 @@
# Maintainers
This file documents current and past maintainers.
- [orhun](https://github.com/orhun)
- [joshka](https://github.com/joshka)
- [kdheepak](https://github.com/kdheepak)
- [Valentin271](https://github.com/Valentin271)
## Past Maintainers
- [fdehau](https://github.com/fdehau)
- [mindoodoo](https://github.com/mindoodoo)
- [sayanarijit](https://github.com/sayanarijit)
- [EdJoPaTo](https://github.com/EdJoPaTo)

View File

@@ -3,144 +3,265 @@
[config]
skip_core_tasks = true
[env]
# all features except the backend ones
NON_BACKEND_FEATURES = "all-widgets,macros,serde"
[tasks.default]
alias = "ci"
[tasks.ci]
description = "Run continuous integration tasks"
dependencies = ["lint", "clippy", "check", "test"]
run_task = [
{ name = "ci-unix", condition = { platforms = [
"linux",
"mac",
] } },
{ name = "ci-windows", condition = { platforms = [
"windows",
] } },
]
[tasks.lint]
description = "Lint code style (formatting, typos, docs, markdown)"
dependencies = ["lint-format", "lint-typos", "lint-docs"]
[tasks.ci-unix]
private = true
dependencies = [
"style-check",
"check-unix",
"test-unix",
"clippy-unix",
]
[tasks.lint-format]
description = "Lint code formatting"
[tasks.ci-windows]
private = true
dependencies = [
"style-check",
"check-windows",
"test-windows",
"clippy-windows",
]
[tasks.style-check]
dependencies = ["fmt", "typos"]
[tasks.fmt]
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all", "--check"]
[tasks.format]
description = "Fix code formatting"
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all"]
[tasks.lint-typos]
description = "Run typo checks"
[tasks.typos]
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
command = "typos"
[tasks.lint-docs]
description = "Check documentation for errors and warnings"
toolchain = "nightly"
command = "cargo"
args = [
"rustdoc",
"--all-features",
"--",
"-Zunstable-options",
"--check",
"-Dwarnings",
[tasks.check]
run_task = [
{ name = "check-unix", condition = { platforms = [
"linux",
"mac",
] } },
{ name = "check-windows", condition = { platforms = [
"windows",
] } },
]
[tasks.lint-markdown]
description = "Check markdown files for errors and warnings"
command = "markdownlint-cli2"
args = ["**/*.md", "!target"]
[tasks.check-unix]
private = true
dependencies = [
"check-crossterm",
"check-termion",
"check-termwiz",
]
[tasks.check]
description = "Check code for errors and warnings"
[tasks.check-windows]
private = true
dependencies = [
"check-crossterm",
"check-termwiz",
]
[tasks.check-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "check-backend"
[tasks.check-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "check-backend"
[tasks.check-termwiz]
env = { TUI_FEATURES = "serde,termwiz" }
run_task = "check-backend"
[tasks.check-backend]
command = "cargo"
args = ["check", "--all-targets", "--all-features"]
condition = { env_set = ["TUI_FEATURES"] }
args = [
"check",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--all-targets",
]
[tasks.build]
description = "Compile the project"
run_task = [
{ name = "build-unix", condition = { platforms = [
"linux",
"mac",
] } },
{ name = "build-windows", condition = { platforms = [
"windows",
] } },
]
[tasks.build-unix]
private = true
dependencies = [
"build-crossterm",
"build-termion",
"build-termwiz",
]
[tasks.build-windows]
private = true
dependencies = [
"build-crossterm",
"build-termwiz",
]
[tasks.build-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "build-backend"
[tasks.build-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "build-backend"
[tasks.build-termwiz]
env = { TUI_FEATURES = "serde,termwiz" }
run_task = "build-backend"
[tasks.build-backend]
command = "cargo"
args = ["build", "--all-targets", "--all-features"]
condition = { env_set = ["TUI_FEATURES"] }
args = [
"build",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--all-targets",
]
[tasks.clippy]
description = "Run Clippy for linting"
run_task = [
{ name = "clippy-unix", condition = { platforms = [
"linux",
"mac",
] } },
{ name = "clippy-windows", condition = { platforms = [
"windows",
] } },
]
[tasks.clippy-unix]
private = true
dependencies = [
"clippy-crossterm",
"clippy-termion",
"clippy-termwiz",
]
[tasks.clippy-windows]
private = true
dependencies = [
"clippy-crossterm",
"clippy-termwiz",
]
[tasks.clippy-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "clippy-backend"
[tasks.clippy-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "clippy-backend"
[tasks.clippy-termwiz]
env = { TUI_FEATURES = "serde,termwiz" }
run_task = "clippy-backend"
[tasks.clippy-backend]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"clippy",
"--all-targets",
"--all-features",
"--no-default-features",
"--tests",
"--benches",
"--features",
"${TUI_FEATURES}",
"--",
"-D",
"warnings",
]
[tasks.install-nextest]
description = "Install cargo-nextest"
install_crate = { crate_name = "cargo-nextest", binary = "cargo-nextest", test_arg = "--help" }
[tasks.test]
description = "Run tests"
run_task = { name = ["test-lib", "test-doc"] }
run_task = [
{ name = "test-unix", condition = { platforms = [
"linux",
"mac",
] } },
{ name = "test-windows", condition = { platforms = [
"windows",
] } },
]
[tasks.test-lib]
description = "Run default tests"
dependencies = ["install-nextest"]
command = "cargo"
args = ["nextest", "run", "--all-targets", "--all-features"]
[tasks.test-unix]
private = true
dependencies = [
"test-crossterm",
"test-termion",
"test-termwiz",
"test-doc",
]
[tasks.test-doc]
description = "Run documentation tests"
command = "cargo"
args = ["test", "--doc", "--all-features"]
[tasks.test-windows]
private = true
dependencies = [
"test-crossterm",
"test-termwiz",
"test-doc",
]
[tasks.test-crossterm]
env = { TUI_FEATURES = "serde,crossterm,all-widgets,macros" }
run_task = "test-backend"
[tasks.test-termion]
env = { TUI_FEATURES = "serde,termion,all-widgets,macros" }
run_task = "test-backend"
[tasks.test-termwiz]
env = { TUI_FEATURES = "serde,termwiz,all-widgets,macros" }
run_task = "test-backend"
[tasks.test-backend]
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
description = "Run backend-specific tests"
dependencies = ["install-nextest"]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"nextest",
"run",
"--all-targets",
"test",
"--no-default-features",
"--features",
"${NON_BACKEND_FEATURES},${@}",
"${TUI_FEATURES}",
"--all-targets",
]
[tasks.coverage]
description = "Generate code coverage report"
[tasks.test-doc]
command = "cargo"
args = [
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--all-features",
]
args = ["test", "--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]
description = "Compile project examples"
command = "cargo"
args = ["build", "--examples", "--release", "--features", "all-widgets"]
args = ["build", "--examples", "--release"]
[tasks.run-examples]
description = "Run project examples"
dependencies = ["build-examples"]
script = '''
#!@duckscript

528
README.md
View File

@@ -1,342 +1,137 @@
# Ratatui
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
`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.
[![Crates.io](https://img.shields.io/crates/v/ratatui?logo=rust&style=for-the-badge)](https://crates.io/crates/ratatui)
[![License](https://img.shields.io/crates/l/ratatui?style=for-the-badge)](./LICENSE) [![GitHub CI
Status](https://img.shields.io/github/actions/workflow/status/tui-rs-revival/ratatui/ci.yml?style=for-the-badge&logo=github)](https://github.com/tui-rs-revival/ratatui/actions?query=workflow%3ACI+)
[![Docs.rs](https://img.shields.io/docsrs/ratatui?logo=rust&style=for-the-badge)](https://docs.rs/crate/ratatui/)
[![Dependency
Status](https://deps.rs/repo/github/tui-rs-revival/ratatui/status.svg?style=for-the-badge)](https://deps.rs/repo/github/tui-rs-revival/ratatui)
[![Codecov](https://img.shields.io/codecov/c/github/tui-rs-revival/ratatui?logo=codecov&style=for-the-badge&token=BAQ8SOKEST)](https://app.codecov.io/gh/tui-rs-revival/ratatui)
[![Discord](https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=for-the-badge)](https://discord.gg/pMCEU9hNEj)
<!-- See RELEASE.md for instructions on creating the demo gif --->
![Demo of Ratatui](https://github.com/tui-rs-revival/ratatui/assets/24392180/93ab0e38-93e0-4ae0-a31b-91ae6c393185)
<details>
<summary>Table of Contents</summary>
- [Ratatui](#ratatui)
- [Installation](#installation)
- [Introduction](#introduction)
- [Other documentation](#other-documentation)
- [Quickstart](#quickstart)
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
- [Drawing the UI](#drawing-the-ui)
- [Handling events](#handling-events)
- [Example](#example)
- [Layout](#layout)
- [Text and styling](#text-and-styling)
- [Status of this fork](#status-of-this-fork)
- [Widgets](#widgets)
- [Built in](#built-in)
- [Third-party libraries, bootstrapping templates and widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
- [Apps](#apps)
- [Alternatives](#alternatives)
- [Acknowledgments](#acknowledgments)
- [License](#license)
* [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)
</details>
<!-- cargo-rdme start -->
![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true)
<div align="center">
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
[![Forum Badge]][Forum]<br>
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
</div>
# Ratatui
[Ratatui][Ratatui Website] is a crate for cooking up terminal user interfaces in Rust. It is a
lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
## Installation
Add `ratatui` as a dependency to your cargo.toml:
```shell
cargo add ratatui
cargo add ratatui --features all-widgets
```
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
[Termwiz]).
Or modify your `Cargo.toml`
```toml
[dependencies]
ratatui = { version = "0.21.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.21.0", features = ["all-widgets"]}
```
## Introduction
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
that for each frame, your app must render all widgets that are supposed to be part of the UI.
This is in contrast to the retained mode style of rendering where widgets are updated and then
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
for more info.
`ratatui` is a terminal UI library that supports multiple backends:
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
* [crossterm](https://github.com/crossterm-rs/crossterm) [default]
* [termion](https://github.com/ticki/termion)
* [termwiz](https://github.com/wez/wezterm/tree/master/termwiz)
## Other documentation
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 Website] - explains the library's concepts and provides step-by-step tutorials
- [Ratatui Forum][Forum] - a place to ask questions and discuss the library
- [API Docs] - the full API documentation for the library on docs.rs.
- [Examples] - a collection of examples that demonstrate how to use the library.
- [Contributing] - Please read this if you are interested in contributing to the project.
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
- [Breaking Changes] - a list of breaking changes in the library.
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
the [Examples] directory. For more guidance on different ways to structure your application see
the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
various [Examples]. There are also several starter templates available in the [templates]
repository.
Every application built with `ratatui` needs to implement the following steps:
- Initialize the terminal
- A main loop to:
- Handle input events
- Draw the UI
- Restore the terminal state
The library contains a [`prelude`] module that re-exports the most commonly used traits and
types for convenience. Most examples in the documentation will use this instead of showing the
full path of each type.
### Initialize and restore the terminal
The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
abstraction over a choice of [`Backend`] implementations that provides functionality to draw
each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
[Termwiz].
Most applications should enter the Alternate Screen when starting and leave it when exiting and
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
module] and the [Backends] section of the [Ratatui Website] for more info.
### Drawing the UI
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
using the provided [`render_widget`] method. After this closure returns, a diff is performed and
only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
for more info.
### Handling events
Ratatui does not include any input handling. Instead event handling can be implemented by
calling backend library methods directly. See the [Handling Events] section of the [Ratatui
Website] for more info. For example, if you are using [Crossterm], you can use the
[`crossterm::event`] module to handle events.
### Example
[hello_world.rs](./examples/hello_world.rs). For more guidance on how to create Ratatui apps, see
the [Docs](https://docs.rs/ratatui) and [Examples](#examples). There is also a starter template
available at [rust-tui-template](https://github.com/tui-rs-revival/rust-tui-template).
```rust
use std::io::{self, stdout};
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
ExecutableCommand,
},
widgets::{Block, Paragraph},
Frame, Terminal,
};
fn main() -> io::Result<()> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut should_quit = false;
while !should_quit {
terminal.draw(ui)?;
should_quit = handle_events()?;
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
fn main() -> Result<(), Box<dyn Error>> {
let mut terminal = setup_terminal()?;
run(&mut terminal)?;
restore_terminal(&mut terminal)?;
Ok(())
}
fn handle_events() -> io::Result<bool> {
if event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
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;
}
}
}
}
Ok(false)
}
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
frame.area(),
);
})
}
```
Running this example produces the following output:
![docsrs-hello]
## Layout
The library comes with a basic yet useful layout management object called [`Layout`] which
allows you to split the available space into multiple areas and then render widgets in each
area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
section of the [Ratatui Website] for more info.
```rust
use ratatui::{
layout::{Constraint, Layout},
widgets::Block,
Frame,
};
fn ui(frame: &mut Frame) {
let [title_area, main_area, status_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.areas(frame.area());
let [left_area, right_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(main_area);
frame.render_widget(Block::bordered().title("Title Bar"), title_area);
frame.render_widget(Block::bordered().title("Status Bar"), status_area);
frame.render_widget(Block::bordered().title("Left"), left_area);
frame.render_widget(Block::bordered().title("Right"), right_area);
}
```
Running this example produces the following output:
![docsrs-layout]
## Text and styling
The [`Text`], [`Line`] and [`Span`] types are the building blocks of the library and are used in
many places. [`Text`] is a list of [`Line`]s and a [`Line`] is a list of [`Span`]s. A [`Span`]
is a string with a specific style.
The [`style` module] provides types that represent the various styling options. The most
important one is [`Style`] which represents the foreground and background colors and the text
attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
[Ratatui Website] for more info.
```rust
use ratatui::{
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span},
widgets::{Block, Paragraph},
Frame,
};
fn ui(frame: &mut Frame) {
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
let line = Line::from(vec![
Span::raw("Hello "),
Span::styled(
"World",
Style::new()
.fg(Color::Green)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
),
"!".red().on_light_yellow().italic(),
]);
frame.render_widget(line, areas[0]);
// using the short-hand syntax and implicit conversions
let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
frame.render_widget(paragraph, areas[1]);
// style the whole widget instead of just the text
let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
frame.render_widget(paragraph, areas[2]);
// use the simpler short-hand syntax
let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
frame.render_widget(paragraph, areas[3]);
}
```
Running this example produces the following output:
![docsrs-styling]
[Ratatui Website]: https://ratatui.rs/
[Installation]: https://ratatui.rs/installation/
[Rendering]: https://ratatui.rs/concepts/rendering/
[Application Patterns]: https://ratatui.rs/concepts/application-patterns/
[Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
[Backends]: https://ratatui.rs/concepts/backends/
[Widgets]: https://ratatui.rs/how-to/widgets/
[Handling Events]: https://ratatui.rs/concepts/event-handling/
[Layout]: https://ratatui.rs/how-to/layout/
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
[templates]: https://github.com/ratatui/templates/
[Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
[Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
[Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
[Create a Pull Request]: https://github.com/ratatui/ratatui/compare
[git-cliff]: https://git-cliff.org
[Conventional Commits]: https://www.conventionalcommits.org
[API Docs]: https://docs.rs/ratatui
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
[docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
[`Frame`]: terminal::Frame
[`render_widget`]: terminal::Frame::render_widget
[`Widget`]: widgets::Widget
[`Layout`]: layout::Layout
[`Text`]: text::Text
[`Line`]: text::Line
[`Span`]: text::Span
[`Style`]: style::Style
[`style` module]: style
[`Stylize`]: style::Stylize
[`Backend`]: backend::Backend
[`backend` module]: backend
[`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
[Crate]: https://crates.io/crates/ratatui
[Crossterm]: https://crates.io/crates/crossterm
[Termion]: https://crates.io/crates/termion
[Termwiz]: https://crates.io/crates/termwiz
[tui-rs]: https://crates.io/crates/tui
[GitHub Sponsors]: https://github.com/sponsors/ratatui
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
[Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
[Discord Server]: https://discord.gg/pMCEU9hNEj
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
[Forum]: https://forum.ratatui.rs
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
<!-- cargo-rdme end -->
## Status of this fork
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
@@ -345,17 +140,47 @@ the community forked the project and created this crate. We look forward to cont
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 is also a [Matrix](https://matrix.org/) bridge available at
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub.
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a
Pull Request].
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
# termion
cargo run --example demo --no-default-features --features=termion
# termwiz
cargo run --example demo --no-default-features --features=termwiz
```
The UI code for the is in [examples/demo/ui.rs](./examples/demo/ui.rs) while the application state
is in [examples/demo/app.rs](./examples/demo/app.rs).
If the user interface contains glyphs that are not displayed correctly by your terminal, you may
want to run the demo without those symbols:
```shell
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
```
More examples are available in the [examples](./examples/) folder.
## Widgets
### Built in
@@ -363,70 +188,75 @@ you are interested in working on a PR or issue opened in the previous repository
The library comes with the following
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
- [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
- [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
- [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
- [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
* [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)
* [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
* [Block](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Block.html)
* [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
* [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
* [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)
* [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
* [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
* [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
Each widget has an associated example which can be found in the [Examples] folder. Run each example
with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
Each wiget 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`.
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
be installed with `cargo install cargo-make`).
### Third-party libraries, bootstrapping templates and widgets
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
`ratatui::text::Text`
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`ratatui::style::Color`
- [templates](https://github.com/ratatui/templates) — Starter templates for
bootstrapping a Rust TUI application with Ratatui & crossterm
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
* [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
`tui::text::Text`
* [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`tui::style::Color`
* [rust-tui-template](https://github.com/orhun/rust-tui-template) — A template for bootstrapping a
Rust TUI application with Tui-rs & crossterm
* [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
* [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
Tui-rs + Crossterm apps
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
- [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
- [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
- [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications
* [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-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
* [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
* [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
* [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
* [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
* [tui-term](https://github.com/a-kenji/tui-term): A pseudoterminal widget library
that enables the rendering of terminal applications as ratatui widgets.
## Apps
Check out [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) for a curated list of
awesome apps/libraries built with `ratatui`!
Check out the list of more than 50 [Apps using
`Ratatui`](https://github.com/tui-rs-revival/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=tui-rs-revival/ratatui)](https://github.com/tui-rs-revival/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 organization.
awesome logo** for the ratatui project and tui-rs-revival organization.
## License

View File

@@ -1,51 +1,30 @@
# Creating a Release
Our release strategy is:
> Release major versions with detailed summaries when necessary, while releasing minor versions
> weekly or as needed without extensive announcements.
>
> Versioning scheme being `0.x.y`, where `x` is the major version and `y` is the minor version.
[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 if necessary. The preferred tool for this is
[vhs](https://github.com/charmbracelet/vhs) (installation instructions in README).
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 demo2
vhs examples/demo2.tape
cargo build --example demo
ttyrec -e 'cargo --quiet run --release --example demo -- --tick-rate 100' demo.rec
ttygif demo.rec
```
1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push.
1. Grab the permalink from <https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif> and
append `?raw=true` to redirect to the actual image url. Then update the link in the main README.
Avoid adding the gif to the git repo as binary files tend to bloat repositories.
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. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)
1. Commit and push the changes.
1. Create a new tag: `git tag -a v[0.x.y]`
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/ratatui/actions) workflow to
1. Wait for [Continuous Deployment](https://github.com/tui-rs-revival/ratatui/actions) workflow to
finish.
## Alpha Releases
Alpha releases are automatically released every Saturday via [cd.yml](./.github/workflows/cd.yml)
and can be manually be created when necessary by triggering the [Continuous
Deployment](https://github.com/ratatui/ratatui/actions/workflows/cd.yml) workflow.
We automatically release an alpha release with a patch level bump + alpha.num weekly (and when we
need to manually). E.g. the last release was 0.22.0, and the most recent alpha release is
0.22.1-alpha.1.
These releases will have whatever happened to be in main at the time of release, so they're useful
for apps that need to get releases from crates.io, but may contain more bugs and be generally less
tested than normal releases.
See [#147](https://github.com/ratatui/ratatui/issues/147) and
[#359](https://github.com/ratatui/ratatui/pull/359) for more info on the alpha release process.

View File

@@ -1,9 +0,0 @@
# Security Policy
## Supported Versions
We only support the latest version of this crate.
## Reporting a Vulnerability
To report secuirity vulnerability, please use the form at <https://github.com/ratatui/ratatui/security/advisories/new>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,112 +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.test-unit]
command = [
"cargo", "test",
"--lib",
"--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"
ctrl-v = "job:coverage-unit-tests-only"
u = "job:test-unit"
n = "job:nextest"

View File

@@ -1,4 +1,4 @@
use criterion::{black_box, criterion_group, BatchSize, Bencher, BenchmarkId, Criterion};
use criterion::{black_box, criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::Rect,
@@ -15,15 +15,15 @@ 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.
fn paragraph(c: &mut Criterion) {
pub fn paragraph(c: &mut Criterion) {
let mut group = c.benchmark_group("paragraph");
for line_count in [64, 2048, MAX_SCROLL_OFFSET] {
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)));
b.iter(|| Paragraph::new(black_box(lines)))
});
// render the paragraph with no scroll
@@ -36,14 +36,14 @@ fn paragraph(c: &mut Criterion) {
// 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((0, line_count / 2)),
&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((0, line_count)),
&Paragraph::new(lines).scroll((0u16, line_count)),
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
);
@@ -59,7 +59,7 @@ fn paragraph(c: &mut Criterion) {
BenchmarkId::new("render_wrap_scroll_full", line_count),
&Paragraph::new(lines)
.wrap(Wrap { trim: false })
.scroll((0, line_count)),
.scroll((0u16, line_count)),
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
);
}
@@ -69,15 +69,9 @@ fn paragraph(c: &mut Criterion) {
/// 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));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377.
bencher.iter_batched(
|| paragraph.to_owned(),
|bench_paragraph| {
bench_paragraph.render(buffer.area, &mut buffer);
},
BatchSize::LargeInput,
);
bencher.iter(|| {
paragraph.clone().render(buffer.area, &mut buffer);
})
}
/// Create a string with the given number of lines filled with nonsense words
@@ -85,10 +79,11 @@ fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
/// 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 = i64::from(count);
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,97 +1,41 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "ratatui"
repo = "ratatui"
# configuration for https://github.com/orhun/git-cliff
[changelog]
# changelog header
header = """
# Changelog
All notable changes to this project will be documented in this file.
<!-- ignore lint rules that are often triggered by content generated from commits / git-cliff -->
<!-- markdownlint-disable line-length no-bare-urls ul-style emphasis-style -->
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
# note that the - before / after the % controls whether whitespace is rendered between each line.
# Getting this right so that the markdown renders with the correct number of lines between headings
# code fences and list items is pretty finicky. Note also that the 4 backticks in the commit macro
# is intentional as this escapes any backticks in the commit body.
# https://tera.netlify.app/docs/#introduction
body = """
{%- if not version %}
## [unreleased]
{% else -%}
## [{{ version }}](https://github.com/ratatui/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif -%}
{% macro commit(commit) -%}
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
{%- if commit.breaking %} [**breaking**]{% endif %}
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
{%- endif %}
{%- for footer in commit.footers %}\n
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
>
{{ footer.token | indent(prefix=" > ", first=true, blank=true) }}
{{- footer.separator }}
{{- footer.value| indent(prefix=" > ", first=false, blank=true) }}
{%- endif %}
{%- endfor %}
{% endmacro -%}
{% 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") %}
{{ self::commit(commit=commit) }}
{%- endfor -%}
{% for commit in commits %}
{%- if not commit.scope %}
{{ self::commit(commit=commit) }}
{%- endif -%}
{%- endfor -%}
{%- endfor %}
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
### New Contributors
{%- endif %}\
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
* @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %} in \
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
{%- endif %}
{%- endfor -%}
{% if version %}
{% if previous.version %}
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
{% endif %}
{% else -%}
{% raw %}\n{% endraw %}
{% endif %}
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
{% endmacro %}
### {{ 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 = false
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
postprocessors = [
{ pattern = '<!-- Please read CONTRIBUTING.md before submitting any pull request. -->', replace = "" },
{ pattern = '>---+\n', replace = '' },
{ pattern = ' +\n', replace = "\n" },
]
[git]
# parse the commits based on https://www.conventionalcommits.org
@@ -102,36 +46,30 @@ filter_unconventional = true
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
{ 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}" },
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/tui-rs-revival/ratatui/issues/${2}))" },
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 00 -->Features" },
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes" },
{ message = "^refactor", group = "<!-- 02 -->Refactor" },
{ message = "^doc", group = "<!-- 03 -->Documentation" },
{ message = "^perf", group = "<!-- 04 -->Performance" },
{ message = "^style", group = "<!-- 05 -->Styling" },
{ message = "^test", group = "<!-- 06 -->Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore\\(deps\\)", skip = true },
{ message = "^chore\\(changelog\\)", skip = true },
{ message = "^[cC]hore", 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" },
# handle some old commits styles from pre 0.4
{ message = "^(Buffer|buffer|Frame|frame|Gauge|gauge|Paragraph|paragraph):", group = "<!-- 07 -->Miscellaneous Tasks" },
{ message = "^\\[", group = "<!-- 07 -->Miscellaneous Tasks" },
{ 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
@@ -141,7 +79,7 @@ tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-rc.1"
# regex for ignoring tags
ignore_tags = "alpha"
ignore_tags = ""
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order

View File

@@ -1,17 +0,0 @@
avoid-breaking-exported-api = false
# https://rust-lang.github.io/rust-clippy/master/index.html#/multiple_crate_versions
# ratatui -> bitflags v2.3
# termwiz -> wezterm-blob-leases -> mac_address -> nix -> bitflags v1.3.2
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
allowed-duplicate-crates = [
"bitflags",
"windows-targets",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]

View File

@@ -1,14 +0,0 @@
coverage: # https://docs.codecov.com/docs/codecovyml-reference#coverage
precision: 1 # e.g. 89.1%
round: down
range: 85..100 # https://docs.codecov.com/docs/coverage-configuration#section-range
status: # https://docs.codecov.com/docs/commit-status
project:
default:
threshold: 1% # Avoid false negatives
ignore:
- "examples"
- "benches"
comment: # https://docs.codecov.com/docs/pull-request-comments
# make the comments less noisy
require_changes: true

View File

@@ -1,7 +1,9 @@
# configuration for https://github.com/EmbarkStudios/cargo-deny
[licenses]
version = 2
default = "deny"
unlicensed = "deny"
copyleft = "deny"
confidence-threshold = 0.8
allow = [
"Apache-2.0",
@@ -14,7 +16,8 @@ allow = [
]
[advisories]
version = 2
unmaintained = "deny"
yanked = "deny"
[bans]
multiple-versions = "allow"

162
examples/barchart.rs Normal file
View File

@@ -0,0 +1,162 @@
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::{
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)>,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
data: vec![
("B1", 9),
("B2", 12),
("B3", 5),
("B4", 8),
("B5", 2),
("B6", 4),
("B7", 5),
("B8", 9),
("B9", 14),
("B10", 15),
("B11", 1),
("B12", 0),
("B13", 4),
("B14", 6),
("B15", 4),
("B16", 6),
("B17", 4),
("B18", 7),
("B19", 13),
("B20", 8),
("B21", 11),
("B22", 9),
("B23", 3),
("B24", 5),
],
}
}
fn on_tick(&mut self) {
let value = self.data.pop().unwrap();
self.data.insert(0, value);
}
}
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::new();
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, &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()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
.bar_width(9)
.bar_style(Style::default().fg(Color::Yellow))
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
f.render_widget(barchart, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let barchart = BarChart::default()
.block(Block::default().title("Data2").borders(Borders::ALL))
.data(&app.data)
.bar_width(5)
.bar_gap(3)
.bar_style(Style::default().fg(Color::Green))
.value_style(
Style::default()
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
);
f.render_widget(barchart, chunks[0]);
let barchart = BarChart::default()
.block(Block::default().title("Data3").borders(Borders::ALL))
.data(&app.data)
.bar_style(Style::default().fg(Color::Red))
.bar_width(7)
.bar_gap(0)
.value_style(Style::default().bg(Color::Red))
.label_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::ITALIC),
);
f.render_widget(barchart, chunks[1]);
}

126
examples/block.rs Normal file
View File

@@ -0,0 +1,126 @@
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::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Style, Stylize},
widgets::{block::title::Title, Block, BorderType, Borders, Padding, Paragraph},
Frame, Terminal,
};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>) {
// Wrapping block for a group
// Just draw the block and the group on the same area and build the group
// with at least a margin of 1
let size = f.size();
// Surrounding block
let block = Block::default()
.borders(Borders::ALL)
.title(Title::from("Main block with round corners").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!["With".yellow(), " background".into()])
.on_green();
f.render_widget(block, top_chunks[0]);
// Top right inner block with styled title aligned to the right
let block = Block::default()
.title(Title::from("Styled title".white().on_red().bold()).alignment(Alignment::Right));
f.render_widget(block, top_chunks[1]);
// Bottom two inner blocks
let bottom_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
// Bottom left block with all default borders
let block = Block::default()
.title("With borders")
.borders(Borders::ALL)
.padding(Padding {
left: 4,
right: 4,
top: 2,
bottom: 2,
});
let text = Paragraph::new("text inside padded block").block(block);
f.render_widget(text, bottom_chunks[0]);
// Bottom right block with styled left and right border
let block = Block::default()
.title("With styled borders and doubled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Double)
.padding(Padding::uniform(1));
let inner_block = Block::default()
.title("Block inside padded block")
.borders(Borders::ALL);
let inner_area = block.inner(bottom_chunks[1]);
f.render_widget(block, bottom_chunks[1]);
f.render_widget(inner_block, inner_area);
}

View File

@@ -1,52 +1,55 @@
//! # [Ratatui] Calendar example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{error::Error, io, rc::Rc};
use color_eyre::Result;
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Margin},
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
DefaultTerminal, Frame,
Frame, Terminal,
};
use time::{Date, Month, OffsetDateTime};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
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)?;
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(draw)?;
let _ = terminal.draw(|f| draw(f));
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break Ok(());
}
#[allow(clippy::single_match)]
match key.code {
KeyCode::Char(_) => {
break;
}
_ => {}
};
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}
fn draw(frame: &mut Frame) {
let area = frame.area().inner(Margin {
vertical: 1,
horizontal: 1,
});
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()
@@ -58,19 +61,49 @@ fn draw(frame: &mut Frame) {
let list = make_dates(start.year());
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area);
let cols = rows.iter().flat_map(|row| {
Layout::horizontal([Constraint::Ratio(1, 4); 4])
.split(*row)
.to_vec()
});
for col in cols {
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);
frame.render_widget(cal, col);
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()
@@ -152,21 +185,22 @@ fn make_dates(current_year: i32) -> CalendarEventStore {
}
mod cals {
#[allow(clippy::wildcard_imports)]
use super::*;
pub fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
use Month::*;
match m {
Month::May => example1(m, y, es),
Month::June => example2(m, y, es),
Month::July | Month::December => example3(m, y, es),
Month::February => example4(m, y, es),
Month::November => example5(m, y, es),
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, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
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));
@@ -176,7 +210,7 @@ mod cals {
.default_style(default_style)
}
fn example1<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
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));
@@ -187,7 +221,7 @@ mod cals {
.show_month_header(Style::default())
}
fn example2<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
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)
@@ -203,7 +237,7 @@ mod cals {
.show_month_header(Style::default())
}
fn example3<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
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);
@@ -219,7 +253,7 @@ mod cals {
.show_month_header(Style::default())
}
fn example4<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
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);
@@ -233,7 +267,7 @@ mod cals {
.default_style(default_style)
}
fn example5<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
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);

193
examples/canvas.rs Normal file
View File

@@ -0,0 +1,193 @@
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::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Stylize},
symbols::Marker,
widgets::{
canvas::{Canvas, Map, MapResolution, Rectangle},
Block, Borders,
},
Frame, Terminal,
};
struct App {
x: f64,
y: f64,
ball: Rectangle,
playground: Rect,
vx: f64,
vy: f64,
dir_x: bool,
dir_y: bool,
tick_count: u64,
marker: Marker,
}
impl App {
fn new() -> App {
App {
x: 0.0,
y: 0.0,
ball: Rectangle {
x: 10.0,
y: 30.0,
width: 10.0,
height: 10.0,
color: Color::Yellow,
},
playground: Rect::new(10, 10, 100, 100),
vx: 1.0,
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
{
self.dir_x = !self.dir_x;
}
if self.ball.y < self.playground.top() as f64
|| self.ball.y + self.ball.height > self.playground.bottom() as f64
{
self.dir_y = !self.dir_y;
}
if self.dir_x {
self.ball.x += self.vx;
} else {
self.ball.x -= self.vx;
}
if self.dir_y {
self.ball.y += self.vy;
} else {
self.ball.y -= self.vy
}
}
}
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::new();
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, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => {
return Ok(());
}
KeyCode::Down => {
app.y += 1.0;
}
KeyCode::Up => {
app.y -= 1.0;
}
KeyCode::Right => {
app.x += 1.0;
}
KeyCode::Left => {
app.x -= 1.0;
}
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.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());
})
.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);
})
.x_bounds([10.0, 110.0])
.y_bounds([10.0, 110.0]);
f.render_widget(canvas, chunks[1]);
}

267
examples/chart.rs Normal file
View File

@@ -0,0 +1,267 @@
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::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style, Stylize},
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] = [
(0.0, 0.0),
(10.0, 1.0),
(20.0, 0.5),
(30.0, 1.5),
(40.0, 1.0),
(50.0, 2.5),
(60.0, 3.0),
];
#[derive(Clone)]
pub struct SinSignal {
x: f64,
interval: f64,
period: f64,
scale: f64,
}
impl SinSignal {
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
SinSignal {
x: 0.0,
interval,
period,
scale,
}
}
}
impl Iterator for SinSignal {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
self.x += self.interval;
Some(point)
}
}
struct App {
signal1: SinSignal,
data1: Vec<(f64, f64)>,
signal2: SinSignal,
data2: Vec<(f64, f64)>,
window: [f64; 2],
}
impl App {
fn new() -> App {
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
let mut signal2 = SinSignal::new(0.1, 2.0, 10.0);
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
App {
signal1,
data1,
signal2,
data2,
window: [0.0, 20.0],
}
}
fn on_tick(&mut self) {
for _ in 0..5 {
self.data1.remove(0);
}
self.data1.extend(self.signal1.by_ref().take(5));
for _ in 0..10 {
self.data2.remove(0);
}
self.data2.extend(self.signal2.by_ref().take(10));
self.window[0] += 1.0;
self.window[1] += 1.0;
}
}
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::new();
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, &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()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]
.as_ref(),
)
.split(size);
let x_labels = vec![
Span::styled(
format!("{}", app.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
Span::styled(
format!("{}", app.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data1),
Dataset::default()
.name("data3")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 1".cyan().bold())
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels(x_labels)
.bounds(app.window),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
.bounds([-20.0, 20.0]),
);
f.render_widget(chart, chunks[0]);
let datasets = vec![Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA)];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 2".cyan().bold())
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".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()]),
);
f.render_widget(chart, chunks[1]);
let datasets = vec![Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA2)];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 3".cyan().bold())
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 50.0])
.labels(vec!["0".bold(), "25".into(), "50".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()]),
);
f.render_widget(chart, chunks[2]);
}

78
examples/custom_widget.rs Normal file
View File

@@ -0,0 +1,78 @@
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::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
layout::Rect,
style::Style,
widgets::Widget,
Frame, Terminal,
};
#[derive(Default)]
struct Label<'a> {
text: &'a str,
}
impl<'a> Widget for Label<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.left(), area.top(), self.text, Style::default());
}
}
impl<'a> Label<'a> {
fn text(mut self, text: &'a str) -> Label<'a> {
self.text = text;
self
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>) {
let size = f.size();
let label = Label::default().text("Test");
f.render_widget(label, size);
}

View File

@@ -73,8 +73,8 @@ pub struct RandomSignal {
}
impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> Self {
Self {
pub fn new(lower: u64, upper: u64) -> RandomSignal {
RandomSignal {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
@@ -97,8 +97,8 @@ pub struct SinSignal {
}
impl SinSignal {
pub const fn new(interval: f64, period: f64, scale: f64) -> Self {
Self {
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
SinSignal {
x: 0.0,
interval,
period,
@@ -122,8 +122,8 @@ pub struct TabsState<'a> {
}
impl<'a> TabsState<'a> {
pub const fn new(titles: Vec<&'a str>) -> Self {
Self { titles, index: 0 }
pub fn new(titles: Vec<&'a str>) -> TabsState {
TabsState { titles, index: 0 }
}
pub fn next(&mut self) {
self.index = (self.index + 1) % self.titles.len();
@@ -144,8 +144,8 @@ pub struct StatefulList<T> {
}
impl<T> StatefulList<T> {
pub fn with_items(items: Vec<T>) -> Self {
Self {
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items,
}
@@ -191,7 +191,9 @@ where
S: Iterator,
{
fn on_tick(&mut self) {
self.points.drain(0..self.tick_rate);
for _ in 0..self.tick_rate {
self.points.remove(0);
}
self.points
.extend(self.source.by_ref().take(self.tick_rate));
}
@@ -235,7 +237,7 @@ pub struct App<'a> {
}
impl<'a> App<'a> {
pub fn new(title: &'a str, enhanced_graphics: bool) -> Self {
pub fn new(title: &'a str, enhanced_graphics: bool) -> App<'a> {
let mut rand_signal = RandomSignal::new(0, 100);
let sparkline_points = rand_signal.by_ref().take(300).collect();
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);

View File

@@ -4,13 +4,13 @@ use std::{
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
Terminal,
};
@@ -26,7 +26,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
// create app and run it
let app = App::new("Crossterm Demo", enhanced_graphics);
let app_result = run_app(&mut terminal, app, tick_rate);
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
@@ -37,7 +37,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
)?;
terminal.show_cursor()?;
if let Err(err) = app_result {
if let Err(err) = res {
println!("{err:?}");
}
@@ -51,18 +51,20 @@ fn run_app<B: Backend>(
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
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()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Left | KeyCode::Char('h') => app.on_left(),
KeyCode::Up | KeyCode::Char('k') => app.on_up(),
KeyCode::Right | KeyCode::Char('l') => app.on_right(),
KeyCode::Down | KeyCode::Char('j') => app.on_down(),
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(),
_ => {}
}
}

38
examples/demo/main.rs Normal file
View File

@@ -0,0 +1,38 @@
mod app;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termwiz")]
mod termwiz;
mod ui;
use std::{error::Error, time::Duration};
use argh::FromArgs;
#[cfg(feature = "crossterm")]
use crate::crossterm::run;
#[cfg(feature = "termion")]
use crate::termion::run;
#[cfg(feature = "termwiz")]
use crate::termwiz::run;
/// Demo
#[derive(Debug, FromArgs)]
struct Cli {
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let tick_rate = Duration::from_millis(cli.tick_rate);
run(tick_rate, cli.enhanced_graphics)?;
Ok(())
}

View File

@@ -1,16 +1,15 @@
#![allow(dead_code)]
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use ratatui::{
backend::{Backend, TermionBackend},
termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::IntoAlternateScreen,
},
Terminal,
};
use termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::IntoAlternateScreen,
};
use crate::{app::App, ui};
@@ -39,15 +38,15 @@ fn run_app<B: Backend>(
) -> Result<(), Box<dyn Error>> {
let events = events(tick_rate);
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;
match events.recv()? {
Event::Input(key) => match key {
Key::Up | Key::Char('k') => app.on_up(),
Key::Down | Key::Char('j') => app.on_down(),
Key::Left | Key::Char('h') => app.on_left(),
Key::Right | Key::Char('l') => app.on_right(),
Key::Char(c) => app.on_key(c),
Key::Up => app.on_up(),
Key::Down => app.on_down(),
Key::Left => app.on_left(),
Key::Right => app.on_right(),
_ => {}
},
Event::Tick => app.on_tick(),

View File

@@ -1,17 +1,11 @@
#![allow(dead_code)]
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use ratatui::{
backend::TermwizBackend,
termwiz::{
input::{InputEvent, KeyCode},
terminal::Terminal as TermwizTerminal,
},
Terminal,
};
use ratatui::{backend::TermwizBackend, Terminal};
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
use crate::{app::App, ui};
@@ -22,12 +16,12 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
// create app and run it
let app = App::new("Termwiz Demo", enhanced_graphics);
let app_result = run_app(&mut terminal, app, tick_rate);
let res = run_app(&mut terminal, app, tick_rate);
terminal.show_cursor()?;
terminal.flush()?;
if let Err(err) = app_result {
if let Err(err) = res {
println!("{err:?}");
}
@@ -38,24 +32,26 @@ fn run_app(
terminal: &mut Terminal<TermwizBackend>,
mut app: App,
tick_rate: Duration,
) -> Result<(), Box<dyn Error>> {
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if let Some(input) = terminal
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))?
.poll_input(Some(timeout))
{
match input {
InputEvent::Key(key_code) => match key_code.key {
KeyCode::UpArrow | KeyCode::Char('k') => app.on_up(),
KeyCode::DownArrow | KeyCode::Char('j') => app.on_down(),
KeyCode::LeftArrow | KeyCode::Char('h') => app.on_left(),
KeyCode::RightArrow | KeyCode::Char('l') => app.on_right(),
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),
_ => {}
},

View File

@@ -1,64 +1,82 @@
use ratatui::{
layout::{Constraint, Layout, Rect},
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{self, Span},
text::{Line, Span},
widgets::{
canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle},
Axis, BarChart, Block, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem, Paragraph,
Row, Sparkline, Table, Tabs, Wrap,
canvas::{Canvas, Circle, Line as CanvasLine, Map, MapResolution, Rectangle},
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
},
Frame,
};
use crate::app::App;
pub fn draw(frame: &mut Frame, app: &mut App) {
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area());
let tabs = app
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size());
let titles = app
.tabs
.titles
.iter()
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect::<Tabs>()
.block(Block::bordered().title(app.title))
.map(|t| Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title(app.title))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index);
frame.render_widget(tabs, chunks[0]);
f.render_widget(tabs, chunks[0]);
match app.tabs.index {
0 => draw_first_tab(frame, app, chunks[1]),
1 => draw_second_tab(frame, app, chunks[1]),
2 => draw_third_tab(frame, app, chunks[1]),
0 => draw_first_tab(f, app, chunks[1]),
1 => draw_second_tab(f, app, chunks[1]),
2 => draw_third_tab(f, app, chunks[1]),
_ => {}
};
}
fn draw_first_tab(frame: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
])
.split(area);
draw_gauges(frame, app, chunks[0]);
draw_charts(frame, app, chunks[1]);
draw_text(frame, chunks[2]);
fn draw_first_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.constraints(
[
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
]
.as_ref(),
)
.split(area);
draw_gauges(f, app, chunks[0]);
draw_charts(f, app, chunks[1]);
draw_text(f, chunks[2]);
}
fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(2),
])
.margin(1)
.split(area);
let block = Block::bordered().title("Graphs");
frame.render_widget(block, area);
fn draw_gauges<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.constraints(
[
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(1),
]
.as_ref(),
)
.margin(1)
.split(area);
let block = Block::default().borders(Borders::ALL).title("Graphs");
f.render_widget(block, area);
let label = format!("{:.2}%", app.progress * 100.0);
let gauge = Gauge::default()
.block(Block::new().title("Gauge:"))
.block(Block::default().title("Gauge:"))
.gauge_style(
Style::default()
.fg(Color::Magenta)
@@ -68,10 +86,10 @@ fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
.use_unicode(app.enhanced_graphics)
.label(label)
.ratio(app.progress);
frame.render_widget(gauge, chunks[0]);
f.render_widget(gauge, chunks[0]);
let sparkline = Sparkline::default()
.block(Block::new().title("Sparkline:"))
.block(Block::default().title("Sparkline:"))
.style(Style::default().fg(Color::Green))
.data(&app.sparkline.points)
.bar_set(if app.enhanced_graphics {
@@ -79,48 +97,55 @@ fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
} else {
symbols::bar::THREE_LEVELS
});
frame.render_widget(sparkline, chunks[1]);
f.render_widget(sparkline, chunks[1]);
let line_gauge = LineGauge::default()
.block(Block::new().title("LineGauge:"))
.filled_style(Style::default().fg(Color::Magenta))
.block(Block::default().title("LineGauge:"))
.gauge_style(Style::default().fg(Color::Magenta))
.line_set(if app.enhanced_graphics {
symbols::line::THICK
} else {
symbols::line::NORMAL
})
.ratio(app.progress);
frame.render_widget(line_gauge, chunks[2]);
f.render_widget(line_gauge, chunks[2]);
}
#[allow(clippy::too_many_lines)]
fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
fn draw_charts<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
let constraints = if app.show_chart {
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
} else {
vec![Constraint::Percentage(100)]
};
let chunks = Layout::horizontal(constraints).split(area);
let chunks = Layout::default()
.constraints(constraints)
.direction(Direction::Horizontal)
.split(area);
{
let chunks = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)])
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
{
let chunks =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.direction(Direction::Horizontal)
.split(chunks[0]);
// Draw tasks
let tasks: Vec<ListItem> = app
.tasks
.items
.iter()
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
.map(|i| ListItem::new(vec![Line::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks)
.block(Block::bordered().title("List"))
.block(Block::default().borders(Borders::ALL).title("List"))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> ");
frame.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
// Draw logs
let info_style = Style::default().fg(Color::Blue);
@@ -138,19 +163,19 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
"WARNING" => warning_style,
_ => info_style,
};
let content = vec![text::Line::from(vec![
let content = vec![Line::from(vec![
Span::styled(format!("{level:<9}"), s),
Span::raw(evt),
])];
ListItem::new(content)
})
.collect();
let logs = List::new(logs).block(Block::bordered().title("List"));
frame.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
}
let barchart = BarChart::default()
.block(Block::bordered().title("Bar chart"))
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
.data(&app.barchart)
.bar_width(3)
.bar_gap(2)
@@ -167,7 +192,7 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
)
.label_style(Style::default().fg(Color::Yellow))
.bar_style(Style::default().fg(Color::Green));
frame.render_widget(barchart, chunks[1]);
f.render_widget(barchart, chunks[1]);
}
if app.show_chart {
let x_labels = vec![
@@ -202,12 +227,14 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
];
let chart = Chart::new(datasets)
.block(
Block::bordered().title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
Block::default()
.title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
@@ -221,21 +248,24 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([-20.0, 20.0])
.labels([
.labels(vec![
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("0"),
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
]),
);
frame.render_widget(chart, chunks[1]);
f.render_widget(chart, chunks[1]);
}
}
fn draw_text(frame: &mut Frame, area: Rect) {
fn draw_text<B>(f: &mut Frame<B>, area: Rect)
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![
Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
Line::from(""),
Line::from(vec![
Span::from("For example: "),
Span::styled("under", Style::default().fg(Color::Red)),
Span::raw(" "),
@@ -244,7 +274,7 @@ fn draw_text(frame: &mut Frame, area: Rect) {
Span::styled("rainbow", Style::default().fg(Color::Blue)),
Span::raw("."),
]),
text::Line::from(vec![
Line::from(vec![
Span::raw("Oh and if you didn't "),
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
Span::raw(" you can "),
@@ -255,23 +285,28 @@ fn draw_text(frame: &mut Frame, area: Rect) {
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
Span::raw(".")
]),
text::Line::from(
Line::from(
"One more thing is that it should display unicode characters: 10€"
),
];
let block = Block::bordered().title(Span::styled(
let block = Block::default().borders(Borders::ALL).title(Span::styled(
"Footer",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
frame.render_widget(paragraph, area);
f.render_widget(paragraph, area);
}
fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
let chunks =
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area);
fn draw_second_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
.direction(Direction::Horizontal)
.split(area);
let up_style = Style::default().fg(Color::Green);
let failure_style = Style::default()
.fg(Color::Red)
@@ -284,24 +319,22 @@ fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
};
Row::new(vec![s.name, s.location, s.status]).style(style)
});
let table = Table::new(
rows,
[
let table = Table::new(rows)
.header(
Row::new(vec!["Server", "Location", "Status"])
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::default().title("Servers").borders(Borders::ALL))
.widths(&[
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(10),
],
)
.header(
Row::new(vec!["Server", "Location", "Status"])
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::bordered().title("Servers"));
frame.render_widget(table, chunks[0]);
]);
f.render_widget(table, chunks[0]);
let map = Canvas::default()
.block(Block::bordered().title("World"))
.block(Block::default().title("World").borders(Borders::ALL))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
@@ -323,7 +356,7 @@ fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
});
for (i, s1) in app.servers.iter().enumerate() {
for s2 in &app.servers[i + 1..] {
ctx.draw(&canvas::Line {
ctx.draw(&CanvasLine {
x1: s1.coords.1,
y1: s1.coords.0,
y2: s2.coords.0,
@@ -352,11 +385,17 @@ fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]);
frame.render_widget(map, chunks[1]);
f.render_widget(map, chunks[1]);
}
fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) {
let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area);
fn draw_third_tab<B>(f: &mut Frame<B>, _app: &mut App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.split(area);
let colors = [
Color::Reset,
Color::Black,
@@ -387,14 +426,12 @@ fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) {
Row::new(cells)
})
.collect();
let table = Table::new(
items,
[
let table = Table::new(items)
.block(Block::default().title("Colors").borders(Borders::ALL))
.widths(&[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
],
)
.block(Block::bordered().title("Colors"));
frame.render_widget(table, chunks[0]);
]);
f.render_widget(table, chunks[0]);
}

167
examples/gauge.rs Normal file
View File

@@ -0,0 +1,167 @@
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::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Gauge},
Frame, Terminal,
};
struct App {
progress1: u16,
progress2: u16,
progress3: f64,
progress4: u16,
}
impl App {
fn new() -> App {
App {
progress1: 0,
progress2: 0,
progress3: 0.45,
progress4: 0,
}
}
fn on_tick(&mut self) {
self.progress1 += 1;
if self.progress1 > 100 {
self.progress1 = 0;
}
self.progress2 += 2;
if self.progress2 > 100 {
self.progress2 = 0;
}
self.progress3 += 0.001;
if self.progress3 > 1.0 {
self.progress3 = 0.0;
}
self.progress4 += 1;
if self.progress4 > 100 {
self.progress4 = 0;
}
}
}
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::new();
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, &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()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(f.size());
let gauge = Gauge::default()
.block(Block::default().title("Gauge1").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Yellow))
.percent(app.progress1);
f.render_widget(gauge, chunks[0]);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge2").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green))
.percent(app.progress2)
.label(label);
f.render_widget(gauge, chunks[1]);
let label = Span::styled(
format!("{:.2}%", app.progress3 * 100.0),
Style::default()
.fg(Color::Red)
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
);
let gauge = Gauge::default()
.block(Block::default().title("Gauge3").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(app.progress3)
.label(label)
.use_unicode(true);
f.render_widget(gauge, chunks[2]);
let label = format!("{}/100", app.progress4);
let gauge = Gauge::default()
.block(Block::default().title("Gauge4"))
.gauge_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::ITALIC),
)
.percent(app.progress4)
.label(label);
f.render_widget(gauge, chunks[3]);
}

80
examples/hello_world.rs Normal file
View File

@@ -0,0 +1,80 @@
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::{backend::CrosstermBackend, widgets::Paragraph, Terminal};
/// 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,72 +1,36 @@
//! # [Ratatui] Inline example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{
collections::{BTreeMap, VecDeque},
error::Error,
io,
sync::mpsc,
thread,
time::{Duration, Instant},
};
use color_eyre::Result;
use rand::distributions::{Distribution, Uniform};
use ratatui::{
backend::Backend,
crossterm::event,
layout::{Constraint, Layout, Rect},
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Line, Span},
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
widgets::{block::title::Title, Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
Frame, Terminal, TerminalOptions, Viewport,
};
fn main() -> Result<()> {
color_eyre::install()?;
let mut terminal = ratatui::init_with_options(TerminalOptions {
viewport: Viewport::Inline(8),
});
let (tx, rx) = mpsc::channel();
input_handling(tx.clone());
let workers = workers(tx);
let mut downloads = downloads();
for w in &workers {
let d = downloads.next(w.id).unwrap();
w.tx.send(d).unwrap();
}
let app_result = run(&mut terminal, workers, downloads, rx);
ratatui::restore();
app_result
}
const NUM_DOWNLOADS: usize = 10;
type DownloadId = usize;
type WorkerId = usize;
enum Event {
Input(event::KeyEvent),
Input(crossterm::event::KeyEvent),
Tick,
Resize,
DownloadUpdate(WorkerId, DownloadId, f64),
DownloadDone(WorkerId, DownloadId),
}
struct Downloads {
pending: VecDeque<Download>,
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
@@ -90,31 +54,65 @@ impl Downloads {
}
}
}
struct DownloadInProgress {
id: DownloadId,
started_at: Instant,
progress: f64,
}
struct Download {
id: DownloadId,
size: usize,
}
struct Worker {
id: WorkerId,
tx: mpsc::Sender<Download>,
}
fn main() -> Result<(), Box<dyn Error>> {
crossterm::terminal::enable_raw_mode()?;
let stdout = io::stdout();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(8),
},
)?;
let (tx, rx) = mpsc::channel();
input_handling(tx.clone());
let workers = workers(tx);
let mut downloads = downloads();
for w in &workers {
let d = downloads.next(w.id).unwrap();
w.tx.send(d).unwrap();
}
run_app(&mut terminal, workers, downloads, rx)?;
crossterm::terminal::disable_raw_mode()?;
terminal.clear()?;
Ok(())
}
fn input_handling(tx: mpsc::Sender<Event>) {
let tick_rate = Duration::from_millis(200);
thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
// poll for tick rate duration, if no events, sent tick event.
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout).unwrap() {
match event::read().unwrap() {
event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout).unwrap() {
match crossterm::event::read().unwrap() {
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
crossterm::event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
_ => {}
};
}
@@ -126,7 +124,6 @@ fn input_handling(tx: mpsc::Sender<Event>) {
});
}
#[allow(clippy::cast_precision_loss, clippy::needless_pass_by_value)]
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
(0..4)
.map(|id| {
@@ -166,23 +163,22 @@ fn downloads() -> Downloads {
}
}
#[allow(clippy::needless_pass_by_value)]
fn run(
terminal: &mut Terminal<impl Backend>,
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
workers: Vec<Worker>,
mut downloads: Downloads,
rx: mpsc::Receiver<Event>,
) -> Result<()> {
) -> Result<(), Box<dyn Error>> {
let mut redraw = true;
loop {
if redraw {
terminal.draw(|frame| draw(frame, &downloads))?;
terminal.draw(|f| ui(f, &downloads))?;
}
redraw = true;
match rx.recv()? {
Event::Input(event) => {
if event.code == event::KeyCode::Char('q') {
if event.code == crossterm::event::KeyCode::Char('q') {
break;
}
}
@@ -193,7 +189,7 @@ fn run(
Event::DownloadUpdate(worker_id, _download_id, progress) => {
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
download.progress = progress;
redraw = false;
redraw = false
}
Event::DownloadDone(worker_id, download_id) => {
let download = downloads.in_progress.remove(&worker_id).unwrap();
@@ -228,25 +224,29 @@ fn run(
Ok(())
}
fn draw(frame: &mut Frame, downloads: &Downloads) {
let area = frame.area();
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
let size = f.size();
let block = Block::new().title(Line::from("Progress").centered());
frame.render_widget(block, area);
let block = Block::default().title(Title::from("Progress").alignment(Alignment::Center));
f.render_widget(block, size);
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [progress_area, main] = vertical.areas(area);
let [list_area, gauge_area] = horizontal.areas(main);
let chunks = Layout::default()
.constraints(vec![Constraint::Length(2), Constraint::Length(4)])
.margin(1)
.split(size);
// total progress
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
#[allow(clippy::cast_precision_loss)]
let progress = LineGauge::default()
.filled_style(Style::default().fg(Color::Blue))
.gauge_style(Style::default().fg(Color::Blue))
.label(format!("{done}/{NUM_DOWNLOADS}"))
.ratio(done as f64 / NUM_DOWNLOADS as f64);
frame.render_widget(progress, progress_area);
f.render_widget(progress, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
.split(chunks[1]);
// in progress downloads
let items: Vec<ListItem> = downloads
@@ -269,22 +269,21 @@ fn draw(frame: &mut Frame, downloads: &Downloads) {
})
.collect();
let list = List::new(items);
frame.render_widget(list, list_area);
f.render_widget(list, chunks[0]);
#[allow(clippy::cast_possible_truncation)]
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
let gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(download.progress / 100.0);
if gauge_area.top().saturating_add(i as u16) > area.bottom() {
if chunks[1].top().saturating_add(i as u16) > size.bottom() {
continue;
}
frame.render_widget(
f.render_widget(
gauge,
Rect {
x: gauge_area.left(),
y: gauge_area.top().saturating_add(i as u16),
width: gauge_area.width,
x: chunks[1].left(),
y: chunks[1].top().saturating_add(i as u16),
width: chunks[1].width,
height: 1,
},
);

71
examples/layout.rs Normal file
View File

@@ -0,0 +1,71 @@
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::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders},
Frame, Terminal,
};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f))?;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Percentage(80),
Constraint::Percentage(10),
]
.as_ref(),
)
.split(f.size());
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]);
}

287
examples/list.rs Normal file
View File

@@ -0,0 +1,287 @@
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 ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, ListState},
Frame, Terminal,
};
struct StatefulList<T> {
state: ListState,
items: Vec<T>,
}
impl<T> StatefulList<T> {
fn with_items(items: Vec<T>) -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items,
}
}
fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
fn unselect(&mut self) {
self.state.select(None);
}
}
/// This struct holds the current state of the app. In particular, it has the `items` field which is
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
/// widget with its state and have access to features such as natural scrolling.
///
/// Check the event handling at the bottom to see how to change the state on incoming events.
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
struct App<'a> {
items: StatefulList<(&'a str, usize)>,
events: Vec<(&'a str, &'a str)>,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
items: StatefulList::with_items(vec![
("Item0", 1),
("Item1", 2),
("Item2", 1),
("Item3", 3),
("Item4", 1),
("Item5", 4),
("Item6", 1),
("Item7", 3),
("Item8", 1),
("Item9", 6),
("Item10", 1),
("Item11", 3),
("Item12", 1),
("Item13", 2),
("Item14", 1),
("Item15", 1),
("Item16", 4),
("Item17", 1),
("Item18", 5),
("Item19", 4),
("Item20", 1),
("Item21", 2),
("Item22", 1),
("Item23", 3),
("Item24", 1),
]),
events: vec![
("Event1", "INFO"),
("Event2", "INFO"),
("Event3", "CRITICAL"),
("Event4", "ERROR"),
("Event5", "INFO"),
("Event6", "INFO"),
("Event7", "WARNING"),
("Event8", "INFO"),
("Event9", "INFO"),
("Event10", "INFO"),
("Event11", "CRITICAL"),
("Event12", "INFO"),
("Event13", "INFO"),
("Event14", "INFO"),
("Event15", "INFO"),
("Event16", "INFO"),
("Event17", "ERROR"),
("Event18", "ERROR"),
("Event19", "INFO"),
("Event20", "INFO"),
("Event21", "WARNING"),
("Event22", "INFO"),
("Event23", "INFO"),
("Event24", "WARNING"),
("Event25", "INFO"),
("Event26", "INFO"),
],
}
}
/// Rotate through the event list.
/// This only exists to simulate some kind of "progress"
fn on_tick(&mut self) {
let event = self.events.remove(0);
self.events.push(event);
}
}
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::new();
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()? {
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(),
_ => {}
}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
// Create two chunks with equal horizontal screen space
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
// Iterate through all elements in the `items` app and append some debug text to it.
let items: Vec<ListItem> = app
.items
.items
.iter()
.map(|i| {
let mut lines = vec![Line::from(i.0)];
for _ in 0..i.1 {
lines.push(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
.italic()
.into(),
);
}
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
})
.collect();
// Create a List from all list items and highlight the currently selected one
let items = List::new(items)
.block(Block::default().borders(Borders::ALL).title("List"))
.highlight_style(
Style::default()
.bg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
// We can now render the item list
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
// Let's do the same for the events.
// The event list doesn't have any state and only displays the current state of the list.
let events: Vec<ListItem> = app
.events
.iter()
.rev()
.map(|&(event, level)| {
// Colorcode the level depending on its type
let s = match level {
"CRITICAL" => Style::default().fg(Color::Red),
"ERROR" => Style::default().fg(Color::Magenta),
"WARNING" => Style::default().fg(Color::Yellow),
"INFO" => Style::default().fg(Color::Blue),
_ => 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(),
]);
// The event gets its own line
let log = Line::from(vec![event.into()]);
// Here several things happen:
// 1. Add a `---` spacing line above the final list entry
// 2. Add the Level + datetime
// 3. Add a spacer line
// 4. Add the actual event
ListItem::new(vec![
Line::from("-".repeat(chunks[1].width as usize)),
header,
Line::from(""),
log,
])
})
.collect();
let events_list = List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.start_corner(Corner::BottomLeft);
f.render_widget(events_list, chunks[1]);
}

143
examples/panic.rs Normal file
View File

@@ -0,0 +1,143 @@
//! 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.
#![deny(clippy::all)]
#![warn(clippy::pedantic, clippy::nursery)]
use std::{error::Error, io};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::Alignment,
text::Line,
widgets::{Block, Borders, Paragraph},
Frame, Terminal,
};
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());
}

167
examples/paragraph.rs Normal file
View File

@@ -0,0 +1,167 @@
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::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Masked, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame, Terminal,
};
struct App {
scroll: u16,
}
impl App {
fn new() -> App {
App { scroll: 0 }
}
fn on_tick(&mut self) {
self.scroll += 1;
self.scroll %= 10;
}
}
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::new();
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, &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()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &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)
.margin(2)
.constraints(
[
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_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),
),
]),
];
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::Gray))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), no wrap"));
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Right alignment, with wrap"))
.alignment(Alignment::Right)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::Gray))
.block(create_block("Center alignment, with wrap, with scroll"))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true })
.scroll((app.scroll, 0));
f.render_widget(paragraph, chunks[3]);
}

126
examples/popup.rs Normal file
View File

@@ -0,0 +1,126 @@
use std::{error::Error, io};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::Stylize,
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame, Terminal,
};
struct App {
show_popup: bool,
}
impl App {
fn new() -> App {
App { show_popup: false }
}
}
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 app = App::new();
let res = run_app(&mut terminal, app);
// 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) -> io::Result<()> {
loop {
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,
_ => {}
}
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let size = f.size();
let chunks = Layout::default()
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref())
.split(size);
let text = if app.show_popup {
"Press p to close the popup"
} else {
"Press p to show the popup"
};
let paragraph = Paragraph::new(text.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();
f.render_widget(block, chunks[1]);
if app.show_popup {
let block = Block::default().title("Popup").borders(Borders::ALL);
let area = centered_rect(60, 20, size);
f.render_widget(Clear, area); //this clears out the background
f.render_widget(block, area);
}
}
/// helper function to create a centered rect using up certain percentage of the available rect `r`
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
]
.as_ref(),
)
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
]
.as_ref(),
)
.split(popup_layout[1])[1]
}

255
examples/scrollbar.rs Normal file
View File

@@ -0,0 +1,255 @@
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::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Margin},
style::{Color, Modifier, Style, Stylize},
text::{Line, Masked, Span},
widgets::{
scrollbar, Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
},
Frame, Terminal,
};
#[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)
.margin(2)
.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,
);
}

177
examples/sparkline.rs Normal file
View File

@@ -0,0 +1,177 @@
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 rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, Sparkline},
Frame, Terminal,
};
#[derive(Clone)]
pub struct RandomSignal {
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> RandomSignal {
RandomSignal {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
}
}
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.distribution.sample(&mut self.rng))
}
}
struct App {
signal: RandomSignal,
data1: Vec<u64>,
data2: Vec<u64>,
data3: Vec<u64>,
}
impl App {
fn new() -> App {
let mut signal = RandomSignal::new(0, 100);
let data1 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
App {
signal,
data1,
data2,
data3,
}
}
fn on_tick(&mut self) {
let value = self.signal.next().unwrap();
self.data1.pop();
self.data1.insert(0, value);
let value = self.signal.next().unwrap();
self.data2.pop();
self.data2.insert(0, value);
let value = self.signal.next().unwrap();
self.data3.pop();
self.data3.insert(0, value);
}
}
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::new();
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, &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()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(7),
Constraint::Min(0),
]
.as_ref(),
)
.split(f.size());
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data1")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data1)
.style(Style::default().fg(Color::Yellow));
f.render_widget(sparkline, chunks[0]);
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data2")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data2)
.style(Style::default().bg(Color::Green));
f.render_widget(sparkline, chunks[1]);
// Multiline
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data3")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data3)
.style(Style::default().fg(Color::Red));
f.render_widget(sparkline, chunks[2]);
}

158
examples/table.rs Normal file
View File

@@ -0,0 +1,158 @@
use std::{error::Error, io};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Cell, Row, Table, TableState},
Frame, Terminal,
};
struct App<'a> {
state: TableState,
items: Vec<Vec<&'a str>>,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
state: TableState::default(),
items: vec![
vec!["Row11", "Row12", "Row13"],
vec!["Row21", "Row22", "Row23"],
vec!["Row31", "Row32", "Row33"],
vec!["Row41", "Row42", "Row43"],
vec!["Row51", "Row52", "Row53"],
vec!["Row61", "Row62\nTest", "Row63"],
vec!["Row71", "Row72", "Row73"],
vec!["Row81", "Row82", "Row83"],
vec!["Row91", "Row92", "Row93"],
vec!["Row101", "Row102", "Row103"],
vec!["Row111", "Row112", "Row113"],
vec!["Row121", "Row122", "Row123"],
vec!["Row131", "Row132", "Row133"],
vec!["Row141", "Row142", "Row143"],
vec!["Row151", "Row152", "Row153"],
vec!["Row161", "Row162", "Row163"],
vec!["Row171", "Row172", "Row173"],
vec!["Row181", "Row182", "Row183"],
vec!["Row191", "Row192", "Row193"],
],
}
}
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
}
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 app = App::new();
let res = run_app(&mut terminal, app);
// 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) -> io::Result<()> {
loop {
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(),
_ => {}
}
}
}
}
}
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);
let normal_style = Style::default().bg(Color::Blue);
let header_cells = ["Header1", "Header2", "Header3"]
.iter()
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
let header = Row::new(header_cells)
.style(normal_style)
.height(1)
.bottom_margin(1);
let rows = app.items.iter().map(|item| {
let height = item
.iter()
.map(|content| content.chars().filter(|c| *c == '\n').count())
.max()
.unwrap_or(0)
+ 1;
let cells = item.iter().map(|c| Cell::from(*c));
Row::new(cells).height(height as u16).bottom_margin(1)
});
let t = Table::new(rows)
.header(header)
.block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
.highlight_symbol(">> ")
.widths(&[
Constraint::Percentage(50),
Constraint::Length(30),
Constraint::Min(10),
]);
f.render_stateful_widget(t, rects[0], &mut app.state);
}

124
examples/tabs.rs Normal file
View File

@@ -0,0 +1,124 @@
use std::{error::Error, io};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style, Stylize},
text::Line,
widgets::{Block, Borders, Tabs},
Frame, Terminal,
};
struct App<'a> {
pub titles: Vec<&'a str>,
pub index: usize,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
titles: vec!["Tab0", "Tab1", "Tab2", "Tab3"],
index: 0,
}
}
pub fn next(&mut self) {
self.index = (self.index + 1) % self.titles.len();
}
pub fn previous(&mut self) {
if self.index > 0 {
self.index -= 1;
} else {
self.index = self.titles.len() - 1;
}
}
}
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 app = App::new();
let res = run_app(&mut terminal, app);
// 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) -> io::Result<()> {
loop {
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(),
_ => {}
}
}
}
}
}
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();
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()])
})
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.select(app.index)
.style(Style::default().fg(Color::Cyan))
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Black),
);
f.render_widget(tabs, chunks[0]);
let inner = match app.index {
0 => Block::default().title("Inner 0").borders(Borders::ALL),
1 => Block::default().title("Inner 1").borders(Borders::ALL),
2 => Block::default().title("Inner 2").borders(Borders::ALL),
3 => Block::default().title("Inner 3").borders(Borders::ALL),
_ => unreachable!(),
};
f.render_widget(inner, chunks[1]);
}

195
examples/user_input.rs Normal file
View File

@@ -0,0 +1,195 @@
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:
/// * 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
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, Borders, List, ListItem, Paragraph},
Frame, Terminal,
};
use unicode_width::UnicodeWidthStr;
enum InputMode {
Normal,
Editing,
}
/// App holds the state of the application
struct App {
/// Current value of the input box
input: String,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
messages: Vec<String>,
}
impl Default for App {
fn default() -> App {
App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
}
}
}
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 app = App::default();
let res = run_app(&mut terminal, app);
// 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) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &app))?;
if let Event::Key(key) = event::read()? {
match app.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('e') => {
app.input_mode = InputMode::Editing;
}
KeyCode::Char('q') => {
return Ok(());
}
_ => {}
},
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Enter => {
app.messages.push(app.input.drain(..).collect());
}
KeyCode::Char(c) => {
app.input.push(c);
}
KeyCode::Backspace => {
app.input.pop();
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
_ => {}
},
_ => {}
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
]
.as_ref(),
)
.split(f.size());
let (msg, style) = match app.input_mode {
InputMode::Normal => (
vec![
"Press ".into(),
"q".bold(),
" to exist, ".into(),
"e".bold(),
" to start editing.".bold(),
],
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(),
],
Style::default(),
),
};
let mut text = Text::from(Line::from(msg));
text.patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[0]);
let input = Paragraph::new(app.input.as_str())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, chunks[1]);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
{}
InputMode::Editing => {
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
// rendering
f.set_cursor(
// Put cursor past the end of the input text
chunks[1].x + app.input.width() as u16 + 1,
// Move one line down, from the border to the input line
chunks[1].y + 1,
)
}
}
let messages: Vec<ListItem> = app
.messages
.iter()
.enumerate()
.map(|(i, m)| {
let content = Line::from(Span::raw(format!("{i}: {m}")));
ListItem::new(content)
})
.collect();
let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, chunks[2]);
}

View File

@@ -1,50 +0,0 @@
[package]
name = "ratatui-core"
version = "0.1.0"
edition = "2021"
[features]
underline-color = []
unstable-widget-ref = []
[dependencies]
bitflags.workspace = true
cassowary.workspace = true
compact_str.workspace = true
document-features.workspace = true
instability.workspace = true
itertools.workspace = true
lru.workspace = true
paste.workspace = true
palette = { workspace = true, optional = true }
serde = { workspace = true, optional = true, features = ["derive"] }
strum = { workspace = true, features = ["derive"] }
termwiz = { workspace = true, optional = true }
unicode-segmentation.workspace = true
unicode-truncate.workspace = true
unicode-width.workspace = true
[dev-dependencies]
argh = "0.1.12"
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
crossterm = { version = "0.28.1", features = ["event-stream"] }
fakeit = "1.1"
font8x8 = "0.3.1"
futures = "0.3.30"
indoc = "2"
octocrab = "0.40.0"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.22.0"
serde_json = "1.0.109"
tokio = { version = "1.39.2", features = [
"rt",
"macros",
"time",
"rt-multi-thread",
] }
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

View File

@@ -1,344 +0,0 @@
#![warn(missing_docs)]
//! 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.
//!
//! Supported terminal backends:
//! - [Crossterm]: enable the `crossterm` feature (enabled by default) and use [`CrosstermBackend`]
//! - [Termion]: enable the `termion` feature and use [`TermionBackend`]
//! - [Termwiz]: enable the `termwiz` feature and use [`TermwizBackend`]
//!
//! Additionally, a [`TestBackend`] is provided for testing purposes.
//!
//! See the [Backend Comparison] section of the [Ratatui Website] for more details on the different
//! backends.
//!
//! Each backend supports a number of features, such as [raw mode](#raw-mode), [alternate
//! screen](#alternate-screen), and [mouse capture](#mouse-capture). These features are generally
//! not enabled by default, and must be enabled by the application before they can be used. See the
//! documentation for each backend for more details.
//!
//! Note: most applications should use the [`Terminal`] struct instead of directly calling methods
//! on the backend.
//!
//! # Example
//!
//! ```rust,no_run
//! use std::io::stdout;
//!
//! use ratatui::prelude::*;
//!
//! let backend = CrosstermBackend::new(stdout());
//! let mut terminal = Terminal::new(backend)?;
//! terminal.clear()?;
//! terminal.draw(|frame| {
//! // -- snip --
//! })?;
//! # std::io::Result::Ok(())
//! ```
//!
//! See the the [Examples] directory for more examples.
//!
//! # Raw Mode
//!
//! Raw mode is a mode where the terminal does not perform any processing or handling of the input
//! and output. This means that features such as echoing input characters, line buffering, and
//! special character processing (e.g., CTRL-C for SIGINT) are disabled. This is useful for
//! applications that want to have complete control over the terminal input and output, processing
//! each keystroke themselves.
//!
//! For example, in raw mode, the terminal will not perform line buffering on the input, so the
//! application will receive each key press as it is typed, instead of waiting for the user to
//! press enter. This makes it suitable for real-time applications like text editors,
//! terminal-based games, and more.
//!
//! Each backend handles raw mode differently, so the behavior may vary depending on the backend
//! being used. Be sure to consult the backend's specific documentation for exact details on how it
//! implements raw mode.
//! # Alternate Screen
//!
//! The alternate screen is a separate buffer that some terminals provide, distinct from the main
//! screen. When activated, the terminal will display the alternate screen, hiding the current
//! content of the main screen. Applications can write to this screen as if it were the regular
//! terminal display, but when the application exits, the terminal will switch back to the main
//! screen, and the contents of the alternate screen will be cleared. This is useful for
//! applications like text editors or terminal games that want to use the full terminal window
//! without disrupting the command line or other terminal content.
//!
//! This creates a seamless transition between the application and the regular terminal session, as
//! the content displayed before launching the application will reappear after the application
//! exits.
//!
//! Note that not all terminal emulators support the alternate screen, and even those that do may
//! handle it differently. As a result, the behavior may vary depending on the backend being used.
//! Always consult the specific backend's documentation to understand how it implements the
//! alternate screen.
//!
//! # Mouse Capture
//!
//! Mouse capture is a mode where the terminal captures mouse events such as clicks, scrolls, and
//! movement, and sends them to the application as special sequences or events. This enables the
//! application to handle and respond to mouse actions, providing a more interactive and graphical
//! user experience within the terminal. It's particularly useful for applications like
//! terminal-based games, text editors, or other programs that require more direct interaction from
//! the user.
//!
//! Each backend handles mouse capture differently, with variations in the types of events that can
//! be captured and how they are represented. As such, the behavior may vary depending on the
//! backend being used, and developers should consult the specific backend's documentation to
//! understand how it implements mouse capture.
//!
//! [`TermionBackend`]: termion/struct.TermionBackend.html
//! [`Terminal`]: crate::terminal::Terminal
//! [`TermionBackend`]: termion/struct.TermionBackend.html
//! [Crossterm]: https://crates.io/crates/crossterm
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
//! [Backend Comparison]:
//! https://ratatui.rs/concepts/backends/comparison/
//! [Ratatui Website]: https://ratatui.rs
use std::io;
use strum::{Display, EnumString};
use crate::{
buffer::Cell,
layout::{Position, Size},
};
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, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ClearType {
/// Clear the entire screen.
All,
/// Clear everything after the cursor.
AfterCursor,
/// Clear everything before the cursor.
BeforeCursor,
/// Clear the current line.
CurrentLine,
/// Clear everything from the cursor until the next newline.
UntilNewLine,
}
/// The window size in characters (columns / rows) as well as pixels.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct WindowSize {
/// Size of the window in characters (columns / rows).
pub columns_rows: Size,
/// Size of the window in pixels.
///
/// The `pixels` fields may not be implemented by all terminals and return `0,0`. See
/// <https://man7.org/linux/man-pages/man4/tty_ioctl.4.html> under section "Get and set window
/// size" / TIOCGWINSZ where the fields are commented as "unused".
pub pixels: Size,
}
/// 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.
///
/// Most applications should not need to interact with the `Backend` trait directly as the
/// [`Terminal`] struct provides a higher level interface for interacting with the terminal.
///
/// [`Terminal`]: crate::terminal::Terminal
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) -> io::Result<()>
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.
///
///
/// See also [`show_cursor`].
/// # Example
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.hide_cursor()?;
/// // do something with hidden cursor
/// backend.show_cursor()?;
/// # std::io::Result::Ok(())
/// ```
///
/// [`show_cursor`]: Self::show_cursor
fn hide_cursor(&mut self) -> io::Result<()>;
/// Show the cursor on the terminal screen.
///
/// See [`hide_cursor`] for an example.
///
/// [`hide_cursor`]: Self::hide_cursor
fn show_cursor(&mut self) -> io::Result<()>;
/// Get the current cursor position on the terminal screen.
///
/// The returned tuple contains the x and y coordinates of the cursor.
/// The origin (0, 0) is at the top left corner of the screen.
///
/// See [`set_cursor_position`] for an example.
///
/// [`set_cursor_position`]: Self::set_cursor_position
fn get_cursor_position(&mut self) -> io::Result<Position>;
/// Set the cursor position on the terminal screen to the given x and y coordinates.
///
/// The origin (0, 0) is at the top left corner of the screen.
///
/// # Example
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::layout::Position;
/// # let mut backend = TestBackend::new(80, 25);
/// backend.set_cursor_position(Position { x: 10, y: 20 })?;
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
/// # std::io::Result::Ok(())
/// ```
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()>;
/// Get the current cursor position on the terminal screen.
///
/// The returned tuple contains the x and y coordinates of the cursor. The origin
/// (0, 0) is at the top left corner of the screen.
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
/// Set the cursor position on the terminal screen to the given x and y coordinates.
///
/// The origin (0, 0) is at the top left corner of the screen.
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.set_cursor_position(Position { x, y })
}
/// Clears the whole terminal screen
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::{Backend, TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.clear()?;
/// # std::io::Result::Ok(())
/// ```
fn clear(&mut self) -> io::Result<()>;
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
///
/// This method is optional and may not be implemented by all backends. The default
/// implementation calls [`clear`] if the `clear_type` is [`ClearType::All`] and returns an
/// error otherwise.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::{prelude::*, backend::{TestBackend, ClearType}};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.clear_region(ClearType::All)?;
/// # std::io::Result::Ok(())
/// ```
///
/// # Errors
///
/// This method will return an error if the terminal screen could not be cleared. It will also
/// return an error if the `clear_type` is not supported by the backend.
///
/// [`clear`]: Self::clear
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
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 in columns/rows as a [`Size`].
///
/// The returned [`Size`] contains the width and height of the terminal screen.
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = TestBackend::new(80, 25);
/// assert_eq!(backend.size()?, Size::new(80, 25));
/// # std::io::Result::Ok(())
/// ```
fn size(&self) -> io::Result<Size>;
/// Get the size of the terminal screen in columns/rows and pixels as a [`WindowSize`].
///
/// The reason for this not returning only the pixel size, given the redundancy with the
/// `size()` method, is that the underlying backends most likely get both values with one
/// syscall, and the user is also most likely to need columns and rows along with pixel size.
fn window_size(&mut self) -> io::Result<WindowSize>;
/// Flush any buffered content to the terminal screen.
fn flush(&mut self) -> io::Result<()>;
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn clear_type_tostring() {
assert_eq!(ClearType::All.to_string(), "All");
assert_eq!(ClearType::AfterCursor.to_string(), "AfterCursor");
assert_eq!(ClearType::BeforeCursor.to_string(), "BeforeCursor");
assert_eq!(ClearType::CurrentLine.to_string(), "CurrentLine");
assert_eq!(ClearType::UntilNewLine.to_string(), "UntilNewLine");
}
#[test]
fn clear_type_from_str() {
assert_eq!("All".parse::<ClearType>(), Ok(ClearType::All));
assert_eq!(
"AfterCursor".parse::<ClearType>(),
Ok(ClearType::AfterCursor)
);
assert_eq!(
"BeforeCursor".parse::<ClearType>(),
Ok(ClearType::BeforeCursor)
);
assert_eq!(
"CurrentLine".parse::<ClearType>(),
Ok(ClearType::CurrentLine)
);
assert_eq!(
"UntilNewLine".parse::<ClearType>(),
Ok(ClearType::UntilNewLine)
);
assert_eq!("".parse::<ClearType>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -1,919 +0,0 @@
//! 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::{self, Write},
io, iter,
};
use unicode_width::UnicodeWidthStr;
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::{Buffer, Cell},
layout::{Position, Rect, Size},
};
/// A [`Backend`] implementation used for integration testing that renders to an memory buffer.
///
/// Note: that although many of the integration and unit tests in ratatui are written using this
/// backend, it is preferable to write unit tests for widgets directly against the buffer rather
/// than using this backend. This backend is intended for integration tests that test the entire
/// terminal UI.
///
/// # Example
///
/// ```rust
/// use ratatui::{backend::TestBackend, prelude::*};
///
/// let mut backend = TestBackend::new(10, 2);
/// backend.clear()?;
/// backend.assert_buffer_lines([" "; 2]);
/// # std::io::Result::Ok(())
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TestBackend {
buffer: Buffer,
scrollback: Buffer,
cursor: bool,
pos: (u16, u16),
}
/// 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) {
let mut overwritten = vec![];
let mut skip: usize = 0;
view.push('"');
for (x, c) in cells.iter().enumerate() {
if skip == 0 {
view.push_str(c.symbol());
} else {
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();
}
view.push('\n');
}
view
}
impl TestBackend {
/// Creates a new `TestBackend` with the specified width and height.
pub fn new(width: u16, height: u16) -> Self {
Self {
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
scrollback: Buffer::empty(Rect::new(0, 0, width, 0)),
cursor: false,
pos: (0, 0),
}
}
/// Returns a reference to the internal buffer of the `TestBackend`.
pub const fn buffer(&self) -> &Buffer {
&self.buffer
}
/// Returns a reference to the internal scrollback buffer of the `TestBackend`.
///
/// The scrollback buffer represents the part of the screen that is currently hidden from view,
/// but that could be accessed by scrolling back in the terminal's history. This would normally
/// be done using the terminal's scrollbar or an equivalent keyboard shortcut.
///
/// The scrollback buffer starts out empty. Lines are appended when they scroll off the top of
/// the main buffer. This happens when lines are appended to the bottom of the main buffer
/// using [`Backend::append_lines`].
///
/// The scrollback buffer has a maximum height of [`u16::MAX`]. If lines are appended to the
/// bottom of the scrollback buffer when it is at its maximum height, a corresponding number of
/// lines will be removed from the top.
pub const fn scrollback(&self) -> &Buffer {
&self.scrollback
}
/// 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));
let scrollback_height = self.scrollback.area.height;
self.scrollback
.resize(Rect::new(0, 0, width, scrollback_height));
}
/// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
///
/// This is a shortcut for `assert_eq!(self.buffer(), &expected)`.
///
/// # Panics
///
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual buffers.
#[allow(deprecated)]
#[track_caller]
pub fn assert_buffer(&self, expected: &Buffer) {
// TODO: use assert_eq!()
crate::assert_buffer_eq!(&self.buffer, expected);
}
/// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected buffer.
///
/// This is a shortcut for `assert_eq!(self.scrollback(), &expected)`.
///
/// # Panics
///
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual buffers.
#[track_caller]
pub fn assert_scrollback(&self, expected: &Buffer) {
assert_eq!(&self.scrollback, expected);
}
/// Asserts that the `TestBackend`'s scrollback buffer is empty.
///
/// # Panics
///
/// When the scrollback buffer is not equal, a panic occurs with a detailed error message
/// showing the differences between the expected and actual buffers.
pub fn assert_scrollback_empty(&self) {
let expected = Buffer {
area: Rect {
width: self.scrollback.area.width,
..Rect::ZERO
},
content: vec![],
};
self.assert_scrollback(&expected);
}
/// Asserts that the `TestBackend`'s buffer is equal to the expected lines.
///
/// This is a shortcut for `assert_eq!(self.buffer(), &Buffer::with_lines(expected))`.
///
/// # Panics
///
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual buffers.
#[track_caller]
pub fn assert_buffer_lines<'line, Lines>(&self, expected: Lines)
where
Lines: IntoIterator,
Lines::Item: Into<crate::text::Line<'line>>,
{
self.assert_buffer(&Buffer::with_lines(expected));
}
/// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected lines.
///
/// This is a shortcut for `assert_eq!(self.scrollback(), &Buffer::with_lines(expected))`.
///
/// # Panics
///
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual buffers.
#[track_caller]
pub fn assert_scrollback_lines<'line, Lines>(&self, expected: Lines)
where
Lines: IntoIterator,
Lines::Item: Into<crate::text::Line<'line>>,
{
self.assert_scrollback(&Buffer::with_lines(expected));
}
/// Asserts that the `TestBackend`'s cursor position is equal to the expected one.
///
/// This is a shortcut for `assert_eq!(self.get_cursor_position().unwrap(), expected)`.
///
/// # Panics
///
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual position.
#[track_caller]
pub fn assert_cursor_position<P: Into<Position>>(&mut self, position: P) {
let actual = self.get_cursor_position().unwrap();
assert_eq!(actual, position.into());
}
}
impl fmt::Display for TestBackend {
/// Formats the `TestBackend` for display by calling the `buffer_view` function
/// on its internal buffer.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", buffer_view(&self.buffer))
}
}
impl Backend for TestBackend {
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, c) in content {
self.buffer[(x, y)] = c.clone();
}
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.cursor = false;
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
self.cursor = true;
Ok(())
}
fn get_cursor_position(&mut self) -> io::Result<Position> {
Ok(self.pos.into())
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
self.pos = position.into().into();
Ok(())
}
fn clear(&mut self) -> io::Result<()> {
self.buffer.reset();
Ok(())
}
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
let region = match clear_type {
ClearType::All => return self.clear(),
ClearType::AfterCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
&mut self.buffer.content[index..]
}
ClearType::BeforeCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1);
&mut self.buffer.content[..index]
}
ClearType::CurrentLine => {
let line_start_index = self.buffer.index_of(0, self.pos.1);
let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
&mut self.buffer.content[line_start_index..=line_end_index]
}
ClearType::UntilNewLine => {
let index = self.buffer.index_of(self.pos.0, self.pos.1);
let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
&mut self.buffer.content[index..=line_end_index]
}
};
for cell in region {
cell.reset();
}
Ok(())
}
/// Inserts n line breaks at the current cursor position.
///
/// After the insertion, the cursor x position will be incremented by 1 (unless it's already
/// at the end of line). This is a common behaviour of terminals in raw mode.
///
/// If the number of lines to append is fewer than the number of lines in the buffer after the
/// cursor y position then the cursor is moved down by n rows.
///
/// If the number of lines to append is greater than the number of lines in the buffer after
/// the cursor y position then that number of empty lines (at most the buffer's height in this
/// case but this limit is instead replaced with scrolling in most backend implementations) will
/// be added after the current position and the cursor will be moved to the last row.
fn append_lines(&mut self, line_count: u16) -> io::Result<()> {
let Position { x: cur_x, y: cur_y } = self.get_cursor_position()?;
let Rect { width, height, .. } = self.buffer.area;
// the next column ensuring that we don't go past the last column
let new_cursor_x = cur_x.saturating_add(1).min(width.saturating_sub(1));
let max_y = height.saturating_sub(1);
let lines_after_cursor = max_y.saturating_sub(cur_y);
if line_count > lines_after_cursor {
// We need to insert blank lines at the bottom and scroll the lines from the top into
// scrollback.
let scroll_by: usize = (line_count - lines_after_cursor).into();
let width: usize = self.buffer.area.width.into();
let cells_to_scrollback = self.buffer.content.len().min(width * scroll_by);
append_to_scrollback(
&mut self.scrollback,
self.buffer.content.splice(
0..cells_to_scrollback,
iter::repeat_with(Default::default).take(cells_to_scrollback),
),
);
self.buffer.content.rotate_left(cells_to_scrollback);
append_to_scrollback(
&mut self.scrollback,
iter::repeat_with(Default::default).take(width * scroll_by - cells_to_scrollback),
);
}
let new_cursor_y = cur_y.saturating_add(line_count).min(max_y);
self.set_cursor_position(Position::new(new_cursor_x, new_cursor_y))?;
Ok(())
}
fn size(&self) -> io::Result<Size> {
Ok(self.buffer.area.as_size())
}
fn window_size(&mut self) -> io::Result<WindowSize> {
// Some arbitrary window pixel size, probably doesn't need much testing.
const WINDOW_PIXEL_SIZE: Size = Size {
width: 640,
height: 480,
};
Ok(WindowSize {
columns_rows: self.buffer.area.as_size(),
pixels: WINDOW_PIXEL_SIZE,
})
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
/// Append the provided cells to the bottom of a scrollback buffer. The number of cells must be a
/// multiple of the buffer's width. If the scrollback buffer ends up larger than 65535 lines tall,
/// then lines will be removed from the top to get it down to size.
fn append_to_scrollback(scrollback: &mut Buffer, cells: impl IntoIterator<Item = Cell>) {
scrollback.content.extend(cells);
let width = scrollback.area.width as usize;
let new_height = (scrollback.content.len() / width).min(u16::MAX as usize);
let keep_from = scrollback
.content
.len()
.saturating_sub(width * u16::MAX as usize);
scrollback.content.drain(0..keep_from);
scrollback.area.height = new_height as u16;
}
#[cfg(test)]
mod tests {
use itertools::Itertools as _;
use super::*;
#[test]
fn new() {
assert_eq!(
TestBackend::new(10, 2),
TestBackend {
buffer: Buffer::with_lines([" "; 2]),
scrollback: Buffer::empty(Rect::new(0, 0, 10, 0)),
cursor: false,
pos: (0, 0),
}
);
}
#[test]
fn test_buffer_view() {
let buffer = Buffer::with_lines(["aaaa"; 2]);
assert_eq!(buffer_view(&buffer), "\"aaaa\"\n\"aaaa\"\n");
}
#[test]
fn buffer_view_with_overwrites() {
let multi_byte_char = "👨‍👩‍👧‍👦"; // renders 8 wide
let buffer = Buffer::with_lines([multi_byte_char]);
assert_eq!(
buffer_view(&buffer),
format!(
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " "), (2, " "), (3, " "), (4, " "), (5, " "), (6, " "), (7, " ")]
"#,
)
);
}
#[test]
fn buffer() {
let backend = TestBackend::new(10, 2);
backend.assert_buffer_lines([" "; 2]);
}
#[test]
fn resize() {
let mut backend = TestBackend::new(10, 2);
backend.resize(5, 5);
backend.assert_buffer_lines([" "; 5]);
}
#[test]
fn assert_buffer() {
let backend = TestBackend::new(10, 2);
backend.assert_buffer_lines([" "; 2]);
}
#[test]
#[should_panic = "buffer contents not equal"]
fn assert_buffer_panics() {
let backend = TestBackend::new(10, 2);
backend.assert_buffer_lines(["aaaaaaaaaa"; 2]);
}
#[test]
#[should_panic = "assertion `left == right` failed"]
fn assert_scrollback_panics() {
let backend = TestBackend::new(10, 2);
backend.assert_scrollback_lines(["aaaaaaaaaa"; 2]);
}
#[test]
fn display() {
let backend = TestBackend::new(10, 2);
assert_eq!(format!("{backend}"), "\" \"\n\" \"\n");
}
#[test]
fn draw() {
let mut backend = TestBackend::new(10, 2);
let cell = Cell::new("a");
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
backend.assert_buffer_lines(["a "; 2]);
}
#[test]
fn hide_cursor() {
let mut backend = TestBackend::new(10, 2);
backend.hide_cursor().unwrap();
assert!(!backend.cursor);
}
#[test]
fn show_cursor() {
let mut backend = TestBackend::new(10, 2);
backend.show_cursor().unwrap();
assert!(backend.cursor);
}
#[test]
fn get_cursor_position() {
let mut backend = TestBackend::new(10, 2);
assert_eq!(backend.get_cursor_position().unwrap(), Position::ORIGIN);
}
#[test]
fn assert_cursor_position() {
let mut backend = TestBackend::new(10, 2);
backend.assert_cursor_position(Position::ORIGIN);
}
#[test]
fn set_cursor_position() {
let mut backend = TestBackend::new(10, 10);
backend
.set_cursor_position(Position { x: 5, y: 5 })
.unwrap();
assert_eq!(backend.pos, (5, 5));
}
#[test]
fn clear() {
let mut backend = TestBackend::new(4, 2);
let cell = Cell::new("a");
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
backend.clear().unwrap();
backend.assert_buffer_lines([" ", " "]);
}
#[test]
fn clear_region_all() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend.clear_region(ClearType::All).unwrap();
backend.assert_buffer_lines([
" ",
" ",
" ",
" ",
" ",
]);
}
#[test]
fn clear_region_after_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend
.set_cursor_position(Position { x: 3, y: 2 })
.unwrap();
backend.clear_region(ClearType::AfterCursor).unwrap();
backend.assert_buffer_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaa ",
" ",
" ",
]);
}
#[test]
fn clear_region_before_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend
.set_cursor_position(Position { x: 5, y: 3 })
.unwrap();
backend.clear_region(ClearType::BeforeCursor).unwrap();
backend.assert_buffer_lines([
" ",
" ",
" ",
" aaaaa",
"aaaaaaaaaa",
]);
}
#[test]
fn clear_region_current_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend
.set_cursor_position(Position { x: 3, y: 1 })
.unwrap();
backend.clear_region(ClearType::CurrentLine).unwrap();
backend.assert_buffer_lines([
"aaaaaaaaaa",
" ",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
}
#[test]
fn clear_region_until_new_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend
.set_cursor_position(Position { x: 3, y: 0 })
.unwrap();
backend.clear_region(ClearType::UntilNewLine).unwrap();
backend.assert_buffer_lines([
"aaa ",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
}
#[test]
fn append_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor_position(Position::ORIGIN).unwrap();
// If the cursor is not at the last line in the terminal the addition of a
// newline simply moves the cursor down and to the right
backend.append_lines(1).unwrap();
backend.assert_cursor_position(Position { x: 1, y: 1 });
backend.append_lines(1).unwrap();
backend.assert_cursor_position(Position { x: 2, y: 2 });
backend.append_lines(1).unwrap();
backend.assert_cursor_position(Position { x: 3, y: 3 });
backend.append_lines(1).unwrap();
backend.assert_cursor_position(Position { x: 4, y: 4 });
// As such the buffer should remain unchanged
backend.assert_buffer_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.assert_scrollback_empty();
}
#[test]
fn append_lines_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
// If the cursor is at the last line in the terminal the addition of a
// newline will scroll the contents of the buffer
backend
.set_cursor_position(Position { x: 0, y: 4 })
.unwrap();
backend.append_lines(1).unwrap();
backend.assert_buffer_lines([
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
]);
backend.assert_scrollback_lines(["aaaaaaaaaa"]);
// It also moves the cursor to the right, as is common of the behaviour of
// terminals in raw-mode
backend.assert_cursor_position(Position { x: 1, y: 4 });
}
#[test]
fn append_multiple_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor_position(Position::ORIGIN).unwrap();
// If the cursor is not at the last line in the terminal the addition of multiple
// newlines simply moves the cursor n lines down and to the right by 1
backend.append_lines(4).unwrap();
backend.assert_cursor_position(Position { x: 1, y: 4 });
// As such the buffer should remain unchanged
backend.assert_buffer_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.assert_scrollback_empty();
}
#[test]
fn append_multiple_lines_past_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend
.set_cursor_position(Position { x: 0, y: 3 })
.unwrap();
backend.append_lines(3).unwrap();
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
" ",
]);
backend.assert_scrollback_lines(["aaaaaaaaaa", "bbbbbbbbbb"]);
}
#[test]
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend
.set_cursor_position(Position { x: 0, y: 4 })
.unwrap();
backend.append_lines(5).unwrap();
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
" ",
" ",
" ",
" ",
" ",
]);
backend.assert_scrollback_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
}
#[test]
fn append_multiple_lines_where_cursor_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor_position(Position::ORIGIN).unwrap();
backend.append_lines(5).unwrap();
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
]);
backend.assert_scrollback_lines(["aaaaaaaaaa"]);
}
#[test]
fn append_multiple_lines_where_cursor_at_end_appends_more_than_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend
.set_cursor_position(Position { x: 0, y: 4 })
.unwrap();
backend.append_lines(8).unwrap();
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
" ",
" ",
" ",
" ",
" ",
]);
backend.assert_scrollback_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
" ",
" ",
]);
}
#[test]
fn append_lines_truncates_beyond_u16_max() -> io::Result<()> {
let mut backend = TestBackend::new(10, 5);
// Fill the scrollback with 65535 + 10 lines.
let row_count = u16::MAX as usize + 10;
for row in 0..=row_count {
if row > 4 {
backend.set_cursor_position(Position { x: 0, y: 4 })?;
backend.append_lines(1)?;
}
let cells = format!("{row:>10}").chars().map(Cell::from).collect_vec();
let content = cells
.iter()
.enumerate()
.map(|(column, cell)| (column as u16, 4.min(row) as u16, cell));
backend.draw(content)?;
}
// check that the buffer contains the last 5 lines appended
backend.assert_buffer_lines([
" 65541",
" 65542",
" 65543",
" 65544",
" 65545",
]);
// TODO: ideally this should be something like:
// let lines = (6..=65545).map(|row| format!("{row:>10}"));
// backend.assert_scrollback_lines(lines);
// but there's some truncation happening in Buffer::with_lines that needs to be fixed
assert_eq!(
Buffer {
area: Rect::new(0, 0, 10, 5),
content: backend.scrollback.content[0..10 * 5].to_vec(),
},
Buffer::with_lines([
" 6",
" 7",
" 8",
" 9",
" 10",
]),
"first 5 lines of scrollback should have been truncated"
);
assert_eq!(
Buffer {
area: Rect::new(0, 0, 10, 5),
content: backend.scrollback.content[10 * 65530..10 * 65535].to_vec(),
},
Buffer::with_lines([
" 65536",
" 65537",
" 65538",
" 65539",
" 65540",
]),
"last 5 lines of scrollback should have been appended"
);
// These checks come after the content checks as otherwise we won't see the failing content
// when these checks fail.
// Make sure the scrollback is the right size.
assert_eq!(backend.scrollback.area.width, 10);
assert_eq!(backend.scrollback.area.height, 65535);
assert_eq!(backend.scrollback.content.len(), 10 * 65535);
Ok(())
}
#[test]
fn size() {
let backend = TestBackend::new(10, 2);
assert_eq!(backend.size().unwrap(), Size::new(10, 2));
}
#[test]
fn flush() {
let mut backend = TestBackend::new(10, 2);
backend.flush().unwrap();
}
}

View File

@@ -1,9 +0,0 @@
#![warn(missing_docs)]
//! A module for the [`Buffer`] and [`Cell`] types.
mod assert;
mod buffer;
mod cell;
pub use buffer::Buffer;
pub use cell::Cell;

View File

@@ -1,69 +0,0 @@
/// Assert that two buffers are equal by comparing their areas and content.
///
/// # Panics
/// When the buffers differ this method panics and displays the differences similar to
/// `assert_eq!()`.
#[deprecated = "use assert_eq!(&actual, &expected)"]
#[macro_export]
macro_rules! assert_buffer_eq {
($actual_expr:expr, $expected_expr:expr) => {
match (&$actual_expr, &$expected_expr) {
(actual, expected) => {
assert!(
actual.area == expected.area,
"buffer areas not equal\nexpected: {expected:?}\nactual: {actual:?}",
);
let nice_diff = expected
.diff(actual)
.into_iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = &expected[(x, y)];
format!("{i}: at ({x}, {y})\n expected: {expected_cell:?}\n actual: {cell:?}")
})
.collect::<Vec<String>>()
.join("\n");
assert!(
nice_diff.is_empty(),
"buffer contents not equal\nexpected: {expected:?}\nactual: {actual:?}\ndiff:\n{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 are not equal in an unexpected way. Please open an issue about this."
);
}
}
};
}
#[allow(deprecated)]
#[cfg(test)]
mod tests {
use crate::prelude::*;
#[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 = "buffer areas not equal"]
#[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 = "buffer contents not equal"]
#[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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,297 +0,0 @@
use compact_str::CompactString;
use crate::prelude::*;
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cell {
/// The string to be drawn in the cell.
///
/// This accepts unicode grapheme clusters which might take up more than one cell.
///
/// This is a [`CompactString`] which is a wrapper around [`String`] that uses a small inline
/// buffer for short strings.
///
/// See <https://github.com/ratatui/ratatui/pull/601> for more information.
symbol: CompactString,
/// The foreground color of the cell.
pub fg: Color,
/// The background color of the cell.
pub bg: Color,
/// The underline color of the cell.
#[cfg(feature = "underline-color")]
pub underline_color: Color,
/// The modifier of the cell.
pub modifier: Modifier,
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
pub skip: bool,
}
impl Cell {
/// An empty `Cell`
pub const EMPTY: Self = Self::new(" ");
/// Creates a new `Cell` with the given symbol.
///
/// This works at compile time and puts the symbol onto the stack. Fails to build when the
/// symbol doesnt fit onto the stack and requires to be placed on the heap. Use
/// `Self::default().set_symbol()` in that case. See [`CompactString::const_new`] for more
/// details on this.
pub const fn new(symbol: &'static str) -> Self {
Self {
symbol: CompactString::const_new(symbol),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
}
/// Gets the symbol of the cell.
#[must_use]
pub fn symbol(&self) -> &str {
self.symbol.as_str()
}
/// Sets the symbol of the cell.
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol = CompactString::new(symbol);
self
}
/// Appends a symbol to the cell.
///
/// This is particularly useful for adding zero-width characters to the cell.
pub(crate) fn append_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol.push_str(symbol);
self
}
/// Sets the symbol of the cell to a single character.
pub fn set_char(&mut self, ch: char) -> &mut Self {
let mut buf = [0; 4];
self.symbol = CompactString::new(ch.encode_utf8(&mut buf));
self
}
/// Sets the foreground color of the cell.
pub fn set_fg(&mut self, color: Color) -> &mut Self {
self.fg = color;
self
}
/// Sets the background color of the cell.
pub fn set_bg(&mut self, color: Color) -> &mut Self {
self.bg = color;
self
}
/// Sets the style of the cell.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Self {
let style = style.into();
if let Some(c) = style.fg {
self.fg = c;
}
if let Some(c) = style.bg {
self.bg = c;
}
#[cfg(feature = "underline-color")]
if let Some(c) = style.underline_color {
self.underline_color = c;
}
self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
self
}
/// Returns the style of the cell.
#[must_use]
pub const fn style(&self) -> Style {
Style {
fg: Some(self.fg),
bg: Some(self.bg),
#[cfg(feature = "underline-color")]
underline_color: Some(self.underline_color),
add_modifier: self.modifier,
sub_modifier: Modifier::empty(),
}
}
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
///
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
pub fn set_skip(&mut self, skip: bool) -> &mut Self {
self.skip = skip;
self
}
/// Resets the cell to the empty state.
pub fn reset(&mut self) {
self.symbol = CompactString::const_new(" ");
self.fg = Color::Reset;
self.bg = Color::Reset;
#[cfg(feature = "underline-color")]
{
self.underline_color = Color::Reset;
}
self.modifier = Modifier::empty();
self.skip = false;
}
}
impl Default for Cell {
fn default() -> Self {
Self::EMPTY
}
}
impl From<char> for Cell {
fn from(ch: char) -> Self {
let mut cell = Self::EMPTY;
cell.set_char(ch);
cell
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
let cell = Cell::new("");
assert_eq!(
cell,
Cell {
symbol: CompactString::const_new(""),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
);
}
#[test]
fn empty() {
let cell = Cell::EMPTY;
assert_eq!(cell.symbol(), " ");
}
#[test]
fn set_symbol() {
let mut cell = Cell::EMPTY;
cell.set_symbol(""); // Multi-byte character
assert_eq!(cell.symbol(), "");
cell.set_symbol("👨‍👩‍👧‍👦"); // Multiple code units combined with ZWJ
assert_eq!(cell.symbol(), "👨‍👩‍👧‍👦");
}
#[test]
fn append_symbol() {
let mut cell = Cell::EMPTY;
cell.set_symbol(""); // Multi-byte character
cell.append_symbol("\u{200B}"); // zero-width space
assert_eq!(cell.symbol(), "\u{200B}");
}
#[test]
fn set_char() {
let mut cell = Cell::EMPTY;
cell.set_char('あ'); // Multi-byte character
assert_eq!(cell.symbol(), "");
}
#[test]
fn set_fg() {
let mut cell = Cell::EMPTY;
cell.set_fg(Color::Red);
assert_eq!(cell.fg, Color::Red);
}
#[test]
fn set_bg() {
let mut cell = Cell::EMPTY;
cell.set_bg(Color::Red);
assert_eq!(cell.bg, Color::Red);
}
#[test]
fn set_style() {
let mut cell = Cell::EMPTY;
cell.set_style(Style::new().fg(Color::Red).bg(Color::Blue));
assert_eq!(cell.fg, Color::Red);
assert_eq!(cell.bg, Color::Blue);
}
#[test]
fn set_skip() {
let mut cell = Cell::EMPTY;
cell.set_skip(true);
assert!(cell.skip);
}
#[test]
fn reset() {
let mut cell = Cell::EMPTY;
cell.set_symbol("");
cell.set_fg(Color::Red);
cell.set_bg(Color::Blue);
cell.set_skip(true);
cell.reset();
assert_eq!(cell.symbol(), " ");
assert_eq!(cell.fg, Color::Reset);
assert_eq!(cell.bg, Color::Reset);
assert!(!cell.skip);
}
#[test]
fn style() {
let cell = Cell::EMPTY;
assert_eq!(
cell.style(),
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
#[cfg(feature = "underline-color")]
underline_color: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
);
}
#[test]
fn default() {
let cell = Cell::default();
assert_eq!(cell.symbol(), " ");
}
#[test]
fn cell_eq() {
let cell1 = Cell::new("");
let cell2 = Cell::new("");
assert_eq!(cell1, cell2);
}
#[test]
fn cell_ne() {
let cell1 = Cell::new("");
let cell2 = Cell::new("");
assert_ne!(cell1, cell2);
}
}

View File

@@ -1,21 +0,0 @@
#![warn(clippy::missing_const_for_fn)]
mod alignment;
mod constraint;
mod direction;
mod flex;
mod layout;
mod margin;
mod position;
mod rect;
mod size;
pub use alignment::Alignment;
pub use constraint::Constraint;
pub use direction::Direction;
pub use flex::Flex;
pub use layout::Layout;
pub use margin::Margin;
pub use position::Position;
pub use rect::*;
pub use size::Size;

View File

@@ -1,31 +0,0 @@
use strum::{Display, EnumString};
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Alignment {
#[default]
Left,
Center,
Right,
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn alignment_to_string() {
assert_eq!(Alignment::Left.to_string(), "Left");
assert_eq!(Alignment::Center.to_string(), "Center");
assert_eq!(Alignment::Right.to_string(), "Right");
}
#[test]
fn alignment_from_str() {
assert_eq!("Left".parse::<Alignment>(), Ok(Alignment::Left));
assert_eq!("Center".parse::<Alignment>(), Ok(Alignment::Center));
assert_eq!("Right".parse::<Alignment>(), Ok(Alignment::Right));
assert_eq!("".parse::<Alignment>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -1,491 +0,0 @@
use std::fmt;
use strum::EnumIs;
/// A constraint that defines the size of a layout element.
///
/// Constraints can be used to specify a fixed size, a percentage of the available space, a ratio of
/// the available space, a minimum or maximum size or a fill proportional value for a layout
/// element.
///
/// Relative constraints (percentage, ratio) are calculated relative to the entire space being
/// divided, rather than the space available after applying more fixed constraints (min, max,
/// length).
///
/// Constraints are prioritized in the following order:
///
/// 1. [`Constraint::Min`]
/// 2. [`Constraint::Max`]
/// 3. [`Constraint::Length`]
/// 4. [`Constraint::Percentage`]
/// 5. [`Constraint::Ratio`]
/// 6. [`Constraint::Fill`]
///
/// # Examples
///
/// `Constraint` provides helper methods to create lists of constraints from various input formats.
///
/// ```rust
/// # use ratatui::prelude::*;
/// // Create a layout with specified lengths for each element
/// let constraints = Constraint::from_lengths([10, 20, 10]);
///
/// // Create a centered layout using ratio or percentage constraints
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
/// let constraints = Constraint::from_percentages([25, 50, 25]);
///
/// // Create a centered layout with a minimum size constraint for specific elements
/// let constraints = Constraint::from_mins([0, 100, 0]);
///
/// // Create a sidebar layout specifying maximum sizes for the columns
/// let constraints = Constraint::from_maxes([30, 170]);
///
/// // Create a layout with fill proportional sizes for each element
/// let constraints = Constraint::from_fills([1, 2, 1]);
/// ```
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, EnumIs)]
pub enum Constraint {
/// Applies a minimum size constraint to the element
///
/// The element size is set to at least the specified amount.
///
/// # Examples
///
/// `[Percentage(100), Min(20)]`
///
/// ```plain
/// ┌────────────────────────────┐┌──────────────────┐
/// │ 30 px ││ 20 px │
/// └────────────────────────────┘└──────────────────┘
/// ```
///
/// `[Percentage(100), Min(10)]`
///
/// ```plain
/// ┌──────────────────────────────────────┐┌────────┐
/// │ 40 px ││ 10 px │
/// └──────────────────────────────────────┘└────────┘
/// ```
Min(u16),
/// Applies a maximum size constraint to the element
///
/// The element size is set to at most the specified amount.
///
/// # Examples
///
/// `[Percentage(0), Max(20)]`
///
/// ```plain
/// ┌────────────────────────────┐┌──────────────────┐
/// │ 30 px ││ 20 px │
/// └────────────────────────────┘└──────────────────┘
/// ```
///
/// `[Percentage(0), Max(10)]`
///
/// ```plain
/// ┌──────────────────────────────────────┐┌────────┐
/// │ 40 px ││ 10 px │
/// └──────────────────────────────────────┘└────────┘
/// ```
Max(u16),
/// Applies a length constraint to the element
///
/// The element size is set to the specified amount.
///
/// # Examples
///
/// `[Length(20), Length(20)]`
///
/// ```plain
/// ┌──────────────────┐┌──────────────────┐
/// │ 20 px ││ 20 px │
/// └──────────────────┘└──────────────────┘
/// ```
///
/// `[Length(20), Length(30)]`
///
/// ```plain
/// ┌──────────────────┐┌────────────────────────────┐
/// │ 20 px ││ 30 px │
/// └──────────────────┘└────────────────────────────┘
/// ```
Length(u16),
/// Applies a percentage of the available space to the element
///
/// Converts the given percentage to a floating-point value and multiplies that with area. This
/// value is rounded back to a integer as part of the layout split calculation.
///
/// **Note**: As this value only accepts a `u16`, certain percentages that cannot be
/// represented exactly (e.g. 1/3) are not possible. You might want to use
/// [`Constraint::Ratio`] or [`Constraint::Fill`] in such cases.
///
/// # Examples
///
/// `[Percentage(75), Fill(1)]`
///
/// ```plain
/// ┌────────────────────────────────────┐┌──────────┐
/// │ 38 px ││ 12 px │
/// └────────────────────────────────────┘└──────────┘
/// ```
///
/// `[Percentage(50), Fill(1)]`
///
/// ```plain
/// ┌───────────────────────┐┌───────────────────────┐
/// │ 25 px ││ 25 px │
/// └───────────────────────┘└───────────────────────┘
/// ```
Percentage(u16),
/// Applies a ratio of the available space to the element
///
/// Converts the given ratio to a floating-point value and multiplies that with area.
/// This value is rounded back to a integer as part of the layout split calculation.
///
/// # Examples
///
/// `[Ratio(1, 2) ; 2]`
///
/// ```plain
/// ┌───────────────────────┐┌───────────────────────┐
/// │ 25 px ││ 25 px │
/// └───────────────────────┘└───────────────────────┘
/// ```
///
/// `[Ratio(1, 4) ; 4]`
///
/// ```plain
/// ┌───────────┐┌──────────┐┌───────────┐┌──────────┐
/// │ 13 px ││ 12 px ││ 13 px ││ 12 px │
/// └───────────┘└──────────┘└───────────┘└──────────┘
/// ```
Ratio(u32, u32),
/// Applies the scaling factor proportional to all other [`Constraint::Fill`] elements
/// to fill excess space
///
/// The element will only expand or fill into excess available space, proportionally matching
/// other [`Constraint::Fill`] elements while satisfying all other constraints.
///
/// # Examples
///
///
/// `[Fill(1), Fill(2), Fill(3)]`
///
/// ```plain
/// ┌──────┐┌───────────────┐┌───────────────────────┐
/// │ 8 px ││ 17 px ││ 25 px │
/// └──────┘└───────────────┘└───────────────────────┘
/// ```
///
/// `[Fill(1), Percentage(50), Fill(1)]`
///
/// ```plain
/// ┌───────────┐┌───────────────────────┐┌──────────┐
/// │ 13 px ││ 25 px ││ 12 px │
/// └───────────┘└───────────────────────┘└──────────┘
/// ```
Fill(u16),
}
impl Constraint {
#[deprecated(
since = "0.26.0",
note = "This field will be hidden in the next minor version."
)]
pub fn apply(&self, length: u16) -> u16 {
match *self {
Self::Percentage(p) => {
let p = f32::from(p) / 100.0;
let length = f32::from(length);
(p * length).min(length) as u16
}
Self::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 = f32::from(length);
(percentage * length).min(length) as u16
}
Self::Length(l) | Self::Fill(l) => length.min(l),
Self::Max(m) => length.min(m),
Self::Min(m) => length.max(m),
}
}
/// Convert an iterator of lengths into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_lengths([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_lengths<T>(lengths: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
lengths.into_iter().map(Self::Length).collect()
}
/// Convert an iterator of ratios into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_ratios<T>(ratios: T) -> Vec<Self>
where
T: IntoIterator<Item = (u32, u32)>,
{
ratios.into_iter().map(|(n, d)| Self::Ratio(n, d)).collect()
}
/// Convert an iterator of percentages into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_percentages([25, 50, 25]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_percentages<T>(percentages: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
percentages.into_iter().map(Self::Percentage).collect()
}
/// Convert an iterator of maxes into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_maxes([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_maxes<T>(maxes: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
maxes.into_iter().map(Self::Max).collect()
}
/// Convert an iterator of mins into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_mins([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_mins<T>(mins: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
mins.into_iter().map(Self::Min).collect()
}
/// Convert an iterator of proportional factors into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_fills([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_fills<T>(proportional_factors: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
proportional_factors.into_iter().map(Self::Fill).collect()
}
}
impl From<u16> for Constraint {
/// Convert a `u16` into a [`Constraint::Length`]
///
/// This is useful when you want to specify a fixed size for a layout, but don't want to
/// explicitly create a [`Constraint::Length`] yourself.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let layout = Layout::new(Direction::Vertical, [1, 2, 3]).split(area);
/// let layout = Layout::horizontal([1, 2, 3]).split(area);
/// let layout = Layout::vertical([1, 2, 3]).split(area);
/// ````
fn from(length: u16) -> Self {
Self::Length(length)
}
}
impl From<&Self> for Constraint {
fn from(constraint: &Self) -> Self {
*constraint
}
}
impl AsRef<Self> for Constraint {
fn as_ref(&self) -> &Self {
self
}
}
impl Default for Constraint {
fn default() -> Self {
Self::Percentage(100)
}
}
impl fmt::Display for Constraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Percentage(p) => write!(f, "Percentage({p})"),
Self::Ratio(n, d) => write!(f, "Ratio({n}, {d})"),
Self::Length(l) => write!(f, "Length({l})"),
Self::Fill(l) => write!(f, "Fill({l})"),
Self::Max(m) => write!(f, "Max({m})"),
Self::Min(m) => write!(f, "Min({m})"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default() {
assert_eq!(Constraint::default(), Constraint::Percentage(100));
}
#[test]
fn to_string() {
assert_eq!(Constraint::Percentage(50).to_string(), "Percentage(50)");
assert_eq!(Constraint::Ratio(1, 2).to_string(), "Ratio(1, 2)");
assert_eq!(Constraint::Length(10).to_string(), "Length(10)");
assert_eq!(Constraint::Max(10).to_string(), "Max(10)");
assert_eq!(Constraint::Min(10).to_string(), "Min(10)");
}
#[test]
fn from_lengths() {
let expected = [
Constraint::Length(1),
Constraint::Length(2),
Constraint::Length(3),
];
assert_eq!(Constraint::from_lengths([1, 2, 3]), expected);
assert_eq!(Constraint::from_lengths(vec![1, 2, 3]), expected);
}
#[test]
fn from_ratios() {
let expected = [
Constraint::Ratio(1, 4),
Constraint::Ratio(1, 2),
Constraint::Ratio(1, 4),
];
assert_eq!(Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]), expected);
assert_eq!(
Constraint::from_ratios(vec![(1, 4), (1, 2), (1, 4)]),
expected
);
}
#[test]
fn from_percentages() {
let expected = [
Constraint::Percentage(25),
Constraint::Percentage(50),
Constraint::Percentage(25),
];
assert_eq!(Constraint::from_percentages([25, 50, 25]), expected);
assert_eq!(Constraint::from_percentages(vec![25, 50, 25]), expected);
}
#[test]
fn from_maxes() {
let expected = [Constraint::Max(1), Constraint::Max(2), Constraint::Max(3)];
assert_eq!(Constraint::from_maxes([1, 2, 3]), expected);
assert_eq!(Constraint::from_maxes(vec![1, 2, 3]), expected);
}
#[test]
fn from_mins() {
let expected = [Constraint::Min(1), Constraint::Min(2), Constraint::Min(3)];
assert_eq!(Constraint::from_mins([1, 2, 3]), expected);
assert_eq!(Constraint::from_mins(vec![1, 2, 3]), expected);
}
#[test]
fn from_fills() {
let expected = [
Constraint::Fill(1),
Constraint::Fill(2),
Constraint::Fill(3),
];
assert_eq!(Constraint::from_fills([1, 2, 3]), expected);
assert_eq!(Constraint::from_fills(vec![1, 2, 3]), expected);
}
#[test]
#[allow(deprecated)]
fn 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);
}
}

View File

@@ -1,28 +0,0 @@
use strum::{Display, EnumString};
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Direction {
Horizontal,
#[default]
Vertical,
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn direction_to_string() {
assert_eq!(Direction::Horizontal.to_string(), "Horizontal");
assert_eq!(Direction::Vertical.to_string(), "Vertical");
}
#[test]
fn direction_from_str() {
assert_eq!("Horizontal".parse::<Direction>(), Ok(Direction::Horizontal));
assert_eq!("Vertical".parse::<Direction>(), Ok(Direction::Vertical));
assert_eq!("".parse::<Direction>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -1,181 +0,0 @@
use strum::{Display, EnumIs, EnumString};
#[allow(unused_imports)]
use super::constraint::Constraint;
/// Defines the options for layout flex justify content in a container.
///
/// This enumeration controls the distribution of space when layout constraints are met.
///
/// - `Legacy`: Fills the available space within the container, putting excess space into the last
/// element.
/// - `Start`: Aligns items to the start of the container.
/// - `End`: Aligns items to the end of the container.
/// - `Center`: Centers items within the container.
/// - `SpaceBetween`: Adds excess space between each element.
/// - `SpaceAround`: Adds excess space around each element.
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash, EnumIs)]
pub enum Flex {
/// Fills the available space within the container, putting excess space into the last
/// constraint of the lowest priority. This matches the default behavior of ratatui and tui
/// applications without [`Flex`]
///
/// The following examples illustrate the allocation of excess in various combinations of
/// constraints. As a refresher, the priorities of constraints are as follows:
///
/// 1. [`Constraint::Min`]
/// 2. [`Constraint::Max`]
/// 3. [`Constraint::Length`]
/// 4. [`Constraint::Percentage`]
/// 5. [`Constraint::Ratio`]
/// 6. [`Constraint::Fill`]
///
/// When every constraint is `Length`, the last element gets the excess.
///
/// ```plain
/// <----------------------------------- 80 px ------------------------------------>
/// ┌──────20 px───────┐┌──────20 px───────┐┌────────────────40 px─────────────────┐
/// │ Length(20) ││ Length(20) ││ Length(20) │
/// └──────────────────┘└──────────────────┘└──────────────────────────────────────┘
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
/// ```
///
/// Fill constraints have the lowest priority amongst all the constraints and hence
/// will always take up any excess space available.
///
/// ```plain
/// <----------------------------------- 80 px ------------------------------------>
/// ┌──────20 px───────┐┌──────20 px───────┐┌──────20 px───────┐┌──────20 px───────┐
/// │ Fill(0) ││ Max(20) ││ Length(20) ││ Length(20) │
/// └──────────────────┘└──────────────────┘└──────────────────┘└──────────────────┘
/// ^^^^^^ EXCESS ^^^^^^
/// ```
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌──────────────────────────60 px───────────────────────────┐┌──────20 px───────┐
/// │ Min(20) ││ Max(20) │
/// └──────────────────────────────────────────────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌────────────────────────────────────80 px─────────────────────────────────────┐
/// │ Max(20) │
/// └──────────────────────────────────────────────────────────────────────────────┘
/// ```
Legacy,
/// Aligns items to the start of the container.
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
/// │Percentage(20)││ Length(20) ││ Fixed(20) │
/// └──────────────┘└──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐┌──────20 px───────┐
/// │ Max(20) ││ Max(20) │
/// └──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐
/// │ Max(20) │
/// └──────────────────┘
/// ```
#[default]
Start,
/// Aligns items to the end of the container.
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
/// │Percentage(20)││ Length(20) ││ Length(20) │
/// └──────────────┘└──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐┌──────20 px───────┐
/// │ Max(20) ││ Max(20) │
/// └──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐
/// │ Max(20) │
/// └──────────────────┘
/// ```
End,
/// Centers items within the container.
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
/// │Percentage(20)││ Length(20) ││ Length(20) │
/// └──────────────┘└──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐┌──────20 px───────┐
/// │ Max(20) ││ Max(20) │
/// └──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐
/// │ Max(20) │
/// └──────────────────┘
/// ```
Center,
/// Adds excess space between each element.
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐ ┌──────20 px───────┐ ┌──────20 px───────┐
/// │Percentage(20)│ │ Length(20) │ │ Length(20) │
/// └──────────────┘ └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐ ┌──────20 px───────┐
/// │ Max(20) │ │ Max(20) │
/// └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌────────────────────────────────────80 px─────────────────────────────────────┐
/// │ Max(20) │
/// └──────────────────────────────────────────────────────────────────────────────┘
/// ```
SpaceBetween,
/// Adds excess space around each element.
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐ ┌──────20 px───────┐ ┌──────20 px───────┐
/// │Percentage(20)│ │ Length(20) │ │ Length(20) │
/// └──────────────┘ └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐ ┌──────20 px───────┐
/// │ Max(20) │ │ Max(20) │
/// └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐
/// │ Max(20) │
/// └──────────────────┘
/// ```
SpaceAround,
}
#[cfg(test)]
mod tests {}

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +0,0 @@
use std::fmt;
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Margin {
pub horizontal: u16,
pub vertical: u16,
}
impl Margin {
pub const fn new(horizontal: u16, vertical: u16) -> Self {
Self {
horizontal,
vertical,
}
}
}
impl fmt::Display for Margin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}", self.horizontal, self.vertical)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn margin_to_string() {
assert_eq!(Margin::new(1, 2).to_string(), "1x2");
}
#[test]
fn margin_new() {
assert_eq!(
Margin::new(1, 2),
Margin {
horizontal: 1,
vertical: 2
}
);
}
}

View File

@@ -1,115 +0,0 @@
#![warn(missing_docs)]
use std::fmt;
use crate::layout::Rect;
/// Position in the terminal
///
/// The position is relative to the top left corner of the terminal window, with the top left corner
/// being (0, 0). The x axis is horizontal increasing to the right, and the y axis is vertical
/// increasing downwards.
///
/// # Examples
///
/// ```
/// use ratatui::layout::{Position, Rect};
///
/// // the following are all equivalent
/// let position = Position { x: 1, y: 2 };
/// let position = Position::new(1, 2);
/// let position = Position::from((1, 2));
/// let position = Position::from(Rect::new(1, 2, 3, 4));
///
/// // position can be converted back into the components when needed
/// let (x, y) = position.into();
/// ```
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Position {
/// The x coordinate of the position
///
/// The x coordinate is relative to the left edge of the terminal window, with the left edge
/// being 0.
pub x: u16,
/// The y coordinate of the position
///
/// The y coordinate is relative to the top edge of the terminal window, with the top edge
/// being 0.
pub y: u16,
}
impl Position {
/// Position at the origin, the top left edge at 0,0
pub const ORIGIN: Self = Self { x: 0, y: 0 };
/// Create a new position
pub const fn new(x: u16, y: u16) -> Self {
Self { x, y }
}
}
impl From<(u16, u16)> for Position {
fn from((x, y): (u16, u16)) -> Self {
Self { x, y }
}
}
impl From<Position> for (u16, u16) {
fn from(position: Position) -> Self {
(position.x, position.y)
}
}
impl From<Rect> for Position {
fn from(rect: Rect) -> Self {
rect.as_position()
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
let position = Position::new(1, 2);
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
#[test]
fn from_tuple() {
let position = Position::from((1, 2));
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
#[test]
fn into_tuple() {
let position = Position::new(1, 2);
let (x, y) = position.into();
assert_eq!(x, 1);
assert_eq!(y, 2);
}
#[test]
fn from_rect() {
let rect = Rect::new(1, 2, 3, 4);
let position = Position::from(rect);
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
#[test]
fn to_string() {
let position = Position::new(1, 2);
assert_eq!(position.to_string(), "(1, 2)");
}
}

View File

@@ -1,650 +0,0 @@
#![warn(missing_docs)]
use std::{
cmp::{max, min},
fmt,
};
use super::{Position, Size};
use crate::prelude::*;
mod iter;
pub use iter::*;
/// A Rectangular area.
///
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
/// area they are supposed to render to.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rect {
/// The x coordinate of the top left corner of the `Rect`.
pub x: u16,
/// The y coordinate of the top left corner of the `Rect`.
pub y: u16,
/// The width of the `Rect`.
pub width: u16,
/// The height of the `Rect`.
pub height: u16,
}
/// Amounts by which to move a [`Rect`](super::Rect).
///
/// Positive numbers move to the right/bottom and negative to the left/top.
///
/// See [`Rect::offset`]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Offset {
/// How much to move on the X axis
pub x: i32,
/// How much to move on the Y axis
pub y: i32,
}
impl fmt::Display for Rect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
}
}
impl Rect {
/// A zero sized Rect at position 0,0
pub const ZERO: Self = Self {
x: 0,
y: 0,
width: 0,
height: 0,
};
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
/// clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
let max_area = u16::MAX;
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
let aspect_ratio = f64::from(width) / f64::from(height);
let max_area_f = f64::from(max_area);
let height_f = (max_area_f / aspect_ratio).sqrt();
let width_f = height_f * aspect_ratio;
(width_f as u16, height_f as u16)
} else {
(width, height)
};
Self {
x,
y,
width: clipped_width,
height: clipped_height,
}
}
/// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
/// clamped to `u16::MAX`.
pub const fn area(self) -> u16 {
self.width.saturating_mul(self.height)
}
/// Returns true if the `Rect` has no area.
pub const fn is_empty(self) -> bool {
self.width == 0 || self.height == 0
}
/// Returns the left coordinate of the `Rect`.
pub const fn left(self) -> u16 {
self.x
}
/// Returns the right coordinate of the `Rect`. This is the first coordinate outside of the
/// `Rect`.
///
/// If the right coordinate is larger than the maximum value of u16, it will be clamped to
/// `u16::MAX`.
pub const fn right(self) -> u16 {
self.x.saturating_add(self.width)
}
/// Returns the top coordinate of the `Rect`.
pub const fn top(self) -> u16 {
self.y
}
/// Returns the bottom coordinate of the `Rect`. This is the first coordinate outside of the
/// `Rect`.
///
/// If the bottom coordinate is larger than the maximum value of u16, it will be clamped to
/// `u16::MAX`.
pub const fn bottom(self) -> u16 {
self.y.saturating_add(self.height)
}
/// Returns a new `Rect` inside the current one, with the given margin on each side.
///
/// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
#[must_use = "method returns the modified value"]
pub const fn inner(self, margin: Margin) -> Self {
let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
let doubled_margin_vertical = margin.vertical.saturating_mul(2);
if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
Self::ZERO
} else {
Self {
x: self.x.saturating_add(margin.horizontal),
y: self.y.saturating_add(margin.vertical),
width: self.width.saturating_sub(doubled_margin_horizontal),
height: self.height.saturating_sub(doubled_margin_vertical),
}
}
}
/// Moves the `Rect` without modifying its size.
///
/// Moves the `Rect` according to the given offset without modifying its [`width`](Rect::width)
/// or [`height`](Rect::height).
/// - Positive `x` moves the whole `Rect` to the right, negative to the left.
/// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
///
/// See [`Offset`] for details.
#[must_use = "method returns the modified value"]
pub fn offset(self, offset: Offset) -> Self {
Self {
x: i32::from(self.x)
.saturating_add(offset.x)
.clamp(0, i32::from(u16::MAX - self.width)) as u16,
y: i32::from(self.y)
.saturating_add(offset.y)
.clamp(0, i32::from(u16::MAX - self.height)) as u16,
..self
}
}
/// Returns a new `Rect` that contains both the current one and the given one.
#[must_use = "method returns the modified value"]
pub fn union(self, other: Self) -> Self {
let x1 = min(self.x, other.x);
let y1 = min(self.y, other.y);
let x2 = max(self.right(), other.right());
let y2 = max(self.bottom(), other.bottom());
Self {
x: x1,
y: y1,
width: x2.saturating_sub(x1),
height: y2.saturating_sub(y1),
}
}
/// Returns a new `Rect` that is the intersection of the current one and the given one.
///
/// If the two `Rect`s do not intersect, the returned `Rect` will have no area.
#[must_use = "method returns the modified value"]
pub fn intersection(self, other: Self) -> Self {
let x1 = max(self.x, other.x);
let y1 = max(self.y, other.y);
let x2 = min(self.right(), other.right());
let y2 = min(self.bottom(), other.bottom());
Self {
x: x1,
y: y1,
width: x2.saturating_sub(x1),
height: y2.saturating_sub(y1),
}
}
/// Returns true if the two `Rect`s intersect.
pub const fn intersects(self, other: Self) -> bool {
self.x < other.right()
&& self.right() > other.x
&& self.y < other.bottom()
&& self.bottom() > other.y
}
/// Returns true if the given position is inside the `Rect`.
///
/// The position is considered inside the `Rect` if it is on the `Rect`'s border.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, layout::Position};
/// let rect = Rect::new(1, 2, 3, 4);
/// assert!(rect.contains(Position { x: 1, y: 2 }));
/// ````
pub const fn contains(self, position: Position) -> bool {
position.x >= self.x
&& position.x < self.right()
&& position.y >= self.y
&& position.y < self.bottom()
}
/// Clamp this `Rect` to fit inside the other `Rect`.
///
/// If the width or height of this `Rect` is larger than the other `Rect`, it will be clamped to
/// the other `Rect`'s width or height.
///
/// If the left or top coordinate of this `Rect` is smaller than the other `Rect`, it will be
/// clamped to the other `Rect`'s left or top coordinate.
///
/// If the right or bottom coordinate of this `Rect` is larger than the other `Rect`, it will be
/// clamped to the other `Rect`'s right or bottom coordinate.
///
/// This is different from [`Rect::intersection`] because it will move this `Rect` to fit inside
/// the other `Rect`, while [`Rect::intersection`] instead would keep this `Rect`'s position and
/// truncate its size to only that which is inside the other `Rect`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.area();
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
/// # }
/// ```
#[must_use = "method returns the modified value"]
pub fn clamp(self, other: Self) -> Self {
let width = self.width.min(other.width);
let height = self.height.min(other.height);
let x = self.x.clamp(other.x, other.right().saturating_sub(width));
let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
Self::new(x, y, width, height)
}
/// An iterator over rows within the `Rect`.
///
/// # Example
///
/// ```
/// # use ratatui::prelude::*;
/// fn render(area: Rect, buf: &mut Buffer) {
/// for row in area.rows() {
/// Line::raw("Hello, world!").render(row, buf);
/// }
/// }
/// ```
pub const fn rows(self) -> Rows {
Rows::new(self)
}
/// An iterator over columns within the `Rect`.
///
/// # Example
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// fn render(area: Rect, buf: &mut Buffer) {
/// if let Some(left) = area.columns().next() {
/// Block::new().borders(Borders::LEFT).render(left, buf);
/// }
/// }
/// ```
pub const fn columns(self) -> Columns {
Columns::new(self)
}
/// An iterator over the positions within the `Rect`.
///
/// The positions are returned in a row-major order (left-to-right, top-to-bottom).
///
/// # Example
///
/// ```
/// # use ratatui::prelude::*;
/// fn render(area: Rect, buf: &mut Buffer) {
/// for position in area.positions() {
/// buf[(position.x, position.y)].set_symbol("x");
/// }
/// }
/// ```
pub const fn positions(self) -> Positions {
Positions::new(self)
}
/// Returns a [`Position`] with the same coordinates as this `Rect`.
///
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// let rect = Rect::new(1, 2, 3, 4);
/// let position = rect.as_position();
/// ````
pub const fn as_position(self) -> Position {
Position {
x: self.x,
y: self.y,
}
}
/// Converts the `Rect` into a size struct.
pub const fn as_size(self) -> Size {
Size {
width: self.width,
height: self.height,
}
}
/// indents the x value of the `Rect` by a given `offset`
///
/// This is pub(crate) for now as we need to stabilize the naming / design of this API.
#[must_use]
pub(crate) const fn indent_x(self, offset: u16) -> Self {
Self {
x: self.x.saturating_add(offset),
width: self.width.saturating_sub(offset),
..self
}
}
}
impl From<(Position, Size)> for Rect {
fn from((position, size): (Position, Size)) -> Self {
Self {
x: position.x,
y: position.y,
width: size.width,
height: size.height,
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[test]
fn to_string() {
assert_eq!(Rect::new(1, 2, 3, 4).to_string(), "3x4+1+2");
}
#[test]
fn new() {
assert_eq!(
Rect::new(1, 2, 3, 4),
Rect {
x: 1,
y: 2,
width: 3,
height: 4
}
);
}
#[test]
fn area() {
assert_eq!(Rect::new(1, 2, 3, 4).area(), 12);
}
#[test]
fn is_empty() {
assert!(!Rect::new(1, 2, 3, 4).is_empty());
assert!(Rect::new(1, 2, 0, 4).is_empty());
assert!(Rect::new(1, 2, 3, 0).is_empty());
}
#[test]
fn left() {
assert_eq!(Rect::new(1, 2, 3, 4).left(), 1);
}
#[test]
fn right() {
assert_eq!(Rect::new(1, 2, 3, 4).right(), 4);
}
#[test]
fn top() {
assert_eq!(Rect::new(1, 2, 3, 4).top(), 2);
}
#[test]
fn bottom() {
assert_eq!(Rect::new(1, 2, 3, 4).bottom(), 6);
}
#[test]
fn inner() {
assert_eq!(
Rect::new(1, 2, 3, 4).inner(Margin::new(1, 2)),
Rect::new(2, 4, 1, 0)
);
}
#[test]
fn offset() {
assert_eq!(
Rect::new(1, 2, 3, 4).offset(Offset { x: 5, y: 6 }),
Rect::new(6, 8, 3, 4),
);
}
#[test]
fn negative_offset() {
assert_eq!(
Rect::new(4, 3, 3, 4).offset(Offset { x: -2, y: -1 }),
Rect::new(2, 2, 3, 4),
);
}
#[test]
fn negative_offset_saturate() {
assert_eq!(
Rect::new(1, 2, 3, 4).offset(Offset { x: -5, y: -6 }),
Rect::new(0, 0, 3, 4),
);
}
/// Offsets a [`Rect`] making it go outside [`u16::MAX`], it should keep its size.
#[test]
fn offset_saturate_max() {
assert_eq!(
Rect::new(u16::MAX - 500, u16::MAX - 500, 100, 100).offset(Offset { x: 1000, y: 1000 }),
Rect::new(u16::MAX - 100, u16::MAX - 100, 100, 100),
);
}
#[test]
fn union() {
assert_eq!(
Rect::new(1, 2, 3, 4).union(Rect::new(2, 3, 4, 5)),
Rect::new(1, 2, 5, 6)
);
}
#[test]
fn intersection() {
assert_eq!(
Rect::new(1, 2, 3, 4).intersection(Rect::new(2, 3, 4, 5)),
Rect::new(2, 3, 2, 3)
);
}
#[test]
fn intersection_underflow() {
assert_eq!(
Rect::new(1, 1, 2, 2).intersection(Rect::new(4, 4, 2, 2)),
Rect::new(4, 4, 0, 0)
);
}
#[test]
fn intersects() {
assert!(Rect::new(1, 2, 3, 4).intersects(Rect::new(2, 3, 4, 5)));
assert!(!Rect::new(1, 2, 3, 4).intersects(Rect::new(5, 6, 7, 8)));
}
// the bounds of this rect are x: [1..=3], y: [2..=5]
#[rstest]
#[case::inside_top_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 2 }, true)]
#[case::inside_top_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 2 }, true)]
#[case::inside_bottom_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 5 }, true)]
#[case::inside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 5 }, true)]
#[case::outside_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 2 }, false)]
#[case::outside_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 2 }, false)]
#[case::outside_top(Rect::new(1, 2, 3, 4), Position { x: 1, y: 1 }, false)]
#[case::outside_bottom(Rect::new(1, 2, 3, 4), Position { x: 1, y: 6 }, false)]
#[case::outside_top_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 1 }, false)]
#[case::outside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 6 }, false)]
fn contains(#[case] rect: Rect, #[case] position: Position, #[case] expected: bool) {
assert_eq!(
rect.contains(position),
expected,
"rect: {rect:?}, position: {position:?}",
);
}
#[test]
fn size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
);
}
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
}
#[test]
fn size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
}
}
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
}
#[test]
fn 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 split() {
let [a, b] = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(Rect::new(0, 0, 2, 1));
assert_eq!(a, Rect::new(0, 0, 1, 1));
assert_eq!(b, Rect::new(1, 0, 1, 1));
}
#[test]
#[should_panic(expected = "invalid number of rects")]
fn split_invalid_number_of_recs() {
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [_a, _b, _c] = layout.areas(Rect::new(0, 0, 2, 1));
}
#[rstest]
#[case::inside(Rect::new(20, 20, 10, 10), Rect::new(20, 20, 10, 10))]
#[case::up_left(Rect::new(5, 5, 10, 10), Rect::new(10, 10, 10, 10))]
#[case::up(Rect::new(20, 5, 10, 10), Rect::new(20, 10, 10, 10))]
#[case::up_right(Rect::new(105, 5, 10, 10), Rect::new(100, 10, 10, 10))]
#[case::left(Rect::new(5, 20, 10, 10), Rect::new(10, 20, 10, 10))]
#[case::right(Rect::new(105, 20, 10, 10), Rect::new(100, 20, 10, 10))]
#[case::down_left(Rect::new(5, 105, 10, 10), Rect::new(10, 100, 10, 10))]
#[case::down(Rect::new(20, 105, 10, 10), Rect::new(20, 100, 10, 10))]
#[case::down_right(Rect::new(105, 105, 10, 10), Rect::new(100, 100, 10, 10))]
#[case::too_wide(Rect::new(5, 20, 200, 10), Rect::new(10, 20, 100, 10))]
#[case::too_tall(Rect::new(20, 5, 10, 200), Rect::new(20, 10, 10, 100))]
#[case::too_large(Rect::new(0, 0, 200, 200), Rect::new(10, 10, 100, 100))]
fn clamp(#[case] rect: Rect, #[case] expected: Rect) {
let other = Rect::new(10, 10, 100, 100);
assert_eq!(rect.clamp(other), expected);
}
#[test]
fn rows() {
let area = Rect::new(0, 0, 3, 2);
let rows: Vec<Rect> = area.rows().collect();
let expected_rows: Vec<Rect> = vec![Rect::new(0, 0, 3, 1), Rect::new(0, 1, 3, 1)];
assert_eq!(rows, expected_rows);
}
#[test]
fn columns() {
let area = Rect::new(0, 0, 3, 2);
let columns: Vec<Rect> = area.columns().collect();
let expected_columns: Vec<Rect> = vec![
Rect::new(0, 0, 1, 2),
Rect::new(1, 0, 1, 2),
Rect::new(2, 0, 1, 2),
];
assert_eq!(columns, expected_columns);
}
#[test]
fn as_position() {
let rect = Rect::new(1, 2, 3, 4);
let position = rect.as_position();
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
#[test]
fn as_size() {
assert_eq!(
Rect::new(1, 2, 3, 4).as_size(),
Size {
width: 3,
height: 4
}
);
}
#[test]
fn from_position_and_size() {
let position = Position { x: 1, y: 2 };
let size = Size {
width: 3,
height: 4,
};
assert_eq!(
Rect::from((position, size)),
Rect {
x: 1,
y: 2,
width: 3,
height: 4
}
);
}
}

View File

@@ -1,143 +0,0 @@
use crate::prelude::*;
/// An iterator over rows within a `Rect`.
pub struct Rows {
/// The `Rect` associated with the rows.
pub rect: Rect,
/// The y coordinate of the row within the `Rect`.
pub current_row: u16,
}
impl Rows {
/// Creates a new `Rows` iterator.
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_row: rect.y,
}
}
}
impl Iterator for Rows {
type Item = Rect;
/// Retrieves the next row within the `Rect`.
///
/// Returns `None` when there are no more rows to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_row >= self.rect.bottom() {
return None;
}
let row = Rect::new(self.rect.x, self.current_row, self.rect.width, 1);
self.current_row += 1;
Some(row)
}
}
/// An iterator over columns within a `Rect`.
pub struct Columns {
/// The `Rect` associated with the columns.
pub rect: Rect,
/// The x coordinate of the column within the `Rect`.
pub current_column: u16,
}
impl Columns {
/// Creates a new `Columns` iterator.
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_column: rect.x,
}
}
}
impl Iterator for Columns {
type Item = Rect;
/// Retrieves the next column within the `Rect`.
///
/// Returns `None` when there are no more columns to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_column >= self.rect.right() {
return None;
}
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
self.current_column += 1;
Some(column)
}
}
/// An iterator over positions within a `Rect`.
///
/// The iterator will yield all positions within the `Rect` in a row-major order.
pub struct Positions {
/// The `Rect` associated with the positions.
pub rect: Rect,
/// The current position within the `Rect`.
pub current_position: Position,
}
impl Positions {
/// Creates a new `Positions` iterator.
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_position: Position::new(rect.x, rect.y),
}
}
}
impl Iterator for Positions {
type Item = Position;
/// Retrieves the next position within the `Rect`.
///
/// Returns `None` when there are no more positions to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_position.y >= self.rect.bottom() {
return None;
}
let position = self.current_position;
self.current_position.x += 1;
if self.current_position.x >= self.rect.right() {
self.current_position.x = self.rect.x;
self.current_position.y += 1;
}
Some(position)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rows() {
let rect = Rect::new(0, 0, 2, 2);
let mut rows = Rows::new(rect);
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.next(), None);
}
#[test]
fn columns() {
let rect = Rect::new(0, 0, 2, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.next(), None);
}
#[test]
fn positions() {
let rect = Rect::new(0, 0, 2, 2);
let mut positions = Positions::new(rect);
assert_eq!(positions.next(), Some(Position::new(0, 0)));
assert_eq!(positions.next(), Some(Position::new(1, 0)));
assert_eq!(positions.next(), Some(Position::new(0, 1)));
assert_eq!(positions.next(), Some(Position::new(1, 1)));
assert_eq!(positions.next(), None);
}
}

View File

@@ -1,76 +0,0 @@
#![warn(missing_docs)]
use std::fmt;
use crate::prelude::*;
/// A simple size struct
///
/// The width and height are stored as `u16` values and represent the number of columns and rows
/// respectively.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Size {
/// The width in columns
pub width: u16,
/// The height in rows
pub height: u16,
}
impl Size {
/// A zero sized Size
pub const ZERO: Self = Self::new(0, 0);
/// Create a new `Size` struct
pub const fn new(width: u16, height: u16) -> Self {
Self { width, height }
}
}
impl From<(u16, u16)> for Size {
fn from((width, height): (u16, u16)) -> Self {
Self { width, height }
}
}
impl From<Rect> for Size {
fn from(rect: Rect) -> Self {
rect.as_size()
}
}
impl fmt::Display for Size {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}x{}", self.width, self.height)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
let size = Size::new(10, 20);
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
#[test]
fn from_tuple() {
let size = Size::from((10, 20));
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
#[test]
fn from_rect() {
let size = Size::from(Rect::new(0, 0, 10, 20));
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
#[test]
fn display() {
assert_eq!(Size::new(10, 20).to_string(), "10x20");
}
}

View File

@@ -1,318 +0,0 @@
//! ![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true)
//!
//! <div align="center">
//!
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
//! Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
//! Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
//! [![Forum Badge]][Forum]<br>
//!
//! [Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
//! [Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
//!
//! </div>
//!
//! # Ratatui
//!
//! [Ratatui][Ratatui Website] is a crate for cooking up terminal user interfaces in Rust. It is a
//! lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
//! Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
//!
//! ## Installation
//!
//! Add `ratatui` as a dependency to your cargo.toml:
//!
//! ```shell
//! cargo add ratatui
//! ```
//!
//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
//! section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
//! [Termwiz]).
//!
//! ## Introduction
//!
//! Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
//! that for each frame, your app must render all widgets that are supposed to be part of the UI.
//! This is in contrast to the retained mode style of rendering where widgets are updated and then
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
//! for more info.
//!
//! You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
//! terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
//!
//! ## Other documentation
//!
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
//! - [Ratatui Forum][Forum] - a place to ask questions and discuss the library
//! - [API Docs] - the full API documentation for the library on docs.rs.
//! - [Examples] - a collection of examples that demonstrate how to use the library.
//! - [Contributing] - Please read this if you are interested in contributing to the project.
//! - [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
//! - [Breaking Changes] - a list of breaking changes in the library.
//!
//! ## 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
//! the [Examples] directory. For more guidance on different ways to structure your application see
//! the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
//! various [Examples]. There are also several starter templates available in the [templates]
//! repository.
//!
//! Every application built with `ratatui` needs to implement the following steps:
//!
//! - Initialize the terminal
//! - A main loop to:
//! - Handle input events
//! - Draw the UI
//! - Restore the terminal state
//!
//! The library contains a [`prelude`] module that re-exports the most commonly used traits and
//! types for convenience. Most examples in the documentation will use this instead of showing the
//! full path of each type.
//!
//! ### Initialize and restore the terminal
//!
//! The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
//! abstraction over a choice of [`Backend`] implementations that provides functionality to draw
//! each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
//! implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
//! [Termwiz].
//!
//! Most applications should enter the Alternate Screen when starting and leave it when exiting and
//! also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
//! module] and the [Backends] section of the [Ratatui Website] for more info.
//!
//! ### Drawing the UI
//!
//! The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
//! [`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
//! using the provided [`render_widget`] method. After this closure returns, a diff is performed and
//! only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
//! for more info.
//!
//! ### Handling events
//!
//! Ratatui does not include any input handling. Instead event handling can be implemented by
//! calling backend library methods directly. See the [Handling Events] section of the [Ratatui
//! Website] for more info. For example, if you are using [Crossterm], you can use the
//! [`crossterm::event`] module to handle events.
//!
//! ### Example
//!
//! ```rust,no_run
//! use ratatui::{
//! crossterm::event::{self, Event, KeyCode, KeyEventKind},
//! widgets::{Block, Paragraph},
//! };
//!
//! fn main() -> std::io::Result<()> {
//! let mut terminal = ratatui::init();
//! loop {
//! terminal.draw(|frame| {
//! frame.render_widget(
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
//! frame.area(),
//! );
//! })?;
//! if let Event::Key(key) = event::read()? {
//! if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
//! break;
//! }
//! }
//! }
//! ratatui::restore();
//! Ok(())
//! }
//! ```
//!
//! Running this example produces the following output:
//!
//! ![docsrs-hello]
//!
//! ## Layout
//!
//! The library comes with a basic yet useful layout management object called [`Layout`] which
//! allows you to split the available space into multiple areas and then render widgets in each
//! area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
//! section of the [Ratatui Website] for more info.
//!
//! ```rust,no_run
//! use ratatui::{
//! layout::{Constraint, Layout},
//! widgets::Block,
//! Frame,
//! };
//!
//! fn draw(frame: &mut Frame) {
//! let [title_area, main_area, status_area] = Layout::vertical([
//! Constraint::Length(1),
//! Constraint::Min(0),
//! Constraint::Length(1),
//! ])
//! .areas(frame.area());
//! let [left_area, right_area] =
//! Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
//! .areas(main_area);
//!
//! frame.render_widget(Block::bordered().title("Title Bar"), title_area);
//! frame.render_widget(Block::bordered().title("Status Bar"), status_area);
//! frame.render_widget(Block::bordered().title("Left"), left_area);
//! frame.render_widget(Block::bordered().title("Right"), right_area);
//! }
//! ```
//!
//! Running this example produces the following output:
//!
//! ![docsrs-layout]
//!
//! ## Text and styling
//!
//! The [`Text`], [`Line`] and [`Span`] types are the building blocks of the library and are used in
//! many places. [`Text`] is a list of [`Line`]s and a [`Line`] is a list of [`Span`]s. A [`Span`]
//! is a string with a specific style.
//!
//! The [`style` module] provides types that represent the various styling options. The most
//! important one is [`Style`] which represents the foreground and background colors and the text
//! attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
//! short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
//! [Ratatui Website] for more info.
//!
//! ```rust,no_run
//! use ratatui::{
//! layout::{Constraint, Layout},
//! style::{Color, Modifier, Style, Stylize},
//! text::{Line, Span},
//! widgets::{Block, Paragraph},
//! Frame,
//! };
//!
//! fn draw(frame: &mut Frame) {
//! let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
//!
//! let line = Line::from(vec![
//! Span::raw("Hello "),
//! Span::styled(
//! "World",
//! Style::new()
//! .fg(Color::Green)
//! .bg(Color::White)
//! .add_modifier(Modifier::BOLD),
//! ),
//! "!".red().on_light_yellow().italic(),
//! ]);
//! frame.render_widget(line, areas[0]);
//!
//! // using the short-hand syntax and implicit conversions
//! let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
//! frame.render_widget(paragraph, areas[1]);
//!
//! // style the whole widget instead of just the text
//! let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
//! frame.render_widget(paragraph, areas[2]);
//!
//! // use the simpler short-hand syntax
//! let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
//! frame.render_widget(paragraph, areas[3]);
//! }
//! ```
//!
//! Running this example produces the following output:
//!
//! ![docsrs-styling]
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
//! [Ratatui Website]: https://ratatui.rs/
//! [Installation]: https://ratatui.rs/installation/
//! [Rendering]: https://ratatui.rs/concepts/rendering/
//! [Application Patterns]: https://ratatui.rs/concepts/application-patterns/
//! [Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
//! [Backends]: https://ratatui.rs/concepts/backends/
//! [Widgets]: https://ratatui.rs/how-to/widgets/
//! [Handling Events]: https://ratatui.rs/concepts/event-handling/
//! [Layout]: https://ratatui.rs/how-to/layout/
//! [Styling Text]: https://ratatui.rs/how-to/render/style-text/
//! [templates]: https://github.com/ratatui/templates/
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
//! [Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
//! [Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
//! [Create a Pull Request]: https://github.com/ratatui/ratatui/compare
//! [git-cliff]: https://git-cliff.org
//! [Conventional Commits]: https://www.conventionalcommits.org
//! [API Docs]: https://docs.rs/ratatui
//! [Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
//! [Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
//! [Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
//! [FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
//! [docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
//! [docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
//! [docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
//! [`Frame`]: terminal::Frame
//! [`render_widget`]: terminal::Frame::render_widget
//! [`Widget`]: widgets::Widget
//! [`Layout`]: layout::Layout
//! [`Text`]: text::Text
//! [`Line`]: text::Line
//! [`Span`]: text::Span
//! [`Style`]: style::Style
//! [`style` module]: style
//! [`Stylize`]: style::Stylize
//! [`Backend`]: backend::Backend
//! [`backend` module]: backend
//! [`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
//! [Crate]: https://crates.io/crates/ratatui
//! [Crossterm]: https://crates.io/crates/crossterm
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz
//! [tui-rs]: https://crates.io/crates/tui
//! [GitHub Sponsors]: https://github.com/sponsors/ratatui
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
//! [CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
//! [CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
//! [Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
//! [Codecov]: https://app.codecov.io/gh/ratatui/ratatui
//! [Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
//! [Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
//! [Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
//! [Discord Server]: https://discord.gg/pMCEU9hNEj
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
//! [Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
//! [Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
//! [Forum]: https://forum.ratatui.rs
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
)]
/// re-export the `crossterm` crate so that users don't have to add it as a dependency
#[cfg(feature = "crossterm")]
pub use crossterm;
#[cfg(feature = "crossterm")]
pub use terminal::{
init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal,
};
pub use terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
/// re-export the `termion` crate so that users don't have to add it as a dependency
#[cfg(all(not(windows), feature = "termion"))]
pub use termion;
/// re-export the `termwiz` crate so that users don't have to add it as a dependency
#[cfg(feature = "termwiz")]
pub use termwiz;
pub mod backend;
pub mod buffer;
pub mod layout;
pub mod prelude;
pub mod style;
pub mod symbols;
mod terminal;
pub mod text;
pub mod widgets;

View File

@@ -1,41 +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::*};
//!
//! #[derive(Debug, Default, PartialEq, Eq)]
//! struct Line;
//!
//! assert_eq!(Line::default(), Line);
//! assert_eq!(text::Line::default(), ratatui::text::Line::from(vec![]));
//! ```
// TODO: re-export the following modules:
// #[cfg(feature = "crossterm")]
// pub use crate::backend::CrosstermBackend;
// #[cfg(all(not(windows), feature = "termion"))]
// pub use crate::backend::TermionBackend;
// #[cfg(feature = "termwiz")]
// pub use crate::backend::TermwizBackend;
pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef};
pub use crate::{
backend::{self, Backend},
buffer::{self, Buffer},
layout::{self, Alignment, Constraint, Direction, Layout, Margin, Position, Rect, Size},
style::{self, Color, Modifier, Style, Stylize},
symbols::{self},
text::{self, Line, Masked, Span, Text},
widgets::{
// block::BlockExt, // TODO
StatefulWidget,
Widget,
},
Frame, Terminal,
};

View File

@@ -1,861 +0,0 @@
//! `style` contains the primitives used to control how your user interface will look.
//!
//! There are two ways to set styles:
//! - Creating and using the [`Style`] struct. (e.g. `Style::new().fg(Color::Red)`).
//! - Using style shorthands. (e.g. `"hello".red()`).
//!
//! # Using the `Style` struct
//!
//! This is the original approach to styling and likely the most common. This is useful when
//! creating style variables to reuse, however the shorthands are often more convenient and
//! readable for most use cases.
//!
//! ## Example
//!
//! ```
//! use ratatui::prelude::*;
//!
//! let heading_style = Style::new()
//! .fg(Color::Black)
//! .bg(Color::Green)
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
//! let span = Span::styled("hello", heading_style);
//! ```
//!
//! # Using style shorthands
//!
//! Originally Ratatui only had the ability to set styles using the `Style` struct. This is still
//! supported, but there are now shorthands for all the styles that can be set. These save you from
//! having to create a `Style` struct every time you want to set a style.
//!
//! The shorthands are implemented in the [`Stylize`] trait which is automatically implemented for
//! many types via the [`Styled`] trait. This means that you can use the shorthands on any type
//! that implements [`Styled`]. E.g.:
//! - Strings and string slices when styled return a [`Span`]
//! - [`Span`]s can be styled again, which will merge the styles.
//! - Many widget types can be styled directly rather than calling their `style()` method.
//!
//! See the [`Stylize`] and [`Styled`] traits for more information. These traits are re-exported in
//! the [`prelude`] module for convenience.
//!
//! ## Example
//!
//! ```
//! use ratatui::{prelude::*, widgets::*};
//!
//! assert_eq!(
//! "hello".red().on_blue().bold(),
//! Span::styled(
//! "hello",
//! Style::default()
//! .fg(Color::Red)
//! .bg(Color::Blue)
//! .add_modifier(Modifier::BOLD)
//! )
//! );
//!
//! assert_eq!(
//! Paragraph::new("hello").red().on_blue().bold(),
//! Paragraph::new("hello").style(
//! Style::default()
//! .fg(Color::Red)
//! .bg(Color::Blue)
//! .add_modifier(Modifier::BOLD)
//! )
//! );
//! ```
//!
//! [`prelude`]: crate::prelude
//! [`Span`]: crate::text::Span
use std::fmt;
use bitflags::bitflags;
pub use color::{Color, ParseColorError};
use stylize::ColorDebugKind;
pub use stylize::{Styled, Stylize};
mod color;
pub mod palette;
#[cfg(feature = "palette")]
mod palette_conversion;
mod stylize;
bitflags! {
/// Modifier changes the way a piece of text is displayed.
///
/// They are bitflags so they can easily be composed.
///
/// `From<Modifier> for Style` is implemented so you can use `Modifier` anywhere that accepts
/// `Into<Style>`.
///
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*};
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Modifier: u16 {
const BOLD = 0b0000_0000_0001;
const DIM = 0b0000_0000_0010;
const ITALIC = 0b0000_0000_0100;
const UNDERLINED = 0b0000_0000_1000;
const SLOW_BLINK = 0b0000_0001_0000;
const RAPID_BLINK = 0b0000_0010_0000;
const REVERSED = 0b0000_0100_0000;
const HIDDEN = 0b0000_1000_0000;
const CROSSED_OUT = 0b0001_0000_0000;
}
}
/// 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");
}
write!(f, "{}", self.0)
}
}
/// Style lets you control the main characteristics of the displayed elements.
///
/// ```rust
/// use ratatui::prelude::*;
///
/// Style::default()
/// .fg(Color::Black)
/// .bg(Color::Green)
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
/// ```
///
/// Styles can also be created with a [shorthand notation](crate::style#using-style-shorthands).
///
/// ```rust
/// # use ratatui::prelude::*;
/// Style::new().black().on_green().italic().bold();
/// ```
///
/// For more information about the style shorthands, see the [`Stylize`] trait.
///
/// We implement conversions from [`Color`] and [`Modifier`] to [`Style`] so you can use them
/// anywhere that accepts `Into<Style>`.
///
/// ```rust
/// # use ratatui::prelude::*;
/// Line::styled("hello", Style::new().fg(Color::Red));
/// // simplifies to
/// Line::styled("hello", Color::Red);
///
/// Line::styled("hello", Style::new().add_modifier(Modifier::BOLD));
/// // simplifies to
/// Line::styled("hello", Modifier::BOLD);
/// ```
///
/// Styles represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
/// just S3.
///
/// ```rust
/// use ratatui::prelude::*;
///
/// let styles = [
/// Style::default()
/// .fg(Color::Blue)
/// .add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::default()
/// .bg(Color::Red)
/// .add_modifier(Modifier::UNDERLINED),
/// #[cfg(feature = "underline-color")]
/// Style::default().underline_color(Color::Green),
/// Style::default()
/// .fg(Color::Yellow)
/// .remove_modifier(Modifier::ITALIC),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Red),
/// #[cfg(feature = "underline-color")]
/// underline_color: Some(Color::Green),
/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED,
/// sub_modifier: Modifier::empty(),
/// },
/// buffer[(0, 0)].style(),
/// );
/// ```
///
/// The default implementation returns a `Style` that does not modify anything. If you wish to
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// use ratatui::prelude::*;
///
/// let styles = [
/// Style::default()
/// .fg(Color::Blue)
/// .add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::reset().fg(Color::Yellow),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Reset),
/// #[cfg(feature = "underline-color")]
/// underline_color: Some(Color::Reset),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// buffer[(0, 0)].style(),
/// );
/// ```
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
#[cfg(feature = "underline-color")]
pub underline_color: Option<Color>,
pub add_modifier: Modifier,
pub sub_modifier: Modifier,
}
/// A custom debug implementation that prints only the fields that are not the default, and unwraps
/// the `Option`s.
impl fmt::Debug for Style {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("Style::new()")?;
if let Some(fg) = self.fg {
fg.stylize_debug(ColorDebugKind::Foreground).fmt(f)?;
}
if let Some(bg) = self.bg {
bg.stylize_debug(ColorDebugKind::Background).fmt(f)?;
}
#[cfg(feature = "underline-color")]
if let Some(underline_color) = self.underline_color {
underline_color
.stylize_debug(ColorDebugKind::Underline)
.fmt(f)?;
}
for modifier in self.add_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".bold()")?,
Modifier::DIM => f.write_str(".dim()")?,
Modifier::ITALIC => f.write_str(".italic()")?,
Modifier::UNDERLINED => f.write_str(".underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".rapid_blink()")?,
Modifier::REVERSED => f.write_str(".reversed()")?,
Modifier::HIDDEN => f.write_str(".hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".crossed_out()")?,
_ => f.write_fmt(format_args!(".add_modifier(Modifier::{modifier:?})"))?,
}
}
for modifier in self.sub_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".not_bold()")?,
Modifier::DIM => f.write_str(".not_dim()")?,
Modifier::ITALIC => f.write_str(".not_italic()")?,
Modifier::UNDERLINED => f.write_str(".not_underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".not_slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".not_rapid_blink()")?,
Modifier::REVERSED => f.write_str(".not_reversed()")?,
Modifier::HIDDEN => f.write_str(".not_hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".not_crossed_out()")?,
_ => f.write_fmt(format_args!(".remove_modifier(Modifier::{modifier:?})"))?,
}
}
Ok(())
}
}
impl Styled for Style {
type Item = Self;
fn style(&self) -> Style {
*self
}
fn set_style<S: Into<Self>>(self, style: S) -> Self::Item {
self.patch(style)
}
}
impl Style {
pub const fn new() -> Self {
Self {
fg: None,
bg: None,
#[cfg(feature = "underline-color")]
underline_color: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
}
/// Returns a `Style` resetting all properties.
pub const fn reset() -> Self {
Self {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
#[cfg(feature = "underline-color")]
underline_color: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::all(),
}
}
/// Changes the foreground color.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
/// ```
#[must_use = "`fg` returns the modified style without modifying the original"]
pub const fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
/// Changes the background color.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
/// ```
#[must_use = "`bg` returns the modified style without modifying the original"]
pub const fn bg(mut self, color: Color) -> Self {
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 and enabled by the `underline-color`
/// feature flag.
///
/// 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::prelude::*;
/// 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 = "underline-color")]
#[must_use = "`underline_color` returns the modified style without modifying the original"]
pub const fn underline_color(mut self, color: Color) -> Self {
self.underline_color = Some(color);
self
}
/// Changes the text emphasis.
///
/// When applied, it adds the given modifier to the `Style` modifiers.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// 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());
/// ```
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
pub const fn add_modifier(mut self, modifier: Modifier) -> Self {
self.sub_modifier = self.sub_modifier.difference(modifier);
self.add_modifier = self.add_modifier.union(modifier);
self
}
/// Changes the text emphasis.
///
/// When applied, it removes the given modifier from the `Style` modifiers.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// 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);
/// ```
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
pub const fn remove_modifier(mut self, modifier: Modifier) -> Self {
self.add_modifier = self.add_modifier.difference(modifier);
self.sub_modifier = self.sub_modifier.union(modifier);
self
}
/// Results in a combined style that is equivalent to applying the two individual styles to
/// a style one after the other.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// ## Examples
/// ```
/// # use ratatui::prelude::*;
/// let style_1 = Style::default().fg(Color::Yellow);
/// let style_2 = Style::default().bg(Color::Red);
/// let combined = style_1.patch(style_2);
/// assert_eq!(
/// Style::default().patch(style_1).patch(style_2),
/// Style::default().patch(combined)
/// );
/// ```
#[must_use = "`patch` returns the modified style without modifying the original"]
pub fn patch<S: Into<Self>>(mut self, other: S) -> Self {
let other = other.into();
self.fg = other.fg.or(self.fg);
self.bg = other.bg.or(self.bg);
#[cfg(feature = "underline-color")]
{
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);
self.sub_modifier.insert(other.sub_modifier);
self
}
}
impl From<Color> for Style {
/// Creates a new `Style` with the given foreground color.
///
/// To specify a foreground and background color, use the `from((fg, bg))` constructor.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::from(Color::Red);
/// ```
fn from(color: Color) -> Self {
Self::new().fg(color)
}
}
impl From<(Color, Color)> for Style {
/// Creates a new `Style` with the given foreground and background colors.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // red foreground, blue background
/// let style = Style::from((Color::Red, Color::Blue));
/// // default foreground, blue background
/// let style = Style::from((Color::Reset, Color::Blue));
/// ```
fn from((fg, bg): (Color, Color)) -> Self {
Self::new().fg(fg).bg(bg)
}
}
impl From<Modifier> for Style {
/// Creates a new `Style` with the given modifier added.
///
/// To specify multiple modifiers, use the `|` operator.
///
/// To specify modifiers to add and remove, use the `from((add_modifier, sub_modifier))`
/// constructor.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // add bold and italic
/// let style = Style::from(Modifier::BOLD|Modifier::ITALIC);
fn from(modifier: Modifier) -> Self {
Self::new().add_modifier(modifier)
}
}
impl From<(Modifier, Modifier)> for Style {
/// Creates a new `Style` with the given modifiers added and removed.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // add bold and italic, remove dim
/// let style = Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
/// ```
fn from((add_modifier, sub_modifier): (Modifier, Modifier)) -> Self {
Self::new()
.add_modifier(add_modifier)
.remove_modifier(sub_modifier)
}
}
impl From<(Color, Modifier)> for Style {
/// Creates a new `Style` with the given foreground color and modifier added.
///
/// To specify multiple modifiers, use the `|` operator.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // red foreground, add bold and italic
/// let style = Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC));
/// ```
fn from((fg, modifier): (Color, Modifier)) -> Self {
Self::new().fg(fg).add_modifier(modifier)
}
}
impl From<(Color, Color, Modifier)> for Style {
/// Creates a new `Style` with the given foreground and background colors and modifier added.
///
/// To specify multiple modifiers, use the `|` operator.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // red foreground, blue background, add bold and italic
/// let style = Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC));
/// ```
fn from((fg, bg, modifier): (Color, Color, Modifier)) -> Self {
Self::new().fg(fg).bg(bg).add_modifier(modifier)
}
}
impl From<(Color, Color, Modifier, Modifier)> for Style {
/// Creates a new `Style` with the given foreground and background colors and modifiers added
/// and removed.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // red foreground, blue background, add bold and italic, remove dim
/// let style = Style::from((
/// Color::Red,
/// Color::Blue,
/// Modifier::BOLD | Modifier::ITALIC,
/// Modifier::DIM,
/// ));
/// ```
fn from((fg, bg, add_modifier, sub_modifier): (Color, Color, Modifier, Modifier)) -> Self {
Self::new()
.fg(fg)
.bg(bg)
.add_modifier(add_modifier)
.remove_modifier(sub_modifier)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case(Style::new(), "Style::new()")]
#[case(Style::new().red(), "Style::new().red()")]
#[case(Style::new().on_blue(), "Style::new().on_blue()")]
#[case(Style::new().bold(), "Style::new().bold()")]
#[case(Style::new().not_italic(), "Style::new().not_italic()")]
fn debug(#[case] style: Style, #[case] expected: &'static str) {
assert_eq!(format!("{style:?}"), expected);
}
#[test]
fn combined_patch_gives_same_result_as_individual_patch() {
let styles = [
Style::new(),
Style::new().fg(Color::Yellow),
Style::new().bg(Color::Yellow),
Style::new().add_modifier(Modifier::BOLD),
Style::new().remove_modifier(Modifier::BOLD),
Style::new().add_modifier(Modifier::ITALIC),
Style::new().remove_modifier(Modifier::ITALIC),
Style::new().add_modifier(Modifier::ITALIC | Modifier::BOLD),
Style::new().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
];
for &a in &styles {
for &b in &styles {
for &c in &styles {
for &d in &styles {
assert_eq!(
Style::new().patch(a).patch(b).patch(c).patch(d),
Style::new().patch(a.patch(b.patch(c.patch(d))))
);
}
}
}
}
}
#[test]
fn combine_individual_modifiers() {
use crate::{buffer::Buffer, layout::Rect};
let mods = [
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[(0, 0)].set_style(Style::reset());
buffer[(0, 0)].set_style(Style::new().add_modifier(m));
let style = buffer[(0, 0)].style();
assert!(style.add_modifier.contains(m));
assert!(!style.sub_modifier.contains(m));
}
}
#[rstest]
#[case(Modifier::empty(), "NONE")]
#[case(Modifier::BOLD, "BOLD")]
#[case(Modifier::DIM, "DIM")]
#[case(Modifier::ITALIC, "ITALIC")]
#[case(Modifier::UNDERLINED, "UNDERLINED")]
#[case(Modifier::SLOW_BLINK, "SLOW_BLINK")]
#[case(Modifier::RAPID_BLINK, "RAPID_BLINK")]
#[case(Modifier::REVERSED, "REVERSED")]
#[case(Modifier::HIDDEN, "HIDDEN")]
#[case(Modifier::CROSSED_OUT, "CROSSED_OUT")]
#[case(Modifier::BOLD | Modifier::DIM, "BOLD | DIM")]
#[case(Modifier::all(), "BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT")]
fn modifier_debug(#[case] modifier: Modifier, #[case] expected: &str) {
assert_eq!(format!("{modifier:?}"), expected);
}
#[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)
);
}
#[rstest]
#[case(Style::new().black(), Color::Black)]
#[case(Style::new().red(), Color::Red)]
#[case(Style::new().green(), Color::Green)]
#[case(Style::new().yellow(), Color::Yellow)]
#[case(Style::new().blue(), Color::Blue)]
#[case(Style::new().magenta(), Color::Magenta)]
#[case(Style::new().cyan(), Color::Cyan)]
#[case(Style::new().white(), Color::White)]
#[case(Style::new().gray(), Color::Gray)]
#[case(Style::new().dark_gray(), Color::DarkGray)]
#[case(Style::new().light_red(), Color::LightRed)]
#[case(Style::new().light_green(), Color::LightGreen)]
#[case(Style::new().light_yellow(), Color::LightYellow)]
#[case(Style::new().light_blue(), Color::LightBlue)]
#[case(Style::new().light_magenta(), Color::LightMagenta)]
#[case(Style::new().light_cyan(), Color::LightCyan)]
#[case(Style::new().white(), Color::White)]
fn fg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
assert_eq!(stylized, Style::new().fg(expected));
}
#[rstest]
#[case(Style::new().on_black(), Color::Black)]
#[case(Style::new().on_red(), Color::Red)]
#[case(Style::new().on_green(), Color::Green)]
#[case(Style::new().on_yellow(), Color::Yellow)]
#[case(Style::new().on_blue(), Color::Blue)]
#[case(Style::new().on_magenta(), Color::Magenta)]
#[case(Style::new().on_cyan(), Color::Cyan)]
#[case(Style::new().on_white(), Color::White)]
#[case(Style::new().on_gray(), Color::Gray)]
#[case(Style::new().on_dark_gray(), Color::DarkGray)]
#[case(Style::new().on_light_red(), Color::LightRed)]
#[case(Style::new().on_light_green(), Color::LightGreen)]
#[case(Style::new().on_light_yellow(), Color::LightYellow)]
#[case(Style::new().on_light_blue(), Color::LightBlue)]
#[case(Style::new().on_light_magenta(), Color::LightMagenta)]
#[case(Style::new().on_light_cyan(), Color::LightCyan)]
#[case(Style::new().on_white(), Color::White)]
fn bg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
assert_eq!(stylized, Style::new().bg(expected));
}
#[rstest]
#[case(Style::new().bold(), Modifier::BOLD)]
#[case(Style::new().dim(), Modifier::DIM)]
#[case(Style::new().italic(), Modifier::ITALIC)]
#[case(Style::new().underlined(), Modifier::UNDERLINED)]
#[case(Style::new().slow_blink(), Modifier::SLOW_BLINK)]
#[case(Style::new().rapid_blink(), Modifier::RAPID_BLINK)]
#[case(Style::new().reversed(), Modifier::REVERSED)]
#[case(Style::new().hidden(), Modifier::HIDDEN)]
#[case(Style::new().crossed_out(), Modifier::CROSSED_OUT)]
fn add_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
assert_eq!(stylized, Style::new().add_modifier(expected));
}
#[rstest]
#[case(Style::new().not_bold(), Modifier::BOLD)]
#[case(Style::new().not_dim(), Modifier::DIM)]
#[case(Style::new().not_italic(), Modifier::ITALIC)]
#[case(Style::new().not_underlined(), Modifier::UNDERLINED)]
#[case(Style::new().not_slow_blink(), Modifier::SLOW_BLINK)]
#[case(Style::new().not_rapid_blink(), Modifier::RAPID_BLINK)]
#[case(Style::new().not_reversed(), Modifier::REVERSED)]
#[case(Style::new().not_hidden(), Modifier::HIDDEN)]
#[case(Style::new().not_crossed_out(), Modifier::CROSSED_OUT)]
fn remove_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
assert_eq!(stylized, Style::new().remove_modifier(expected));
}
#[test]
fn reset_can_be_stylized() {
assert_eq!(Style::new().reset(), Style::reset());
}
#[test]
fn from_color() {
assert_eq!(Style::from(Color::Red), Style::new().fg(Color::Red));
}
#[test]
fn from_color_color() {
assert_eq!(
Style::from((Color::Red, Color::Blue)),
Style::new().fg(Color::Red).bg(Color::Blue)
);
}
#[test]
fn from_modifier() {
assert_eq!(
Style::from(Modifier::BOLD | Modifier::ITALIC),
Style::new()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
);
}
#[test]
fn from_modifier_modifier() {
assert_eq!(
Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM)),
Style::new()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
.remove_modifier(Modifier::DIM)
);
}
#[test]
fn from_color_modifier() {
assert_eq!(
Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC)),
Style::new()
.fg(Color::Red)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
);
}
#[test]
fn from_color_color_modifier() {
assert_eq!(
Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC)),
Style::new()
.fg(Color::Red)
.bg(Color::Blue)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
);
}
#[test]
fn from_color_color_modifier_modifier() {
assert_eq!(
Style::from((
Color::Red,
Color::Blue,
Modifier::BOLD | Modifier::ITALIC,
Modifier::DIM
)),
Style::new()
.fg(Color::Red)
.bg(Color::Blue)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
.remove_modifier(Modifier::DIM)
);
}
}

View File

@@ -1,712 +0,0 @@
#![allow(clippy::unreadable_literal)]
use std::{fmt, str::FromStr};
use crate::style::stylize::{ColorDebug, ColorDebugKind};
/// ANSI Color
///
/// All colors from the [ANSI color table] 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
///
/// `From<Color> for Style` is implemented by creating a style with the foreground color set to the
/// given color. This allows you to use colors anywhere that accepts `Into<Style>`.
///
/// # Example
///
/// ```
/// use std::str::FromStr;
///
/// use ratatui::prelude::*;
///
/// 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));
/// ```
///
/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
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.
///
/// Note that only terminals that support 24-bit true color will display this correctly.
/// Notably versions of Windows Terminal prior to Windows 10 and macOS Terminal.app do not
/// support this.
///
/// If the terminal does not support true color, code using the [`TermwizBackend`] will
/// fallback to the default text color. Crossterm and Termion do not have this capability and
/// the display will be unpredictable (e.g. Terminal.app may display glitched blinking text).
/// See <https://github.com/ratatui/ratatui/issues/475> for an example of this problem.
///
/// See also: <https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit>
///
/// [`TermwizBackend`]: crate::backend::TermwizBackend
Rgb(u8, u8, u8),
/// An 8-bit 256 color.
///
/// See also <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>
Indexed(u8),
}
impl Color {
/// Convert a u32 to a Color
///
/// The u32 should be in the format 0x00RRGGBB.
pub const fn from_u32(u: u32) -> Self {
let r = (u >> 16) as u8;
let g = (u >> 8) as u8;
let b = u as u8;
Self::Rgb(r, g, b)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Color {
/// This utilises the [`fmt::Display`] implementation for serialization.
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Color {
/// This is used to deserialize a value into Color via serde.
///
/// This implementation uses the `FromStr` trait to deserialize strings, so named colours, RGB,
/// and indexed values are able to be deserialized. In addition, values that were produced by
/// the the older serialization implementation of Color are also able to be deserialized.
///
/// Prior to v0.26.0, Ratatui would be serialized using a map for indexed and RGB values, for
/// examples in json `{"Indexed": 10}` and `{"Rgb": [255, 0, 255]}` respectively. Now they are
/// serialized using the string representation of the index and the RGB hex value, for example
/// in json it would now be `"10"` and `"#FF00FF"` respectively.
///
/// See the [`Color`] documentation for more information on color names.
///
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
///
/// #[derive(Debug, serde::Deserialize)]
/// struct Theme {
/// color: Color,
/// }
///
/// # fn get_theme() -> Result<(), serde_json::Error> {
/// let theme: Theme = serde_json::from_str(r#"{"color": "bright-white"}"#)?;
/// assert_eq!(theme.color, Color::White);
///
/// let theme: Theme = serde_json::from_str(r##"{"color": "#00FF00"}"##)?;
/// assert_eq!(theme.color, Color::Rgb(0, 255, 0));
///
/// let theme: Theme = serde_json::from_str(r#"{"color": "42"}"#)?;
/// assert_eq!(theme.color, Color::Indexed(42));
///
/// let err = serde_json::from_str::<Theme>(r#"{"color": "invalid"}"#).unwrap_err();
/// assert!(err.is_data());
/// assert_eq!(
/// err.to_string(),
/// "Failed to parse Colors at line 1 column 20"
/// );
///
/// // Deserializing from the previous serialization implementation
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Rgb":[255,0,255]}}"#)?;
/// assert_eq!(theme.color, Color::Rgb(255, 0, 255));
///
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Indexed":10}}"#)?;
/// assert_eq!(theme.color, Color::Indexed(10));
/// # Ok(())
/// # }
/// ```
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
/// Colors are currently serialized with the `Display` implementation, so
/// RGB values are serialized via hex, for example "#FFFFFF".
///
/// Previously they were serialized using serde derive, which encoded
/// RGB values as a map, for example { "rgb": [255, 255, 255] }.
///
/// The deserialization implementation utilises a `Helper` struct
/// to be able to support both formats for backwards compatibility.
#[derive(serde::Deserialize)]
enum ColorWrapper {
Rgb(u8, u8, u8),
Indexed(u8),
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum ColorFormat {
V2(String),
V1(ColorWrapper),
}
let multi_type = ColorFormat::deserialize(deserializer)
.map_err(|err| serde::de::Error::custom(format!("Failed to parse Colors: {err}")))?;
match multi_type {
ColorFormat::V2(s) => FromStr::from_str(&s).map_err(serde::de::Error::custom),
ColorFormat::V1(color_wrapper) => match color_wrapper {
ColorWrapper::Rgb(red, green, blue) => Ok(Self::Rgb(red, green, blue)),
ColorWrapper::Indexed(index) => Ok(Self::Indexed(index)),
},
}
}
}
/// Error type indicating a failure to parse a color string.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct ParseColorError;
impl fmt::Display for ParseColorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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`] documentation for more information on the supported color names.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
///
/// use ratatui::prelude::*;
///
/// 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 Some((r, g, b)) = parse_hex_color(s) {
Self::Rgb(r, g, b)
} else {
return Err(ParseColorError);
}
}
},
)
}
}
fn parse_hex_color(input: &str) -> Option<(u8, u8, u8)> {
if !input.starts_with('#') || input.len() != 7 {
return None;
}
let r = u8::from_str_radix(input.get(1..3)?, 16).ok()?;
let g = u8::from_str_radix(input.get(3..5)?, 16).ok()?;
let b = u8::from_str_radix(input.get(5..7)?, 16).ok()?;
Some((r, g, b))
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Reset => write!(f, "Reset"),
Self::Black => write!(f, "Black"),
Self::Red => write!(f, "Red"),
Self::Green => write!(f, "Green"),
Self::Yellow => write!(f, "Yellow"),
Self::Blue => write!(f, "Blue"),
Self::Magenta => write!(f, "Magenta"),
Self::Cyan => write!(f, "Cyan"),
Self::Gray => write!(f, "Gray"),
Self::DarkGray => write!(f, "DarkGray"),
Self::LightRed => write!(f, "LightRed"),
Self::LightGreen => write!(f, "LightGreen"),
Self::LightYellow => write!(f, "LightYellow"),
Self::LightBlue => write!(f, "LightBlue"),
Self::LightMagenta => write!(f, "LightMagenta"),
Self::LightCyan => write!(f, "LightCyan"),
Self::White => write!(f, "White"),
Self::Rgb(r, g, b) => write!(f, "#{r:02X}{g:02X}{b:02X}"),
Self::Indexed(i) => write!(f, "{i}"),
}
}
}
impl Color {
pub(crate) const fn stylize_debug(self, kind: ColorDebugKind) -> ColorDebug {
ColorDebug { kind, color: self }
}
/// Converts a HSL representation to a `Color::Rgb` instance.
///
/// The `from_hsl` function converts the Hue, Saturation and Lightness values to a
/// corresponding `Color` RGB equivalent.
///
/// Hue values should be in the range [0, 360].
/// Saturation and L values should be in the range [0, 100].
/// Values that are not in the range are clamped to be within the range.
///
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
///
/// let color: Color = Color::from_hsl(360.0, 100.0, 100.0);
/// assert_eq!(color, Color::Rgb(255, 255, 255));
///
/// let color: Color = Color::from_hsl(0.0, 0.0, 0.0);
/// assert_eq!(color, Color::Rgb(0, 0, 0));
/// ```
pub fn from_hsl(h: f64, s: f64, l: f64) -> Self {
// Clamp input values to valid ranges
let h = h.clamp(0.0, 360.0);
let s = s.clamp(0.0, 100.0);
let l = l.clamp(0.0, 100.0);
// Delegate to the function for normalized HSL to RGB conversion
normalized_hsl_to_rgb(h / 360.0, s / 100.0, l / 100.0)
}
}
/// Converts normalized HSL (Hue, Saturation, Lightness) values to RGB (Red, Green, Blue) color
/// representation. H, S, and L values should be in the range [0, 1].
///
/// Based on <https://github.com/killercup/hsl-rs/blob/b8a30e11afd75f262e0550725333293805f4ead0/src/lib.rs>
fn normalized_hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) -> Color {
// This function can be made into `const` in the future.
// This comment contains the relevant information for making it `const`.
//
// If it is `const` and made public, users can write the following:
//
// ```rust
// const SLATE_50: Color = normalized_hsl_to_rgb(0.210, 0.40, 0.98);
// ```
//
// For it to be const now, we need `#![feature(const_fn_floating_point_arithmetic)]`
// Tracking issue: https://github.com/rust-lang/rust/issues/57241
//
// We would also need to remove the use of `.round()` in this function, i.e.:
//
// ```rust
// Color::Rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
// ```
// Initialize RGB components
let red: f64;
let green: f64;
let blue: f64;
// Check if the color is achromatic (grayscale)
if saturation == 0.0 {
red = lightness;
green = lightness;
blue = lightness;
} else {
// Calculate RGB components for colored cases
let q = if lightness < 0.5 {
lightness * (1.0 + saturation)
} else {
lightness + saturation - lightness * saturation
};
let p = 2.0 * lightness - q;
red = hue_to_rgb(p, q, hue + 1.0 / 3.0);
green = hue_to_rgb(p, q, hue);
blue = hue_to_rgb(p, q, hue - 1.0 / 3.0);
}
// Scale RGB components to the range [0, 255] and create a Color::Rgb instance
Color::Rgb(
(red * 255.0).round() as u8,
(green * 255.0).round() as u8,
(blue * 255.0).round() as u8,
)
}
/// Helper function to calculate RGB component for a specific hue value.
fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
// Adjust the hue value to be within the valid range [0, 1]
let mut t = t;
if t < 0.0 {
t += 1.0;
}
if t > 1.0 {
t -= 1.0;
}
// Calculate the RGB component based on the hue value
if t < 1.0 / 6.0 {
p + (q - p) * 6.0 * t
} else if t < 1.0 / 2.0 {
q
} else if t < 2.0 / 3.0 {
p + (q - p) * (2.0 / 3.0 - t) * 6.0
} else {
p
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
#[cfg(feature = "serde")]
use serde::de::{Deserialize, IntoDeserializer};
use super::*;
#[test]
fn test_hsl_to_rgb() {
// Test with valid HSL values
let color = Color::from_hsl(120.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(159, 223, 159));
// Test with H value at upper bound
let color = Color::from_hsl(360.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(223, 159, 159));
// Test with H value exceeding the upper bound
let color = Color::from_hsl(400.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(223, 159, 159));
// Test with S and L values exceeding the upper bound
let color = Color::from_hsl(240.0, 120.0, 150.0);
assert_eq!(color, Color::Rgb(255, 255, 255));
// Test with H, S, and L values below the lower bound
let color = Color::from_hsl(-20.0, -50.0, -20.0);
assert_eq!(color, Color::Rgb(0, 0, 0));
// Test with S and L values below the lower bound
let color = Color::from_hsl(60.0, -20.0, -10.0);
assert_eq!(color, Color::Rgb(0, 0, 0));
}
#[test]
fn from_u32() {
assert_eq!(Color::from_u32(0x000000), Color::Rgb(0, 0, 0));
assert_eq!(Color::from_u32(0xFF0000), Color::Rgb(255, 0, 0));
assert_eq!(Color::from_u32(0x00FF00), Color::Rgb(0, 255, 0));
assert_eq!(Color::from_u32(0x0000FF), Color::Rgb(0, 0, 255));
assert_eq!(Color::from_u32(0xFFFFFF), Color::Rgb(255, 255, 255));
}
#[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
"#1🦀2", // len 7 but on char boundaries shouldnt panic
"resett", // typo
"lightblackk", // typo
];
for bad_color in bad_colors {
assert!(
Color::from_str(bad_color).is_err(),
"bad color: '{bad_color}'"
);
}
}
#[test]
fn display() {
assert_eq!(format!("{}", Color::Black), "Black");
assert_eq!(format!("{}", Color::Red), "Red");
assert_eq!(format!("{}", Color::Green), "Green");
assert_eq!(format!("{}", Color::Yellow), "Yellow");
assert_eq!(format!("{}", Color::Blue), "Blue");
assert_eq!(format!("{}", Color::Magenta), "Magenta");
assert_eq!(format!("{}", Color::Cyan), "Cyan");
assert_eq!(format!("{}", Color::Gray), "Gray");
assert_eq!(format!("{}", Color::DarkGray), "DarkGray");
assert_eq!(format!("{}", Color::LightRed), "LightRed");
assert_eq!(format!("{}", Color::LightGreen), "LightGreen");
assert_eq!(format!("{}", Color::LightYellow), "LightYellow");
assert_eq!(format!("{}", Color::LightBlue), "LightBlue");
assert_eq!(format!("{}", Color::LightMagenta), "LightMagenta");
assert_eq!(format!("{}", Color::LightCyan), "LightCyan");
assert_eq!(format!("{}", Color::White), "White");
assert_eq!(format!("{}", Color::Indexed(10)), "10");
assert_eq!(format!("{}", Color::Rgb(255, 0, 0)), "#FF0000");
assert_eq!(format!("{}", Color::Reset), "Reset");
}
#[cfg(feature = "serde")]
#[test]
fn deserialize() -> Result<(), serde::de::value::Error> {
assert_eq!(
Color::Black,
Color::deserialize("Black".into_deserializer())?
);
assert_eq!(
Color::Magenta,
Color::deserialize("magenta".into_deserializer())?
);
assert_eq!(
Color::LightGreen,
Color::deserialize("LightGreen".into_deserializer())?
);
assert_eq!(
Color::White,
Color::deserialize("bright-white".into_deserializer())?
);
assert_eq!(
Color::Indexed(42),
Color::deserialize("42".into_deserializer())?
);
assert_eq!(
Color::Rgb(0, 255, 0),
Color::deserialize("#00ff00".into_deserializer())?
);
Ok(())
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_error() {
let color: Result<_, serde::de::value::Error> =
Color::deserialize("invalid".into_deserializer());
assert!(color.is_err());
let color: Result<_, serde::de::value::Error> =
Color::deserialize("#00000000".into_deserializer());
assert!(color.is_err());
}
#[cfg(feature = "serde")]
#[test]
fn serialize_then_deserialize() -> Result<(), serde_json::Error> {
let json_rgb = serde_json::to_string(&Color::Rgb(255, 0, 255))?;
assert_eq!(json_rgb, r##""#FF00FF""##);
assert_eq!(
serde_json::from_str::<Color>(&json_rgb)?,
Color::Rgb(255, 0, 255)
);
let json_white = serde_json::to_string(&Color::White)?;
assert_eq!(json_white, r#""White""#);
let json_indexed = serde_json::to_string(&Color::Indexed(10))?;
assert_eq!(json_indexed, r#""10""#);
assert_eq!(
serde_json::from_str::<Color>(&json_indexed)?,
Color::Indexed(10)
);
Ok(())
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_with_previous_format() -> Result<(), serde_json::Error> {
assert_eq!(Color::White, serde_json::from_str::<Color>("\"White\"")?);
assert_eq!(
Color::Rgb(255, 0, 255),
serde_json::from_str::<Color>(r#"{"Rgb":[255,0,255]}"#)?
);
assert_eq!(
Color::Indexed(10),
serde_json::from_str::<Color>(r#"{"Indexed":10}"#)?
);
Ok(())
}
}

View File

@@ -1,6 +0,0 @@
#![allow(clippy::unreadable_literal)]
//! A module for defining color palettes.
pub mod material;
pub mod tailwind;

View File

@@ -1,606 +0,0 @@
//! Material design color palettes.
//!
//! Represents the colors from the 2014 [Material design color palettes][palettes] by Google.
//!
//! [palettes]: https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors
//!
//! There are 16 palettes with accent colors, and 3 palettes without accent colors. Each palette
//! has 10 colors, with variants from 50 to 900. The accent palettes also have 4 accent colors
//! with variants from 100 to 700. Black and White are also included for completeness and to avoid
//! being affected by any terminal theme that might be in use.
//!
//! This module exists to provide a convenient way to use the colors from the
//! [`matdesign-color` crate] in your application.
//!
//! <style>
//! .color { display: flex; align-items: center; }
//! .color > div { width: 2rem; height: 2rem; }
//! .color > div.name { width: 150px; !important; }
//! </style>
//! <div style="overflow-x: auto">
//! <div style="display: flex; flex-direction:column; text-align: left">
//! <div class="color" style="font-size:0.8em">
//! <div class="name"></div>
//! <div>C50</div>
//! <div>C100</div>
//! <div>C200</div>
//! <div>C300</div>
//! <div>C400</div>
//! <div>C500</div>
//! <div>C600</div>
//! <div>C700</div>
//! <div>C800</div>
//! <div>C900</div>
//! <div>A100</div>
//! <div>A200</div>
//! <div>A400</div>
//! <div>A700</div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`RED`]</div>
//! <div style="background-color: #FFEBEE"></div>
//! <div style="background-color: #FFCDD2"></div>
//! <div style="background-color: #EF9A9A"></div>
//! <div style="background-color: #E57373"></div>
//! <div style="background-color: #EF5350"></div>
//! <div style="background-color: #F44336"></div>
//! <div style="background-color: #E53935"></div>
//! <div style="background-color: #D32F2F"></div>
//! <div style="background-color: #C62828"></div>
//! <div style="background-color: #B71C1C"></div>
//! <div style="background-color: #FF8A80"></div>
//! <div style="background-color: #FF5252"></div>
//! <div style="background-color: #FF1744"></div>
//! <div style="background-color: #D50000"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PINK`]</div>
//! <div style="background-color: #FCE4EC"></div>
//! <div style="background-color: #F8BBD0"></div>
//! <div style="background-color: #F48FB1"></div>
//! <div style="background-color: #F06292"></div>
//! <div style="background-color: #EC407A"></div>
//! <div style="background-color: #E91E63"></div>
//! <div style="background-color: #D81B60"></div>
//! <div style="background-color: #C2185B"></div>
//! <div style="background-color: #AD1457"></div>
//! <div style="background-color: #880E4F"></div>
//! <div style="background-color: #FF80AB"></div>
//! <div style="background-color: #FF4081"></div>
//! <div style="background-color: #F50057"></div>
//! <div style="background-color: #C51162"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PURPLE`]</div>
//! <div style="background-color: #F3E5F5"></div>
//! <div style="background-color: #E1BEE7"></div>
//! <div style="background-color: #CE93D8"></div>
//! <div style="background-color: #BA68C8"></div>
//! <div style="background-color: #AB47BC"></div>
//! <div style="background-color: #9C27B0"></div>
//! <div style="background-color: #8E24AA"></div>
//! <div style="background-color: #7B1FA2"></div>
//! <div style="background-color: #6A1B9A"></div>
//! <div style="background-color: #4A148C"></div>
//! <div style="background-color: #EA80FC"></div>
//! <div style="background-color: #E040FB"></div>
//! <div style="background-color: #D500F9"></div>
//! <div style="background-color: #AA00FF"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`DEEP_PURPLE`]</div>
//! <div style="background-color: #EDE7F6"></div>
//! <div style="background-color: #D1C4E9"></div>
//! <div style="background-color: #B39DDB"></div>
//! <div style="background-color: #9575CD"></div>
//! <div style="background-color: #7E57C2"></div>
//! <div style="background-color: #673AB7"></div>
//! <div style="background-color: #5E35B1"></div>
//! <div style="background-color: #512DA8"></div>
//! <div style="background-color: #4527A0"></div>
//! <div style="background-color: #311B92"></div>
//! <div style="background-color: #B388FF"></div>
//! <div style="background-color: #7C4DFF"></div>
//! <div style="background-color: #651FFF"></div>
//! <div style="background-color: #6200EA"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`INDIGO`]</div>
//! <div style="background-color: #E8EAF6"></div>
//! <div style="background-color: #C5CAE9"></div>
//! <div style="background-color: #9FA8DA"></div>
//! <div style="background-color: #7986CB"></div>
//! <div style="background-color: #5C6BC0"></div>
//! <div style="background-color: #3F51B5"></div>
//! <div style="background-color: #3949AB"></div>
//! <div style="background-color: #303F9F"></div>
//! <div style="background-color: #283593"></div>
//! <div style="background-color: #1A237E"></div>
//! <div style="background-color: #8C9EFF"></div>
//! <div style="background-color: #536DFE"></div>
//! <div style="background-color: #3D5AFE"></div>
//! <div style="background-color: #304FFE"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLUE`]</div>
//! <div style="background-color: #E3F2FD"></div>
//! <div style="background-color: #BBDEFB"></div>
//! <div style="background-color: #90CAF9"></div>
//! <div style="background-color: #64B5F6"></div>
//! <div style="background-color: #42A5F5"></div>
//! <div style="background-color: #2196F3"></div>
//! <div style="background-color: #1E88E5"></div>
//! <div style="background-color: #1976D2"></div>
//! <div style="background-color: #1565C0"></div>
//! <div style="background-color: #0D47A1"></div>
//! <div style="background-color: #82B1FF"></div>
//! <div style="background-color: #448AFF"></div>
//! <div style="background-color: #2979FF"></div>
//! <div style="background-color: #2962FF"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIGHT_BLUE`]</div>
//! <div style="background-color: #E1F5FE"></div>
//! <div style="background-color: #B3E5FC"></div>
//! <div style="background-color: #81D4FA"></div>
//! <div style="background-color: #4FC3F7"></div>
//! <div style="background-color: #29B6F6"></div>
//! <div style="background-color: #03A9F4"></div>
//! <div style="background-color: #039BE5"></div>
//! <div style="background-color: #0288D1"></div>
//! <div style="background-color: #0277BD"></div>
//! <div style="background-color: #01579B"></div>
//! <div style="background-color: #80D8FF"></div>
//! <div style="background-color: #40C4FF"></div>
//! <div style="background-color: #00B0FF"></div>
//! <div style="background-color: #0091EA"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`CYAN`]</div>
//! <div style="background-color: #E0F7FA"></div>
//! <div style="background-color: #B2EBF2"></div>
//! <div style="background-color: #80DEEA"></div>
//! <div style="background-color: #4DD0E1"></div>
//! <div style="background-color: #26C6DA"></div>
//! <div style="background-color: #00BCD4"></div>
//! <div style="background-color: #00ACC1"></div>
//! <div style="background-color: #0097A7"></div>
//! <div style="background-color: #00838F"></div>
//! <div style="background-color: #006064"></div>
//! <div style="background-color: #84FFFF"></div>
//! <div style="background-color: #18FFFF"></div>
//! <div style="background-color: #00E5FF"></div>
//! <div style="background-color: #00B8D4"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`TEAL`]</div>
//! <div style="background-color: #E0F2F1"></div>
//! <div style="background-color: #B2DFDB"></div>
//! <div style="background-color: #80CBC4"></div>
//! <div style="background-color: #4DB6AC"></div>
//! <div style="background-color: #26A69A"></div>
//! <div style="background-color: #009688"></div>
//! <div style="background-color: #00897B"></div>
//! <div style="background-color: #00796B"></div>
//! <div style="background-color: #00695C"></div>
//! <div style="background-color: #004D40"></div>
//! <div style="background-color: #A7FFEB"></div>
//! <div style="background-color: #64FFDA"></div>
//! <div style="background-color: #1DE9B6"></div>
//! <div style="background-color: #00BFA5"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GREEN`]</div>
//! <div style="background-color: #E8F5E9"></div>
//! <div style="background-color: #C8E6C9"></div>
//! <div style="background-color: #A5D6A7"></div>
//! <div style="background-color: #81C784"></div>
//! <div style="background-color: #66BB6A"></div>
//! <div style="background-color: #4CAF50"></div>
//! <div style="background-color: #43A047"></div>
//! <div style="background-color: #388E3C"></div>
//! <div style="background-color: #2E7D32"></div>
//! <div style="background-color: #1B5E20"></div>
//! <div style="background-color: #B9F6CA"></div>
//! <div style="background-color: #69F0AE"></div>
//! <div style="background-color: #00E676"></div>
//! <div style="background-color: #00C853"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIGHT_GREEN`]</div>
//! <div style="background-color: #F1F8E9"></div>
//! <div style="background-color: #DCEDC8"></div>
//! <div style="background-color: #C5E1A5"></div>
//! <div style="background-color: #AED581"></div>
//! <div style="background-color: #9CCC65"></div>
//! <div style="background-color: #8BC34A"></div>
//! <div style="background-color: #7CB342"></div>
//! <div style="background-color: #689F38"></div>
//! <div style="background-color: #558B2F"></div>
//! <div style="background-color: #33691E"></div>
//! <div style="background-color: #CCFF90"></div>
//! <div style="background-color: #B2FF59"></div>
//! <div style="background-color: #76FF03"></div>
//! <div style="background-color: #64DD17"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIME`]</div>
//! <div style="background-color: #F9FBE7"></div>
//! <div style="background-color: #F0F4C3"></div>
//! <div style="background-color: #E6EE9C"></div>
//! <div style="background-color: #DCE775"></div>
//! <div style="background-color: #D4E157"></div>
//! <div style="background-color: #CDDC39"></div>
//! <div style="background-color: #C0CA33"></div>
//! <div style="background-color: #AFB42B"></div>
//! <div style="background-color: #9E9D24"></div>
//! <div style="background-color: #827717"></div>
//! <div style="background-color: #F4FF81"></div>
//! <div style="background-color: #EEFF41"></div>
//! <div style="background-color: #C6FF00"></div>
//! <div style="background-color: #AEEA00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`YELLOW`]</div>
//! <div style="background-color: #FFFDE7"></div>
//! <div style="background-color: #FFF9C4"></div>
//! <div style="background-color: #FFF59D"></div>
//! <div style="background-color: #FFF176"></div>
//! <div style="background-color: #FFEE58"></div>
//! <div style="background-color: #FFEB3B"></div>
//! <div style="background-color: #FDD835"></div>
//! <div style="background-color: #FBC02D"></div>
//! <div style="background-color: #F9A825"></div>
//! <div style="background-color: #F57F17"></div>
//! <div style="background-color: #FFFF8D"></div>
//! <div style="background-color: #FFFF00"></div>
//! <div style="background-color: #FFEA00"></div>
//! <div style="background-color: #FFD600"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`AMBER`]</div>
//! <div style="background-color: #FFF8E1"></div>
//! <div style="background-color: #FFECB3"></div>
//! <div style="background-color: #FFE082"></div>
//! <div style="background-color: #FFD54F"></div>
//! <div style="background-color: #FFCA28"></div>
//! <div style="background-color: #FFC107"></div>
//! <div style="background-color: #FFB300"></div>
//! <div style="background-color: #FFA000"></div>
//! <div style="background-color: #FF8F00"></div>
//! <div style="background-color: #FF6F00"></div>
//! <div style="background-color: #FFE57F"></div>
//! <div style="background-color: #FFD740"></div>
//! <div style="background-color: #FFC400"></div>
//! <div style="background-color: #FFAB00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`ORANGE`]</div>
//! <div style="background-color: #FFF3E0"></div>
//! <div style="background-color: #FFE0B2"></div>
//! <div style="background-color: #FFCC80"></div>
//! <div style="background-color: #FFB74D"></div>
//! <div style="background-color: #FFA726"></div>
//! <div style="background-color: #FF9800"></div>
//! <div style="background-color: #FB8C00"></div>
//! <div style="background-color: #F57C00"></div>
//! <div style="background-color: #EF6C00"></div>
//! <div style="background-color: #E65100"></div>
//! <div style="background-color: #FFD180"></div>
//! <div style="background-color: #FFAB40"></div>
//! <div style="background-color: #FF9100"></div>
//! <div style="background-color: #FF6D00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`DEEP_ORANGE`]</div>
//! <div style="background-color: #FBE9E7"></div>
//! <div style="background-color: #FFCCBC"></div>
//! <div style="background-color: #FFAB91"></div>
//! <div style="background-color: #FF8A65"></div>
//! <div style="background-color: #FF7043"></div>
//! <div style="background-color: #FF5722"></div>
//! <div style="background-color: #F4511E"></div>
//! <div style="background-color: #E64A19"></div>
//! <div style="background-color: #D84315"></div>
//! <div style="background-color: #BF360C"></div>
//! <div style="background-color: #FF9E80"></div>
//! <div style="background-color: #FF6E40"></div>
//! <div style="background-color: #FF3D00"></div>
//! <div style="background-color: #DD2C00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BROWN`]</div>
//! <div style="background-color: #EFEBE9"></div>
//! <div style="background-color: #D7CCC8"></div>
//! <div style="background-color: #BCAAA4"></div>
//! <div style="background-color: #A1887F"></div>
//! <div style="background-color: #8D6E63"></div>
//! <div style="background-color: #795548"></div>
//! <div style="background-color: #6D4C41"></div>
//! <div style="background-color: #5D4037"></div>
//! <div style="background-color: #4E342E"></div>
//! <div style="background-color: #3E2723"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GRAY`]</div>
//! <div style="background-color: #FAFAFA"></div>
//! <div style="background-color: #F5F5F5"></div>
//! <div style="background-color: #EEEEEE"></div>
//! <div style="background-color: #E0E0E0"></div>
//! <div style="background-color: #BDBDBD"></div>
//! <div style="background-color: #9E9E9E"></div>
//! <div style="background-color: #757575"></div>
//! <div style="background-color: #616161"></div>
//! <div style="background-color: #424242"></div>
//! <div style="background-color: #212121"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLUE_GRAY`]</div>
//! <div style="background-color: #ECEFF1"></div>
//! <div style="background-color: #CFD8DC"></div>
//! <div style="background-color: #B0BEC5"></div>
//! <div style="background-color: #90A4AE"></div>
//! <div style="background-color: #78909C"></div>
//! <div style="background-color: #607D8B"></div>
//! <div style="background-color: #546E7A"></div>
//! <div style="background-color: #455A64"></div>
//! <div style="background-color: #37474F"></div>
//! <div style="background-color: #263238"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLACK`]</div>
//! <div class="bw" style="width: 350px; background-color: #000000"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`WHITE`]</div>
//! <div style="width: 350px; background-color: #FFFFFF"></div>
//! </div>
//! </div>
//! </div>
//!
//! # Example
//!
//! ```rust
//! # use ratatui::prelude::*;
//! use ratatui::style::palette::material::{BLUE, RED};
//!
//! assert_eq!(RED.c500, Color::Rgb(244, 67, 54));
//! assert_eq!(BLUE.c500, Color::Rgb(33, 150, 243));
//! ```
//!
//! [`matdesign-color` crate]: https://crates.io/crates/matdesign-color
use crate::prelude::*;
/// A palette of colors for use in Material design with accent colors
///
/// This is a collection of colors that are used in Material design. They consist of a set of
/// colors from 50 to 900, and a set of accent colors from 100 to 700.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct AccentedPalette {
pub c50: Color,
pub c100: Color,
pub c200: Color,
pub c300: Color,
pub c400: Color,
pub c500: Color,
pub c600: Color,
pub c700: Color,
pub c800: Color,
pub c900: Color,
pub a100: Color,
pub a200: Color,
pub a400: Color,
pub a700: Color,
}
/// A palette of colors for use in Material design without accent colors
///
/// This is a collection of colors that are used in Material design. They consist of a set of
/// colors from 50 to 900.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct NonAccentedPalette {
pub c50: Color,
pub c100: Color,
pub c200: Color,
pub c300: Color,
pub c400: Color,
pub c500: Color,
pub c600: Color,
pub c700: Color,
pub c800: Color,
pub c900: Color,
}
impl AccentedPalette {
/// Create a new `AccentedPalette` from the given variants
///
/// The variants should be in the format [0x00RRGGBB, ...]
pub const fn from_variants(variants: [u32; 14]) -> Self {
Self {
c50: Color::from_u32(variants[0]),
c100: Color::from_u32(variants[1]),
c200: Color::from_u32(variants[2]),
c300: Color::from_u32(variants[3]),
c400: Color::from_u32(variants[4]),
c500: Color::from_u32(variants[5]),
c600: Color::from_u32(variants[6]),
c700: Color::from_u32(variants[7]),
c800: Color::from_u32(variants[8]),
c900: Color::from_u32(variants[9]),
a100: Color::from_u32(variants[10]),
a200: Color::from_u32(variants[11]),
a400: Color::from_u32(variants[12]),
a700: Color::from_u32(variants[13]),
}
}
}
impl NonAccentedPalette {
/// Create a new `NonAccented` from the given variants
///
/// The variants should be in the format [0x00RRGGBB, ...]
pub const fn from_variants(variants: [u32; 10]) -> Self {
Self {
c50: Color::from_u32(variants[0]),
c100: Color::from_u32(variants[1]),
c200: Color::from_u32(variants[2]),
c300: Color::from_u32(variants[3]),
c400: Color::from_u32(variants[4]),
c500: Color::from_u32(variants[5]),
c600: Color::from_u32(variants[6]),
c700: Color::from_u32(variants[7]),
c800: Color::from_u32(variants[8]),
c900: Color::from_u32(variants[9]),
}
}
}
// Accented palettes
pub const RED: AccentedPalette = AccentedPalette::from_variants(variants::RED);
pub const PINK: AccentedPalette = AccentedPalette::from_variants(variants::PINK);
pub const PURPLE: AccentedPalette = AccentedPalette::from_variants(variants::PURPLE);
pub const DEEP_PURPLE: AccentedPalette = AccentedPalette::from_variants(variants::DEEP_PURPLE);
pub const INDIGO: AccentedPalette = AccentedPalette::from_variants(variants::INDIGO);
pub const BLUE: AccentedPalette = AccentedPalette::from_variants(variants::BLUE);
pub const LIGHT_BLUE: AccentedPalette = AccentedPalette::from_variants(variants::LIGHT_BLUE);
pub const CYAN: AccentedPalette = AccentedPalette::from_variants(variants::CYAN);
pub const TEAL: AccentedPalette = AccentedPalette::from_variants(variants::TEAL);
pub const GREEN: AccentedPalette = AccentedPalette::from_variants(variants::GREEN);
pub const LIGHT_GREEN: AccentedPalette = AccentedPalette::from_variants(variants::LIGHT_GREEN);
pub const LIME: AccentedPalette = AccentedPalette::from_variants(variants::LIME);
pub const YELLOW: AccentedPalette = AccentedPalette::from_variants(variants::YELLOW);
pub const AMBER: AccentedPalette = AccentedPalette::from_variants(variants::AMBER);
pub const ORANGE: AccentedPalette = AccentedPalette::from_variants(variants::ORANGE);
pub const DEEP_ORANGE: AccentedPalette = AccentedPalette::from_variants(variants::DEEP_ORANGE);
// Unaccented palettes
pub const BROWN: NonAccentedPalette = NonAccentedPalette::from_variants(variants::BROWN);
pub const GRAY: NonAccentedPalette = NonAccentedPalette::from_variants(variants::GRAY);
pub const BLUE_GRAY: NonAccentedPalette = NonAccentedPalette::from_variants(variants::BLUE_GRAY);
// Black and white included for completeness
pub const BLACK: Color = Color::from_u32(0x000000);
pub const WHITE: Color = Color::from_u32(0xFFFFFF);
mod variants {
pub const RED: [u32; 14] = [
0xFFEBEE, 0xFFCDD2, 0xEF9A9A, 0xE57373, 0xEF5350, 0xF44336, 0xE53935, 0xD32F2F, 0xC62828,
0xB71C1C, 0xFF8A80, 0xFF5252, 0xFF1744, 0xD50000,
];
pub const PINK: [u32; 14] = [
0xFCE4EC, 0xF8BBD0, 0xF48FB1, 0xF06292, 0xEC407A, 0xE91E63, 0xD81B60, 0xC2185B, 0xAD1457,
0x880E4F, 0xFF80AB, 0xFF4081, 0xF50057, 0xC51162,
];
pub const PURPLE: [u32; 14] = [
0xF3E5F5, 0xE1BEE7, 0xCE93D8, 0xBA68C8, 0xAB47BC, 0x9C27B0, 0x8E24AA, 0x7B1FA2, 0x6A1B9A,
0x4A148C, 0xEA80FC, 0xE040FB, 0xD500F9, 0xAA00FF,
];
pub const DEEP_PURPLE: [u32; 14] = [
0xEDE7F6, 0xD1C4E9, 0xB39DDB, 0x9575CD, 0x7E57C2, 0x673AB7, 0x5E35B1, 0x512DA8, 0x4527A0,
0x311B92, 0xB388FF, 0x7C4DFF, 0x651FFF, 0x6200EA,
];
pub const INDIGO: [u32; 14] = [
0xE8EAF6, 0xC5CAE9, 0x9FA8DA, 0x7986CB, 0x5C6BC0, 0x3F51B5, 0x3949AB, 0x303F9F, 0x283593,
0x1A237E, 0x8C9EFF, 0x536DFE, 0x3D5AFE, 0x304FFE,
];
pub const BLUE: [u32; 14] = [
0xE3F2FD, 0xBBDEFB, 0x90CAF9, 0x64B5F6, 0x42A5F5, 0x2196F3, 0x1E88E5, 0x1976D2, 0x1565C0,
0x0D47A1, 0x82B1FF, 0x448AFF, 0x2979FF, 0x2962FF,
];
pub const LIGHT_BLUE: [u32; 14] = [
0xE1F5FE, 0xB3E5FC, 0x81D4FA, 0x4FC3F7, 0x29B6F6, 0x03A9F4, 0x039BE5, 0x0288D1, 0x0277BD,
0x01579B, 0x80D8FF, 0x40C4FF, 0x00B0FF, 0x0091EA,
];
pub const CYAN: [u32; 14] = [
0xE0F7FA, 0xB2EBF2, 0x80DEEA, 0x4DD0E1, 0x26C6DA, 0x00BCD4, 0x00ACC1, 0x0097A7, 0x00838F,
0x006064, 0x84FFFF, 0x18FFFF, 0x00E5FF, 0x00B8D4,
];
pub const TEAL: [u32; 14] = [
0xE0F2F1, 0xB2DFDB, 0x80CBC4, 0x4DB6AC, 0x26A69A, 0x009688, 0x00897B, 0x00796B, 0x00695C,
0x004D40, 0xA7FFEB, 0x64FFDA, 0x1DE9B6, 0x00BFA5,
];
pub const GREEN: [u32; 14] = [
0xE8F5E9, 0xC8E6C9, 0xA5D6A7, 0x81C784, 0x66BB6A, 0x4CAF50, 0x43A047, 0x388E3C, 0x2E7D32,
0x1B5E20, 0xB9F6CA, 0x69F0AE, 0x00E676, 0x00C853,
];
pub const LIGHT_GREEN: [u32; 14] = [
0xF1F8E9, 0xDCEDC8, 0xC5E1A5, 0xAED581, 0x9CCC65, 0x8BC34A, 0x7CB342, 0x689F38, 0x558B2F,
0x33691E, 0xCCFF90, 0xB2FF59, 0x76FF03, 0x64DD17,
];
pub const LIME: [u32; 14] = [
0xF9FBE7, 0xF0F4C3, 0xE6EE9C, 0xDCE775, 0xD4E157, 0xCDDC39, 0xC0CA33, 0xAFB42B, 0x9E9D24,
0x827717, 0xF4FF81, 0xEEFF41, 0xC6FF00, 0xAEEA00,
];
pub const YELLOW: [u32; 14] = [
0xFFFDE7, 0xFFF9C4, 0xFFF59D, 0xFFF176, 0xFFEE58, 0xFFEB3B, 0xFDD835, 0xFBC02D, 0xF9A825,
0xF57F17, 0xFFFF8D, 0xFFFF00, 0xFFEA00, 0xFFD600,
];
pub const AMBER: [u32; 14] = [
0xFFF8E1, 0xFFECB3, 0xFFE082, 0xFFD54F, 0xFFCA28, 0xFFC107, 0xFFB300, 0xFFA000, 0xFF8F00,
0xFF6F00, 0xFFE57F, 0xFFD740, 0xFFC400, 0xFFAB00,
];
pub const ORANGE: [u32; 14] = [
0xFFF3E0, 0xFFE0B2, 0xFFCC80, 0xFFB74D, 0xFFA726, 0xFF9800, 0xFB8C00, 0xF57C00, 0xEF6C00,
0xE65100, 0xFFD180, 0xFFAB40, 0xFF9100, 0xFF6D00,
];
pub const DEEP_ORANGE: [u32; 14] = [
0xFBE9E7, 0xFFCCBC, 0xFFAB91, 0xFF8A65, 0xFF7043, 0xFF5722, 0xF4511E, 0xE64A19, 0xD84315,
0xBF360C, 0xFF9E80, 0xFF6E40, 0xFF3D00, 0xDD2C00,
];
pub const BROWN: [u32; 10] = [
0xEFEBE9, 0xD7CCC8, 0xBCAAA4, 0xA1887F, 0x8D6E63, 0x795548, 0x6D4C41, 0x5D4037, 0x4E342E,
0x3E2723,
];
pub const GRAY: [u32; 10] = [
0xFAFAFA, 0xF5F5F5, 0xEEEEEE, 0xE0E0E0, 0xBDBDBD, 0x9E9E9E, 0x757575, 0x616161, 0x424242,
0x212121,
];
pub const BLUE_GRAY: [u32; 10] = [
0xECEFF1, 0xCFD8DC, 0xB0BEC5, 0x90A4AE, 0x78909C, 0x607D8B, 0x546E7A, 0x455A64, 0x37474F,
0x263238,
];
}

View File

@@ -1,652 +0,0 @@
//! Represents the Tailwind CSS [default color palette][palette].
//!
//! [palette]: https://tailwindcss.com/docs/customizing-colors#default-color-palette
//!
//! There are 22 palettes. Each palette has 11 colors, with variants from 50 to 950. Black and White
//! are also included for completeness and to avoid being affected by any terminal theme that might
//! be in use.
//!
//! <style>
//! .color { display: flex; align-items: center; }
//! .color > div { width: 2rem; height: 2rem; }
//! .color > div.name { width: 150px; !important; }
//! </style>
//! <div style="overflow-x: auto">
//! <div style="display: flex; flex-direction:column; text-align: left">
//! <div class="color" style="font-size:0.8em">
//! <div class="name"></div>
//! <div>C50</div> <div>C100</div> <div>C200</div> <div>C300</div> <div>C400</div>
//! <div>C500</div> <div>C600</div> <div>C700</div> <div>C800</div> <div>C900</div>
//! <div>C950</div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`SLATE`]</div>
//! <div style="background-color: #f8fafc"></div> <div style="background-color: #f1f5f9"></div>
//! <div style="background-color: #e2e8f0"></div> <div style="background-color: #cbd5e1"></div>
//! <div style="background-color: #94a3b8"></div> <div style="background-color: #64748b"></div>
//! <div style="background-color: #475569"></div> <div style="background-color: #334155"></div>
//! <div style="background-color: #1e293b"></div> <div style="background-color: #0f172a"></div>
//! <div style="background-color: #020617"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GRAY`]</div>
//! <div style="background-color: #f9fafb"></div> <div style="background-color: #f3f4f6"></div>
//! <div style="background-color: #e5e7eb"></div> <div style="background-color: #d1d5db"></div>
//! <div style="background-color: #9ca3af"></div> <div style="background-color: #6b7280"></div>
//! <div style="background-color: #4b5563"></div> <div style="background-color: #374151"></div>
//! <div style="background-color: #1f2937"></div> <div style="background-color: #111827"></div>
//! <div style="background-color: #0a0a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`ZINC`]</div>
//! <div style="background-color: #fafafa"></div> <div style="background-color: #f5f5f5"></div>
//! <div style="background-color: #e5e5e5"></div> <div style="background-color: #d4d4d4"></div>
//! <div style="background-color: #a1a1aa"></div> <div style="background-color: #71717a"></div>
//! <div style="background-color: #52525b"></div> <div style="background-color: #404040"></div>
//! <div style="background-color: #262626"></div> <div style="background-color: #171717"></div>
//! <div style="background-color: #0a0a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`NEUTRAL`]</div>
//! <div style="background-color: #fafafa"></div> <div style="background-color: #f5f5f5"></div>
//! <div style="background-color: #e5e5e5"></div> <div style="background-color: #d4d4d4"></div>
//! <div style="background-color: #a3a3a3"></div> <div style="background-color: #737373"></div>
//! <div style="background-color: #525252"></div> <div style="background-color: #404040"></div>
//! <div style="background-color: #262626"></div> <div style="background-color: #171717"></div>
//! <div style="background-color: #0a0a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`STONE`]</div>
//! <div style="background-color: #fafaf9"></div> <div style="background-color: #f5f5f4"></div>
//! <div style="background-color: #e7e5e4"></div> <div style="background-color: #d6d3d1"></div>
//! <div style="background-color: #a8a29e"></div> <div style="background-color: #78716c"></div>
//! <div style="background-color: #57534e"></div> <div style="background-color: #44403c"></div>
//! <div style="background-color: #292524"></div> <div style="background-color: #1c1917"></div>
//! <div style="background-color: #0c0a09"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`RED`]</div>
//! <div style="background-color: #fef2f2"></div> <div style="background-color: #fee2e2"></div>
//! <div style="background-color: #fecaca"></div> <div style="background-color: #fca5a5"></div>
//! <div style="background-color: #f87171"></div> <div style="background-color: #ef4444"></div>
//! <div style="background-color: #dc2626"></div> <div style="background-color: #b91c1c"></div>
//! <div style="background-color: #991b1b"></div> <div style="background-color: #7f1d1d"></div>
//! <div style="background-color: #450a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`ORANGE`]</div>
//! <div style="background-color: #fff7ed"></div> <div style="background-color: #ffedd5"></div>
//! <div style="background-color: #fed7aa"></div> <div style="background-color: #fdba74"></div>
//! <div style="background-color: #fb923c"></div> <div style="background-color: #f97316"></div>
//! <div style="background-color: #ea580c"></div> <div style="background-color: #c2410c"></div>
//! <div style="background-color: #9a3412"></div> <div style="background-color: #7c2d12"></div>
//! <div style="background-color: #431407"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`AMBER`]</div>
//! <div style="background-color: #fffbeb"></div> <div style="background-color: #fef3c7"></div>
//! <div style="background-color: #fde68a"></div> <div style="background-color: #fcd34d"></div>
//! <div style="background-color: #fbbf24"></div> <div style="background-color: #f59e0b"></div>
//! <div style="background-color: #d97706"></div> <div style="background-color: #b45309"></div>
//! <div style="background-color: #92400e"></div> <div style="background-color: #78350f"></div>
//! <div style="background-color: #451a03"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`YELLOW`]</div>
//! <div style="background-color: #fefce8"></div> <div style="background-color: #fef9c3"></div>
//! <div style="background-color: #fef08a"></div> <div style="background-color: #fde047"></div>
//! <div style="background-color: #facc15"></div> <div style="background-color: #eab308"></div>
//! <div style="background-color: #ca8a04"></div> <div style="background-color: #a16207"></div>
//! <div style="background-color: #854d0e"></div> <div style="background-color: #713f12"></div>
//! <div style="background-color: #422006"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIME`]</div>
//! <div style="background-color: #f7fee7"></div> <div style="background-color: #ecfccb"></div>
//! <div style="background-color: #d9f99d"></div> <div style="background-color: #bef264"></div>
//! <div style="background-color: #a3e635"></div> <div style="background-color: #84cc16"></div>
//! <div style="background-color: #65a30d"></div> <div style="background-color: #4d7c0f"></div>
//! <div style="background-color: #3f6212"></div> <div style="background-color: #365314"></div>
//! <div style="background-color: #1a2e05"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GREEN`]</div>
//! <div style="background-color: #f0fdf4"></div> <div style="background-color: #dcfce7"></div>
//! <div style="background-color: #bbf7d0"></div> <div style="background-color: #86efac"></div>
//! <div style="background-color: #4ade80"></div> <div style="background-color: #22c55e"></div>
//! <div style="background-color: #16a34a"></div> <div style="background-color: #15803d"></div>
//! <div style="background-color: #166534"></div> <div style="background-color: #14532d"></div>
//! <div style="background-color: #052e16"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`EMERALD`]</div>
//! <div style="background-color: #ecfdf5"></div> <div style="background-color: #d1fae5"></div>
//! <div style="background-color: #a7f3d0"></div> <div style="background-color: #6ee7b7"></div>
//! <div style="background-color: #34d399"></div> <div style="background-color: #10b981"></div>
//! <div style="background-color: #059669"></div> <div style="background-color: #047857"></div>
//! <div style="background-color: #065f46"></div> <div style="background-color: #064e3b"></div>
//! <div style="background-color: #022c22"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`TEAL`]</div>
//! <div style="background-color: #f0fdfa"></div> <div style="background-color: #ccfbf1"></div>
//! <div style="background-color: #99f6e4"></div> <div style="background-color: #5eead4"></div>
//! <div style="background-color: #2dd4bf"></div> <div style="background-color: #14b8a6"></div>
//! <div style="background-color: #0d9488"></div> <div style="background-color: #0f766e"></div>
//! <div style="background-color: #115e59"></div> <div style="background-color: #134e4a"></div>
//! <div style="background-color: #042f2e"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`CYAN`]</div>
//! <div style="background-color: #ecfeff"></div> <div style="background-color: #cffafe"></div>
//! <div style="background-color: #a5f3fc"></div> <div style="background-color: #67e8f9"></div>
//! <div style="background-color: #22d3ee"></div> <div style="background-color: #06b6d4"></div>
//! <div style="background-color: #0891b2"></div> <div style="background-color: #0e7490"></div>
//! <div style="background-color: #155e75"></div> <div style="background-color: #164e63"></div>
//! <div style="background-color: #083344"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`SKY`]</div>
//! <div style="background-color: #f0f9ff"></div> <div style="background-color: #e0f2fe"></div>
//! <div style="background-color: #bae6fd"></div> <div style="background-color: #7dd3fc"></div>
//! <div style="background-color: #38bdf8"></div> <div style="background-color: #0ea5e9"></div>
//! <div style="background-color: #0284c7"></div> <div style="background-color: #0369a1"></div>
//! <div style="background-color: #075985"></div> <div style="background-color: #0c4a6e"></div>
//! <div style="background-color: #082f49"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLUE`]</div>
//! <div style="background-color: #eff6ff"></div> <div style="background-color: #dbeafe"></div>
//! <div style="background-color: #bfdbfe"></div> <div style="background-color: #93c5fd"></div>
//! <div style="background-color: #60a5fa"></div> <div style="background-color: #3b82f6"></div>
//! <div style="background-color: #2563eb"></div> <div style="background-color: #1d4ed8"></div>
//! <div style="background-color: #1e40af"></div> <div style="background-color: #1e3a8a"></div>
//! <div style="background-color: #172554"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`INDIGO`]</div>
//! <div style="background-color: #eef2ff"></div> <div style="background-color: #e0e7ff"></div>
//! <div style="background-color: #c7d2fe"></div> <div style="background-color: #a5b4fc"></div>
//! <div style="background-color: #818cf8"></div> <div style="background-color: #6366f1"></div>
//! <div style="background-color: #4f46e5"></div> <div style="background-color: #4338ca"></div>
//! <div style="background-color: #3730a3"></div> <div style="background-color: #312e81"></div>
//! <div style="background-color: #1e1b4b"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`VIOLET`]</div>
//! <div style="background-color: #f5f3ff"></div> <div style="background-color: #ede9fe"></div>
//! <div style="background-color: #ddd6fe"></div> <div style="background-color: #c4b5fd"></div>
//! <div style="background-color: #a78bfa"></div> <div style="background-color: #8b5cf6"></div>
//! <div style="background-color: #7c3aed"></div> <div style="background-color: #6d28d9"></div>
//! <div style="background-color: #5b21b6"></div> <div style="background-color: #4c1d95"></div>
//! <div style="background-color: #2e1065"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PURPLE`]</div>
//! <div style="background-color: #faf5ff"></div> <div style="background-color: #f3e8ff"></div>
//! <div style="background-color: #e9d5ff"></div> <div style="background-color: #d8b4fe"></div>
//! <div style="background-color: #c084fc"></div> <div style="background-color: #a855f7"></div>
//! <div style="background-color: #9333ea"></div> <div style="background-color: #7e22ce"></div>
//! <div style="background-color: #6b21a8"></div> <div style="background-color: #581c87"></div>
//! <div style="background-color: #4c136e"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`FUCHSIA`]</div>
//! <div style="background-color: #fdf4ff"></div> <div style="background-color: #fae8ff"></div>
//! <div style="background-color: #f5d0fe"></div> <div style="background-color: #f0abfc"></div>
//! <div style="background-color: #e879f9"></div> <div style="background-color: #d946ef"></div>
//! <div style="background-color: #c026d3"></div> <div style="background-color: #a21caf"></div>
//! <div style="background-color: #86198f"></div> <div style="background-color: #701a75"></div>
//! <div style="background-color: #4e145b"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PINK`]</div>
//! <div style="background-color: #fdf2f8"></div> <div style="background-color: #fce7f3"></div>
//! <div style="background-color: #fbcfe8"></div> <div style="background-color: #f9a8d4"></div>
//! <div style="background-color: #f472b6"></div> <div style="background-color: #ec4899"></div>
//! <div style="background-color: #db2777"></div> <div style="background-color: #be185d"></div>
//! <div style="background-color: #9d174d"></div> <div style="background-color: #831843"></div>
//! <div style="background-color: #5f0b37"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLACK`]</div>
//! <div style="background-color: #000000; width:22rem"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`WHITE`]</div>
//! <div style="background-color: #ffffff; width:22rem"></div>
//! </div>
//! </div>
//! </div>
//!
//! # Example
//!
//! ```rust
//! # use ratatui::prelude::*;
//! use ratatui::style::palette::tailwind::{BLUE, RED};
//!
//! assert_eq!(RED.c500, Color::Rgb(239, 68, 68));
//! assert_eq!(BLUE.c500, Color::Rgb(59, 130, 246));
//! ```
use crate::prelude::*;
pub struct Palette {
pub c50: Color,
pub c100: Color,
pub c200: Color,
pub c300: Color,
pub c400: Color,
pub c500: Color,
pub c600: Color,
pub c700: Color,
pub c800: Color,
pub c900: Color,
pub c950: Color,
}
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #000000"></div></div>
pub const BLACK: Color = Color::from_u32(0x000000);
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #ffffff"></div></div>
pub const WHITE: Color = Color::from_u32(0xffffff);
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f8fafc"></div><div style="background-color: #f1f5f9"></div><div style="background-color: #e2e8f0"></div><div style="background-color: #cbd5e1"></div><div style="background-color: #94a3b8"></div><div style="background-color: #64748b"></div><div style="background-color: #475569"></div><div style="background-color: #334155"></div><div style="background-color: #1e293b"></div><div style="background-color: #0f172a"></div><div style="background-color: #020617"></div></div>
pub const SLATE: Palette = Palette {
c50: Color::from_u32(0xf8fafc),
c100: Color::from_u32(0xf1f5f9),
c200: Color::from_u32(0xe2e8f0),
c300: Color::from_u32(0xcbd5e1),
c400: Color::from_u32(0x94a3b8),
c500: Color::from_u32(0x64748b),
c600: Color::from_u32(0x475569),
c700: Color::from_u32(0x334155),
c800: Color::from_u32(0x1e293b),
c900: Color::from_u32(0x0f172a),
c950: Color::from_u32(0x020617),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f9fafb"></div><div style="background-color: #f3f4f6"></div><div style="background-color: #e5e7eb"></div><div style="background-color: #d1d5db"></div><div style="background-color: #9ca3af"></div><div style="background-color: #6b7280"></div><div style="background-color: #4b5563"></div><div style="background-color: #374151"></div><div style="background-color: #1f2937"></div><div style="background-color: #111827"></div><div style="background-color: #030712"></div></div>
pub const GRAY: Palette = Palette {
c50: Color::from_u32(0xf9fafb),
c100: Color::from_u32(0xf3f4f6),
c200: Color::from_u32(0xe5e7eb),
c300: Color::from_u32(0xd1d5db),
c400: Color::from_u32(0x9ca3af),
c500: Color::from_u32(0x6b7280),
c600: Color::from_u32(0x4b5563),
c700: Color::from_u32(0x374151),
c800: Color::from_u32(0x1f2937),
c900: Color::from_u32(0x111827),
c950: Color::from_u32(0x030712),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafafa"></div><div style="background-color: #f5f5f5"></div><div style="background-color: #e5e5e5"></div><div style="background-color: #d4d4d4"></div><div style="background-color: #a1a1aa"></div><div style="background-color: #71717a"></div><div style="background-color: #52525b"></div><div style="background-color: #404040"></div><div style="background-color: #262626"></div><div style="background-color: #171717"></div><div style="background-color: #09090b"></div></div>
pub const ZINC: Palette = Palette {
c50: Color::from_u32(0xfafafa),
c100: Color::from_u32(0xf4f4f5),
c200: Color::from_u32(0xe4e4e7),
c300: Color::from_u32(0xd4d4d8),
c400: Color::from_u32(0xa1a1aa),
c500: Color::from_u32(0x71717a),
c600: Color::from_u32(0x52525b),
c700: Color::from_u32(0x3f3f46),
c800: Color::from_u32(0x27272a),
c900: Color::from_u32(0x18181b),
c950: Color::from_u32(0x09090b),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafafa"></div><div style="background-color: #f5f5f5"></div><div style="background-color: #e5e5e5"></div><div style="background-color: #d4d4d4"></div><div style="background-color: #a3a3a3"></div><div style="background-color: #737373"></div><div style="background-color: #525252"></div><div style="background-color: #404040"></div><div style="background-color: #262626"></div><div style="background-color: #171717"></div><div style="background-color: #0a0a0a"></div></div>
pub const NEUTRAL: Palette = Palette {
c50: Color::from_u32(0xfafafa),
c100: Color::from_u32(0xf5f5f5),
c200: Color::from_u32(0xe5e5e5),
c300: Color::from_u32(0xd4d4d4),
c400: Color::from_u32(0xa3a3a3),
c500: Color::from_u32(0x737373),
c600: Color::from_u32(0x525252),
c700: Color::from_u32(0x404040),
c800: Color::from_u32(0x262626),
c900: Color::from_u32(0x171717),
c950: Color::from_u32(0x0a0a0a),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafaf9"></div><div style="background-color: #f5f5f4"></div><div style="background-color: #e7e5e4"></div><div style="background-color: #d6d3d1"></div><div style="background-color: #a8a29e"></div><div style="background-color: #78716c"></div><div style="background-color: #57534e"></div><div style="background-color: #44403c"></div><div style="background-color: #292524"></div><div style="background-color: #1c1917"></div><div style="background-color: #0c0a09"></div></div>
pub const STONE: Palette = Palette {
c50: Color::from_u32(0xfafaf9),
c100: Color::from_u32(0xf5f5f4),
c200: Color::from_u32(0xe7e5e4),
c300: Color::from_u32(0xd6d3d1),
c400: Color::from_u32(0xa8a29e),
c500: Color::from_u32(0x78716c),
c600: Color::from_u32(0x57534e),
c700: Color::from_u32(0x44403c),
c800: Color::from_u32(0x292524),
c900: Color::from_u32(0x1c1917),
c950: Color::from_u32(0x0c0a09),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fef2f2"></div><div style="background-color: #fee2e2"></div><div style="background-color: #fecaca"></div><div style="background-color: #fca5a5"></div><div style="background-color: #f87171"></div><div style="background-color: #ef4444"></div><div style="background-color: #dc2626"></div><div style="background-color: #b91c1c"></div><div style="background-color: #991b1b"></div><div style="background-color: #7f1d1d"></div><div style="background-color: #450a0a"></div></div>
pub const RED: Palette = Palette {
c50: Color::from_u32(0xfef2f2),
c100: Color::from_u32(0xfee2e2),
c200: Color::from_u32(0xfecaca),
c300: Color::from_u32(0xfca5a5),
c400: Color::from_u32(0xf87171),
c500: Color::from_u32(0xef4444),
c600: Color::from_u32(0xdc2626),
c700: Color::from_u32(0xb91c1c),
c800: Color::from_u32(0x991b1b),
c900: Color::from_u32(0x7f1d1d),
c950: Color::from_u32(0x450a0a),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fff7ed"></div><div style="background-color: #ffedd5"></div><div style="background-color: #fed7aa"></div><div style="background-color: #fdba74"></div><div style="background-color: #fb923c"></div><div style="background-color: #f97316"></div><div style="background-color: #ea580c"></div><div style="background-color: #c2410c"></div><div style="background-color: #9a3412"></div><div style="background-color: #7c2d12"></div><div style="background-color: #431407"></div></div>
pub const ORANGE: Palette = Palette {
c50: Color::from_u32(0xfff7ed),
c100: Color::from_u32(0xffedd5),
c200: Color::from_u32(0xfed7aa),
c300: Color::from_u32(0xfdba74),
c400: Color::from_u32(0xfb923c),
c500: Color::from_u32(0xf97316),
c600: Color::from_u32(0xea580c),
c700: Color::from_u32(0xc2410c),
c800: Color::from_u32(0x9a3412),
c900: Color::from_u32(0x7c2d12),
c950: Color::from_u32(0x431407),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fffbeb"></div><div style="background-color: #fef3c7"></div><div style="background-color: #fde68a"></div><div style="background-color: #fcd34d"></div><div style="background-color: #fbbf24"></div><div style="background-color: #f59e0b"></div><div style="background-color: #d97706"></div><div style="background-color: #b45309"></div><div style="background-color: #92400e"></div><div style="background-color: #78350f"></div><div style="background-color: #451a03"></div></div>
pub const AMBER: Palette = Palette {
c50: Color::from_u32(0xfffbeb),
c100: Color::from_u32(0xfef3c7),
c200: Color::from_u32(0xfde68a),
c300: Color::from_u32(0xfcd34d),
c400: Color::from_u32(0xfbbf24),
c500: Color::from_u32(0xf59e0b),
c600: Color::from_u32(0xd97706),
c700: Color::from_u32(0xb45309),
c800: Color::from_u32(0x92400e),
c900: Color::from_u32(0x78350f),
c950: Color::from_u32(0x451a03),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fefce8"></div><div style="background-color: #fef9c3"></div><div style="background-color: #fef08a"></div><div style="background-color: #fde047"></div><div style="background-color: #facc15"></div><div style="background-color: #eab308"></div><div style="background-color: #ca8a04"></div><div style="background-color: #a16207"></div><div style="background-color: #854d0e"></div><div style="background-color: #713f12"></div><div style="background-color: #422006"></div></div>
pub const YELLOW: Palette = Palette {
c50: Color::from_u32(0xfefce8),
c100: Color::from_u32(0xfef9c3),
c200: Color::from_u32(0xfef08a),
c300: Color::from_u32(0xfde047),
c400: Color::from_u32(0xfacc15),
c500: Color::from_u32(0xeab308),
c600: Color::from_u32(0xca8a04),
c700: Color::from_u32(0xa16207),
c800: Color::from_u32(0x854d0e),
c900: Color::from_u32(0x713f12),
c950: Color::from_u32(0x422006),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f7fee7"></div><div style="background-color: #ecfccb"></div><div style="background-color: #d9f99d"></div><div style="background-color: #bef264"></div><div style="background-color: #a3e635"></div><div style="background-color: #84cc16"></div><div style="background-color: #65a30d"></div><div style="background-color: #4d7c0f"></div><div style="background-color: #3f6212"></div><div style="background-color: #365314"></div><div style="background-color: #1a2e05"></div></div>
pub const LIME: Palette = Palette {
c50: Color::from_u32(0xf7fee7),
c100: Color::from_u32(0xecfccb),
c200: Color::from_u32(0xd9f99d),
c300: Color::from_u32(0xbef264),
c400: Color::from_u32(0xa3e635),
c500: Color::from_u32(0x84cc16),
c600: Color::from_u32(0x65a30d),
c700: Color::from_u32(0x4d7c0f),
c800: Color::from_u32(0x3f6212),
c900: Color::from_u32(0x365314),
c950: Color::from_u32(0x1a2e05),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0fdf4"></div><div style="background-color: #dcfce7"></div><div style="background-color: #bbf7d0"></div><div style="background-color: #86efac"></div><div style="background-color: #4ade80"></div><div style="background-color: #22c55e"></div><div style="background-color: #16a34a"></div><div style="background-color: #15803d"></div><div style="background-color: #166534"></div><div style="background-color: #14532d"></div><div style="background-color: #052e16"></div></div>
pub const GREEN: Palette = Palette {
c50: Color::from_u32(0xf0fdf4),
c100: Color::from_u32(0xdcfce7),
c200: Color::from_u32(0xbbf7d0),
c300: Color::from_u32(0x86efac),
c400: Color::from_u32(0x4ade80),
c500: Color::from_u32(0x22c55e),
c600: Color::from_u32(0x16a34a),
c700: Color::from_u32(0x15803d),
c800: Color::from_u32(0x166534),
c900: Color::from_u32(0x14532d),
c950: Color::from_u32(0x052e16),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0fdfa"></div><div style="background-color: #ccfbf1"></div><div style="background-color: #99f6e4"></div><div style="background-color: #5eead4"></div><div style="background-color: #2dd4bf"></div><div style="background-color: #14b8a6"></div><div style="background-color: #0d9488"></div><div style="background-color: #0f766e"></div><div style="background-color: #115e59"></div><div style="background-color: #134e4a"></div><div style="background-color: #042f2e"></div></div>
pub const EMERALD: Palette = Palette {
c50: Color::from_u32(0xecfdf5),
c100: Color::from_u32(0xd1fae5),
c200: Color::from_u32(0xa7f3d0),
c300: Color::from_u32(0x6ee7b7),
c400: Color::from_u32(0x34d399),
c500: Color::from_u32(0x10b981),
c600: Color::from_u32(0x059669),
c700: Color::from_u32(0x047857),
c800: Color::from_u32(0x065f46),
c900: Color::from_u32(0x064e3b),
c950: Color::from_u32(0x022c22),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f5fdf4"></div><div style="background-color: #e7f9e7"></div><div style="background-color: #c6f6d5"></div><div style="background-color: #9ae6b4"></div><div style="background-color: #68d391"></div><div style="background-color: #48bb78"></div><div style="background-color: #38a169"></div><div style="background-color: #2f855a"></div><div style="background-color: #276749"></div><div style="background-color: #22543d"></div><div style="background-color: #0d3321"></div></div>
pub const TEAL: Palette = Palette {
c50: Color::from_u32(0xf0fdfa),
c100: Color::from_u32(0xccfbf1),
c200: Color::from_u32(0x99f6e4),
c300: Color::from_u32(0x5eead4),
c400: Color::from_u32(0x2dd4bf),
c500: Color::from_u32(0x14b8a6),
c600: Color::from_u32(0x0d9488),
c700: Color::from_u32(0x0f766e),
c800: Color::from_u32(0x115e59),
c900: Color::from_u32(0x134e4a),
c950: Color::from_u32(0x042f2e),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #ecfeff"></div><div style="background-color: #cffafe"></div><div style="background-color: #a5f3fc"></div><div style="background-color: #67e8f9"></div><div style="background-color: #22d3ee"></div><div style="background-color: #06b6d4"></div><div style="background-color: #0891b2"></div><div style="background-color: #0e7490"></div><div style="background-color: #155e75"></div><div style="background-color: #164e63"></div><div style="background-color: #083344"></div></div>
pub const CYAN: Palette = Palette {
c50: Color::from_u32(0xecfeff),
c100: Color::from_u32(0xcffafe),
c200: Color::from_u32(0xa5f3fc),
c300: Color::from_u32(0x67e8f9),
c400: Color::from_u32(0x22d3ee),
c500: Color::from_u32(0x06b6d4),
c600: Color::from_u32(0x0891b2),
c700: Color::from_u32(0x0e7490),
c800: Color::from_u32(0x155e75),
c900: Color::from_u32(0x164e63),
c950: Color::from_u32(0x083344),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0f9ff"></div><div style="background-color: #e0f2fe"></div><div style="background-color: #bae6fd"></div><div style="background-color: #7dd3fc"></div><div style="background-color: #38bdf8"></div><div style="background-color: #0ea5e9"></div><div style="background-color: #0284c7"></div><div style="background-color: #0369a1"></div><div style="background-color: #075985"></div><div style="background-color: #0c4a6e"></div><div style="background-color: #082f49"></div></div>
pub const SKY: Palette = Palette {
c50: Color::from_u32(0xf0f9ff),
c100: Color::from_u32(0xe0f2fe),
c200: Color::from_u32(0xbae6fd),
c300: Color::from_u32(0x7dd3fc),
c400: Color::from_u32(0x38bdf8),
c500: Color::from_u32(0x0ea5e9),
c600: Color::from_u32(0x0284c7),
c700: Color::from_u32(0x0369a1),
c800: Color::from_u32(0x075985),
c900: Color::from_u32(0x0c4a6e),
c950: Color::from_u32(0x082f49),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #eff6ff"></div><div style="background-color: #dbeafe"></div><div style="background-color: #bfdbfe"></div><div style="background-color: #93c5fd"></div><div style="background-color: #60a5fa"></div><div style="background-color: #3b82f6"></div><div style="background-color: #2563eb"></div><div style="background-color: #1d4ed8"></div><div style="background-color: #1e40af"></div><div style="background-color: #1e3a8a"></div><div style="background-color: #172554"></div></div>
pub const BLUE: Palette = Palette {
c50: Color::from_u32(0xeff6ff),
c100: Color::from_u32(0xdbeafe),
c200: Color::from_u32(0xbfdbfe),
c300: Color::from_u32(0x93c5fd),
c400: Color::from_u32(0x60a5fa),
c500: Color::from_u32(0x3b82f6),
c600: Color::from_u32(0x2563eb),
c700: Color::from_u32(0x1d4ed8),
c800: Color::from_u32(0x1e40af),
c900: Color::from_u32(0x1e3a8a),
c950: Color::from_u32(0x172554),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #eef2ff"></div><div style="background-color: #e0e7ff"></div><div style="background-color: #c7d2fe"></div><div style="background-color: #a5b4fc"></div><div style="background-color: #818cf8"></div><div style="background-color: #6366f1"></div><div style="background-color: #4f46e5"></div><div style="background-color: #4338ca"></div><div style="background-color: #3730a3"></div><div style="background-color: #312e81"></div><div style="background-color: #1e1b4b"></div></div>
pub const INDIGO: Palette = Palette {
c50: Color::from_u32(0xeef2ff),
c100: Color::from_u32(0xe0e7ff),
c200: Color::from_u32(0xc7d2fe),
c300: Color::from_u32(0xa5b4fc),
c400: Color::from_u32(0x818cf8),
c500: Color::from_u32(0x6366f1),
c600: Color::from_u32(0x4f46e5),
c700: Color::from_u32(0x4338ca),
c800: Color::from_u32(0x3730a3),
c900: Color::from_u32(0x312e81),
c950: Color::from_u32(0x1e1b4b),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f5f3ff"></div><div style="background-color: #ede9fe"></div><div style="background-color: #ddd6fe"></div><div style="background-color: #c4b5fd"></div><div style="background-color: #a78bfa"></div><div style="background-color: #8b5cf6"></div><div style="background-color: #7c3aed"></div><div style="background-color: #6d28d9"></div><div style="background-color: #5b21b6"></div><div style="background-color: #4c1d95"></div><div style="background-color: #2e1065"></div></div>
pub const VIOLET: Palette = Palette {
c50: Color::from_u32(0xf5f3ff),
c100: Color::from_u32(0xede9fe),
c200: Color::from_u32(0xddd6fe),
c300: Color::from_u32(0xc4b5fd),
c400: Color::from_u32(0xa78bfa),
c500: Color::from_u32(0x8b5cf6),
c600: Color::from_u32(0x7c3aed),
c700: Color::from_u32(0x6d28d9),
c800: Color::from_u32(0x5b21b6),
c900: Color::from_u32(0x4c1d95),
c950: Color::from_u32(0x2e1065),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #faf5ff"></div><div style="background-color: #f3e8ff"></div><div style="background-color: #e9d5ff"></div><div style="background-color: #d8b4fe"></div><div style="background-color: #c084fc"></div><div style="background-color: #a855f7"></div><div style="background-color: #9333ea"></div><div style="background-color: #7e22ce"></div><div style="background-color: #6b21a8"></div><div style="background-color: #581c87"></div><div style="background-color: #3b0764"></div></div>
pub const PURPLE: Palette = Palette {
c50: Color::from_u32(0xfaf5ff),
c100: Color::from_u32(0xf3e8ff),
c200: Color::from_u32(0xe9d5ff),
c300: Color::from_u32(0xd8b4fe),
c400: Color::from_u32(0xc084fc),
c500: Color::from_u32(0xa855f7),
c600: Color::from_u32(0x9333ea),
c700: Color::from_u32(0x7e22ce),
c800: Color::from_u32(0x6b21a8),
c900: Color::from_u32(0x581c87),
c950: Color::from_u32(0x3b0764),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fdf4ff"></div><div style="background-color: #fae8ff"></div><div style="background-color: #f5d0fe"></div><div style="background-color: #f0abfc"></div><div style="background-color: #e879f9"></div><div style="background-color: #d946ef"></div><div style="background-color: #c026d3"></div><div style="background-color: #a21caf"></div><div style="background-color: #86198f"></div><div style="background-color: #701a75"></div><div style="background-color: #4a044e"></div></div>
pub const FUCHSIA: Palette = Palette {
c50: Color::from_u32(0xfdf4ff),
c100: Color::from_u32(0xfae8ff),
c200: Color::from_u32(0xf5d0fe),
c300: Color::from_u32(0xf0abfc),
c400: Color::from_u32(0xe879f9),
c500: Color::from_u32(0xd946ef),
c600: Color::from_u32(0xc026d3),
c700: Color::from_u32(0xa21caf),
c800: Color::from_u32(0x86198f),
c900: Color::from_u32(0x701a75),
c950: Color::from_u32(0x4a044e),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fdf2f8"></div><div style="background-color: #fce7f3"></div><div style="background-color: #fbcfe8"></div><div style="background-color: #f9a8d4"></div><div style="background-color: #f472b6"></div><div style="background-color: #ec4899"></div><div style="background-color: #db2777"></div><div style="background-color: #be185d"></div><div style="background-color: #9d174d"></div><div style="background-color: #831843"></div><div style="background-color: #500724"></div></div>
pub const PINK: Palette = Palette {
c50: Color::from_u32(0xfdf2f8),
c100: Color::from_u32(0xfce7f3),
c200: Color::from_u32(0xfbcfe8),
c300: Color::from_u32(0xf9a8d4),
c400: Color::from_u32(0xf472b6),
c500: Color::from_u32(0xec4899),
c600: Color::from_u32(0xdb2777),
c700: Color::from_u32(0xbe185d),
c800: Color::from_u32(0x9d174d),
c900: Color::from_u32(0x831843),
c950: Color::from_u32(0x500724),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fff1f2"></div><div style="background-color: #ffe4e6"></div><div style="background-color: #fecdd3"></div><div style="background-color: #fda4af"></div><div style="background-color: #fb7185"></div><div style="background-color: #f43f5e"></div><div style="background-color: #e11d48"></div><div style="background-color: #be123c"></div><div style="background-color: #9f1239"></div><div style="background-color: #881337"></div><div style="background-color: #4c0519"></div></div>
pub const ROSE: Palette = Palette {
c50: Color::from_u32(0xfff1f2),
c100: Color::from_u32(0xffe4e6),
c200: Color::from_u32(0xfecdd3),
c300: Color::from_u32(0xfda4af),
c400: Color::from_u32(0xfb7185),
c500: Color::from_u32(0xf43f5e),
c600: Color::from_u32(0xe11d48),
c700: Color::from_u32(0xbe123c),
c800: Color::from_u32(0x9f1239),
c900: Color::from_u32(0x881337),
c950: Color::from_u32(0x4c0519),
};

View File

@@ -1,83 +0,0 @@
//! Conversions from colors in the `palette` crate to [`Color`].
use ::palette::{
bool_mask::LazySelect,
num::{Arithmetics, MulSub, PartialCmp, Powf, Real},
LinSrgb,
};
use palette::{stimulus::IntoStimulus, Srgb};
use super::Color;
/// Convert an [`palette::Srgb`] color to a [`Color`].
///
/// # Examples
///
/// ```
/// use palette::Srgb;
/// use ratatui::style::Color;
///
/// let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
/// ```
impl<T: IntoStimulus<u8>> From<Srgb<T>> for Color {
fn from(color: Srgb<T>) -> Self {
let (red, green, blue) = color.into_format().into_components();
Self::Rgb(red, green, blue)
}
}
/// Convert a [`palette::LinSrgb`] color to a [`Color`].
///
/// Note: this conversion only works for floating point linear sRGB colors. If you have a linear
/// sRGB color in another format, you need to convert it to floating point first.
///
/// # Examples
///
/// ```
/// use palette::LinSrgb;
/// use ratatui::style::Color;
///
/// let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
/// ```
impl<T: IntoStimulus<u8>> From<LinSrgb<T>> for Color
where
T: Real + Powf + MulSub + Arithmetics + PartialCmp + Clone,
T::Mask: LazySelect<T>,
{
fn from(color: LinSrgb<T>) -> Self {
let srgb_color = Srgb::<T>::from_linear(color);
Self::from(srgb_color)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_srgb() {
const RED: Color = Color::Rgb(255, 0, 0);
assert_eq!(Color::from(Srgb::new(255u8, 0, 0)), RED);
assert_eq!(Color::from(Srgb::new(65535u16, 0, 0)), RED);
assert_eq!(Color::from(Srgb::new(1.0f32, 0.0, 0.0)), RED);
assert_eq!(
Color::from(Srgb::new(0.5f32, 0.5, 0.5)),
Color::Rgb(128, 128, 128)
);
}
#[test]
fn from_lin_srgb() {
const RED: Color = Color::Rgb(255, 0, 0);
assert_eq!(Color::from(LinSrgb::new(1.0f32, 0.0, 0.0)), RED);
assert_eq!(Color::from(LinSrgb::new(1.0f64, 0.0, 0.0)), RED);
assert_eq!(
Color::from(LinSrgb::new(0.5f32, 0.5, 0.5)),
Color::Rgb(188, 188, 188)
);
}
}

View File

@@ -1,576 +0,0 @@
use std::fmt;
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;
/// Returns the style of the object.
fn style(&self) -> Style;
/// Sets the style of the object.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item;
}
/// A helper struct to make it easy to debug using the `Stylize` method names
pub(crate) struct ColorDebug {
pub kind: ColorDebugKind,
pub color: Color,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub(crate) enum ColorDebugKind {
Foreground,
Background,
#[cfg(feature = "underline-color")]
Underline,
}
impl fmt::Debug for ColorDebug {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(feature = "underline-color")]
let is_underline = self.kind == ColorDebugKind::Underline;
#[cfg(not(feature = "underline-color"))]
let is_underline = false;
if is_underline
|| matches!(
self.color,
Color::Reset | Color::Indexed(_) | Color::Rgb(_, _, _)
)
{
match self.kind {
ColorDebugKind::Foreground => write!(f, ".fg(")?,
ColorDebugKind::Background => write!(f, ".bg(")?,
#[cfg(feature = "underline-color")]
ColorDebugKind::Underline => write!(f, ".underline_color(")?,
}
write!(f, "Color::{:?}", self.color)?;
write!(f, ")")?;
return Ok(());
}
match self.kind {
ColorDebugKind::Foreground => write!(f, ".")?,
ColorDebugKind::Background => write!(f, ".on_")?,
// TODO: .underline_color_xxx is not implemented on Stylize yet, but it should be
#[cfg(feature = "underline-color")]
ColorDebugKind::Underline => {
unreachable!("covered by the first part of the if statement")
}
}
match self.color {
Color::Black => write!(f, "black")?,
Color::Red => write!(f, "red")?,
Color::Green => write!(f, "green")?,
Color::Yellow => write!(f, "yellow")?,
Color::Blue => write!(f, "blue")?,
Color::Magenta => write!(f, "magenta")?,
Color::Cyan => write!(f, "cyan")?,
Color::Gray => write!(f, "gray")?,
Color::DarkGray => write!(f, "dark_gray")?,
Color::LightRed => write!(f, "light_red")?,
Color::LightGreen => write!(f, "light_green")?,
Color::LightYellow => write!(f, "light_yellow")?,
Color::LightBlue => write!(f, "light_blue")?,
Color::LightMagenta => write!(f, "light_magenta")?,
Color::LightCyan => write!(f, "light_cyan")?,
Color::White => write!(f, "white")?,
_ => unreachable!("covered by the first part of the if statement"),
}
write!(f, "()")
}
}
/// 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 ")."]
#[must_use = concat!("`", stringify!($color), "` returns the modified style without modifying the original")]
fn $color(self) -> T {
self.fg(Color::[<$color:camel>])
}
#[doc = "Sets the background color to [`" $color "`](Color::" $color:camel ")."]
#[must_use = concat!("`on_", stringify!($color), "` returns the modified style without modifying the original")]
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."]
#[must_use = concat!("`", stringify!($modifier), "` returns the modified style without modifying the original")]
fn [<$modifier>](self) -> T {
self.add_modifier(Modifier::[<$modifier:upper>])
}
}
paste! {
#[doc = "Removes the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
#[must_use = concat!("`not_", stringify!($modifier), "` returns the modified style without modifying the original")]
fn [<not_ $modifier>](self) -> T {
self.remove_modifier(Modifier::[<$modifier:upper>])
}
}
};
}
/// An extension trait for styling objects.
///
/// For any type that implements `Stylize`, the provided methods in this trait can be used to style
/// the type further. This trait is automatically implemented for any type that implements the
/// [`Styled`] trait which e.g.: [`String`], [`&str`], [`Span`], [`Style`] and many Widget types.
///
/// This results in much more ergonomic styling of text and widgets. For example, instead of
/// writing:
///
/// ```rust,ignore
/// let text = Span::styled("Hello", Style::default().fg(Color::Red).bg(Color::Blue));
/// ```
///
/// You can write:
///
/// ```rust,ignore
/// let text = "Hello".red().on_blue();
/// ```
///
/// This trait implements a provided method for every color as both foreground and background
/// (prefixed by `on_`), and all modifiers as both an additive and subtractive modifier (prefixed
/// by `not_`). The `reset()` method is also provided to reset the style.
///
/// # Examples
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// let span = "hello".red().on_blue().bold();
/// let line = Line::from(vec![
/// "hello".red().on_blue().bold(),
/// "world".green().on_yellow().not_bold(),
/// ]);
/// let paragraph = Paragraph::new(line).italic().underlined();
/// let block = Block::bordered().title("Title").on_white().bold();
/// ```
pub trait Stylize<'a, T>: Sized {
#[must_use = "`bg` returns the modified style without modifying the original"]
fn bg<C: Into<Color>>(self, color: C) -> T;
#[must_use = "`fg` returns the modified style without modifying the original"]
fn fg<C: Into<Color>>(self, color: C) -> T;
#[must_use = "`reset` returns the modified style without modifying the original"]
fn reset(self) -> T;
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
fn add_modifier(self, modifier: Modifier) -> T;
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
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<C: Into<Color>>(self, color: C) -> T {
let style = self.style().bg(color.into());
self.set_style(style)
}
fn fg<C: Into<Color>>(self, color: C) -> 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<S: Into<Style>>(self, style: S) -> Self::Item {
Span::styled(self, style)
}
}
impl Styled for String {
type Item = Span<'static>;
fn style(&self) -> Style {
Style::default()
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
Span::styled(self, style)
}
}
#[cfg(test)]
mod tests {
use itertools::Itertools;
use rstest::rstest;
use super::*;
#[test]
fn str_styled() {
assert_eq!("hello".style(), Style::default());
assert_eq!(
"hello".set_style(Style::new().cyan()),
Span::styled("hello", Style::new().cyan())
);
assert_eq!("hello".black(), Span::from("hello").black());
assert_eq!("hello".red(), Span::from("hello").red());
assert_eq!("hello".green(), Span::from("hello").green());
assert_eq!("hello".yellow(), Span::from("hello").yellow());
assert_eq!("hello".blue(), Span::from("hello").blue());
assert_eq!("hello".magenta(), Span::from("hello").magenta());
assert_eq!("hello".cyan(), Span::from("hello").cyan());
assert_eq!("hello".gray(), Span::from("hello").gray());
assert_eq!("hello".dark_gray(), Span::from("hello").dark_gray());
assert_eq!("hello".light_red(), Span::from("hello").light_red());
assert_eq!("hello".light_green(), Span::from("hello").light_green());
assert_eq!("hello".light_yellow(), Span::from("hello").light_yellow());
assert_eq!("hello".light_blue(), Span::from("hello").light_blue());
assert_eq!("hello".light_magenta(), Span::from("hello").light_magenta());
assert_eq!("hello".light_cyan(), Span::from("hello").light_cyan());
assert_eq!("hello".white(), Span::from("hello").white());
assert_eq!("hello".on_black(), Span::from("hello").on_black());
assert_eq!("hello".on_red(), Span::from("hello").on_red());
assert_eq!("hello".on_green(), Span::from("hello").on_green());
assert_eq!("hello".on_yellow(), Span::from("hello").on_yellow());
assert_eq!("hello".on_blue(), Span::from("hello").on_blue());
assert_eq!("hello".on_magenta(), Span::from("hello").on_magenta());
assert_eq!("hello".on_cyan(), Span::from("hello").on_cyan());
assert_eq!("hello".on_gray(), Span::from("hello").on_gray());
assert_eq!("hello".on_dark_gray(), Span::from("hello").on_dark_gray());
assert_eq!("hello".on_light_red(), Span::from("hello").on_light_red());
assert_eq!(
"hello".on_light_green(),
Span::from("hello").on_light_green()
);
assert_eq!(
"hello".on_light_yellow(),
Span::from("hello").on_light_yellow()
);
assert_eq!("hello".on_light_blue(), Span::from("hello").on_light_blue());
assert_eq!(
"hello".on_light_magenta(),
Span::from("hello").on_light_magenta()
);
assert_eq!("hello".on_light_cyan(), Span::from("hello").on_light_cyan());
assert_eq!("hello".on_white(), Span::from("hello").on_white());
assert_eq!("hello".bold(), Span::from("hello").bold());
assert_eq!("hello".dim(), Span::from("hello").dim());
assert_eq!("hello".italic(), Span::from("hello").italic());
assert_eq!("hello".underlined(), Span::from("hello").underlined());
assert_eq!("hello".slow_blink(), Span::from("hello").slow_blink());
assert_eq!("hello".rapid_blink(), Span::from("hello").rapid_blink());
assert_eq!("hello".reversed(), Span::from("hello").reversed());
assert_eq!("hello".hidden(), Span::from("hello").hidden());
assert_eq!("hello".crossed_out(), Span::from("hello").crossed_out());
assert_eq!("hello".not_bold(), Span::from("hello").not_bold());
assert_eq!("hello".not_dim(), Span::from("hello").not_dim());
assert_eq!("hello".not_italic(), Span::from("hello").not_italic());
assert_eq!(
"hello".not_underlined(),
Span::from("hello").not_underlined()
);
assert_eq!(
"hello".not_slow_blink(),
Span::from("hello").not_slow_blink()
);
assert_eq!(
"hello".not_rapid_blink(),
Span::from("hello").not_rapid_blink()
);
assert_eq!("hello".not_reversed(), Span::from("hello").not_reversed());
assert_eq!("hello".not_hidden(), Span::from("hello").not_hidden());
assert_eq!(
"hello".not_crossed_out(),
Span::from("hello").not_crossed_out()
);
assert_eq!("hello".reset(), Span::from("hello").reset());
}
#[test]
fn string_styled() {
let s = String::from("hello");
assert_eq!(s.style(), Style::default());
assert_eq!(
s.clone().set_style(Style::new().cyan()),
Span::styled("hello", Style::new().cyan())
);
assert_eq!(s.clone().black(), Span::from("hello").black());
assert_eq!(s.clone().on_black(), Span::from("hello").on_black());
assert_eq!(s.clone().bold(), Span::from("hello").bold());
assert_eq!(s.clone().not_bold(), Span::from("hello").not_bold());
assert_eq!(s.clone().reset(), Span::from("hello").reset());
}
#[test]
fn temporary_string_styled() {
// to_string() is used to create a temporary String, which is then styled. Without the
// `Styled` trait impl for `String`, this would fail to compile with the error: "temporary
// value dropped while borrowed"
let s = "hello".to_string().red();
assert_eq!(s, Span::from("hello").red());
// format!() is used to create a temporary String inside a closure, which suffers the same
// issue as above without the `Styled` trait impl for `String`
let items = [String::from("a"), String::from("b")];
let sss = items.iter().map(|s| format!("{s}{s}").red()).collect_vec();
assert_eq!(sss, [Span::from("aa").red(), Span::from("bb").red()]);
}
#[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 bg = Style::default().bg(Color::Cyan);
let 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", bg));
assert_eq!("hello".red().cyan(), Span::styled("hello", 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)
);
}
#[rstest]
#[case(ColorDebugKind::Foreground, Color::Black, ".black()")]
#[case(ColorDebugKind::Foreground, Color::Red, ".red()")]
#[case(ColorDebugKind::Foreground, Color::Green, ".green()")]
#[case(ColorDebugKind::Foreground, Color::Yellow, ".yellow()")]
#[case(ColorDebugKind::Foreground, Color::Blue, ".blue()")]
#[case(ColorDebugKind::Foreground, Color::Magenta, ".magenta()")]
#[case(ColorDebugKind::Foreground, Color::Cyan, ".cyan()")]
#[case(ColorDebugKind::Foreground, Color::Gray, ".gray()")]
#[case(ColorDebugKind::Foreground, Color::DarkGray, ".dark_gray()")]
#[case(ColorDebugKind::Foreground, Color::LightRed, ".light_red()")]
#[case(ColorDebugKind::Foreground, Color::LightGreen, ".light_green()")]
#[case(ColorDebugKind::Foreground, Color::LightYellow, ".light_yellow()")]
#[case(ColorDebugKind::Foreground, Color::LightBlue, ".light_blue()")]
#[case(ColorDebugKind::Foreground, Color::LightMagenta, ".light_magenta()")]
#[case(ColorDebugKind::Foreground, Color::LightCyan, ".light_cyan()")]
#[case(ColorDebugKind::Foreground, Color::White, ".white()")]
#[case(
ColorDebugKind::Foreground,
Color::Indexed(10),
".fg(Color::Indexed(10))"
)]
#[case(
ColorDebugKind::Foreground,
Color::Rgb(255, 0, 0),
".fg(Color::Rgb(255, 0, 0))"
)]
#[case(ColorDebugKind::Background, Color::Black, ".on_black()")]
#[case(ColorDebugKind::Background, Color::Red, ".on_red()")]
#[case(ColorDebugKind::Background, Color::Green, ".on_green()")]
#[case(ColorDebugKind::Background, Color::Yellow, ".on_yellow()")]
#[case(ColorDebugKind::Background, Color::Blue, ".on_blue()")]
#[case(ColorDebugKind::Background, Color::Magenta, ".on_magenta()")]
#[case(ColorDebugKind::Background, Color::Cyan, ".on_cyan()")]
#[case(ColorDebugKind::Background, Color::Gray, ".on_gray()")]
#[case(ColorDebugKind::Background, Color::DarkGray, ".on_dark_gray()")]
#[case(ColorDebugKind::Background, Color::LightRed, ".on_light_red()")]
#[case(ColorDebugKind::Background, Color::LightGreen, ".on_light_green()")]
#[case(ColorDebugKind::Background, Color::LightYellow, ".on_light_yellow()")]
#[case(ColorDebugKind::Background, Color::LightBlue, ".on_light_blue()")]
#[case(ColorDebugKind::Background, Color::LightMagenta, ".on_light_magenta()")]
#[case(ColorDebugKind::Background, Color::LightCyan, ".on_light_cyan()")]
#[case(ColorDebugKind::Background, Color::White, ".on_white()")]
#[case(
ColorDebugKind::Background,
Color::Indexed(10),
".bg(Color::Indexed(10))"
)]
#[case(
ColorDebugKind::Background,
Color::Rgb(255, 0, 0),
".bg(Color::Rgb(255, 0, 0))"
)]
#[cfg(feature = "underline-color")]
#[case(
ColorDebugKind::Underline,
Color::Black,
".underline_color(Color::Black)"
)]
#[cfg(feature = "underline-color")]
#[case(ColorDebugKind::Underline, Color::Red, ".underline_color(Color::Red)")]
#[cfg(feature = "underline-color")]
#[case(
ColorDebugKind::Underline,
Color::Green,
".underline_color(Color::Green)"
)]
#[cfg(feature = "underline-color")]
#[case(
ColorDebugKind::Underline,
Color::Yellow,
".underline_color(Color::Yellow)"
)]
fn stylize_debug(#[case] kind: ColorDebugKind, #[case] color: Color, #[case] expected: &str) {
let debug = color.stylize_debug(kind);
assert_eq!(format!("{debug:?}"), expected);
}
}

View File

@@ -1,228 +0,0 @@
use strum::{Display, EnumString};
pub mod border;
pub mod line;
pub mod block {
pub const FULL: &str = "";
pub const SEVEN_EIGHTHS: &str = "";
pub const THREE_QUARTERS: &str = "";
pub const FIVE_EIGHTHS: &str = "";
pub const HALF: &str = "";
pub const THREE_EIGHTHS: &str = "";
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
pub three_quarters: &'static str,
pub five_eighths: &'static str,
pub half: &'static str,
pub three_eighths: &'static str,
pub one_quarter: &'static str,
pub one_eighth: &'static str,
pub empty: &'static str,
}
impl Default for Set {
fn default() -> Self {
NINE_LEVELS
}
}
pub const THREE_LEVELS: Set = Set {
full: FULL,
seven_eighths: FULL,
three_quarters: HALF,
five_eighths: HALF,
half: HALF,
three_eighths: HALF,
one_quarter: HALF,
one_eighth: " ",
empty: " ",
};
pub const NINE_LEVELS: Set = Set {
full: FULL,
seven_eighths: SEVEN_EIGHTHS,
three_quarters: THREE_QUARTERS,
five_eighths: FIVE_EIGHTHS,
half: HALF,
three_eighths: THREE_EIGHTHS,
one_quarter: ONE_QUARTER,
one_eighth: ONE_EIGHTH,
empty: " ",
};
}
pub mod half_block {
pub const UPPER: char = '▀';
pub const LOWER: char = '▄';
pub const FULL: char = '█';
}
pub mod bar {
pub const FULL: &str = "";
pub const SEVEN_EIGHTHS: &str = "";
pub const THREE_QUARTERS: &str = "";
pub const FIVE_EIGHTHS: &str = "";
pub const HALF: &str = "";
pub const THREE_EIGHTHS: &str = "";
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
pub three_quarters: &'static str,
pub five_eighths: &'static str,
pub half: &'static str,
pub three_eighths: &'static str,
pub one_quarter: &'static str,
pub one_eighth: &'static str,
pub empty: &'static str,
}
impl Default for Set {
fn default() -> Self {
NINE_LEVELS
}
}
pub const THREE_LEVELS: Set = Set {
full: FULL,
seven_eighths: FULL,
three_quarters: HALF,
five_eighths: HALF,
half: HALF,
three_eighths: HALF,
one_quarter: HALF,
one_eighth: " ",
empty: " ",
};
pub const NINE_LEVELS: Set = Set {
full: FULL,
seven_eighths: SEVEN_EIGHTHS,
three_quarters: THREE_QUARTERS,
five_eighths: FIVE_EIGHTHS,
half: HALF,
three_eighths: THREE_EIGHTHS,
one_quarter: ONE_QUARTER,
one_eighth: ONE_EIGHTH,
empty: " ",
};
}
pub const DOT: &str = "";
pub mod braille {
pub const BLANK: u16 = 0x2800;
pub const DOTS: [[u16; 2]; 4] = [
[0x0001, 0x0008],
[0x0002, 0x0010],
[0x0004, 0x0020],
[0x0040, 0x0080],
];
}
/// Marker to use when plotting data points
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
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,
/// Use the [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) block to
/// represent data points.
///
/// This is a 2x4 grid of dots, where each dot can be either on or off.
///
/// Note: Support for this marker is limited to terminals and fonts that support Unicode
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
/// characters (`<60>`) instead of Braille dots (`⠓`, `⣇`, `⣿`).
Braille,
/// Use the unicode block and half block characters (`█`, `▄`, and `▀`) to represent points in
/// a grid that is double the resolution of the terminal. Because each terminal cell is
/// generally about twice as tall as it is wide, this allows for a square grid of pixels.
HalfBlock,
}
pub mod scrollbar {
use super::{block, line};
/// Scrollbar Set
/// ```text
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ end
/// │ │ └──── track
/// │ └──────── thumb
/// └─────────── begin
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
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: "",
};
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn marker_tostring() {
assert_eq!(Marker::Dot.to_string(), "Dot");
assert_eq!(Marker::Block.to_string(), "Block");
assert_eq!(Marker::Bar.to_string(), "Bar");
assert_eq!(Marker::Braille.to_string(), "Braille");
}
#[test]
fn marker_from_str() {
assert_eq!("Dot".parse::<Marker>(), Ok(Marker::Dot));
assert_eq!("Block".parse::<Marker>(), Ok(Marker::Block));
assert_eq!("Bar".parse::<Marker>(), Ok(Marker::Bar));
assert_eq!("Braille".parse::<Marker>(), Ok(Marker::Braille));
assert_eq!("".parse::<Marker>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -1,509 +0,0 @@
use super::{block, line};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub top_left: &'static str,
pub top_right: &'static str,
pub bottom_left: &'static str,
pub bottom_right: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_top: &'static str,
pub horizontal_bottom: &'static str,
}
impl Default for Set {
fn default() -> Self {
PLAIN
}
}
/// Border Set with a single line width
///
/// ```text
/// ┌─────┐
/// │xxxxx│
/// │xxxxx│
/// └─────┘
/// ```
pub const PLAIN: Set = Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: line::NORMAL.vertical,
vertical_right: line::NORMAL.vertical,
horizontal_top: line::NORMAL.horizontal,
horizontal_bottom: line::NORMAL.horizontal,
};
/// Border Set with a single line width and rounded corners
///
/// ```text
/// ╭─────╮
/// │xxxxx│
/// │xxxxx│
/// ╰─────╯
/// ```
pub const ROUNDED: Set = Set {
top_left: line::ROUNDED.top_left,
top_right: line::ROUNDED.top_right,
bottom_left: line::ROUNDED.bottom_left,
bottom_right: line::ROUNDED.bottom_right,
vertical_left: line::ROUNDED.vertical,
vertical_right: line::ROUNDED.vertical,
horizontal_top: line::ROUNDED.horizontal,
horizontal_bottom: line::ROUNDED.horizontal,
};
/// Border Set with a double line width
///
/// ```text
/// ╔═════╗
/// ║xxxxx║
/// ║xxxxx║
/// ╚═════╝
/// ```
pub const DOUBLE: Set = Set {
top_left: line::DOUBLE.top_left,
top_right: line::DOUBLE.top_right,
bottom_left: line::DOUBLE.bottom_left,
bottom_right: line::DOUBLE.bottom_right,
vertical_left: line::DOUBLE.vertical,
vertical_right: line::DOUBLE.vertical,
horizontal_top: line::DOUBLE.horizontal,
horizontal_bottom: line::DOUBLE.horizontal,
};
/// Border Set with a thick line width
///
/// ```text
/// ┏━━━━━┓
/// ┃xxxxx┃
/// ┃xxxxx┃
/// ┗━━━━━┛
/// ```
pub const THICK: Set = Set {
top_left: line::THICK.top_left,
top_right: line::THICK.top_right,
bottom_left: line::THICK.bottom_left,
bottom_right: line::THICK.bottom_right,
vertical_left: line::THICK.vertical,
vertical_right: line::THICK.vertical,
horizontal_top: line::THICK.horizontal,
horizontal_bottom: line::THICK.horizontal,
};
pub const QUADRANT_TOP_LEFT: &str = "";
pub const QUADRANT_TOP_RIGHT: &str = "";
pub const QUADRANT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_HALF: &str = "";
pub const QUADRANT_BOTTOM_HALF: &str = "";
pub const QUADRANT_LEFT_HALF: &str = "";
pub const QUADRANT_RIGHT_HALF: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BLOCK: &str = "";
/// Quadrant used for setting a border outside a block by one half cell "pixel".
///
/// ```text
/// ▛▀▀▀▀▀▜
/// ▌xxxxx▐
/// ▌xxxxx▐
/// ▙▄▄▄▄▄▟
/// ```
pub const QUADRANT_OUTSIDE: Set = Set {
top_left: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT,
top_right: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT,
bottom_left: QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT,
vertical_left: QUADRANT_LEFT_HALF,
vertical_right: QUADRANT_RIGHT_HALF,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
/// Quadrant used for setting a border inside a block by one half cell "pixel".
///
/// ```text
/// ▗▄▄▄▄▄▖
/// ▐xxxxx▌
/// ▐xxxxx▌
/// ▝▀▀▀▀▀▘
/// ```
pub const QUADRANT_INSIDE: Set = Set {
top_right: QUADRANT_BOTTOM_LEFT,
top_left: QUADRANT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_LEFT,
bottom_left: QUADRANT_TOP_RIGHT,
vertical_left: QUADRANT_RIGHT_HALF,
vertical_right: QUADRANT_LEFT_HALF,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
pub const ONE_EIGHTH_TOP_EIGHT: &str = "";
pub const ONE_EIGHTH_BOTTOM_EIGHT: &str = "";
pub const ONE_EIGHTH_LEFT_EIGHT: &str = "";
pub const ONE_EIGHTH_RIGHT_EIGHT: &str = "";
/// Wide border set based on McGugan box technique
///
/// ```text
/// ▁▁▁▁▁▁▁
/// ▏xxxxx▕
/// ▏xxxxx▕
/// ▔▔▔▔▔▔▔
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_WIDE: Set = Set {
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
bottom_right: ONE_EIGHTH_TOP_EIGHT,
bottom_left: ONE_EIGHTH_TOP_EIGHT,
vertical_left: ONE_EIGHTH_LEFT_EIGHT,
vertical_right: ONE_EIGHTH_RIGHT_EIGHT,
horizontal_top: ONE_EIGHTH_BOTTOM_EIGHT,
horizontal_bottom: ONE_EIGHTH_TOP_EIGHT,
};
/// Tall border set based on McGugan box technique
///
/// ```text
/// ▕▔▔▏
/// ▕xx▏
/// ▕xx▏
/// ▕▁▁▏
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_TALL: Set = Set {
top_right: ONE_EIGHTH_LEFT_EIGHT,
top_left: ONE_EIGHTH_RIGHT_EIGHT,
bottom_right: ONE_EIGHTH_LEFT_EIGHT,
bottom_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_right: ONE_EIGHTH_LEFT_EIGHT,
horizontal_top: ONE_EIGHTH_TOP_EIGHT,
horizontal_bottom: ONE_EIGHTH_BOTTOM_EIGHT,
};
/// Wide proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using half blocks for top and bottom, and full
/// blocks for right and left sides to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▄▄▄▄
/// █xx█
/// █xx█
/// ▀▀▀▀
/// ```
pub const PROPORTIONAL_WIDE: Set = Set {
top_right: QUADRANT_BOTTOM_HALF,
top_left: QUADRANT_BOTTOM_HALF,
bottom_right: QUADRANT_TOP_HALF,
bottom_left: QUADRANT_TOP_HALF,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
/// Tall proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using full blocks for all sides, except for the top and bottom,
/// which use half blocks to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▕█▀▀█
/// ▕█xx█
/// ▕█xx█
/// ▕█▄▄█
/// ```
pub const PROPORTIONAL_TALL: Set = Set {
top_right: QUADRANT_BLOCK,
top_left: QUADRANT_BLOCK,
bottom_right: QUADRANT_BLOCK,
bottom_left: QUADRANT_BLOCK,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
/// Solid border set
///
/// The border is created by using full blocks for all sides.
///
/// ```text
/// ████
/// █xx█
/// █xx█
/// ████
/// ```
pub const FULL: Set = Set {
top_left: block::FULL,
top_right: block::FULL,
bottom_left: block::FULL,
bottom_right: block::FULL,
vertical_left: block::FULL,
vertical_right: block::FULL,
horizontal_top: block::FULL,
horizontal_bottom: block::FULL,
};
/// Empty border set
///
/// The border is created by using empty strings for all sides.
///
/// This is useful for ensuring that the border style is applied to a border on a block with a title
/// without actually drawing a border.
///
/// ░ Example
///
/// `░` represents the content in the area not covered by the border to make it easier to see the
/// blank symbols.
///
/// ```text
/// ░░░░░░░░
/// ░░ ░░
/// ░░ ░░ ░░
/// ░░ ░░ ░░
/// ░░ ░░
/// ░░░░░░░░
/// ```
pub const EMPTY: Set = Set {
top_left: " ",
top_right: " ",
bottom_left: " ",
bottom_right: " ",
vertical_left: " ",
vertical_right: " ",
horizontal_top: " ",
horizontal_bottom: " ",
};
#[cfg(test)]
mod tests {
use indoc::{formatdoc, indoc};
use super::*;
#[test]
fn default() {
assert_eq!(Set::default(), PLAIN);
}
/// A helper function to render a border set to a string.
///
/// '░' (U+2591 Light Shade) is used as a placeholder for empty space to make it easier to see
/// the size of the border symbols.
fn render(set: Set) -> String {
formatdoc!(
"░░░░░░
░{}{}{}{}░
░{}░░{}░
░{}░░{}░
░{}{}{}{}░
░░░░░░",
set.top_left,
set.horizontal_top,
set.horizontal_top,
set.top_right,
set.vertical_left,
set.vertical_right,
set.vertical_left,
set.vertical_right,
set.bottom_left,
set.horizontal_bottom,
set.horizontal_bottom,
set.bottom_right
)
}
#[test]
fn plain() {
assert_eq!(
render(PLAIN),
indoc!(
"░░░░░░
░┌──┐░
░│░░│░
░│░░│░
░└──┘░
░░░░░░"
)
);
}
#[test]
fn rounded() {
assert_eq!(
render(ROUNDED),
indoc!(
"░░░░░░
░╭──╮░
░│░░│░
░│░░│░
░╰──╯░
░░░░░░"
)
);
}
#[test]
fn double() {
assert_eq!(
render(DOUBLE),
indoc!(
"░░░░░░
░╔══╗░
░║░░║░
░║░░║░
░╚══╝░
░░░░░░"
)
);
}
#[test]
fn thick() {
assert_eq!(
render(THICK),
indoc!(
"░░░░░░
░┏━━┓░
░┃░░┃░
░┃░░┃░
░┗━━┛░
░░░░░░"
)
);
}
#[test]
fn quadrant_outside() {
assert_eq!(
render(QUADRANT_OUTSIDE),
indoc!(
"░░░░░░
░▛▀▀▜░
░▌░░▐░
░▌░░▐░
░▙▄▄▟░
░░░░░░"
)
);
}
#[test]
fn quadrant_inside() {
assert_eq!(
render(QUADRANT_INSIDE),
indoc!(
"░░░░░░
░▗▄▄▖░
░▐░░▌░
░▐░░▌░
░▝▀▀▘░
░░░░░░"
)
);
}
#[test]
fn one_eighth_wide() {
assert_eq!(
render(ONE_EIGHTH_WIDE),
indoc!(
"░░░░░░
░▁▁▁▁░
░▏░░▕░
░▏░░▕░
░▔▔▔▔░
░░░░░░"
)
);
}
#[test]
fn one_eighth_tall() {
assert_eq!(
render(ONE_EIGHTH_TALL),
indoc!(
"░░░░░░
░▕▔▔▏░
░▕░░▏░
░▕░░▏░
░▕▁▁▏░
░░░░░░"
)
);
}
#[test]
fn proportional_wide() {
assert_eq!(
render(PROPORTIONAL_WIDE),
indoc!(
"░░░░░░
░▄▄▄▄░
░█░░█░
░█░░█░
░▀▀▀▀░
░░░░░░"
)
);
}
#[test]
fn proportional_tall() {
assert_eq!(
render(PROPORTIONAL_TALL),
indoc!(
"░░░░░░
░█▀▀█░
░█░░█░
░█░░█░
░█▄▄█░
░░░░░░"
)
);
}
#[test]
fn full() {
assert_eq!(
render(FULL),
indoc!(
"░░░░░░
░████░
░█░░█░
░█░░█░
░████░
░░░░░░"
)
);
}
#[test]
fn empty() {
assert_eq!(
render(EMPTY),
indoc!(
"░░░░░░
░ ░
░ ░░ ░
░ ░░ ░
░ ░
░░░░░░"
)
);
}
}

View File

@@ -1,208 +0,0 @@
pub const VERTICAL: &str = "";
pub const DOUBLE_VERTICAL: &str = "";
pub const THICK_VERTICAL: &str = "";
pub const HORIZONTAL: &str = "";
pub const DOUBLE_HORIZONTAL: &str = "";
pub const THICK_HORIZONTAL: &str = "";
pub const TOP_RIGHT: &str = "";
pub const ROUNDED_TOP_RIGHT: &str = "";
pub const DOUBLE_TOP_RIGHT: &str = "";
pub const THICK_TOP_RIGHT: &str = "";
pub const TOP_LEFT: &str = "";
pub const ROUNDED_TOP_LEFT: &str = "";
pub const DOUBLE_TOP_LEFT: &str = "";
pub const THICK_TOP_LEFT: &str = "";
pub const BOTTOM_RIGHT: &str = "";
pub const ROUNDED_BOTTOM_RIGHT: &str = "";
pub const DOUBLE_BOTTOM_RIGHT: &str = "";
pub const THICK_BOTTOM_RIGHT: &str = "";
pub const BOTTOM_LEFT: &str = "";
pub const ROUNDED_BOTTOM_LEFT: &str = "";
pub const DOUBLE_BOTTOM_LEFT: &str = "";
pub const THICK_BOTTOM_LEFT: &str = "";
pub const VERTICAL_LEFT: &str = "";
pub const DOUBLE_VERTICAL_LEFT: &str = "";
pub const THICK_VERTICAL_LEFT: &str = "";
pub const VERTICAL_RIGHT: &str = "";
pub const DOUBLE_VERTICAL_RIGHT: &str = "";
pub const THICK_VERTICAL_RIGHT: &str = "";
pub const HORIZONTAL_DOWN: &str = "";
pub const DOUBLE_HORIZONTAL_DOWN: &str = "";
pub const THICK_HORIZONTAL_DOWN: &str = "";
pub const HORIZONTAL_UP: &str = "";
pub const DOUBLE_HORIZONTAL_UP: &str = "";
pub const THICK_HORIZONTAL_UP: &str = "";
pub const CROSS: &str = "";
pub const DOUBLE_CROSS: &str = "";
pub const THICK_CROSS: &str = "";
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub vertical: &'static str,
pub horizontal: &'static str,
pub top_right: &'static str,
pub top_left: &'static str,
pub bottom_right: &'static str,
pub bottom_left: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_down: &'static str,
pub horizontal_up: &'static str,
pub cross: &'static str,
}
impl Default for Set {
fn default() -> Self {
NORMAL
}
}
pub const NORMAL: Set = Set {
vertical: VERTICAL,
horizontal: HORIZONTAL,
top_right: TOP_RIGHT,
top_left: TOP_LEFT,
bottom_right: BOTTOM_RIGHT,
bottom_left: BOTTOM_LEFT,
vertical_left: VERTICAL_LEFT,
vertical_right: VERTICAL_RIGHT,
horizontal_down: HORIZONTAL_DOWN,
horizontal_up: HORIZONTAL_UP,
cross: CROSS,
};
pub const ROUNDED: Set = Set {
top_right: ROUNDED_TOP_RIGHT,
top_left: ROUNDED_TOP_LEFT,
bottom_right: ROUNDED_BOTTOM_RIGHT,
bottom_left: ROUNDED_BOTTOM_LEFT,
..NORMAL
};
pub const DOUBLE: Set = Set {
vertical: DOUBLE_VERTICAL,
horizontal: DOUBLE_HORIZONTAL,
top_right: DOUBLE_TOP_RIGHT,
top_left: DOUBLE_TOP_LEFT,
bottom_right: DOUBLE_BOTTOM_RIGHT,
bottom_left: DOUBLE_BOTTOM_LEFT,
vertical_left: DOUBLE_VERTICAL_LEFT,
vertical_right: DOUBLE_VERTICAL_RIGHT,
horizontal_down: DOUBLE_HORIZONTAL_DOWN,
horizontal_up: DOUBLE_HORIZONTAL_UP,
cross: DOUBLE_CROSS,
};
pub const THICK: Set = Set {
vertical: THICK_VERTICAL,
horizontal: THICK_HORIZONTAL,
top_right: THICK_TOP_RIGHT,
top_left: THICK_TOP_LEFT,
bottom_right: THICK_BOTTOM_RIGHT,
bottom_left: THICK_BOTTOM_LEFT,
vertical_left: THICK_VERTICAL_LEFT,
vertical_right: THICK_VERTICAL_RIGHT,
horizontal_down: THICK_HORIZONTAL_DOWN,
horizontal_up: THICK_HORIZONTAL_UP,
cross: THICK_CROSS,
};
#[cfg(test)]
mod tests {
use indoc::{formatdoc, indoc};
use super::*;
#[test]
fn default() {
assert_eq!(Set::default(), NORMAL);
}
/// A helper function to render a set of symbols.
fn render(set: Set) -> String {
formatdoc!(
"{}{}{}{}
{}{}{}{}
{}{}{}{}
{}{}{}{}",
set.top_left,
set.horizontal,
set.horizontal_down,
set.top_right,
set.vertical,
" ",
set.vertical,
set.vertical,
set.vertical_right,
set.horizontal,
set.cross,
set.vertical_left,
set.bottom_left,
set.horizontal,
set.horizontal_up,
set.bottom_right
)
}
#[test]
fn normal() {
assert_eq!(
render(NORMAL),
indoc!(
"┌─┬┐
│ ││
├─┼┤
└─┴┘"
)
);
}
#[test]
fn rounded() {
assert_eq!(
render(ROUNDED),
indoc!(
"╭─┬╮
│ ││
├─┼┤
╰─┴╯"
)
);
}
#[test]
fn double() {
assert_eq!(
render(DOUBLE),
indoc!(
"╔═╦╗
║ ║║
╠═╬╣
╚═╩╝"
)
);
}
#[test]
fn thick() {
assert_eq!(
render(THICK),
indoc!(
"┏━┳┓
┃ ┃┃
┣━╋┫
┗━┻┛"
)
);
}
}

View File

@@ -1,40 +0,0 @@
#![deny(missing_docs)]
//! Provides the [`Terminal`], [`Frame`] and related types.
//!
//! The [`Terminal`] is the main interface of this library. It is responsible for drawing and
//! maintaining the state of the different widgets that compose your application.
//!
//! The [`Frame`] is a consistent view into the terminal state for rendering. It is obtained via
//! the closure argument of [`Terminal::draw`]. It is used to render widgets to the terminal and
//! control the cursor position.
//!
//! # Example
//!
//! ```rust,no_run
//! use std::io::stdout;
//!
//! use ratatui::{prelude::*, widgets::Paragraph};
//!
//! let backend = CrosstermBackend::new(stdout());
//! let mut terminal = Terminal::new(backend)?;
//! terminal.draw(|frame| {
//! let area = frame.area();
//! frame.render_widget(Paragraph::new("Hello world!"), area);
//! })?;
//! # std::io::Result::Ok(())
//! ```
//!
//! [Crossterm]: https://crates.io/crates/crossterm
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz
//! [`backend`]: crate::backend
//! [`Backend`]: crate::backend::Backend
//! [`Buffer`]: crate::buffer::Buffer
mod frame;
mod terminal;
mod viewport;
pub use frame::{CompletedFrame, Frame};
pub use terminal::{Options as TerminalOptions, Terminal};
pub use viewport::Viewport;

View File

@@ -1,228 +0,0 @@
use crate::prelude::*;
/// A consistent view into the terminal state for rendering a single frame.
///
/// This is obtained via the closure argument of [`Terminal::draw`]. It is used to render widgets
/// to the terminal and control the cursor position.
///
/// The changes drawn to the frame are applied only to the current [`Buffer`]. After the closure
/// returns, the current buffer is compared to the previous buffer and only the changes are applied
/// to the terminal. This avoids drawing redundant cells.
///
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Hash)]
pub struct Frame<'a> {
/// Where should the cursor be after drawing this frame?
///
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
pub(crate) cursor_position: Option<Position>,
/// The area of the viewport
pub(crate) viewport_area: Rect,
/// The buffer that is used to draw the current frame
pub(crate) buffer: &'a mut Buffer,
/// The frame count indicating the sequence number of this frame.
pub(crate) count: usize,
}
/// `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, Eq, PartialEq, Hash)]
pub struct CompletedFrame<'a> {
/// The buffer that was used to draw the last frame.
pub buffer: &'a Buffer,
/// The size of the last frame.
pub area: Rect,
/// The frame count indicating the sequence number of this frame.
pub count: usize,
}
impl Frame<'_> {
/// The area of the current frame
///
/// This is guaranteed not to change during rendering, so may be called multiple times.
///
/// If your app listens for a resize event from the backend, it should ignore the values from
/// the event for any calculations that are used to render the current frame and use this value
/// instead as this is the area of the buffer that is used to render the current frame.
pub const fn area(&self) -> Rect {
self.viewport_area
}
/// The area of the current frame
///
/// This is guaranteed not to change during rendering, so may be called multiple times.
///
/// If your app listens for a resize event from the backend, it should ignore the values from
/// the event for any calculations that are used to render the current frame and use this value
/// instead as this is the area of the buffer that is used to render the current frame.
#[deprecated = "use .area() as it's the more correct name"]
pub const fn size(&self) -> Rect {
self.viewport_area
}
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// # Example
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let block = Block::new();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget(block, area);
/// ```
///
/// [`Layout`]: crate::layout::Layout
pub fn render_widget<W: Widget>(&mut self, widget: W, area: Rect) {
widget.render(area, self.buffer);
}
/// Render a [`WidgetRef`] to the current buffer using [`WidgetRef::render_ref`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// # Example
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let block = Block::new();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget_ref(block, area);
/// # }
/// ```
#[allow(clippy::needless_pass_by_value)]
#[instability::unstable(feature = "widget-ref")]
pub fn render_widget_ref<W: WidgetRef>(&mut self, widget: W, area: Rect) {
widget.render_ref(area, self.buffer);
}
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
/// given [`StatefulWidget`].
///
/// # Example
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let mut state = ListState::default().with_selected(Some(1));
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_stateful_widget(list, area, &mut state);
/// ```
///
/// [`Layout`]: crate::layout::Layout
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidget,
{
widget.render(area, self.buffer, state);
}
/// Render a [`StatefulWidgetRef`] to the current buffer using
/// [`StatefulWidgetRef::render_ref`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// The last argument should be an instance of the [`StatefulWidgetRef::State`] associated to
/// the given [`StatefulWidgetRef`].
///
/// # Example
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let mut state = ListState::default().with_selected(Some(1));
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_stateful_widget_ref(list, area, &mut state);
/// # }
/// ```
#[allow(clippy::needless_pass_by_value)]
#[instability::unstable(feature = "widget-ref")]
pub fn render_stateful_widget_ref<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidgetRef,
{
widget.render_ref(area, self.buffer, state);
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to [`Terminal::hide_cursor`],
/// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and
/// stick with it.
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) {
self.cursor_position = Some(position.into());
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to [`Terminal::hide_cursor`],
/// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and
/// stick with it.
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
pub fn set_cursor(&mut self, x: u16, y: u16) {
self.set_cursor_position(Position { x, y });
}
/// Gets the buffer that this `Frame` draws into as a mutable reference.
pub fn buffer_mut(&mut self) -> &mut Buffer {
self.buffer
}
/// Returns the current frame count.
///
/// This method provides access to the frame count, which is a sequence number indicating
/// how many frames have been rendered up to (but not including) this one. It can be used
/// for purposes such as animation, performance tracking, or debugging.
///
/// Each time a frame has been rendered, this count is incremented,
/// providing a consistent way to reference the order and number of frames processed by the
/// terminal. When count reaches its maximum value (`usize::MAX`), it wraps around to zero.
///
/// This count is particularly useful when dealing with dynamic content or animations where the
/// state of the display changes over time. By tracking the frame count, developers can
/// synchronize updates or changes to the content with the rendering process.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let current_count = frame.count();
/// println!("Current frame count: {}", current_count);
/// ```
pub const fn count(&self) -> usize {
self.count
}
}

View File

@@ -1,727 +0,0 @@
use std::io;
use crate::{
backend::ClearType, buffer::Cell, prelude::*, CompletedFrame, TerminalOptions, Viewport,
};
/// An interface to interact and draw [`Frame`]s on the user's terminal.
///
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
/// state of the buffers, cursor and viewport.
///
/// The [`Terminal`] is generic over a [`Backend`] implementation which is used to interface with
/// the underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
/// information.
///
/// The `Terminal` struct maintains two buffers: the current and the previous.
/// When the widgets are drawn, the changes are accumulated in the current buffer.
/// At the end of each draw pass, the two buffers are compared, and only the changes
/// between these buffers are written to the terminal, avoiding any redundant operations.
/// After flushing these changes, the buffers are swapped to prepare for the next draw cycle.
///
/// The terminal also has a viewport which is the area of the terminal that is currently visible to
/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
///
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
/// application with the new size. This will automatically resize the internal buffers to match the
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
///
/// # Examples
///
/// ```rust,no_run
/// # use ratatui::prelude::*;
/// use std::io::stdout;
///
/// use ratatui::widgets::Paragraph;
///
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// })?;
/// # std::io::Result::Ok(())
/// ```
///
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Termion]: https://crates.io/crates/termion
/// [Termwiz]: https://crates.io/crates/termwiz
/// [`backend`]: crate::backend
/// [`Backend`]: crate::backend::Backend
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Terminal<B>
where
B: Backend,
{
/// The backend used to interface with the terminal
backend: B,
/// Holds the results of the current and previous draw calls. The two are compared at the end
/// of each draw pass to output the necessary updates to the terminal
buffers: [Buffer; 2],
/// Index of the current buffer in the previous array
current: usize,
/// Whether the cursor is currently hidden
hidden_cursor: bool,
/// Viewport
viewport: Viewport,
/// Area of the viewport
viewport_area: Rect,
/// Last known area of the terminal. Used to detect if the internal buffers have to be resized.
last_known_area: Rect,
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
/// and the terminal resized.
last_known_cursor_pos: Position,
/// Number of frames rendered up until current time.
frame_count: usize,
}
/// Options to pass to [`Terminal::with_options`]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Options {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.hidden_cursor {
if let Err(err) = self.show_cursor() {
eprintln!("Failed to show the cursor: {err}");
}
}
}
}
impl<B> Terminal<B>
where
B: Backend,
{
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// let terminal = Terminal::new(backend)?;
/// # std::io::Result::Ok(())
/// ```
pub fn new(backend: B) -> io::Result<Self> {
Self::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)
}
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
///
/// # Example
///
/// ```rust
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, backend::TestBackend, Viewport, TerminalOptions};
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Self> {
let area = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => {
Rect::from((Position::ORIGIN, backend.size()?))
}
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (area, Position::ORIGIN),
Viewport::Inline(height) => {
compute_inline_size(&mut backend, height, area.as_size(), 0)?
}
Viewport::Fixed(area) => (area, area.as_position()),
};
Ok(Self {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_area: area,
last_known_cursor_pos: cursor_pos,
frame_count: 0,
})
}
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
pub fn get_frame(&mut self) -> Frame {
let count = self.frame_count;
Frame {
cursor_position: None,
viewport_area: self.viewport_area,
buffer: self.current_buffer_mut(),
count,
}
}
/// Gets the current buffer as a mutable reference.
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
/// Gets the backend
pub const fn backend(&self) -> &B {
&self.backend
}
/// Gets the backend as a mutable reference
pub fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
/// Obtains a difference between the previous and the current buffer and passes it to the
/// current backend for drawing.
pub fn flush(&mut self) -> io::Result<()> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
if let Some((col, row, _)) = updates.last() {
self.last_known_cursor_pos = Position { x: *col, y: *row };
}
self.backend.draw(updates.into_iter())
}
/// Updates the Terminal so that internal buffers match the requested area.
///
/// Requested area will be saved to remain consistent when rendering. This leads to a full clear
/// of the screen.
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
let next_area = match self.viewport {
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
.y
.saturating_sub(self.viewport_area.top());
compute_inline_size(
&mut self.backend,
height,
area.as_size(),
offset_in_previous_viewport,
)?
.0
}
Viewport::Fixed(_) | Viewport::Fullscreen => area,
};
self.set_viewport_area(next_area);
self.clear()?;
self.last_known_area = area;
Ok(())
}
fn set_viewport_area(&mut self, area: Rect) {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport_area = area;
}
/// 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 area = Rect::from((Position::ORIGIN, self.size()?));
if area != self.last_known_area {
self.resize(area)?;
}
};
Ok(())
}
/// Draws a single frame to the terminal.
///
/// Returns a [`CompletedFrame`] if successful, otherwise a [`std::io::Error`].
///
/// If the render callback passed to this method can fail, use [`try_draw`] instead.
///
/// Applications should call `draw` or [`try_draw`] in a loop to continuously render the
/// terminal. These methods are the main entry points for drawing to the terminal.
///
/// [`try_draw`]: Terminal::try_draw
///
/// This method will:
///
/// - autoresize the terminal if necessary
/// - call the render callback, passing it a [`Frame`] reference to render to
/// - flush the current internal state by copying the current buffer to the backend
/// - move the cursor to the last known position if it was set during the rendering closure
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
///
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
/// purposes, but it is often not used in regular applicationss.
///
/// The render callback should fully render the entire frame when called, including areas that
/// are unchanged from the previous frame. This is because each frame is compared to the
/// previous frame to determine what has changed, and only the changes are written to the
/// terminal. If the render callback does not fully render the frame, the terminal will not be
/// in a consistent state.
///
/// # Examples
///
/// ```
/// # use ratatui::layout::Position;
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
/// # let mut terminal = ratatui::Terminal::new(backend)?;
/// use ratatui::widgets::Paragraph;
///
/// // with a closure
/// terminal.draw(|frame| {
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// })?;
///
/// // or with a function
/// terminal.draw(render)?;
///
/// fn render(frame: &mut ratatui::Frame) {
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
/// }
/// # std::io::Result::Ok(())
/// ```
pub fn draw<F>(&mut self, render_callback: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame),
{
self.try_draw(|frame| {
render_callback(frame);
io::Result::Ok(())
})
}
/// Tries to draw a single frame to the terminal.
///
/// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
/// [`Result::Err`] containing the [`std::io::Error`] that caused the failure.
///
/// This is the equivalent of [`Terminal::draw`] but the render callback is a function or
/// closure that returns a `Result` instead of nothing.
///
/// Applications should call `try_draw` or [`draw`] in a loop to continuously render the
/// terminal. These methods are the main entry points for drawing to the terminal.
///
/// [`draw`]: Terminal::draw
///
/// This method will:
///
/// - autoresize the terminal if necessary
/// - call the render callback, passing it a [`Frame`] reference to render to
/// - flush the current internal state by copying the current buffer to the backend
/// - move the cursor to the last known position if it was set during the rendering closure
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
///
/// The render callback passed to `try_draw` can return any [`Result`] with an error type that
/// can be converted into an [`std::io::Error`] using the [`Into`] trait. This makes it possible
/// to use the `?` operator to propagate errors that occur during rendering. If the render
/// callback returns an error, the error will be returned from `try_draw` as an
/// [`std::io::Error`] and the terminal will not be updated.
///
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
/// purposes, but it is often not used in regular applicationss.
///
/// The render callback should fully render the entire frame when called, including areas that
/// are unchanged from the previous frame. This is because each frame is compared to the
/// previous frame to determine what has changed, and only the changes are written to the
/// terminal. If the render function does not fully render the frame, the terminal will not be
/// in a consistent state.
///
/// # Examples
///
/// ```should_panic
/// # use ratatui::layout::Position;;
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
/// # let mut terminal = ratatui::Terminal::new(backend)?;
/// use std::io;
///
/// use ratatui::widgets::Paragraph;
///
/// // with a closure
/// terminal.try_draw(|frame| {
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// io::Result::Ok(())
/// })?;
///
/// // or with a function
/// terminal.try_draw(render)?;
///
/// fn render(frame: &mut ratatui::Frame) -> io::Result<()> {
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
/// Ok(())
/// }
/// # io::Result::Ok(())
/// ```
pub fn try_draw<F, E>(&mut self, render_callback: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame) -> Result<(), E>,
E: Into<io::Error>,
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;
let mut frame = self.get_frame();
render_callback(&mut frame).map_err(Into::into)?;
// We can't change the cursor position right away because we have to flush the frame to
// stdout first. But we also can't keep the frame around, since it holds a &mut to
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;
// Draw to stdout
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some(position) => {
self.show_cursor()?;
self.set_cursor_position(position)?;
}
}
self.swap_buffers();
// Flush
self.backend.flush()?;
let completed_frame = CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_area,
count: self.frame_count,
};
// increment frame count before returning from draw
self.frame_count = self.frame_count.wrapping_add(1);
Ok(completed_frame)
}
/// Hides the cursor.
pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
/// Shows the cursor.
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call and is returned as a tuple of
/// `(x, y)` coordinates.
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
/// Sets the cursor position.
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.set_cursor_position(Position { x, y })
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call.
pub fn get_cursor_position(&mut self) -> io::Result<Position> {
self.backend.get_cursor_position()
}
/// Sets the cursor position.
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let position = position.into();
self.backend.set_cursor_position(position)?;
self.last_known_cursor_pos = position;
Ok(())
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
self.backend
.set_cursor_position(self.viewport_area.as_position())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(area) => {
for y in area.top()..area.bottom() {
self.backend.set_cursor_position(Position { x: 0, y })?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
}
}
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
}
/// 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<Size> {
self.backend.size()
}
/// Insert some content before the current inline viewport. This has no effect when the
/// viewport is not inline.
///
/// The `draw_fn` closure will be called to draw into a writable `Buffer` that is `height`
/// lines tall. The content of that `Buffer` will then be inserted before the viewport.
///
/// If the viewport isn't yet at the bottom of the screen, inserted lines will push it towards
/// the bottom. Once the viewport is at the bottom of the screen, inserted lines will scroll
/// the area of the screen above the viewport upwards.
///
/// Before:
/// ```ignore
/// +---------------------+
/// | pre-existing line 1 |
/// | pre-existing line 2 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// | |
/// | |
/// +---------------------+
/// ```
///
/// After inserting 2 lines:
/// ```ignore
/// +---------------------+
/// | pre-existing line 1 |
/// | pre-existing line 2 |
/// | inserted line 1 |
/// | inserted line 2 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// +---------------------+
/// ```
///
/// After inserting 2 more lines:
/// ```ignore
/// +---------------------+
/// | pre-existing line 2 |
/// | inserted line 1 |
/// | inserted line 2 |
/// | inserted line 3 |
/// | inserted line 4 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// ```
///
/// If more lines are inserted than there is space on the screen, then the top lines will go
/// directly into the terminal's scrollback buffer. At the limit, if the viewport takes up the
/// whole screen, all lines will be inserted directly into the scrollback buffer.
///
/// # Examples
///
/// ## Insert a single line before the current viewport
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
/// Paragraph::new(Line::from(vec![
/// Span::raw("This line will be added "),
/// Span::styled("before", Style::default().fg(Color::Blue)),
/// Span::raw(" the current viewport"),
/// ]))
/// .render(buf.area, buf);
/// });
/// ```
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
where
F: FnOnce(&mut Buffer),
{
if !matches!(self.viewport, Viewport::Inline(_)) {
return Ok(());
}
// The approach of this function is to first render all of the lines to insert into a
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
// this buffer onto the screen.
let area = Rect {
x: 0,
y: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
let mut buffer = buffer.content.as_slice();
// Use i32 variables so we don't have worry about overflowed u16s when adding, or about
// negative results when subtracting.
let mut drawn_height: i32 = self.viewport_area.top().into();
let mut buffer_height: i32 = height.into();
let viewport_height: i32 = self.viewport_area.height.into();
let screen_height: i32 = self.last_known_area.height.into();
// The algorithm here is to loop, drawing large chunks of text (up to a screen-full at a
// time), until the remainder of the buffer plus the viewport fits on the screen. We choose
// this loop condition because it guarantees that we can write the remainder of the buffer
// with just one call to Self::draw_lines().
while buffer_height + viewport_height > screen_height {
// We will draw as much of the buffer as possible on this iteration in order to make
// forward progress. So we have:
//
// to_draw = min(buffer_height, screen_height)
//
// We may need to scroll the screen up to make room to draw. We choose the minimal
// possible scroll amount so we don't end up with the viewport sitting in the middle of
// the screen when this function is done. The amount to scroll by is:
//
// scroll_up = max(0, drawn_height + to_draw - screen_height)
//
// We want `scroll_up` to be enough so that, after drawing, we have used the whole
// screen (drawn_height - scroll_up + to_draw = screen_height). However, there might
// already be enough room on the screen to draw without scrolling (drawn_height +
// to_draw <= screen_height). In this case, we just don't scroll at all.
let to_draw = buffer_height.min(screen_height);
let scroll_up = 0.max(drawn_height + to_draw - screen_height);
self.scroll_up(scroll_up as u16)?;
buffer = self.draw_lines((drawn_height - scroll_up) as u16, to_draw as u16, buffer)?;
drawn_height += to_draw - scroll_up;
buffer_height -= to_draw;
}
// There is now enough room on the screen for the remaining buffer plus the viewport,
// though we may still need to scroll up some of the existing text first. It's possible
// that by this point we've drained the buffer, but we may still need to scroll up to make
// room for the viewport.
//
// We want to scroll up the exact amount that will leave us completely filling the screen.
// However, it's possible that the viewport didn't start on the bottom of the screen and
// the added lines weren't enough to push it all the way to the bottom. We deal with this
// case by just ensuring that our scroll amount is non-negative.
//
// We want:
// screen_height = drawn_height - scroll_up + buffer_height + viewport_height
// Or, equivalently:
// scroll_up = drawn_height + buffer_height + viewport_height - screen_height
let scroll_up = 0.max(drawn_height + buffer_height + viewport_height - screen_height);
self.scroll_up(scroll_up as u16)?;
self.draw_lines(
(drawn_height - scroll_up) as u16,
buffer_height as u16,
buffer,
)?;
drawn_height += buffer_height - scroll_up;
self.set_viewport_area(Rect {
y: drawn_height as u16,
..self.viewport_area
});
// Clear the viewport off the screen. We didn't clear earlier for two reasons. First, it
// wasn't necessary because the buffer we drew out of isn't sparse, so it overwrote
// whatever was on the screen. Second, there is a weird bug with tmux where a full screen
// clear plus immediate scrolling causes some garbage to go into the scrollback.
self.clear()?;
Ok(())
}
/// Draw lines at the given vertical offset. The slice of cells must contain enough cells
/// for the requested lines. A slice of the unused cells are returned.
fn draw_lines<'a>(
&mut self,
y_offset: u16,
lines_to_draw: u16,
cells: &'a [Cell],
) -> io::Result<&'a [Cell]> {
let width: usize = self.last_known_area.width.into();
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
if lines_to_draw > 0 {
let iter = to_draw
.iter()
.enumerate()
.map(|(i, c)| ((i % width) as u16, y_offset + (i / width) as u16, c));
self.backend.draw(iter)?;
self.backend.flush()?;
}
Ok(remainder)
}
/// Scroll the whole screen up by the given number of lines.
fn scroll_up(&mut self, lines_to_scroll: u16) -> io::Result<()> {
if lines_to_scroll > 0 {
self.set_cursor_position(Position::new(
0,
self.last_known_area.height.saturating_sub(1),
))?;
self.backend.append_lines(lines_to_scroll)?;
}
Ok(())
}
}
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Size,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, Position)> {
let pos = backend.get_cursor_position()?;
let mut row = pos.y;
let max_height = size.height.min(height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
backend.append_lines(lines_after_cursor)?;
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Ok((
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
))
}

View File

@@ -1,54 +0,0 @@
use std::fmt;
use crate::prelude::*;
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
/// currently visible to the user. It can be either fullscreen, inline or fixed.
///
/// When the viewport is fullscreen, the whole terminal is used to draw the application.
///
/// When the viewport is inline, it is drawn inline with the rest of the terminal. The height of
/// the viewport is fixed, but the width is the same as the terminal width.
///
/// When the viewport is fixed, it is drawn in a fixed area of the terminal. The area is specified
/// by a [`Rect`].
///
/// See [`Terminal::with_options`] for more information.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub enum Viewport {
/// The viewport is fullscreen
#[default]
Fullscreen,
/// The viewport is inline with the rest of the terminal.
///
/// The viewport's height is fixed and specified in number of lines. The width is the same as
/// the terminal's width. The viewport is drawn below the cursor position.
Inline(u16),
/// The viewport is drawn in a fixed area of the terminal. The area is specified by a [`Rect`].
Fixed(Rect),
}
impl fmt::Display for Viewport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Fullscreen => write!(f, "Fullscreen"),
Self::Inline(height) => write!(f, "Inline({height})"),
Self::Fixed(area) => write!(f, "Fixed({area})"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn viewport_to_string() {
assert_eq!(Viewport::Fullscreen.to_string(), "Fullscreen");
assert_eq!(Viewport::Inline(5).to_string(), "Inline(5)");
assert_eq!(
Viewport::Fixed(Rect::new(0, 0, 5, 5)).to_string(),
"Fixed(5x5+0+0)"
);
}
}

View File

@@ -1,81 +0,0 @@
use crate::{prelude::*, style::Styled};
const NBSP: &str = "\u{00a0}";
const ZWSP: &str = "\u{200b}";
/// 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, Eq, PartialEq, Hash)]
pub struct StyledGrapheme<'a> {
pub symbol: &'a str,
pub style: Style,
}
impl<'a> StyledGrapheme<'a> {
/// Creates a new `StyledGrapheme` with the given symbol and style.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> Self {
Self {
symbol,
style: style.into(),
}
}
pub fn is_whitespace(&self) -> bool {
let symbol = self.symbol;
symbol == ZWSP || symbol.chars().all(char::is_whitespace) && symbol != NBSP
}
}
impl<'a> Styled for StyledGrapheme<'a> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
self.style = style.into();
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
let style = Style::new().yellow();
let sg = StyledGrapheme::new("a", style);
assert_eq!(sg.symbol, "a");
assert_eq!(sg.style, style);
}
#[test]
fn style() {
let style = Style::new().yellow();
let sg = StyledGrapheme::new("a", style);
assert_eq!(sg.style(), style);
}
#[test]
fn set_style() {
let style = Style::new().yellow().on_red();
let style2 = Style::new().green();
let sg = StyledGrapheme::new("a", style).set_style(style2);
assert_eq!(sg.style, style2);
}
#[test]
fn stylize() {
let style = Style::new().yellow().on_red();
let sg = StyledGrapheme::new("a", style).green();
assert_eq!(sg.style, Style::new().green().on_red());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,846 +0,0 @@
use std::{borrow::Cow, fmt};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// Represents a part of a line that is contiguous and where all characters share the same style.
///
/// A `Span` is the smallest unit of text that can be styled. It is usually combined in the [`Line`]
/// type to represent a line of text where each `Span` may have a different style.
///
/// # Constructor Methods
///
/// - [`Span::default`] creates an span with empty content and the default style.
/// - [`Span::raw`] creates an span with the specified content and the default style.
/// - [`Span::styled`] creates an span with the specified content and style.
///
/// # Setter Methods
///
/// These methods are fluent setters. They return a new `Span` with the specified property set.
///
/// - [`Span::content`] sets the content of the span.
/// - [`Span::style`] sets the style of the span.
///
/// # Other Methods
///
/// - [`Span::patch_style`] patches the style of the span, adding modifiers from the given style.
/// - [`Span::reset_style`] resets the style of the span.
/// - [`Span::width`] returns the unicode width of the content held by this span.
/// - [`Span::styled_graphemes`] returns an iterator over the graphemes held by this span.
///
/// # Examples
///
/// A `Span` with `style` set to [`Style::default()`] can be created from a `&str`, a `String`, or
/// any type convertible to [`Cow<str>`].
///
/// ```rust
/// use ratatui::prelude::*;
///
/// let span = Span::raw("test content");
/// let span = Span::raw(String::from("test content"));
/// let span = Span::from("test content");
/// let span = Span::from(String::from("test content"));
/// let span: Span = "test content".into();
/// let span: Span = String::from("test content").into();
/// ```
///
/// Styled spans can be created using [`Span::styled`] or by converting strings using methods from
/// the [`Stylize`] trait.
///
/// ```rust
/// use ratatui::prelude::*;
///
/// let span = Span::styled("test content", Style::new().green());
/// let span = Span::styled(String::from("test content"), Style::new().green());
///
/// // using Stylize trait shortcuts
/// let span = "test content".green();
/// let span = String::from("test content").green();
/// ```
///
/// `Span` implements the [`Styled`] trait, which allows it to be styled using the shortcut methods
/// defined in the [`Stylize`] trait.
///
/// ```rust
/// use ratatui::prelude::*;
///
/// let span = Span::raw("test content").green().on_yellow().italic();
/// let span = Span::raw(String::from("test content"))
/// .green()
/// .on_yellow()
/// .italic();
/// ```
///
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Usually
/// apps will use the [`Paragraph`] widget instead of rendering `Span` directly, as it handles text
/// wrapping and alignment for you.
///
/// ```rust
/// use ratatui::prelude::*;
///
/// # fn render_frame(frame: &mut Frame) {
/// frame.render_widget("test content".green().on_yellow().italic(), frame.area());
/// # }
/// ```
/// [`Line`]: crate::text::Line
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Stylize`]: crate::style::Stylize
/// [`Cow<str>`]: std::borrow::Cow
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Span<'a> {
/// The style of the span.
pub style: Style,
/// The content of the span as a Clone-on-write string.
pub content: Cow<'a, str>,
}
impl fmt::Debug for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.style == Style::default() {
return write!(f, "Span({:?})", self.content);
}
f.debug_struct("Span")
.field("style", &self.style)
.field("content", &self.content)
.finish()
}
}
impl<'a> Span<'a> {
/// Create a span with the default style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// Span::raw("test content");
/// Span::raw(String::from("test content"));
/// ```
pub fn raw<T>(content: T) -> Self
where
T: Into<Cow<'a, str>>,
{
Self {
content: content.into(),
style: Style::default(),
}
}
/// Create a span with the specified style.
///
/// `content` accepts any type that is convertible to [`Cow<str>`] (e.g. `&str`, `String`,
/// `&String`, etc.).
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::new().yellow().on_green().italic();
/// Span::styled("test content", style);
/// Span::styled(String::from("test content"), style);
/// ```
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
S: Into<Style>,
{
Self {
content: content.into(),
style: style.into(),
}
}
/// Sets the content of the span.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// Accepts any type that can be converted to [`Cow<str>`] (e.g. `&str`, `String`, `&String`,
/// etc.).
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut span = Span::default().content("content");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn content<T>(mut self, content: T) -> Self
where
T: Into<Cow<'a, str>>,
{
self.content = content.into();
self
}
/// Sets the style of the span.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// In contrast to [`Span::patch_style`], this method replaces the style of the span instead of
/// patching it.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut span = Span::default().style(Style::new().green());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
/// Patches the style of the Span, adding modifiers from the given style.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let span = Span::styled("test content", Style::new().green().italic())
/// .patch_style(Style::new().red().on_yellow().bold());
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
self
}
/// Resets the style of the Span.
///
/// This is Equivalent to calling `patch_style(Style::reset())`.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let span = Span::styled(
/// "Test Content",
/// Style::new().dark_gray().on_yellow().italic(),
/// )
/// .reset_style();
/// assert_eq!(span.style, Style::reset());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn reset_style(self) -> Self {
self.patch_style(Style::reset())
}
/// Returns the unicode 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 the `Span`'s `style` to get the
/// resulting [`Style`].
///
/// `base_style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`],
/// or your own type that implements [`Into<Style>`]).
///
/// # Example
///
/// ```rust
/// use std::iter::Iterator;
///
/// use ratatui::{prelude::*, text::StyledGrapheme};
///
/// let span = Span::styled("Test", Style::new().green().italic());
/// let style = Style::new().red().on_yellow();
/// assert_eq!(
/// span.styled_graphemes(style)
/// .collect::<Vec<StyledGrapheme>>(),
/// vec![
/// StyledGrapheme::new("T", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("e", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("s", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("t", Style::new().green().on_yellow().italic()),
/// ],
/// );
/// ```
pub fn styled_graphemes<S: Into<Style>>(
&'a self,
base_style: S,
) -> impl Iterator<Item = StyledGrapheme<'a>> {
let style = base_style.into().patch(self.style);
self.content
.as_ref()
.graphemes(true)
.filter(|g| *g != "\n")
.map(move |g| StyledGrapheme { symbol: g, style })
}
/// Converts this Span into a left-aligned [`Line`]
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let line = "Test Content".green().italic().into_left_aligned_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn into_left_aligned_line(self) -> Line<'a> {
Line::from(self).left_aligned()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_left_aligned_line"]
pub fn to_left_aligned_line(self) -> Line<'a> {
self.into_left_aligned_line()
}
/// Converts this Span into a center-aligned [`Line`]
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let line = "Test Content".green().italic().into_centered_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn into_centered_line(self) -> Line<'a> {
Line::from(self).centered()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_centered_line"]
pub fn to_centered_line(self) -> Line<'a> {
self.into_centered_line()
}
/// Converts this Span into a right-aligned [`Line`]
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let line = "Test Content".green().italic().into_right_aligned_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn into_right_aligned_line(self) -> Line<'a> {
Line::from(self).right_aligned()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_right_aligned_line"]
pub fn to_right_aligned_line(self) -> Line<'a> {
self.into_right_aligned_line()
}
}
impl<'a, T> From<T> for Span<'a>
where
T: Into<Cow<'a, str>>,
{
fn from(s: T) -> Self {
Span::raw(s.into())
}
}
impl<'a> std::ops::Add<Self> for Span<'a> {
type Output = Line<'a>;
fn add(self, rhs: Self) -> Self::Output {
Line::from_iter([self, rhs])
}
}
impl<'a> Styled for Span<'a> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl Widget for Span<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
impl WidgetRef for Span<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
if area.is_empty() {
return;
}
let Rect { mut x, y, .. } = area;
for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
let symbol_width = grapheme.symbol.width();
let next_x = x.saturating_add(symbol_width as u16);
if next_x > area.right() {
break;
}
if i == 0 {
// the first grapheme is always set on the cell
buf[(x, y)]
.set_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else if x == area.x {
// there is one or more zero-width graphemes in the first cell, so the first cell
// must be appended to.
buf[(x, y)]
.append_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else if symbol_width == 0 {
// append zero-width graphemes to the previous cell
buf[(x - 1, y)]
.append_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else {
// just a normal grapheme (not first, not zero-width, not overflowing the area)
buf[(x, y)]
.set_symbol(grapheme.symbol)
.set_style(grapheme.style);
}
// multi-width graphemes must clear the cells of characters that are hidden by the
// grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
// overwritten.
for x_hidden in (x + 1)..next_x {
// it may seem odd that the style of the hidden cells are not set to the style of
// the grapheme, but this is how the existing buffer.set_span() method works.
buf[(x_hidden, y)].reset();
}
x = next_x;
}
}
}
/// A trait for converting a value to a [`Span`].
///
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
/// such, `ToSpan` shouln't be implemented directly: [`Display`] should be implemented instead, and
/// you get the `ToSpan` implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToSpan {
/// Converts the value to a [`Span`].
fn to_span(&self) -> Span<'_>;
}
/// # Panics
///
/// In this implementation, the `to_span` method panics if the `Display` implementation returns an
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
/// returns an error itself.
impl<T: fmt::Display> ToSpan for T {
fn to_span(&self) -> Span<'_> {
Span::raw(self.to_string())
}
}
impl fmt::Display for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for line in self.content.lines() {
fmt::Display::fmt(line, f)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use buffer::Cell;
use rstest::fixture;
use super::*;
#[fixture]
fn small_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
#[test]
fn default() {
let span = Span::default();
assert_eq!(span.content, Cow::Borrowed(""));
assert_eq!(span.style, Style::default());
}
#[test]
fn raw_str() {
let span = Span::raw("test content");
assert_eq!(span.content, Cow::Borrowed("test content"));
assert_eq!(span.style, Style::default());
}
#[test]
fn raw_string() {
let content = String::from("test content");
let span = Span::raw(content.clone());
assert_eq!(span.content, Cow::Owned::<str>(content));
assert_eq!(span.style, Style::default());
}
#[test]
fn styled_str() {
let style = Style::new().red();
let span = Span::styled("test content", style);
assert_eq!(span.content, Cow::Borrowed("test content"));
assert_eq!(span.style, Style::new().red());
}
#[test]
fn styled_string() {
let content = String::from("test content");
let style = Style::new().green();
let span = Span::styled(content.clone(), style);
assert_eq!(span.content, Cow::Owned::<str>(content));
assert_eq!(span.style, style);
}
#[test]
fn set_content() {
let span = Span::default().content("test content");
assert_eq!(span.content, Cow::Borrowed("test content"));
}
#[test]
fn set_style() {
let span = Span::default().style(Style::new().green());
assert_eq!(span.style, Style::new().green());
}
#[test]
fn from_ref_str_borrowed_cow() {
let content = "test content";
let span = Span::from(content);
assert_eq!(span.content, Cow::Borrowed(content));
assert_eq!(span.style, Style::default());
}
#[test]
fn from_string_ref_str_borrowed_cow() {
let content = String::from("test content");
let span = Span::from(content.as_str());
assert_eq!(span.content, Cow::Borrowed(content.as_str()));
assert_eq!(span.style, Style::default());
}
#[test]
fn from_string_owned_cow() {
let content = String::from("test content");
let span = Span::from(content.clone());
assert_eq!(span.content, Cow::Owned::<str>(content));
assert_eq!(span.style, Style::default());
}
#[test]
fn from_ref_string_borrowed_cow() {
let content = String::from("test content");
let span = Span::from(&content);
assert_eq!(span.content, Cow::Borrowed(content.as_str()));
assert_eq!(span.style, Style::default());
}
#[test]
fn to_span() {
assert_eq!(42.to_span(), Span::raw("42"));
assert_eq!("test".to_span(), Span::raw("test"));
}
#[test]
fn reset_style() {
let span = Span::styled("test content", Style::new().green()).reset_style();
assert_eq!(span.style, Style::reset());
}
#[test]
fn patch_style() {
let span = Span::styled("test content", Style::new().green().on_yellow())
.patch_style(Style::new().red().bold());
assert_eq!(span.style, Style::new().red().on_yellow().bold());
}
#[test]
fn width() {
assert_eq!(Span::raw("").width(), 0);
assert_eq!(Span::raw("test").width(), 4);
assert_eq!(Span::raw("test content").width(), 12);
// Needs reconsideration: https://github.com/ratatui/ratatui/issues/1271
assert_eq!(Span::raw("test\ncontent").width(), 12);
}
#[test]
fn stylize() {
let span = Span::raw("test content").green();
assert_eq!(span.content, Cow::Borrowed("test content"));
assert_eq!(span.style, Style::new().green());
let span = Span::styled("test content", Style::new().green());
let stylized = span.on_yellow().bold();
assert_eq!(stylized.content, Cow::Borrowed("test content"));
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
}
#[test]
fn display_span() {
let span = Span::raw("test content");
assert_eq!(format!("{span}"), "test content");
assert_eq!(format!("{span:.4}"), "test");
}
#[test]
fn display_newline_span() {
let span = Span::raw("test\ncontent");
assert_eq!(format!("{span}"), "testcontent");
}
#[test]
fn display_styled_span() {
let stylized_span = Span::styled("stylized test content", Style::new().green());
assert_eq!(format!("{stylized_span}"), "stylized test content");
assert_eq!(format!("{stylized_span:.8}"), "stylized");
}
#[test]
fn left_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_left_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_centered_line();
assert_eq!(line.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_right_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Right));
}
mod widget {
use rstest::rstest;
use super::*;
#[test]
fn render() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines([Line::from(vec![
"test content".green().on_yellow(),
" ".into(),
])]);
assert_eq!(buf, expected);
}
#[rstest]
#[case::x(20, 0)]
#[case::y(0, 20)]
#[case::both(20, 20)]
fn render_out_of_bounds(mut small_buf: Buffer, #[case] x: u16, #[case] y: u16) {
let out_of_bounds = Rect::new(x, y, 10, 1);
Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
assert_eq!(small_buf, Buffer::empty(small_buf.area));
}
/// When the content of the span is longer than the area passed to render, the content
/// should be truncated
#[test]
fn render_truncates_too_long_content() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
span.render(Rect::new(0, 0, 5, 1), &mut buf);
let expected = Buffer::with_lines([Line::from(vec![
"test ".green().on_yellow(),
" ".into(),
])]);
assert_eq!(buf, expected);
}
/// When there is already a style set on the buffer, the style of the span should be
/// patched with the existing style
#[test]
fn render_patches_existing_style() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
buf.set_style(buf.area, Style::new().italic());
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines([Line::from(vec![
"test content".green().on_yellow().italic(),
" ".italic(),
])]);
assert_eq!(buf, expected);
}
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
/// of the hidden characters are cleared.
#[test]
fn render_multi_width_symbol() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test 😃 content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(buf.area, &mut buf);
// The existing code in buffer.set_line() handles multi-width graphemes by clearing the
// cells of the hidden characters. This test ensures that the existing behavior is
// preserved.
let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
assert_eq!(buf, expected);
}
/// When the span contains a multi-width grapheme that does not fit in the area passed to
/// render, the entire grapheme will be truncated.
#[test]
fn render_multi_width_symbol_truncates_entire_symbol() {
// the 😃 emoji is 2 columns wide so it will be truncated
let style = Style::new().green().on_yellow();
let span = Span::styled("test 😃 content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
span.render(buf.area, &mut buf);
let expected =
Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
assert_eq!(buf, expected);
}
/// When the area passed to render overflows the buffer, the content should be truncated
/// to fit the buffer.
#[test]
fn render_overflowing_area_truncates() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(Rect::new(10, 0, 20, 1), &mut buf);
let expected = Buffer::with_lines([Line::from(vec![
" ".into(),
"test ".green().on_yellow(),
])]);
assert_eq!(buf, expected);
}
#[test]
fn render_first_zero_width() {
let span = Span::raw("\u{200B}abc");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("\u{200B}a"), Cell::new("b"), Cell::new("c"),]
);
}
#[test]
fn render_second_zero_width() {
let span = Span::raw("a\u{200B}bc");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a\u{200B}"), Cell::new("b"), Cell::new("c")]
);
}
#[test]
fn render_middle_zero_width() {
let span = Span::raw("ab\u{200B}c");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a"), Cell::new("b\u{200B}"), Cell::new("c")]
);
}
#[test]
fn render_last_zero_width() {
let span = Span::raw("abc\u{200B}");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a"), Cell::new("b"), Cell::new("c\u{200B}")]
);
}
#[test]
fn render_with_newlines() {
let span = Span::raw("a\nb");
let mut buf = Buffer::empty(Rect::new(0, 0, 2, 1));
span.render(buf.area, &mut buf);
assert_eq!(buf.content(), [Cell::new("a"), Cell::new("b")]);
}
}
/// Regression test for <https://github.com/ratatui/ratatui/issues/1160> One line contains
/// some Unicode Left-Right-Marks (U+200E)
///
/// The issue was that a zero-width character at the end of the buffer causes the buffer bounds
/// to be exceeded (due to a position + 1 calculation that fails to account for the possibility
/// that the next position might not be available).
#[test]
fn issue_1160() {
let span = Span::raw("Hello\u{200E}");
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[
Cell::new("H"),
Cell::new("e"),
Cell::new("l"),
Cell::new("l"),
Cell::new("o\u{200E}"),
]
);
}
#[test]
fn add() {
assert_eq!(
Span::default() + Span::default(),
Line::from(vec![Span::default(), Span::default()])
);
assert_eq!(
Span::default() + Span::raw("test"),
Line::from(vec![Span::default(), Span::raw("test")])
);
assert_eq!(
Span::raw("test") + Span::default(),
Line::from(vec![Span::raw("test"), Span::default()])
);
assert_eq!(
Span::raw("test") + Span::raw("content"),
Line::from(vec![Span::raw("test"), Span::raw("content")])
);
}
}

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