Compare commits

..

1 Commits

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

View File

@@ -1,2 +0,0 @@
[alias]
xtask = "run --package xtask --"

View File

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

View File

@@ -1,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

11
.github/CODEOWNERS vendored
View File

@@ -1,11 +0,0 @@
# See <https://help.github.com/articles/about-codeowners/>
# for more info about CODEOWNERS file
# It uses the same pattern rule for gitignore file
# <https://git-scm.com/docs/gitignore#_pattern_format>
# Maintainers
* @ratatui/maintainers

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +0,0 @@
github: ratatui
open_collective: ratatui

View File

@@ -2,12 +2,12 @@
name: Bug report
about: Create an issue about a bug you encountered
title: ''
labels: 'Type: Bug'
labels: bug
assignees: ''
---
<!--
Hi there, sorry `ratatui` is not working as expected.
Hi there, sorry `tui` is not working as expected.
Please fill this bug report conscientiously.
A detailed and complete issue is more likely to be processed quickly.
-->
@@ -17,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 +1,17 @@
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
## Description
<!--
A clear and concise description of what this PR changes.
-->
## Testing guidelines
<!--
A clear and concise description of how the changes can be tested.
For example, you can include a command to run the relevant tests or examples.
You can also include screenshots of the expected behavior.
-->
## Checklist
* [ ] I have read the [contributing guidelines](../CONTRIBUTING.md).
* [ ] I have added relevant tests.
* [ ] I have documented all new additions.

View File

@@ -1,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,56 +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: dawidd6/action-download-artifact@v6
with:
name: ${{ env.BENCHMARK_RESULTS }}
run_id: ${{ github.event.workflow_run.id }}
- name: Download PR Event
uses: dawidd6/action-download-artifact@v6
with:
name: ${{ env.PR_EVENT }}
run_id: ${{ github.event.workflow_run.id }}
- 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.pull_request.head.ref);
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 "$PR_HEAD" \
--start-point "$PR_BASE" \
--start-point-hash "$PR_BASE_SHA" \
--start-point-clone-thresholds \
--start-point-reset \
--testbed ubuntu-latest \
--adapter rust_criterion \
--err \
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
--ci-number "$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}"

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,185 +1,71 @@
name: Continuous Integration
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: CI
env:
CI_CARGO_MAKE_VERSION: 0.35.8
# 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.
jobs:
# Lint the formatting of the codebase.
lint-formatting:
name: Check Formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with: { components: rustfmt }
- run: cargo xtask lint-formatting
# Check for typos in the codebase.
# See <https://github.com/crate-ci/typos/>
lint-typos:
name: Check Typos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@master
# Check for any disallowed dependencies in the codebase due to license / security issues.
# See <https://github.com/EmbarkStudios/cargo-deny>
dependencies:
name: Check Dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
# Check for any unused dependencies in the codebase.
# See <https://github.com/bnjbvr/cargo-machete/>
cargo-machete:
name: Check Unused Dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: bnjbvr/cargo-machete@v0.7.0
# Run cargo clippy.
lint-clippy:
name: Check Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with: { components: clippy }
- uses: Swatinem/rust-cache@v2
- run: cargo xtask lint-clippy
# Run markdownlint on all markdown files in the repository.
lint-markdown:
name: Check Markdown
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v18
with:
globs: |
'**/*.md'
'!target'
# Run cargo coverage. This will generate a coverage report and upload it to codecov.
# <https://app.codecov.io/gh/ratatui/ratatui>
coverage:
name: Coverage Report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: Swatinem/rust-cache@v2
- run: cargo xtask coverage
- uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
# Run cargo check. This is a fast way to catch any obvious errors in the code.
check:
name: Check ${{ matrix.os }} ${{ matrix.toolchain }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
toolchain: ["1.74.0", "stable"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- uses: Swatinem/rust-cache@v2
- run: cargo xtask check
# Check if README.md is up-to-date with the crate's documentation.
check-readme:
name: Check README
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-rdme
- run: cargo xtask check-readme
# 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
lint-docs:
name: Check Docs
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/install@cargo-docs-rs
- uses: Swatinem/rust-cache@v2
- run: cargo xtask lint-docs
# Run cargo test on the documentation of the crate. This will catch any code examples that don't
# compile, or any other issues in the documentation.
test-docs:
name: Test Docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo xtask test-docs
# Run cargo test on the libraries of the crate.
test-libs:
name: Test Libs ${{ matrix.toolchain }}
linux:
name: Linux
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
toolchain: ["1.74.0", "stable"]
rust: ["1.56.1", "stable"]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo xtask test-libs
# Run cargo test on all the backends.
test-backends:
name: Test ${{matrix.backend}} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
with:
rust-version: ${{ matrix.rust }}
components: rustfmt,clippy
- uses: actions/checkout@v1
- name: "Get cargo bin directory"
id: cargo-bin-dir
run: echo "::set-output name=dir::$HOME/.cargo/bin"
- name: "Cache cargo make"
id: cache-cargo-make
uses: actions/cache@v2
with:
path: ${{ steps.cargo-bin-dir.outputs.dir }}/cargo-make
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Install cargo-make"
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Format / Build / Test"
run: cargo make ci
env:
RUST_BACKTRACE: full
windows:
name: Windows
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
backend: [crossterm, termion, termwiz]
exclude:
# termion is not supported on windows
- os: windows-latest
backend: termion
rust: ["1.56.1", "stable"]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo xtask test-backend ${{ matrix.backend }}
- uses: actions/checkout@v1
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
with:
rust-version: ${{ matrix.rust }}
components: rustfmt,clippy
- uses: actions/checkout@v1
- name: "Get cargo bin directory"
id: cargo-bin-dir
run: echo "::set-output name=dir::$HOME\.cargo\bin"
- name: "Cache cargo make"
id: cache-cargo-make
uses: actions/cache@v2
with:
path: ${{ steps.cargo-bin-dir.outputs.dir }}\cargo-make.exe
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Install cargo-make"
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
- name: "Format / Build / Test"
run: cargo make ci
env:
RUST_BACKTRACE: full

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 }}

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
target
Cargo.lock
*.log
*.rs.rustfmt
.gdb_history

View File

@@ -1,15 +0,0 @@
# 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,890 +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:
- [Unreleased](#unreleased)
- The `From` impls for backend types are now replaced with more specific traits
- [v0.29.0](#v0290)
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
- Removed public fields from `Rect` iterators
- `Line` now implements `From<Cow<str>`
- `Table::highlight_style` is now `Table::row_highlight_style`
- `Tabs::select` now accepts `Into<Option<usize>>`
- `Color::from_hsl` is now behind the `palette` feature
- [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
## Unreleased (0.30.0)
### `WidgetRef` no longer has a blanket implementation of Widget
Previously there was a blanket implementation of Widget for WidgetRef. This has been reversed to
instead be a blanket implementation of WidgetRef for all &W where W: Widget. Any widgets that
previously implemented WidgetRef directly should now instead implement Widget for a reference to the
type.
```diff
-impl WidgetRef for Foo {
- fn render_ref(&self, area: Rect, buf: &mut Buffer)
+impl Widget for &Foo {
+ fn render(self, area: Rect, buf: &mut Buffer)
}
```
### The `From` impls for backend types are now replaced with more specific traits [#1464]
[#1464]: https://github.com/ratatui/ratatui/pull/1464
Crossterm gains `ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm}`
Termwiz gains `ratatui::backend::termwiz::{FromTermwiz, IntoTermwiz}`
This is necessary in order to avoid the orphan rule when implementing `From` for crossterm types
once the crossterm types are moved to a separate crate.
```diff
+ use ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm};
let crossterm_color = crossterm::style::Color::Black;
- let ratatui_color = crossterm_color.into();
- let ratatui_color = ratatui::style::Color::from(crossterm_color);
+ let ratatui_color = ratatui::style::Color::from_crossterm(crossterm_color);
- let crossterm_color = ratatui_color.into();
- let crossterm_color = crossterm::style::Color::from(ratatui_color);
+ let crossterm_color = ratatui_color.into_crossterm();
let crossterm_attribute = crossterm::style::types::Attribute::Bold;
- let ratatui_modifier = crossterm_attribute.into();
- let ratatui_modifier = ratatui::style::Modifier::from(crossterm_attribute);
+ let ratatui_modifier = ratatui::style::Modifier::from_crossterm(crossterm_attribute);
- let crossterm_attribute = ratatui_modifier.into();
- let crossterm_attribute = crossterm::style::types::Attribute::from(ratatui_modifier);
+ let crossterm_attribute = ratatui_modifier.into_crossterm();
```
Similar conversions for `ContentStyle` -> `Style` and `Attributes` -> `Modifier` exist for
Crossterm and the various Termion and Termwiz types as well.
### `Bar::label()` and `BarGroup::label()` now accepts `Into<Line<'a>>`. ([#1471])
[#1471]: https://github.com/ratatui/ratatui/pull/1471
Previously `Bar::label()` and `BarGroup::label()` accepted `Line<'a>`, but they now accepts `Into<Line<'a>>`.
for `Bar::label()`:
```diff
- Bar::default().label("foo".into());
+ Bar::default().label("foo");
```
for `BarGroup::label()`:
```diff
- BarGroup::default().label("bar".into());
+ BarGroup::default().label("bar");
```
### `Bar::text_value` now accepts `Into<String>` ([#1471])
Previously `Bar::text_value` accepted `String`, but now it accepts `Into<String>`.
for `Bar::text_value()`:
```diff
- Bar::default().text_value("foobar".into());
+ Bar::default().text_value("foobar");
```
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0)
### `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const ([#1326])
[#1326]: https://github.com/ratatui/ratatui/pull/1326
The `Sparkline::data` method has been modified to accept `IntoIterator<Item = SparklineBar>`
instead of `&[u64]`.
`SparklineBar` is a struct that contains an `Option<u64>` value, which represents an possible
_absent_ value, as distinct from a `0` value. This change allows the `Sparkline` to style
data points differently, depending on whether they are present or absent.
`SparklineBar` also contains an `Option<Style>` that will be used to apply a style the bar in
addition to any other styling applied to the `Sparkline`.
Several `From` implementations have been added to `SparklineBar` to support existing callers who
provide `&[u64]` and other types that can be converted to `SparklineBar`, such as `Option<u64>`.
If you encounter any type inference issues, you may need to provide an explicit type for the data
passed to `Sparkline::data`. For example, if you are passing a single value, you may need to use
`into()` to convert it to form that can be used as a `SparklineBar`:
```diff
let value = 1u8;
- Sparkline::default().data(&[value.into()]);
+ Sparkline::default().data(&[u64::from(value)]);
```
As a consequence of this change, the `data` method is no longer a `const fn`.
### `Color::from_hsl` is now behind the `palette` feature and accepts `palette::Hsl` ([#1418])
[#1418]: https://github.com/ratatui/ratatui/pull/1418
Previously `Color::from_hsl` accepted components as individual f64 parameters. It now accepts a
single `palette::Hsl` value and is gated behind a `palette` feature flag.
```diff
- Color::from_hsl(360.0, 100.0, 100.0)
+ Color::from_hsl(Hsl::new(360.0, 100.0, 100.0))
```
### Removed public fields from `Rect` iterators ([#1358], [#1424])
[#1358]: https://github.com/ratatui/ratatui/pull/1358
[#1424]: https://github.com/ratatui/ratatui/pull/1424
The `pub` modifier has been removed from fields on the `Columns`,`Rows`, and `Positions` iterators.
These fields were not intended to be public and should not have been accessed directly.
### `Rect::area()` now returns u32 instead of u16 ([#1378])
[#1378]: https://github.com/ratatui/ratatui/pull/1378
This is likely to impact anything which relies on `Rect::area` maxing out at u16::MAX. It can now
return up to u16::MAX * u16::MAX (2^32 - 2^17 + 1).
### `Line` now implements `From<Cow<str>` ([#1373])
[#1373]: https://github.com/ratatui/ratatui/pull/1373
As this adds an extra conversion, ambiguous inferred expressions may no longer compile.
```rust
// given:
struct Foo { ... }
impl From<Foo> for String { ... }
impl From<Foo> for Cow<str> { ... }
let foo = Foo { ... };
let line = Line::from(foo); // now fails due to now ambiguous inferred type
// replace with e.g.
let line = Line::from(String::from(foo));
```
### `Tabs::select()` now accepts `Into<Option<usize>>` ([#1413])
[#1413]: https://github.com/ratatui/ratatui/pull/1413
Previously `Tabs::select()` accepted `usize`, but it now accepts `Into<Option<usize>>`. This breaks
any code already using parameter type inference:
```diff
let selected = 1u8;
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
```
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
[#1331]: https://github.com/ratatui/ratatui/pull/1331
The `Table::highlight_style` is now deprecated in favor of `Table::row_highlight_style`.
Also, the serialized output of the `TableState` will now include the "selected_column" field.
Software that manually parse the serialized the output (with anything other than the `Serialize`
implementation on `TableState`) may have to be refactored if the "selected_column" field is not
accounted for. This does not affect users who rely on the `Deserialize`, or `Serialize`
implementation on the state.
## [v0.28.0](https://github.com/ratatui/ratatui/releases/tag/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])
[#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,33 @@
# Contribution guidelines
# Contributing
First off, thank you for considering contributing to Ratatui.
## Building
If your contribution is not straightforward, please first discuss the change you wish to make by
creating a new issue before making the change, or starting a discussion on
[discord](https://discord.gg/pMCEU9hNEj).
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
## Reporting issues
`tui` is an ordinary Rust project where common tasks are managed with [cargo-make].
It wraps common `cargo` commands with sane defaults depending on your platform of choice.
Building the project should be as easy as running `cargo make build`.
Before reporting an issue on the [issue tracker](https://github.com/ratatui/ratatui/issues),
please check that it has not already been reported by searching for some related keywords. Please
also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues
found.
## :hammer_and_wrench: Pull requests
## Pull requests
All contributions are obviously welcome. Please include as many details as possible in your PR
description to help the reviewer (follow the provided template). Make sure to highlight changes
which may need additional attention, or you are uncertain about. Any idea with a large scale impact
on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
### Keep PRs small, intentional, and focused
Try to do one pull request per change. The time taken to review a PR grows exponential with the size
of the change. Small focused PRs will generally be much more faster to review. PRs that include both
refactoring (or reformatting) with actual changes are more difficult to review as every line of the
change becomes a place where a bug may have been introduced. Consider splitting refactoring /
reformatting changes into a separate PR from those that make a behavioral change, as the tests help
guarantee that the behavior is unchanged.
### Code formatting
Run `cargo xtask 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 xtask 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.
## Implementation Guidelines
### Setup
TL;DR: Clone the repo and build it using `cargo xtask`.
Ratatui is an ordinary Rust project where common tasks are managed with
[cargo-xtask](https://github.com/matklad/cargo-xtask). It wraps common `cargo` commands with sane
defaults depending on your platform of choice. Building the project should be as easy as running
`cargo xtask build`.
```shell
git clone https://github.com/ratatui/ratatui.git
cd ratatui
cargo xtask 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.
All contributions are obviously welcome.
Please include as many details as possible in your PR description to help the reviewer (follow the provided template).
Make sure to highlight changes which may need additional attention or you are uncertain about.
Any idea with a large scale impact on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
## Continuous Integration
We use GitHub Actions for the CI where we perform the following checks:
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 xtask 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 `tui` is a test again the `TestBackend`.
It allows you to assert the content of the output buffer that would have been flushed to the terminal after a given draw call.
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.

3953
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +1,93 @@
[workspace]
resolver = "2"
members = ["ratatui", "ratatui-*", "xtask"]
default-members = [
"ratatui",
"ratatui-core",
"ratatui-crossterm",
# this is not included as it doesn't compile on windows
# "ratatui-termion",
"ratatui-termwiz",
"ratatui-widgets",
]
[workspace.package]
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
documentation = "https://docs.rs/ratatui/latest/ratatui/"
repository = "https://github.com/ratatui/ratatui"
homepage = "https://ratatui.rs"
[package]
name = "tui"
version = "0.18.0"
authors = ["Florian Dehau <work@fdehau.com>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
documentation = "https://docs.rs/tui/0.18.0/tui/"
keywords = ["tui", "terminal", "dashboard"]
categories = ["command-line-interface"]
repository = "https://github.com/fdehau/tui-rs"
readme = "README.md"
license = "MIT"
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
autoexamples = true
edition = "2021"
rust-version = "1.74.0"
[workspace.dependencies]
bitflags = "2.6.0"
color-eyre = "0.6.3"
crossterm = "0.28.1"
document-features = "0.2.7"
indoc = "2.0.5"
instability = "0.3.3"
itertools = "0.13.0"
pretty_assertions = "1.4.1"
ratatui = { path = "ratatui", version = "0.30.0-alpha.0" }
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.0" }
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.0" }
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.0" }
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.0" }
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.0" }
rstest = "0.23.0"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
strum = { version = "0.26.3", features = ["derive"] }
termwiz = { version = "0.22.0" }
unicode-segmentation = "1.12.0"
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
unicode-width = "=0.2.0"
termion = "4.0.0"
[badges]
# Improve benchmark consistency
[profile.bench]
codegen-units = 1
lto = true
[features]
default = ["crossterm"]
[dependencies]
bitflags = "1.3"
cassowary = "0.3"
unicode-segmentation = "1.2"
unicode-width = "0.1"
termion = { version = "1.5", optional = true }
crossterm = { version = "0.23", optional = true }
serde = { version = "1", optional = true, features = ["derive"]}
[dev-dependencies]
rand = "0.8"
argh = "0.1"
[[example]]
name = "barchart"
required-features = ["crossterm"]
[[example]]
name = "block"
required-features = ["crossterm"]
[[example]]
name = "canvas"
required-features = ["crossterm"]
[[example]]
name = "chart"
required-features = ["crossterm"]
[[example]]
name = "custom_widget"
required-features = ["crossterm"]
[[example]]
name = "gauge"
required-features = ["crossterm"]
[[example]]
name = "layout"
required-features = ["crossterm"]
[[example]]
name = "list"
required-features = ["crossterm"]
[[example]]
name = "panic"
required-features = ["crossterm"]
[[example]]
name = "paragraph"
required-features = ["crossterm"]
[[example]]
name = "popup"
required-features = ["crossterm"]
[[example]]
name = "sparkline"
required-features = ["crossterm"]
[[example]]
name = "table"
required-features = ["crossterm"]
[[example]]
name = "tabs"
required-features = ["crossterm"]
[[example]]
name = "user_input"
required-features = ["crossterm"]

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)

160
Makefile.toml Normal file
View File

@@ -0,0 +1,160 @@
[config]
skip_core_tasks = true
[tasks.ci]
run_task = [
{ name = "ci-unix", condition = { platforms = ["linux", "mac"] } },
{ name = "ci-windows", condition = { platforms = ["windows"] } },
]
[tasks.ci-unix]
private = true
dependencies = [
"fmt",
"check-crossterm",
"check-termion",
"test-crossterm",
"test-termion",
"clippy-crossterm",
"clippy-termion",
"test-doc",
]
[tasks.ci-windows]
private = true
dependencies = [
"fmt",
"check-crossterm",
"test-crossterm",
"clippy-crossterm",
"test-doc",
]
[tasks.fmt]
command = "cargo"
args = [
"fmt",
"--all",
"--",
"--check",
]
[tasks.check-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "check"
[tasks.check-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "check"
[tasks.check]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"check",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--all-targets",
]
[tasks.build-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "build"
[tasks.build-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "build"
[tasks.build]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"build",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--all-targets",
]
[tasks.clippy-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "clippy"
[tasks.clippy-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "clippy"
[tasks.clippy]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"clippy",
"--all-targets",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--",
"-D",
"warnings",
]
[tasks.test-crossterm]
env = { TUI_FEATURES = "serde,crossterm" }
run_task = "test"
[tasks.test-termion]
env = { TUI_FEATURES = "serde,termion" }
run_task = "test"
[tasks.test]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"test",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--lib",
"--tests",
"--examples",
]
[tasks.test-doc]
command = "cargo"
args = [
"test",
"--doc",
]
[tasks.run-example]
private = true
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
command = "cargo"
args = [
"run",
"--release",
"--example",
"${TUI_EXAMPLE_NAME}"
]
[tasks.build-examples]
command = "cargo"
args = [
"build",
"--examples",
"--release"
]
[tasks.run-examples]
dependencies = ["build-examples"]
script = '''
#!@duckscript
files = glob_array ./examples/*.rs
for file in ${files}
name = basename ${file}
name = substring ${name} -3
set_env TUI_EXAMPLE_NAME ${name}
cm_run_task run-example
end
'''

442
README.md
View File

@@ -1,380 +1,126 @@
<details>
<summary>Table of Contents</summary>
# tui-rs
- [Ratatui](#ratatui)
- [Quick Start](#quickstart)
- [Other documentation](#other-documentation)
- [Introduction](#introduction)
- [Quickstart](#quickstart)
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
- [Drawing the UI](#drawing-the-ui)
- [Handling events](#handling-events)
- [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)
[![Build Status](https://github.com/fdehau/tui-rs/workflows/CI/badge.svg)](https://github.com/fdehau/tui-rs/actions?query=workflow%3ACI+)
[![Crate Status](https://img.shields.io/crates/v/tui.svg)](https://crates.io/crates/tui)
[![Docs Status](https://docs.rs/tui/badge.svg)](https://docs.rs/crate/tui/)
</details>
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
<!-- cargo-rdme start -->
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
user interfaces and dashboards. It is heavily inspired by the `Javascript`
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
`Go` library [termui](https://github.com/gizak/termui).
![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true)
The library supports multiple backends:
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
- [termion](https://github.com/ticki/termion)
<div align="center">
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.
[![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>
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.
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
### Rust version requirements
</div>
Since version 0.17.0, `tui` requires **rustc version 1.56.1 or greater**.
# Ratatui
### [Documentation](https://docs.rs/tui)
[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.
### Demo
## Quickstart
The demo shown in the gif can be run with all available backends.
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
```shell
cargo add ratatui crossterm
```
# crossterm
cargo run --example demo --release -- --tick-rate 200
# termion
cargo run --example demo --no-default-features --features=termion --release -- --tick-rate 200
```
Then you can create a simple "Hello World" application:
where `tick-rate` is the UI refresh rate in ms.
```rust
use crossterm::event::{self, Event};
use ratatui::{text::Text, Frame};
The UI code is in [examples/demo/ui.rs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/demo/ui.rs) while the
application state is in [examples/demo/app.rs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/demo/app.rs).
fn main() {
let mut terminal = ratatui::init();
loop {
terminal.draw(draw).expect("failed to draw frame");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
If the user interface contains glyphs that are not displayed correctly by your terminal, you may want to run
the demo without those symbols:
fn draw(frame: &mut Frame) {
let text = Text::raw("Hello World!");
frame.render_widget(text, frame.area());
}
```
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
```
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.
### Widgets
## Other documentation
The library comes with the following list of widgets:
- [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.
* [Block](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/block.rs)
* [Gauge](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/gauge.rs)
* [Sparkline](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/sparkline.rs)
* [Chart](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/chart.rs)
* [BarChart](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/barchart.rs)
* [List](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/list.rs)
* [Table](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/table.rs)
* [Paragraph](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/paragraph.rs)
* [Canvas (with line, point cloud, map)](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/canvas.rs)
* [Tabs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/tabs.rs)
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.
Click on each item to see the source of the example. Run the examples with with
cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
## Introduction
You can run all examples by running `cargo make run-examples` (require
`cargo-make` that can be installed with `cargo install cargo-make`).
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.
### Third-party 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]).
* [tui-logger](https://github.com/gin66/tui-logger)
Every application built with `ratatui` needs to implement the following steps:
### Apps using tui
- Initialize the terminal
- A main loop that:
- Draws the UI
- Handles input events
- Restore the terminal state
* [spotify-tui](https://github.com/Rigellute/spotify-tui)
* [bandwhich](https://github.com/imsnif/bandwhich)
* [kmon](https://github.com/orhun/kmon)
* [gpg-tui](https://github.com/orhun/gpg-tui)
* [ytop](https://github.com/cjbassi/ytop)
* [zenith](https://github.com/bvaisvil/zenith)
* [bottom](https://github.com/ClementTsang/bottom)
* [oha](https://github.com/hatoo/oha)
* [gitui](https://github.com/extrawurst/gitui)
* [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli)
* [desed](https://github.com/SoptikHa2/desed)
* [diskonaut](https://github.com/imsnif/diskonaut)
* [tickrs](https://github.com/tarkah/tickrs)
* [rusty-krab-manager](https://github.com/aryakaul/rusty-krab-manager)
* [termchat](https://github.com/lemunozm/termchat)
* [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui)
* [gping](https://github.com/orf/gping/)
* [Vector](https://vector.dev)
* [KDash](https://github.com/kdash-rs/kdash)
* [xplr](https://github.com/sayanarijit/xplr)
* [minesweep](https://github.com/cpcloud/minesweep-rs)
* [Battleship.rs](https://github.com/deepu105/battleship-rs)
* [termscp](https://github.com/veeso/termscp)
* [joshuto](https://github.com/kamiyaa/joshuto)
* [adsb_deku/radar](https://github.com/wcampbell0x2a/adsb_deku#radar-tui)
* [hoard](https://github.com/Hyde46/hoard)
* [tokio-console](https://github.com/tokio-rs/console): a diagnostics and debugging tool for asynchronous Rust programs.
* [hwatch](https://github.com/blacknon/hwatch): a alternative watch command that records the result of command execution and can display its history and diffs.
* [ytui-music](https://github.com/sudipghimire533/ytui-music): listen to music from youtube inside your terminal.
* [mqttui](https://github.com/EdJoPaTo/mqttui): subscribe or publish to a MQTT Topic quickly from the terminal.
* [meteo-tui](https://github.com/16arpi/meteo-tui): french weather via the command line.
* [picterm](https://github.com/ksk001100/picterm): preview images in your terminal.
* [gobang](https://github.com/TaKO8Ki/gobang): a cross-platform TUI database management tool.
### Initialize and restore the terminal
### Alternatives
The [`Terminal`] type is the main entry point for any Ratatui application. It is generic over a
a choice of [`Backend`] implementations that each provide functionality to draw frames, clear
the screen, hide the cursor, etc. There are backend implementations for [Crossterm], [Termion]
and [Termwiz].
The simplest way to initialize the terminal is to use the [`init`] function which returns a
[`DefaultTerminal`] instance with the default options, enters the Alternate Screen and Raw mode
and sets up a panic hook that restores the terminal in case of panic. This instance can then be
used to draw frames and interact with the terminal state. (The [`DefaultTerminal`] instance is a
type alias for a terminal with the [`crossterm`] backend.) The [`restore`] function restores the
terminal to its original state.
```rust
fn main() -> std::io::Result<()> {
let mut terminal = ratatui::init();
let result = run(&mut terminal);
ratatui::restore();
result
}
```
See the [`backend` module] and the [Backends] section of the [Ratatui Website] for more info on
the alternate screen and raw mode.
### Drawing the UI
Drawing the UI is done by calling the [`Terminal::draw`] method on the terminal instance. This
method takes a closure that is called with a [`Frame`] instance. 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.
The closure passed to the [`Terminal::draw`] method should handle the rendering of a full frame.
```rust
use ratatui::{widgets::Paragraph, Frame};
fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
loop {
terminal.draw(|frame| draw(frame))?;
if handle_events()? {
break Ok(());
}
}
}
fn draw(frame: &mut Frame) {
let text = Paragraph::new("Hello World!");
frame.render_widget(text, frame.area());
}
```
### 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.
```rust
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
fn handle_events() -> std::io::Result<bool> {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Char('q') => return Ok(true),
// handle other key events
_ => {}
},
// handle other events
_ => {}
}
Ok(false)
}
```
## 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 draw(frame: &mut Frame) {
use Constraint::{Fill, Length, Min};
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
let [title_area, main_area, status_area] = vertical.areas(frame.area());
let horizontal = Layout::horizontal([Fill(1); 2]);
let [left_area, right_area] = horizontal.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:
```text
Title Bar───────────────────────────────────
┌Left────────────────┐┌Right───────────────┐
│ ││ │
└────────────────────┘└────────────────────┘
Status Bar──────────────────────────────────
```
## 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 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]);
}
```
[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/ratatui/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
[`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 -->
## Contributing
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).
We have also recently launched the [Ratatui Forum][Forum], For bugs and features, we rely on GitHub.
Please [Report a bug], [Request a Feature] or [Create a Pull Request].
Please make sure you read the [contributing](./CONTRIBUTING.md) guidelines, especially if you are
interested in working on a PR or issue opened in the previous repository.
## Built with Ratatui
Ratatui has a number of built-in [widgets](https://docs.rs/ratatui/latest/ratatui/widgets/), as well
as many contributed by external contributors. Check out the [Showcase](https://ratatui.rs/showcase/)
section of the website, or the [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) repo
for a curated list of awesome apps/libraries built with `ratatui`!
## Alternatives
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) or
[iocraft](https://github.com/ccbrown/iocraft/) for an alternative solutions
to build text user interfaces in Rust.
## Acknowledgments
None of this could be possible without [**Florian Dehau**](https://github.com/fdehau) who originally
created [tui-rs] which inspired many Rust TUIs.
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
awesome logo** for the ratatui project and ratatui organization.
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an
alternative solution to build text user interfaces in Rust.
## License
[MIT](./LICENSE)
[MIT](LICENSE)

View File

@@ -1,51 +0,0 @@
# 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).
```shell
cargo build --example demo2
vhs examples/demo2.tape
```
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.
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. Push the tag: `git push --tags`
1. Wait for [Continuous Deployment](https://github.com/ratatui/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>

BIN
assets/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,137 +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"]
need_stdout = false
[jobs.check-all]
command = ["cargo", "check", "--all-targets", "--all-features"]
need_stdout = false
[jobs.check-crossterm]
command = [
"cargo",
"check",
"--all-targets",
"--no-default-features",
"--features",
"crossterm",
]
need_stdout = false
[jobs.check-termion]
command = [
"cargo",
"check",
"--all-targets",
"--no-default-features",
"--features",
"termion",
]
need_stdout = false
[jobs.check-termwiz]
command = [
"cargo",
"check",
"--all-targets",
"--no-default-features",
"--features",
"termwiz",
]
need_stdout = false
[jobs.clippy]
command = ["cargo", "clippy", "--all-targets"]
need_stdout = false
[jobs.test]
command = ["cargo", "test", "--all-features"]
need_stdout = true
[jobs.test-unit]
command = ["cargo", "test", "--lib", "--all-features"]
need_stdout = true
[jobs.doc]
command = [
"cargo",
"+nightly",
"doc",
"-Zunstable-options",
"-Zrustdoc-scrape-examples",
"--all-features",
"--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",
"--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",
]
[jobs.coverage-unit-tests-only]
command = [
"cargo",
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--lib",
"--all-features",
]
[jobs.hack]
command = [
"cargo",
"hack",
"test",
"--lib",
"--each-feature",
# "--all-targets",
"--workspace",
]
# 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]
ctrl-h = "job:hack"
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,138 +0,0 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "ratatui"
repo = "ratatui"
[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 -->
"""
# 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.
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.remote.username %} by @{{ commit.remote.username }}{%- endif -%}\
{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}){%- endif %}\
{%- if commit.breaking %} [**breaking**]{% endif %}
{% endmacro -%}
{% 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 %}
"""
# remove the leading and trailing whitespace from the template
trim = false
# 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
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
{ 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" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-rc.1"
# regex for ignoring tags
ignore_tags = "alpha"
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

View File

@@ -1,24 +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
# (also, memoffset, syn, nix, strsim, windows-sys
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
allowed-duplicate-crates = [
"bitflags",
"memoffset",
"nix",
"strsim",
"syn",
"windows-sys",
"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",
"unicode-width",
]

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

View File

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

161
examples/barchart.rs Normal file
View File

@@ -0,0 +1,161 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{BarChart, Block, Borders},
Frame, Terminal,
};
struct App<'a> {
data: Vec<(&'a str, u64)>,
}
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]);
}

119
examples/block.rs Normal file
View File

@@ -0,0 +1,119 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, BorderType, 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(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("Main block with round corners")
.title_alignment(Alignment::Center)
.border_type(BorderType::Rounded);
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(4)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
// Top two inner blocks
let top_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
// Top left inner block with green background
let block = Block::default()
.title(vec![
Span::styled("With", Style::default().fg(Color::Yellow)),
Span::from(" background"),
])
.style(Style::default().bg(Color::Green));
f.render_widget(block, top_chunks[0]);
// Top right inner block with styled title aligned to the right
let block = Block::default()
.title(Span::styled(
"Styled title",
Style::default()
.fg(Color::White)
.bg(Color::Red)
.add_modifier(Modifier::BOLD),
))
.title_alignment(Alignment::Right);
f.render_widget(block, top_chunks[1]);
// Bottom two inner blocks
let bottom_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
// Bottom left block with all default borders
let block = Block::default().title("With borders").borders(Borders::ALL);
f.render_widget(block, bottom_chunks[0]);
// Bottom right block with styled left and right border
let block = Block::default()
.title("With styled borders and doubled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Double);
f.render_widget(block, bottom_chunks[1]);
}

180
examples/canvas.rs Normal file
View File

@@ -0,0 +1,180 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::Span,
widgets::{
canvas::{Canvas, Map, MapResolution, Rectangle},
Block, Borders,
},
Frame, Terminal,
};
struct App {
x: f64,
y: f64,
ball: Rectangle,
playground: Rect,
vx: f64,
vy: f64,
dir_x: bool,
dir_y: bool,
}
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,
}
}
fn on_tick(&mut self) {
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"))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
resolution: MapResolution::High,
});
ctx.print(
app.x,
-app.y,
Span::styled("You are here", Style::default().fg(Color::Yellow)),
);
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]);
f.render_widget(canvas, chunks[0]);
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.paint(|ctx| {
ctx.draw(&app.ball);
})
.x_bounds([10.0, 110.0])
.y_bounds([10.0, 110.0]);
f.render_widget(canvas, chunks[1]);
}

301
examples/chart.rs Normal file
View File

@@ -0,0 +1,301 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
symbols,
text::Span,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
Frame, Terminal,
};
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
const DATA2: [(f64, f64); 7] = [
(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(Span::styled(
"Chart 1",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::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![
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("0"),
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
])
.bounds([-20.0, 20.0]),
);
f.render_widget(chart, chunks[0]);
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(Span::styled(
"Chart 2",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[1]);
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(Span::styled(
"Chart 3",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 50.0])
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("25"),
Span::styled("50", Style::default().add_modifier(Modifier::BOLD)),
]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec![
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5", Style::default().add_modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[2]);
}

77
examples/custom_widget.rs Normal file
View File

@@ -0,0 +1,77 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
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

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

@@ -1,21 +1,19 @@
use crate::{app::App, ui};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use ratatui::{
use tui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
Terminal,
};
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
@@ -26,7 +24,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,8 +35,8 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
)?;
terminal.show_cursor()?;
if let Err(err) = app_result {
println!("{err:?}");
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
@@ -51,20 +49,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),
_ => {}
}
match key.code {
KeyCode::Char(c) => app.on_key(c),
KeyCode::Left => app.on_left(),
KeyCode::Up => app.on_up(),
KeyCode::Right => app.on_right(),
KeyCode::Down => app.on_down(),
_ => {}
}
}
}

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

@@ -0,0 +1,31 @@
mod app;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "termion")]
mod termion;
mod ui;
#[cfg(feature = "crossterm")]
use crate::crossterm::run;
#[cfg(feature = "termion")]
use crate::termion::run;
use argh::FromArgs;
use std::{error::Error, time::Duration};
/// Demo
#[derive(Debug, FromArgs)]
struct Cli {
/// 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,27 +1,21 @@
#![allow(dead_code)]
use crate::{app::App, ui};
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use ratatui::{
use termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::AlternateScreen,
};
use tui::{
backend::{Backend, TermionBackend},
termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::IntoAlternateScreen,
},
Terminal,
};
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
let stdout = io::stdout()
.into_raw_mode()
.unwrap()
.into_alternate_screen()
.unwrap();
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
@@ -39,15 +33,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(),
@@ -70,14 +64,14 @@ fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
let stdin = io::stdin();
for key in stdin.keys().flatten() {
if let Err(err) = keys_tx.send(Event::Input(key)) {
eprintln!("{err}");
eprintln!("{}", err);
return;
}
}
});
thread::spawn(move || loop {
if let Err(err) = tx.send(Event::Tick) {
eprintln!("{err}");
eprintln!("{}", err);
break;
}
thread::sleep(tick_rate);

View File

@@ -1,77 +1,93 @@
use ratatui::{
layout::{Constraint, Layout, Rect},
use crate::app::App;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{self, Span},
text::{Span, Spans},
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
widgets::{
canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle},
Axis, BarChart, Block, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem, Paragraph,
Row, Sparkline, Table, Tabs, Wrap,
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| Spans::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title(app.title))
.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)
.bg(Color::Black)
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
)
.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 +95,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![Spans::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 +161,19 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
"WARNING" => warning_style,
_ => info_style,
};
let content = vec![text::Line::from(vec![
Span::styled(format!("{level:<9}"), s),
let content = vec![Spans::from(vec![
Span::styled(format!("{:<9}", level), s),
Span::raw(evt),
])];
ListItem::new(content)
})
.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 +190,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 +225,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 +246,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![
Spans::from("This is a paragraph with several lines. You can change style your text the way you want"),
Spans::from(""),
Spans::from(vec![
Span::from("For example: "),
Span::styled("under", Style::default().fg(Color::Red)),
Span::raw(" "),
@@ -244,7 +272,7 @@ fn draw_text(frame: &mut Frame, area: Rect) {
Span::styled("rainbow", Style::default().fg(Color::Blue)),
Span::raw("."),
]),
text::Line::from(vec![
Spans::from(vec![
Span::raw("Oh and if you didn't "),
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
Span::raw(" you can "),
@@ -255,23 +283,28 @@ fn draw_text(frame: &mut Frame, area: Rect) {
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
Span::raw(".")
]),
text::Line::from(
Spans::from(
"One more thing is that it should display unicode characters: 10€"
),
];
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 +317,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,
@@ -315,15 +346,9 @@ fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
height: 10.0,
color: Color::Yellow,
});
ctx.draw(&Circle {
x: app.servers[2].coords.1,
y: app.servers[2].coords.0,
radius: 10.0,
color: Color::Green,
});
for (i, s1) in app.servers.iter().enumerate() {
for s2 in &app.servers[i + 1..] {
ctx.draw(&canvas::Line {
ctx.draw(&Line {
x1: s1.coords.1,
y1: s1.coords.0,
y2: s2.coords.0,
@@ -352,11 +377,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,
@@ -380,21 +411,19 @@ fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) {
.iter()
.map(|c| {
let cells = vec![
Cell::from(Span::raw(format!("{c:?}: "))),
Cell::from(Span::raw(format!("{:?}: ", c))),
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
Cell::from(Span::styled("Background", Style::default().bg(*c))),
];
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]);
}

166
examples/gauge.rs Normal file
View File

@@ -0,0 +1,166 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
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.progress2);
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]);
}

70
examples/layout.rs Normal file
View File

@@ -0,0 +1,70 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, 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]);
}

286
examples/list.rs Normal file
View File

@@ -0,0 +1,286 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, List, ListItem, ListState},
Frame, Terminal,
};
struct StatefulList<T> {
state: ListState,
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()? {
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![Spans::from(i.0)];
for _ in 0..i.1 {
lines.push(Spans::from(Span::styled(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
Style::default().add_modifier(Modifier::ITALIC),
)));
}
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
})
.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 = Spans::from(vec![
Span::styled(format!("{:<9}", level), s),
Span::raw(" "),
Span::styled(
"2020-01-01 10:00:00",
Style::default().add_modifier(Modifier::ITALIC),
),
]);
// The event gets its own line
let log = Spans::from(vec![Span::raw(event)]);
// 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![
Spans::from("-".repeat(chunks[1].width as usize)),
header,
Spans::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]);
}

142
examples/panic.rs Normal file
View File

@@ -0,0 +1,142 @@
//! 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;
use std::io;
use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use tui::backend::{Backend, CrosstermBackend};
use tui::layout::Alignment;
use tui::text::Spans;
use tui::widgets::{Block, Borders, Paragraph};
use tui::{Frame, Terminal};
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 {
Spans::from("HOOK IS CURRENTLY **ENABLED**")
} else {
Spans::from("HOOK IS CURRENTLY **DISABLED**")
},
Spans::from(""),
Spans::from("press `p` to panic"),
Spans::from("press `e` to enable the terminal-resetting panic hook"),
Spans::from("press any other key to quit without panic"),
Spans::from(""),
Spans::from("when you panic without the chained hook,"),
Spans::from("you will likely have to reset your terminal afterwards"),
Spans::from("with the `reset` command"),
Spans::from(""),
Spans::from("with the chained panic hook enabled,"),
Spans::from("you should see the panic report as you would without tui"),
Spans::from(""),
Spans::from("try first without the panic handler to see the difference"),
];
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());
}

171
examples/paragraph.rs Normal file
View File

@@ -0,0 +1,171 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, Paragraph, Wrap},
Frame, Terminal,
};
struct App {
scroll: u16,
}
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().style(Style::default().bg(Color::White).fg(Color::Black));
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(5)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(size);
let text = vec![
Spans::from("This is a line "),
Spans::from(Span::styled(
"This is a line ",
Style::default().fg(Color::Red),
)),
Spans::from(Span::styled(
"This is a line",
Style::default().bg(Color::Blue),
)),
Spans::from(Span::styled(
"This is a longer line",
Style::default().add_modifier(Modifier::CROSSED_OUT),
)),
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
Spans::from(Span::styled(
"This is a line",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::ITALIC),
)),
];
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.style(Style::default().bg(Color::White).fg(Color::Black))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Left, no wrap"))
.alignment(Alignment::Left);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Left, wrap"))
.alignment(Alignment::Left)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Center, wrap"))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true })
.scroll((app.scroll, 0));
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Right, wrap"))
.alignment(Alignment::Right)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[3]);
}

128
examples/popup.rs Normal file
View File

@@ -0,0 +1,128 @@
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame, Terminal,
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
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()? {
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(Span::styled(
text,
Style::default().add_modifier(Modifier::SLOW_BLINK),
))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[0]);
let block = Block::default()
.title("Content")
.borders(Borders::ALL)
.style(Style::default().bg(Color::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]
}

176
examples/sparkline.rs Normal file
View File

@@ -0,0 +1,176 @@
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 std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, Sparkline},
Frame, Terminal,
};
#[derive(Clone)]
pub struct RandomSignal {
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]);
}

155
examples/table.rs Normal file
View File

@@ -0,0 +1,155 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Layout},
style::{Color, Modifier, Style},
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()? {
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 crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
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()? {
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().style(Style::default().bg(Color::White).fg(Color::Black));
f.render_widget(block, size);
let titles = app
.titles
.iter()
.map(|t| {
let (first, rest) = t.split_at(1);
Spans::from(vec![
Span::styled(first, Style::default().fg(Color::Yellow)),
Span::styled(rest, Style::default().fg(Color::Green)),
])
})
.collect();
let tabs = Tabs::new(titles)
.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]);
}

245
examples/text_input.rs Normal file
View File

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

192
examples/user_input.rs Normal file
View File

@@ -0,0 +1,192 @@
/// 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},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, Borders, List, ListItem, Paragraph},
Frame, Terminal,
};
use unicode_width::UnicodeWidthStr;
enum InputMode {
Normal,
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 => 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![
Span::raw("Press "),
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit, "),
Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to start editing."),
],
Style::default().add_modifier(Modifier::RAPID_BLINK),
),
InputMode::Editing => (
vec![
Span::raw("Press "),
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to stop editing, "),
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to record the message"),
],
Style::default(),
),
};
let mut text = Text::from(Spans::from(msg));
text.patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[0]);
let input = Paragraph::new(app.input.as_ref())
.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 tui-rs to put it at the specified coordinates after rendering
f.set_cursor(
// Put cursor past the end of the input text
chunks[1].x + app.input.width() as u16 + 1,
// 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 = vec![Spans::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,70 +0,0 @@
[package]
name = "ratatui-core"
description = """
Core types and traits for the Ratatui Terminal UI library.
Widget libraries should use this crate. Applications should use the main Ratatui crate.
"""
version = "0.1.0-alpha.0"
readme = "README.md"
authors.workspace = true
documentation.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
license.workspace = true
exclude.workspace = true
edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = []
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
palette = ["dep:palette"]
## enables the backend code that sets the underline color. Underline color is only supported by
## the Crossterm backend, and is not supported on Windows 7.
underline-color = []
## Use terminal scrolling regions to make some operations less prone to
## flickering. (i.e. Terminal::insert_before).
scrolling-regions = []
## enables serialization and deserialization of style and color types using the [`serde`] crate.
## This is useful if you want to save themes to a file.
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
compact_str = "0.8.0"
document-features = { workspace = true, optional = true }
indoc.workspace = true
itertools.workspace = true
lru = "0.12.0"
palette = { version = "0.7.6", optional = true }
paste = "1.0.2"
serde = { workspace = true, optional = true }
strum.workspace = true
unicode-segmentation.workspace = true
unicode-truncate = "2"
unicode-width.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true
ratatui = { workspace = true, features = ["crossterm", "termwiz"] }
rstest.workspace = true
serde_json.workspace = true
[target.'cfg(not(windows))'.dev-dependencies]
ratatui = { workspace = true, features = ["termion"] }
[lints.clippy]
# we often split up a module into multiple files with the main type in a file named after the
# module, so we want to allow this pattern
module_inception = "allow"

View File

@@ -1,41 +0,0 @@
# Ratatui Core
[![Crates.io](https://img.shields.io/crates/v/ratatui-core)](https://crates.io/crates/ratatui-core)
[![Documentation](https://docs.rs/ratatui-core/badge.svg)](https://docs.rs/ratatui-core)
[![License](https://img.shields.io/crates/l/ratatui-core)](../LICENSE)
<!-- ⚠️ DO NOT EDIT THIS FILE DIRECTLY, EDIT lib.rs AND THEN RUN `cargo rdme` to update this file. -->
<!-- cargo-rdme start -->
**ratatui-core** is the core library of the [ratatui] project,
providing the essential building blocks for creating rich terminal user interfaces in Rust.
[ratatui]: https://github.com/ratatui/ratatui
### Why `ratatui-core`?
The `ratatui-core` crate is split from the main [`ratatui`](https://crates.io/crates/ratatui) crate
to offer better stability for widget library authors. Widget libraries should generally depend
on `ratatui-core`, benefiting from a stable API and reducing the need for frequent updates.
Applications, on the other hand, should depend on the main `ratatui` crate, which includes
built-in widgets and additional features.
## Installation
Add `ratatui-core` to your `Cargo.toml`:
```shell
cargo add ratatui-core
```
## Contributing
We welcome contributions from the community! Please see our [CONTRIBUTING](../CONTRIBUTING.md)
guide for more details on how to get involved.
### License
This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
<!-- cargo-rdme end -->

View File

@@ -1,411 +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::{backend::CrosstermBackend, Terminal};
//!
//! 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.
//!
//! [`CrosstermBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.CrosstermBackend.html
//! [`TermionBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.TermionBackend.html
//! [`TermwizBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.TermwizBackend.html
//! [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.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/ratatui/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`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
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::{TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::backend::Backend;
///
/// 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::{TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::{backend::Backend, layout::Position};
///
/// 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::{TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::backend::Backend;
///
/// 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::{backend::{TestBackend}};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::backend::{Backend, ClearType};
///
/// 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::{backend::{TestBackend}};
/// # let backend = TestBackend::new(80, 25);
/// use ratatui::{backend::Backend, layout::Size};
///
/// 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<()>;
/// Scroll a region of the screen upwards, where a region is specified by a (half-open) range
/// of rows.
///
/// Each row in the region is replaced by the row `line_count` rows below it, except the bottom
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
/// than the number of rows in the region, then all rows are replaced with empty rows.
///
/// If the region includes row 0, then `line_count` rows are copied into the bottom of the
/// scrollback buffer. These rows are first taken from the old contents of the region, starting
/// from the top. If there aren't sufficient rows in the region, then the remainder are empty
/// rows.
///
/// The position of the cursor afterwards is undefined.
///
/// The behavior is designed to match what ANSI terminals do when scrolling regions are
/// established. With ANSI terminals, a scrolling region can be established with the "^[[X;Yr"
/// sequence, where X and Y define the lines of the region. The scrolling region can be reset
/// to be the whole screen with the "^[[r" sequence.
///
/// When a scrolling region is established in an ANSI terminal, various operations' behaviors
/// are changed in such a way that the scrolling region acts like a "virtual screen". In
/// particular, the scrolling sequence "^[[NS", which scrolls lines up by a count of N.
///
/// On an ANSI terminal, this method will probably translate to something like:
/// "^[[X;Yr^[[NS^[[r". That is, set the scrolling region, scroll up, then reset the scrolling
/// region.
///
/// For examples of how this function is expected to work, refer to the tests for
/// [`TestBackend::scroll_region_up`].
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, line_count: u16)
-> io::Result<()>;
/// Scroll a region of the screen downwards, where a region is specified by a (half-open) range
/// of rows.
///
/// Each row in the region is replaced by the row `line_count` rows above it, except the top
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
/// than the number of rows in the region, then all rows are replaced with empty rows.
///
/// The position of the cursor afterwards is undefined.
///
/// See the documentation for [`Self::scroll_region_down`] for more information about how this
/// is expected to be implemented for ANSI terminals. All of that applies, except the ANSI
/// sequence to scroll down is "^[[NT".
///
/// This function is asymmetrical with regards to the scrollback buffer. The reason is that
/// this how terminals seem to implement things.
///
/// For examples of how this function is expected to work, refer to the tests for
/// [`TestBackend::scroll_region_down`].
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(
&mut self,
region: std::ops::Range<u16>,
line_count: u16,
) -> 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));
}
}

File diff suppressed because it is too large Load Diff

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,73 +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::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
};
#[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::style::{Color, Modifier, Style};
/// 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,22 +0,0 @@
#![warn(clippy::missing_const_for_fn)]
//! Provides types and traits for working with layout and positioning in the terminal.
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, Spacing};
pub use margin::Margin;
pub use position::Position;
pub use rect::{Columns, Offset, Positions, Rect, Rows};
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,499 +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_core::layout::Constraint;
///
/// // 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_core::layout::{Constraint, Layout, Rect};
///
/// # 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_core::layout::{Constraint, Layout, Rect};
///
/// # 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_core::layout::{Constraint, Layout, Rect};
///
/// # 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_core::layout::{Constraint, Layout, Rect};
///
/// # 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_core::layout::{Constraint, Layout, Rect};
///
/// # 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_core::layout::{Constraint, Layout, Rect};
///
/// # 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_core::layout::{Constraint, Direction, Layout, Rect};
///
/// # 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 crate::layout::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_core::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,646 +0,0 @@
#![warn(missing_docs)]
use std::{
cmp::{max, min},
fmt,
};
use crate::layout::{Margin, Position, Size};
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`](crate::layout::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 both bounds within `u16`.
///
/// If the width or height would cause the right or bottom coordinate to be larger than the
/// maximum value of `u16`, the width or height will be clamped to keep the right or bottom
/// coordinate within `u16`.
///
/// # Examples
///
/// ```
/// use ratatui_core::layout::Rect;
///
/// let rect = Rect::new(1, 2, 3, 4);
/// ```
pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
// these calculations avoid using min so that this function can be const
let max_width = u16::MAX - x;
let max_height = u16::MAX - y;
let width = if width > max_width { max_width } else { width };
let height = if height > max_height {
max_height
} else {
height
};
Self {
x,
y,
width,
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) -> u32 {
(self.width as u32) * (self.height as u32)
}
/// 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_core::layout::{Position, Rect};
///
/// 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_core::layout::Rect;
///
/// let area = Rect::new(0, 0, 100, 100);
/// let rect = Rect::new(80, 80, 30, 30).clamp(area);
/// assert_eq!(rect, Rect::new(70, 70, 30, 30));
/// ```
#[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_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
///
/// 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_core::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// for (i, column) in area.columns().enumerate() {
/// Text::from(format!("{}", i)).render(column, 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_core::{buffer::Buffer, layout::Rect};
///
/// 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_core::layout::Rect;
///
/// 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::*;
use crate::layout::{Constraint, Layout};
#[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() {
assert_eq!(
Rect::new(u16::MAX - 100, u16::MAX - 1000, 200, 2000),
Rect {
x: u16::MAX - 100,
y: u16::MAX - 1000,
width: 100,
height: 1000
}
);
}
#[test]
fn size_preservation() {
assert_eq!(
Rect::new(u16::MAX - 100, u16::MAX - 1000, 100, 1000),
Rect {
x: u16::MAX - 100,
y: u16::MAX - 1000,
width: 100,
height: 1000
}
);
}
#[test]
fn can_be_const() {
const RECT: Rect = Rect {
x: 0,
y: 0,
width: 10,
height: 10,
};
const _AREA: u32 = 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,329 +0,0 @@
use crate::layout::{Position, Rect};
/// An iterator over rows within a `Rect`.
pub struct Rows {
/// The `Rect` associated with the rows.
rect: Rect,
/// The y coordinate of the row within the `Rect` when iterating forwards.
current_row_fwd: u16,
/// The y coordinate of the row within the `Rect` when iterating backwards.
current_row_back: u16,
}
impl Rows {
/// Creates a new `Rows` iterator.
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_row_fwd: rect.y,
current_row_back: rect.bottom(),
}
}
}
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_fwd >= self.current_row_back {
return None;
}
let row = Rect::new(self.rect.x, self.current_row_fwd, self.rect.width, 1);
self.current_row_fwd += 1;
Some(row)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let start_count = self.current_row_fwd.saturating_sub(self.rect.top());
let end_count = self.rect.bottom().saturating_sub(self.current_row_back);
let count = self
.rect
.height
.saturating_sub(start_count)
.saturating_sub(end_count) as usize;
(count, Some(count))
}
}
impl DoubleEndedIterator for Rows {
/// Retrieves the previous row within the `Rect`.
///
/// Returns `None` when there are no more rows to iterate through.
fn next_back(&mut self) -> Option<Self::Item> {
if self.current_row_back <= self.current_row_fwd {
return None;
}
self.current_row_back -= 1;
let row = Rect::new(self.rect.x, self.current_row_back, self.rect.width, 1);
Some(row)
}
}
/// An iterator over columns within a `Rect`.
pub struct Columns {
/// The `Rect` associated with the columns.
rect: Rect,
/// The x coordinate of the column within the `Rect` when iterating forwards.
current_column_fwd: u16,
/// The x coordinate of the column within the `Rect` when iterating backwards.
current_column_back: u16,
}
impl Columns {
/// Creates a new `Columns` iterator.
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_column_fwd: rect.x,
current_column_back: rect.right(),
}
}
}
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_fwd >= self.current_column_back {
return None;
}
let column = Rect::new(self.current_column_fwd, self.rect.y, 1, self.rect.height);
self.current_column_fwd += 1;
Some(column)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let start_count = self.current_column_fwd.saturating_sub(self.rect.left());
let end_count = self.rect.right().saturating_sub(self.current_column_back);
let count = self
.rect
.width
.saturating_sub(start_count)
.saturating_sub(end_count) as usize;
(count, Some(count))
}
}
impl DoubleEndedIterator for Columns {
/// Retrieves the previous column within the `Rect`.
///
/// Returns `None` when there are no more columns to iterate through.
fn next_back(&mut self) -> Option<Self::Item> {
if self.current_column_back <= self.current_column_fwd {
return None;
}
self.current_column_back -= 1;
let column = Rect::new(self.current_column_back, self.rect.y, 1, self.rect.height);
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.
rect: Rect,
/// The current position within the `Rect`.
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)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let row_count = self.rect.bottom().saturating_sub(self.current_position.y);
if row_count == 0 {
return (0, Some(0));
}
let column_count = self.rect.right().saturating_sub(self.current_position.x);
// subtract 1 from the row count to account for the current row
let count = (row_count - 1)
.saturating_mul(self.rect.width)
.saturating_add(column_count) as usize;
(count, Some(count))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rows() {
let rect = Rect::new(0, 0, 2, 3);
let mut rows = Rows::new(rect);
assert_eq!(rows.size_hint(), (3, Some(3)));
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.size_hint(), (2, Some(2)));
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.size_hint(), (1, Some(1)));
assert_eq!(rows.next(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next_back(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
}
#[test]
fn rows_back() {
let rect = Rect::new(0, 0, 2, 3);
let mut rows = Rows::new(rect);
assert_eq!(rows.size_hint(), (3, Some(3)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.size_hint(), (2, Some(2)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.size_hint(), (1, Some(1)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next_back(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
}
#[test]
fn rows_meet_in_the_middle() {
let rect = Rect::new(0, 0, 2, 4);
let mut rows = Rows::new(rect);
assert_eq!(rows.size_hint(), (4, Some(4)));
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.size_hint(), (3, Some(3)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 3, 2, 1)));
assert_eq!(rows.size_hint(), (2, Some(2)));
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.size_hint(), (1, Some(1)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next_back(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
}
#[test]
fn columns() {
let rect = Rect::new(0, 0, 3, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.size_hint(), (3, Some(3)));
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.size_hint(), (2, Some(2)));
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.size_hint(), (1, Some(1)));
assert_eq!(columns.next(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next_back(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
}
#[test]
fn columns_back() {
let rect = Rect::new(0, 0, 3, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.size_hint(), (3, Some(3)));
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.size_hint(), (2, Some(2)));
assert_eq!(columns.next_back(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.size_hint(), (1, Some(1)));
assert_eq!(columns.next_back(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next_back(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
}
#[test]
fn columns_meet_in_the_middle() {
let rect = Rect::new(0, 0, 4, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.size_hint(), (4, Some(4)));
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.size_hint(), (3, Some(3)));
assert_eq!(columns.next_back(), Some(Rect::new(3, 0, 1, 2)));
assert_eq!(columns.size_hint(), (2, Some(2)));
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.size_hint(), (1, Some(1)));
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next_back(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
}
/// We allow a total of `65536` columns in the range `(0..=65535)`. In this test we iterate
/// forward and skip the first `65534` columns, and expect the next column to be `65535` and
/// the subsequent columns to be `None`.
#[test]
fn columns_max() {
let rect = Rect::new(0, 0, u16::MAX, 1);
let mut columns = Columns::new(rect).skip(usize::from(u16::MAX - 1));
assert_eq!(columns.next(), Some(Rect::new(u16::MAX - 1, 0, 1, 1)));
assert_eq!(columns.next(), None);
}
/// We allow a total of `65536` columns in the range `(0..=65535)`. In this test we iterate
/// backward and skip the last `65534` columns, and expect the next column to be `0` and the
/// subsequent columns to be `None`.
#[test]
fn columns_min() {
let rect = Rect::new(0, 0, u16::MAX, 1);
let mut columns = Columns::new(rect).rev().skip(usize::from(u16::MAX - 1));
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 1)));
assert_eq!(columns.next(), None);
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.size_hint(), (4, Some(4)));
assert_eq!(positions.next(), Some(Position::new(0, 0)));
assert_eq!(positions.size_hint(), (3, Some(3)));
assert_eq!(positions.next(), Some(Position::new(1, 0)));
assert_eq!(positions.size_hint(), (2, Some(2)));
assert_eq!(positions.next(), Some(Position::new(0, 1)));
assert_eq!(positions.size_hint(), (1, Some(1)));
assert_eq!(positions.next(), Some(Position::new(1, 1)));
assert_eq!(positions.size_hint(), (0, Some(0)));
assert_eq!(positions.next(), None);
assert_eq!(positions.size_hint(), (0, Some(0)));
}
}

View File

@@ -1,76 +0,0 @@
#![warn(missing_docs)]
use std::fmt;
use crate::layout::Rect;
/// 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,47 +0,0 @@
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_cfg))]
#![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"
)]
//! **ratatui-core** is the core library of the [ratatui] project,
//! providing the essential building blocks for creating rich terminal user interfaces in Rust.
//!
//! [ratatui]: https://github.com/ratatui/ratatui
//!
//! ## Why `ratatui-core`?
//!
//! The `ratatui-core` crate is split from the main [`ratatui`](https://crates.io/crates/ratatui) crate
//! to offer better stability for widget library authors. Widget libraries should generally depend
//! on `ratatui-core`, benefiting from a stable API and reducing the need for frequent updates.
//!
//! Applications, on the other hand, should depend on the main `ratatui` crate, which includes
//! built-in widgets and additional features.
//!
//! # Installation
//!
//! Add `ratatui-core` to your `Cargo.toml`:
//!
//! ```shell
//! cargo add ratatui-core
//! ```
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
//! # Contributing
//!
//! We welcome contributions from the community! Please see our [CONTRIBUTING](../CONTRIBUTING.md)
//! guide for more details on how to get involved.
//!
//! ## License
//!
//! This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
pub mod backend;
pub mod buffer;
pub mod layout;
pub mod style;
pub mod symbols;
pub mod text;
pub mod widgets;

View File

@@ -1,910 +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_core::{
//! style::{Color, Modifier, Style},
//! text::Span,
//! };
//!
//! 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.
//!
//! ## Example
//!
//! ```
//! use ratatui_core::{
//! style::{Color, Modifier, Style, Stylize},
//! text::{Span, Text},
//! };
//!
//! assert_eq!(
//! "hello".red().on_blue().bold(),
//! Span::styled(
//! "hello",
//! Style::default()
//! .fg(Color::Red)
//! .bg(Color::Blue)
//! .add_modifier(Modifier::BOLD)
//! )
//! );
//!
//! assert_eq!(
//! Text::from("hello").red().on_blue().bold(),
//! Text::from("hello").style(
//! Style::default()
//! .fg(Color::Red)
//! .bg(Color::Blue)
//! .add_modifier(Modifier::BOLD)
//! )
//! );
//! ```
//!
//! [`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_core::style::Modifier;
///
/// 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_core::style::{Color, Modifier, Style};
///
/// 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_core::style::{Style, Stylize};
///
/// 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_core::{
/// style::{Color, Modifier, Style},
/// text::Line,
/// };
///
/// 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_core::{
/// buffer::Buffer,
/// layout::Rect,
/// style::{Color, Modifier, Style},
/// };
///
/// 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_core::{
/// buffer::Buffer,
/// layout::Rect,
/// style::{Color, Modifier, Style},
/// };
///
/// 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 {
/// The foreground color.
pub fg: Option<Color>,
/// The background color.
pub bg: Option<Color>,
/// The underline color.
#[cfg(feature = "underline-color")]
pub underline_color: Option<Color>,
/// The modifiers to add.
pub add_modifier: Modifier,
/// The modifiers to remove.
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()")?;
self.fmt_stylize(f)?;
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 {
/// Returns a `Style` with default properties.
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_core::style::{Color, Style};
///
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
/// ```
#[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_core::style::{Color, Style};
///
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
/// ```
#[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_core::style::{Color, Modifier, Style};
///
/// let style = Style::default()
/// .underline_color(Color::Blue)
/// .add_modifier(Modifier::UNDERLINED);
/// let diff = Style::default()
/// .underline_color(Color::Red)
/// .add_modifier(Modifier::UNDERLINED);
/// assert_eq!(
/// style.patch(diff),
/// Style::default()
/// .underline_color(Color::Red)
/// .add_modifier(Modifier::UNDERLINED)
/// );
/// ```
#[cfg(feature = "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_core::style::{Modifier, Style};
///
/// let style = Style::default().add_modifier(Modifier::BOLD);
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
/// assert_eq!(patched.sub_modifier, Modifier::empty());
/// ```
#[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_core::style::{Modifier, Style};
///
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
/// ```
#[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_core::style::{Color, Modifier, Style};
///
/// let style_1 = Style::default().fg(Color::Yellow);
/// let style_2 = Style::default().bg(Color::Red);
/// let combined = style_1.patch(style_2);
/// 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
}
/// Formats the style in a way that can be copy-pasted into code using the style shorthands.
///
/// This is useful for debugging and for generating code snippets.
pub(crate) fn fmt_stylize(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Debug;
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 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_core::style::{Color, Style};
///
/// 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_core::style::{Color, Style};
///
/// // 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_core::style::{Style, Modifier};
///
/// // 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_core::style::{Modifier, Style};
///
/// // 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_core::style::{Color, Modifier, Style};
///
/// // 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_core::style::{Color, Modifier, Style};
///
/// // 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_core::style::{Color, Modifier, Style};
///
/// // 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()")]
#[case(
Style::new().red().on_blue().bold().italic().not_dim().not_hidden(),
"Style::new().red().on_blue().bold().italic().not_dim().not_hidden()"
)]
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,737 +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_core::style::Color;
///
/// 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>
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 std::str::FromStr;
///
/// use ratatui_core::style::Color;
///
/// #[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_core::style::Color;
///
/// let color: Color = Color::from_str("blue").unwrap();
/// assert_eq!(color, Color::Blue);
///
/// let color: Color = Color::from_str("#FF0000").unwrap();
/// assert_eq!(color, Color::Rgb(255, 0, 0));
///
/// let color: Color = Color::from_str("10").unwrap();
/// assert_eq!(color, Color::Indexed(10));
///
/// let color: Result<Color, _> = Color::from_str("invalid_color");
/// assert!(color.is_err());
/// ```
impl FromStr for Color {
type Err = ParseColorError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(
// There is a mix of different color names and formats in the wild.
// This is an attempt to support as many as possible.
match s
.to_lowercase()
.replace([' ', '-', '_'], "")
.replace("bright", "light")
.replace("grey", "gray")
.replace("silver", "gray")
.replace("lightblack", "darkgray")
.replace("lightwhite", "white")
.replace("lightgray", "white")
.as_ref()
{
"reset" => Self::Reset,
"black" => Self::Black,
"red" => Self::Red,
"green" => Self::Green,
"yellow" => Self::Yellow,
"blue" => Self::Blue,
"magenta" => Self::Magenta,
"cyan" => Self::Cyan,
"gray" => Self::Gray,
"darkgray" => Self::DarkGray,
"lightred" => Self::LightRed,
"lightgreen" => Self::LightGreen,
"lightyellow" => Self::LightYellow,
"lightblue" => Self::LightBlue,
"lightmagenta" => Self::LightMagenta,
"lightcyan" => Self::LightCyan,
"white" => Self::White,
_ => {
if let Ok(index) = s.parse::<u8>() {
Self::Indexed(index)
} else if let 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 [-180..180]. Values outside this range are normalized by
/// wrapping.
///
/// Saturation and L values should be in the range [0.0..1.0]. Values outside this range are
/// clamped.
///
/// Clamping to valid ranges happens before conversion to RGB.
///
/// # Examples
///
/// ```
/// use palette::Hsl;
/// use ratatui_core::style::Color;
///
/// // Minimum Lightness is black
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(0, 0, 0));
///
/// // Maximum Lightness is white
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 1.0));
/// assert_eq!(color, Color::Rgb(255, 255, 255));
///
/// // Minimum Saturation is fully desaturated red = gray
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.5));
/// assert_eq!(color, Color::Rgb(128, 128, 128));
///
/// // Bright red
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 1.0, 0.5));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
///
/// // Bright blue
/// let color: Color = Color::from_hsl(Hsl::new(-120.0, 1.0, 0.5));
/// assert_eq!(color, Color::Rgb(0, 0, 255));
/// ```
#[cfg(feature = "palette")]
pub fn from_hsl(hsl: palette::Hsl) -> Self {
use palette::{Clamp, FromColor, Srgb};
let hsl = hsl.clamp();
let Srgb {
red,
green,
blue,
standard: _,
}: Srgb<u8> = Srgb::from_color(hsl).into();
Self::Rgb(red, green, blue)
}
/// Converts a `HSLuv` representation to a `Color::Rgb` instance.
///
/// The `from_hsluv` function converts the Hue, Saturation and Lightness values to a
/// corresponding `Color` RGB equivalent.
///
/// Hue values should be in the range [-180.0..180.0]. Values outside this range are normalized
/// by wrapping.
///
/// Saturation and L values should be in the range [0.0..100.0]. Values outside this range are
/// clamped.
///
/// Clamping to valid ranges happens before conversion to RGB.
///
/// # Examples
///
/// ```
/// use palette::Hsluv;
/// use ratatui_core::style::Color;
///
/// // Minimum Lightness is black
/// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 100.0, 0.0));
/// assert_eq!(color, Color::Rgb(0, 0, 0));
///
/// // Maximum Lightness is white
/// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 100.0));
/// assert_eq!(color, Color::Rgb(255, 255, 255));
///
/// // Minimum Saturation is fully desaturated red = gray
/// let color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 50.0));
/// assert_eq!(color, Color::Rgb(119, 119, 119));
///
/// // Bright Red
/// let color = Color::from_hsluv(Hsluv::new(12.18, 100.0, 53.2));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
///
/// // Bright Blue
/// let color = Color::from_hsluv(Hsluv::new(-94.13, 100.0, 32.3));
/// assert_eq!(color, Color::Rgb(0, 0, 255));
/// ```
#[cfg(feature = "palette")]
pub fn from_hsluv(hsluv: palette::Hsluv) -> Self {
use palette::{Clamp, FromColor, Srgb};
let hsluv = hsluv.clamp();
let Srgb {
red,
green,
blue,
standard: _,
}: Srgb<u8> = Srgb::from_color(hsluv).into();
Self::Rgb(red, green, blue)
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
#[cfg(feature = "palette")]
use palette::{Hsl, Hsluv};
#[cfg(feature = "palette")]
use rstest::rstest;
#[cfg(feature = "serde")]
use serde::de::{Deserialize, IntoDeserializer};
use super::*;
#[cfg(feature = "palette")]
#[rstest]
#[case::black(Hsl::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
#[case::white(Hsl::new(0.0, 0.0, 1.0), Color::Rgb(255, 255, 255))]
#[case::valid(Hsl::new(120.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
#[case::min_hue(Hsl::new(-180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
#[case::max_hue(Hsl::new(180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
#[case::min_saturation(Hsl::new(0.0, 0.0, 0.5), Color::Rgb(128, 128, 128))]
#[case::max_saturation(Hsl::new(0.0, 1.0, 0.5), Color::Rgb(255, 0, 0))]
#[case::min_lightness(Hsl::new(0.0, 0.5, 0.0), Color::Rgb(0, 0, 0))]
#[case::max_lightness(Hsl::new(0.0, 0.5, 1.0), Color::Rgb(255, 255, 255))]
#[case::under_hue_wraps(Hsl::new(-240.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
#[case::over_hue_wraps(Hsl::new(480.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
#[case::under_saturation_clamps(Hsl::new(0.0, -0.5, 0.75), Color::Rgb(191, 191, 191))]
#[case::over_saturation_clamps(Hsl::new(0.0, 1.2, 0.75), Color::Rgb(255, 128, 128))]
#[case::under_lightness_clamps(Hsl::new(0.0, 0.5, -0.20), Color::Rgb(0, 0, 0))]
#[case::over_lightness_clamps(Hsl::new(0.0, 0.5, 1.5), Color::Rgb(255, 255, 255))]
#[case::under_saturation_lightness_clamps(Hsl::new(0.0, -0.5, -0.20), Color::Rgb(0, 0, 0))]
#[case::over_saturation_lightness_clamps(Hsl::new(0.0, 1.2, 1.5), Color::Rgb(255, 255, 255))]
fn test_hsl_to_rgb(#[case] hsl: palette::Hsl, #[case] expected: Color) {
assert_eq!(Color::from_hsl(hsl), expected);
}
#[cfg(feature = "palette")]
#[rstest]
#[case::black(Hsluv::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
#[case::white(Hsluv::new(0.0, 0.0, 100.0), Color::Rgb(255, 255, 255))]
#[case::valid(Hsluv::new(120.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
#[case::min_hue(Hsluv::new(-180.0, 50.0, 75.0), Color::Rgb(135,196, 188))]
#[case::max_hue(Hsluv::new(180.0, 50.0, 75.0), Color::Rgb(135, 196, 188))]
#[case::min_saturation(Hsluv::new(0.0, 0.0, 75.0), Color::Rgb(185, 185, 185))]
#[case::max_saturation(Hsluv::new(0.0, 100.0, 75.0), Color::Rgb(255, 156, 177))]
#[case::min_lightness(Hsluv::new(0.0, 50.0, 0.0), Color::Rgb(0, 0, 0))]
#[case::max_lightness(Hsluv::new(0.0, 50.0, 100.0), Color::Rgb(255, 255, 255))]
#[case::under_hue_wraps(Hsluv::new(-240.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
#[case::over_hue_wraps(Hsluv::new(480.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
#[case::under_saturation_clamps(Hsluv::new(0.0, -50.0, 75.0), Color::Rgb(185, 185, 185))]
#[case::over_saturation_clamps(Hsluv::new(0.0, 150.0, 75.0), Color::Rgb(255, 156, 177))]
#[case::under_lightness_clamps(Hsluv::new(0.0, 50.0, -20.0), Color::Rgb(0, 0, 0))]
#[case::over_lightness_clamps(Hsluv::new(0.0, 50.0, 150.0), Color::Rgb(255, 255, 255))]
#[case::under_saturation_lightness_clamps(Hsluv::new(0.0, -50.0, -20.0), Color::Rgb(0, 0, 0))]
#[case::over_saturation_lightness_clamps(
Hsluv::new(0.0, 150.0, 150.0),
Color::Rgb(255, 255, 255)
)]
fn test_hsluv_to_rgb(#[case] hsluv: palette::Hsluv, #[case] expected: Color) {
assert_eq!(Color::from_hsluv(hsluv), expected);
}
#[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,608 +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_core::style::{
//! palette::material::{BLUE, RED},
//! Color,
//! };
//!
//! 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::style::Color;
/// 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,654 +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_core::style::{
//! palette::tailwind::{BLUE, RED},
//! Color,
//! };
//!
//! assert_eq!(RED.c500, Color::Rgb(239, 68, 68));
//! assert_eq!(BLUE.c500, Color::Rgb(59, 130, 246));
//! ```
use crate::style::Color;
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 crate::style::Color;
/// Convert an [`palette::Srgb`] color to a [`Color`].
///
/// # Examples
///
/// ```
/// use palette::Srgb;
/// use ratatui_core::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_core::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,575 +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
/// ```ignore
/// use ratatui_core::{
/// style::{Color, Modifier, Style, Stylize},
/// text::Line,
/// widgets::{Block, Paragraph},
/// };
///
/// 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(Color::Black, ".black()")]
#[case(Color::Red, ".red()")]
#[case(Color::Green, ".green()")]
#[case(Color::Yellow, ".yellow()")]
#[case(Color::Blue, ".blue()")]
#[case(Color::Magenta, ".magenta()")]
#[case(Color::Cyan, ".cyan()")]
#[case(Color::Gray, ".gray()")]
#[case(Color::DarkGray, ".dark_gray()")]
#[case(Color::LightRed, ".light_red()")]
#[case(Color::LightGreen, ".light_green()")]
#[case(Color::LightYellow, ".light_yellow()")]
#[case(Color::LightBlue, ".light_blue()")]
#[case(Color::LightMagenta, ".light_magenta()")]
#[case(Color::LightCyan, ".light_cyan()")]
#[case(Color::White, ".white()")]
#[case(Color::Indexed(10), ".fg(Color::Indexed(10))")]
#[case(Color::Rgb(255, 0, 0), ".fg(Color::Rgb(255, 0, 0))")]
fn stylize_debug_foreground(#[case] color: Color, #[case] expected: &str) {
let debug = color.stylize_debug(ColorDebugKind::Foreground);
assert_eq!(format!("{debug:?}"), expected);
}
#[rstest]
#[case(Color::Black, ".on_black()")]
#[case(Color::Red, ".on_red()")]
#[case(Color::Green, ".on_green()")]
#[case(Color::Yellow, ".on_yellow()")]
#[case(Color::Blue, ".on_blue()")]
#[case(Color::Magenta, ".on_magenta()")]
#[case(Color::Cyan, ".on_cyan()")]
#[case(Color::Gray, ".on_gray()")]
#[case(Color::DarkGray, ".on_dark_gray()")]
#[case(Color::LightRed, ".on_light_red()")]
#[case(Color::LightGreen, ".on_light_green()")]
#[case(Color::LightYellow, ".on_light_yellow()")]
#[case(Color::LightBlue, ".on_light_blue()")]
#[case(Color::LightMagenta, ".on_light_magenta()")]
#[case(Color::LightCyan, ".on_light_cyan()")]
#[case(Color::White, ".on_white()")]
#[case(Color::Indexed(10), ".bg(Color::Indexed(10))")]
#[case(Color::Rgb(255, 0, 0), ".bg(Color::Rgb(255, 0, 0))")]
fn stylize_debug_background(#[case] color: Color, #[case] expected: &str) {
let debug = color.stylize_debug(ColorDebugKind::Background);
assert_eq!(format!("{debug:?}"), expected);
}
#[cfg(feature = "underline-color")]
#[rstest]
#[case(Color::Black, ".underline_color(Color::Black)")]
#[case(Color::Red, ".underline_color(Color::Red)")]
#[case(Color::Green, ".underline_color(Color::Green)")]
#[case(Color::Yellow, ".underline_color(Color::Yellow)")]
#[case(Color::Blue, ".underline_color(Color::Blue)")]
#[case(Color::Magenta, ".underline_color(Color::Magenta)")]
#[case(Color::Cyan, ".underline_color(Color::Cyan)")]
#[case(Color::Gray, ".underline_color(Color::Gray)")]
#[case(Color::DarkGray, ".underline_color(Color::DarkGray)")]
#[case(Color::LightRed, ".underline_color(Color::LightRed)")]
#[case(Color::LightGreen, ".underline_color(Color::LightGreen)")]
#[case(Color::LightYellow, ".underline_color(Color::LightYellow)")]
#[case(Color::LightBlue, ".underline_color(Color::LightBlue)")]
#[case(Color::LightMagenta, ".underline_color(Color::LightMagenta)")]
#[case(Color::LightCyan, ".underline_color(Color::LightCyan)")]
#[case(Color::White, ".underline_color(Color::White)")]
#[case(Color::Indexed(10), ".underline_color(Color::Indexed(10))")]
#[case(Color::Rgb(255, 0, 0), ".underline_color(Color::Rgb(255, 0, 0))")]
fn stylize_debug_underline(#[case] color: Color, #[case] expected: &str) {
let debug = color.stylize_debug(ColorDebugKind::Underline);
assert_eq!(format!("{debug:?}"), expected);
}
}

View File

@@ -1,238 +0,0 @@
//! Symbols and markers for drawing various widgets.
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 crate::symbols::{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: "",
};
}
pub mod shade {
pub const EMPTY: &str = " ";
pub const LIGHT: &str = "";
pub const MEDIUM: &str = "";
pub const DARK: &str = "";
pub const FULL: &str = "";
}
#[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 crate::symbols::{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,64 +0,0 @@
//! Primitives for styled text.
//!
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish, those
//! strings may be associated to a set of styles. `ratatui` has three ways to represent them:
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//!
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`] is
//! a [`Line`].
//!
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations
//! so that you can start by using simple `String` or `&str` and then promote them to the previous
//! primitives when you need additional styling capabilities.
//!
//! For example, for the `Block` widget, all the following calls are valid to set its `title`
//! property (which is a [`Line`] under the hood):
//!
//! ```rust,ignore
//! use ratatui_core::{
//! style::{Color, Style},
//! text::{Line, Span},
//! widgets::Block,
//! };
//!
//! // A simple string with no styling.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
//! // ])
//! let block = Block::new().title("My title");
//!
//! // A simple string with a unique style.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
//! // ])
//! let block = Block::new().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
//!
//! // A string with multiple styles.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
//! // Span { content: Cow::Borrowed(" title"), .. }
//! // ])
//! let block = Block::new().title(vec![
//! Span::styled("My", Style::default().fg(Color::Yellow)),
//! Span::raw(" title"),
//! ]);
//! ```
mod grapheme;
pub use grapheme::StyledGrapheme;
mod line;
pub use line::{Line, ToLine};
mod masked;
pub use masked::Masked;
mod span;
pub use span::{Span, ToSpan};
mod text;
pub use text::{Text, ToText};

View File

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

View File

@@ -1,900 +0,0 @@
use std::{borrow::Cow, fmt};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::Rect,
style::{Style, Styled},
text::{Line, StyledGrapheme},
widgets::Widget,
};
/// 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_core::text::Span;
///
/// 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_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// 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_core::{style::Stylize, text::Span};
///
/// 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`]. Often
/// apps will use the `Paragraph` widget instead of rendering `Span` directly, as it handles text
/// wrapping and alignment for you.
///
/// ```rust,ignore
/// use ratatui::{style::Stylize, Frame};
///
/// # fn render_frame(frame: &mut Frame) {
/// frame.render_widget("test content".green().on_yellow().italic(), frame.area());
/// # }
/// ```
/// [`Line`]: crate::text::Line
/// [`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.content.is_empty() {
write!(f, "Span::default()")?;
} else {
write!(f, "Span::from({:?})", self.content)?;
}
if self.style != Style::default() {
self.style.fmt_stylize(f)?;
}
Ok(())
}
}
impl<'a> Span<'a> {
/// Create a span with the default style.
///
/// # Examples
///
/// ```rust
/// use ratatui_core::text::Span;
///
/// 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_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let style = Style::new().yellow().on_green().italic();
/// Span::styled("test content", style);
/// Span::styled(String::from("test content"), style);
/// ```
///
/// [`Color`]: crate::style::Color
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_core::text::Span;
///
/// 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_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let mut span = Span::default().style(Style::new().green());
/// ```
///
/// [`Color`]: crate::style::Color
#[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_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// 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());
/// ```
///
/// [`Color`]: crate::style::Color
#[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_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// 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_core::{
/// style::{Style, Stylize},
/// text::{Span, 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()),
/// ],
/// );
/// ```
///
/// [`Color`]: crate::style::Color
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_core::style::Stylize;
///
/// 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_core::style::Stylize;
///
/// 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_core::style::Stylize;
///
/// 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) {
Widget::render(&self, area, buf);
}
}
impl Widget for &Span<'_> {
fn render(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 rstest::{fixture, rstest};
use super::*;
use crate::{buffer::Cell, layout::Alignment, style::Stylize};
#[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")])
);
}
#[rstest]
#[case::default(Span::default(), "Span::default()")]
#[case::raw(Span::raw("test"), r#"Span::from("test")"#)]
#[case::styled(Span::styled("test", Style::new().green()), r#"Span::from("test").green()"#)]
#[case::styled_italic(
Span::styled("test", Style::new().green().italic()),
r#"Span::from("test").green().italic()"#
)]
fn debug(#[case] span: Span, #[case] expected: &str) {
assert_eq!(format!("{span:?}"), expected);
}
}

File diff suppressed because it is too large Load Diff

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