Compare commits
85 Commits
ratatui-te
...
js/fix-lay
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e602369afe | ||
|
|
bce6604379 | ||
|
|
c8d5d385b3 | ||
|
|
3fe3fe316e | ||
|
|
0f10944ca0 | ||
|
|
1bd3181d8e | ||
|
|
e04fef7354 | ||
|
|
bbca3870d5 | ||
|
|
1fe64de09a | ||
|
|
72334ed61c | ||
|
|
c1b8528b69 | ||
|
|
cfebd68e18 | ||
|
|
572749f388 | ||
|
|
cdf189e15a | ||
|
|
98f85b8650 | ||
|
|
0148b62f0c | ||
|
|
9bc573931c | ||
|
|
64e6d1953c | ||
|
|
c0148a00fe | ||
|
|
44727c7b4a | ||
|
|
1e9d1253f9 | ||
|
|
aae46032bc | ||
|
|
0e10170e19 | ||
|
|
40e96a2a04 | ||
|
|
02ca5870c5 | ||
|
|
8e3bd11d60 | ||
|
|
017af11b2b | ||
|
|
821611f76f | ||
|
|
ae43ea796a | ||
|
|
5fa342cc52 | ||
|
|
08b21fa55c | ||
|
|
32ccf38606 | ||
|
|
04428f6f74 | ||
|
|
84427c2857 | ||
|
|
ca5a9bb019 | ||
|
|
1f0c2ee18e | ||
|
|
6b3323fb10 | ||
|
|
11c5862728 | ||
|
|
055522ef7b | ||
|
|
0b025db72b | ||
|
|
617d31851a | ||
|
|
4c301e891d | ||
|
|
b9da1926a0 | ||
|
|
6dcd53bc6b | ||
|
|
d99984f1e9 | ||
|
|
8e2d568428 | ||
|
|
d02995fda1 | ||
|
|
4c708ddf8a | ||
|
|
5620e06b1a | ||
|
|
cfb65e64ba | ||
|
|
92bb9b2219 | ||
|
|
d6647db744 | ||
|
|
3f48bde3c6 | ||
|
|
80bc818723 | ||
|
|
0c3872f1c5 | ||
|
|
9f9ec332b0 | ||
|
|
0951da52f9 | ||
|
|
3de41a8249 | ||
|
|
d41b8d6334 | ||
|
|
4c86513790 | ||
|
|
7bc78bca1b | ||
|
|
b6fbfcdd1c | ||
|
|
21e3b598ce | ||
|
|
ca2ad4a1f9 | ||
|
|
272f5c05dc | ||
|
|
dfd0736f3a | ||
|
|
a112953c59 | ||
|
|
e1e400406c | ||
|
|
1399d95ae0 | ||
|
|
488e5f020f | ||
|
|
e48aa9ec09 | ||
|
|
68b9f67f59 | ||
|
|
bbe1cf9497 | ||
|
|
671c2b4fd4 | ||
|
|
12cb5a28fe | ||
|
|
861fbdf5cf | ||
|
|
bf9ecf19de | ||
|
|
ac22d21deb | ||
|
|
770cb7c3c3 | ||
|
|
92b6a16bde | ||
|
|
89b74214d9 | ||
|
|
e32a5bf442 | ||
|
|
3728a0d90a | ||
|
|
b32f78195b | ||
|
|
7d84d42103 |
81
.github/copilot-instructions.md
vendored
Normal file
81
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
# GitHub Copilot Code Review Instructions
|
||||
|
||||
## General Review Principles
|
||||
|
||||
### Pull Request Size and Scope
|
||||
- **Flag large PRs**: Comment if a PR changes more than 500 lines or touches many unrelated areas
|
||||
- **Suggest splitting**: Recommend breaking large changes into smaller, focused PRs
|
||||
- **Question scope creep**: Ask about unrelated changes that seem outside the PR's stated purpose
|
||||
|
||||
### Code Quality and Style
|
||||
- **Check adherence to style guidelines**: Verify changes follow the project's Rust conventions in [CONTRIBUTING.md](https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md#code-formatting)
|
||||
- **Verify xtask compliance**: Ensure `cargo xtask format` and `cargo xtask lint` would pass
|
||||
- **Look for AI-generated patterns**: Be suspicious of verbose, overly-commented, or non-idiomatic code
|
||||
|
||||
### Architectural Considerations
|
||||
- **Reference ARCHITECTURE.md**: Point to [ARCHITECTURE.md](https://github.com/ratatui/ratatui/blob/main/ARCHITECTURE.md) for changes affecting crate boundaries
|
||||
- **Question fundamental changes**: Flag modifications to core configuration, linting rules, or build setup without clear justification
|
||||
- **Verify appropriate crate placement**: Ensure changes are in the correct crate per the modular structure
|
||||
|
||||
### Breaking Changes and Deprecation
|
||||
- **Require deprecation**: Insist on deprecation warnings rather than immediate removal of public APIs
|
||||
- **Ask for migration path**: Request clear upgrade instructions for breaking changes
|
||||
- **Suggest feature flags**: Recommend feature flags for experimental or potentially disruptive changes
|
||||
- **Reference versioning policy**: Point to the requirement of at least one version notice before removal
|
||||
|
||||
### Testing and Documentation
|
||||
- **Verify test coverage**: Ensure new functionality includes appropriate tests
|
||||
- **Check for test removal**: Question any removal of existing tests without clear justification
|
||||
- **Require documentation**: Ensure public APIs are documented with examples
|
||||
- **Validate examples**: Check that code examples are minimal and follow project style
|
||||
|
||||
### Specific Areas of Concern
|
||||
|
||||
#### Configuration Changes
|
||||
- **Lint configuration**: Question changes to `.clippy.toml`, `rustfmt.toml`, or CI configuration
|
||||
- **Cargo.toml modifications**: Scrutinize dependency changes or workspace modifications
|
||||
- **Build system changes**: Require justification for xtask or build process modifications
|
||||
|
||||
#### Large Code Additions
|
||||
- **Question necessity**: Ask if large code additions could be implemented more simply
|
||||
- **Check for duplication**: Look for code that duplicates existing functionality
|
||||
- **Verify integration**: Ensure new code integrates well with existing patterns
|
||||
|
||||
#### File Organization
|
||||
- **Validate module structure**: Ensure new modules follow the project's organization
|
||||
- **Check import organization**: Verify imports follow the std/external/local grouping pattern
|
||||
- **Review file placement**: Confirm files are in appropriate locations per ARCHITECTURE.md
|
||||
|
||||
## Comment Templates
|
||||
|
||||
### For Large PRs
|
||||
```
|
||||
This PR seems quite large with changes across multiple areas. Consider splitting it into smaller, focused PRs:
|
||||
- Core functionality changes
|
||||
- Documentation updates
|
||||
- Test additions
|
||||
- Configuration changes
|
||||
|
||||
See our [contribution guidelines](https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md#keep-prs-small-intentional-and-focused) for more details.
|
||||
```
|
||||
|
||||
### For Breaking Changes
|
||||
```
|
||||
This appears to introduce breaking changes. Please consider:
|
||||
- Adding deprecation warnings instead of immediate removal
|
||||
- Providing a clear migration path in the PR description
|
||||
- Following our [deprecation policy](https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md#deprecation-notice)
|
||||
```
|
||||
|
||||
### For Configuration Changes
|
||||
```
|
||||
Changes to project configuration (linting, formatting, build) should be discussed first. Please explain:
|
||||
- Why this change is necessary
|
||||
- What problem it solves
|
||||
- Whether it affects contributor workflow
|
||||
```
|
||||
|
||||
### For Style Issues
|
||||
```
|
||||
Please run `cargo xtask format` and `cargo xtask lint` to ensure code follows our style guidelines. See [CONTRIBUTING.md](https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md#code-formatting) for details.
|
||||
```
|
||||
65
.github/workflows/check-pr.yml
vendored
65
.github/workflows/check-pr.yml
vendored
@@ -1,6 +1,13 @@
|
||||
name: Check Pull Requests
|
||||
|
||||
# Set the permissions of the github token to the minimum and only enable what is needed
|
||||
# See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
# this workflow is required to be run on pull_request_target as it modifies the PR comments
|
||||
# care should be taken that the jobs do not run any untrusted input
|
||||
# zizmor: ignore[dangerous-triggers]
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
@@ -8,23 +15,21 @@ on:
|
||||
- synchronize
|
||||
- labeled
|
||||
- unlabeled
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-title:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR title
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: amannn/action-semantic-pull-request@v5
|
||||
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # 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
|
||||
- uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2
|
||||
if: always() && (steps.check_pr_title.outputs.error_message != null)
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
@@ -39,40 +44,42 @@ jobs:
|
||||
|
||||
# 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
|
||||
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
|
||||
check-breaking-change-label:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
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']
|
||||
})
|
||||
- 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') }}
|
||||
|
||||
10
.github/workflows/check-semver.yml
vendored
10
.github/workflows/check-semver.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Check Semver
|
||||
|
||||
# Set the permissions of the github token to the minimum and only enable what is needed
|
||||
# See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -11,6 +15,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check semver
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
uses: obi1kenobi/cargo-semver-checks-action@5b298c9520f7096a4683c0bd981a7ac5a7e249ae # v2
|
||||
|
||||
147
.github/workflows/ci.yml
vendored
147
.github/workflows/ci.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Continuous Integration
|
||||
|
||||
# Set the permissions of the github token to the minimum and only enable what is needed
|
||||
# See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
@@ -25,11 +29,15 @@ jobs:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with: { components: rustfmt }
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: taiki-e/install-action@v2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2
|
||||
with:
|
||||
tool: taplo-cli
|
||||
- run: cargo xtask format --check
|
||||
@@ -40,8 +48,10 @@ jobs:
|
||||
name: Check Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@master
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: crate-ci/typos@392b78fe18a52790c53f42456e46124f77346842 # master
|
||||
|
||||
# Check for any disallowed dependencies in the codebase due to license / security issues.
|
||||
# See <https://github.com/EmbarkStudios/cargo-deny>
|
||||
@@ -49,9 +59,15 @@ jobs:
|
||||
name: Check Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: taiki-e/install-action@cargo-deny
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2
|
||||
with:
|
||||
tool: cargo-deny
|
||||
- run: cargo deny --log-level info --all-features check
|
||||
|
||||
# Check for any unused dependencies in the codebase.
|
||||
@@ -60,8 +76,10 @@ jobs:
|
||||
name: Check Unused Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: bnjbvr/cargo-machete@v0.8.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: bnjbvr/cargo-machete@b54422fa3319b3cac180f6030b663fe57af51635 # v0.8.0
|
||||
|
||||
# Run cargo clippy.
|
||||
#
|
||||
@@ -77,12 +95,14 @@ jobs:
|
||||
toolchain: ["stable", "beta"]
|
||||
continue-on-error: ${{ matrix.toolchain == 'beta' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask clippy
|
||||
|
||||
# Run markdownlint on all markdown files in the repository.
|
||||
@@ -90,8 +110,10 @@ jobs:
|
||||
name: Check Markdown
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v19
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20
|
||||
with:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
@@ -103,14 +125,19 @@ jobs:
|
||||
name: Coverage Report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: llvm-tools
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2
|
||||
with:
|
||||
tool: cargo-llvm-cov
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask coverage
|
||||
- uses: codecov/codecov-action@v5
|
||||
- uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
@@ -125,28 +152,35 @@ jobs:
|
||||
toolchain: ["1.85.0", "stable"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask check --all-features
|
||||
|
||||
build-no-std:
|
||||
name: Build No-Std
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: x86_64-unknown-none
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
# This makes it easier to debug the exact versions of the dependencies
|
||||
- run: cargo tree --target x86_64-unknown-none -p ratatui-core
|
||||
- run: cargo tree --target x86_64-unknown-none -p ratatui-widgets
|
||||
- run: cargo tree --target x86_64-unknown-none -p ratatui-macros
|
||||
- run: cargo tree --target x86_64-unknown-none -p ratatui --no-default-features
|
||||
- run: cargo build --target x86_64-unknown-none -p ratatui-core
|
||||
- run: cargo build --target x86_64-unknown-none -p ratatui-widgets
|
||||
- run: cargo build --target x86_64-unknown-none -p ratatui-macros
|
||||
- run: cargo build --target x86_64-unknown-none -p ratatui --no-default-features
|
||||
|
||||
# Check if README.md is up-to-date with the crate's documentation.
|
||||
@@ -154,9 +188,13 @@ jobs:
|
||||
name: Check README
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: taiki-e/install-action@cargo-rdme
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2
|
||||
with:
|
||||
tool: cargo-rdme
|
||||
- run: cargo xtask readme --check
|
||||
|
||||
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account the
|
||||
@@ -167,10 +205,19 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: dtolnay/install@cargo-docs-rs
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: nightly
|
||||
- uses: dtolnay/install@74f735cdf643820234e37ae1c4089a08fd266d8a # master
|
||||
with:
|
||||
crate: cargo-docs-rs
|
||||
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask docs
|
||||
|
||||
# Run cargo test on the documentation of the crate. This will catch any code examples that don't
|
||||
@@ -179,9 +226,13 @@ jobs:
|
||||
name: Test Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask test-docs
|
||||
|
||||
# Run cargo test on the libraries of the crate.
|
||||
@@ -193,9 +244,13 @@ jobs:
|
||||
matrix:
|
||||
toolchain: ["1.85.0", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask test-libs
|
||||
|
||||
# Run cargo test on all the backends.
|
||||
@@ -212,7 +267,11 @@ jobs:
|
||||
- os: windows-latest
|
||||
backend: termion
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask test-backend ${{ matrix.backend }}
|
||||
|
||||
17
.github/workflows/release-alpha.yml
vendored
17
.github/workflows/release-alpha.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Release alpha version
|
||||
|
||||
# Set the permissions of the github token to the minimum and only enable what is needed
|
||||
# See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
@@ -15,25 +19,30 @@ jobs:
|
||||
publish-alpha:
|
||||
name: Create an alpha release
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'ratatui/ratatui'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# probably needs to be set to true for git-cliff to work - check build
|
||||
persist-credentials: false
|
||||
|
||||
- name: Calculate the next release
|
||||
run: .github/workflows/calculate-alpha-release.bash
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Publish
|
||||
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v4
|
||||
uses: orhun/git-cliff-action@4a4a951bc43fafe41cd2348d181853f52356bee7 # v4
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
|
||||
@@ -41,7 +50,7 @@ jobs:
|
||||
OUTPUT: BODY.md
|
||||
|
||||
- name: Publish on GitHub
|
||||
uses: ncipollo/release-action@v1
|
||||
uses: ncipollo/release-action@bcfe5470707e8832e12347755757cec0eb3c22af # v1
|
||||
with:
|
||||
tag: ${{ env.NEXT_TAG }}
|
||||
prerelease: true
|
||||
|
||||
35
.github/workflows/release-plz.yml
vendored
35
.github/workflows/release-plz.yml
vendored
@@ -1,8 +1,8 @@
|
||||
name: Release-plz
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
# Set the permissions of the github token to the minimum and only enable what is needed
|
||||
# See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -14,26 +14,38 @@ jobs:
|
||||
# Release unpublished packages.
|
||||
release-plz-release:
|
||||
name: Release-plz release
|
||||
environment: release
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ratatui' }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: rust-lang/crates-io-auth-action@e919bc7605cde86df457cf5b93c5e103838bd879 # v1
|
||||
id: auth
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@v0.5
|
||||
uses: release-plz/action@068d76d2aa32d3c9cd0b1ccdd9ac921e28ba2be9 # v0.5
|
||||
with:
|
||||
command: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
||||
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
|
||||
|
||||
# Create a PR with the new versions and changelog, preparing the next release.
|
||||
release-plz-pr:
|
||||
name: Release-plz PR
|
||||
permissions:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ratatui' }}
|
||||
concurrency:
|
||||
@@ -41,13 +53,16 @@ jobs:
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@v0.5
|
||||
uses: release-plz/action@068d76d2aa32d3c9cd0b1ccdd9ac921e28ba2be9 # v0.5
|
||||
with:
|
||||
command: release-pr
|
||||
env:
|
||||
|
||||
20
.github/workflows/release-stable.yml
vendored
20
.github/workflows/release-stable.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Release stable version
|
||||
|
||||
# Set the permissions of the github token to the minimum and only enable what is needed
|
||||
# See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -13,12 +17,14 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# this possibly needs to be set to true for git-cliff to work - check build results
|
||||
persist-credentials: false
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v4
|
||||
uses: orhun/git-cliff-action@4a4a951bc43fafe41cd2348d181853f52356bee7 # v4
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --latest --strip header
|
||||
@@ -26,7 +32,7 @@ jobs:
|
||||
OUTPUT: BODY.md
|
||||
|
||||
- name: Publish on GitHub
|
||||
uses: ncipollo/release-action@v1
|
||||
uses: ncipollo/release-action@bcfe5470707e8832e12347755757cec0eb3c22af # v1
|
||||
with:
|
||||
prerelease: false
|
||||
bodyFile: BODY.md
|
||||
@@ -36,10 +42,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Publish
|
||||
run: cargo publish --token ${{ secrets.CARGO_TOKEN }}
|
||||
|
||||
26
.github/workflows/zizmor.yml
vendored
Normal file
26
.github/workflows/zizmor.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: GitHub Actions Security Analysis with zizmor 🌈
|
||||
|
||||
# docs https://docs.zizmor.sh/integrations/#github-actions
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Run zizmor 🌈
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@f52a838cfabf134edcbaa7c8b3677dde20045018 # v0.1.1
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ target
|
||||
*.rs.rustfmt
|
||||
.gdb_history
|
||||
.idea/
|
||||
.env
|
||||
|
||||
203
ARCHITECTURE.md
Normal file
203
ARCHITECTURE.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Ratatui Architecture
|
||||
|
||||
This document provides a comprehensive overview of Ratatui's architecture and crate
|
||||
organization, introduced in version 0.30.0.
|
||||
|
||||
## Overview
|
||||
|
||||
Starting with Ratatui 0.30.0, the project was reorganized from a single monolithic crate into
|
||||
a modular workspace consisting of multiple specialized crates. This architectural decision was
|
||||
made to improve modularity, reduce compilation times, enable more flexible dependency
|
||||
management, and provide better API stability for third-party widget libraries.
|
||||
|
||||
## Crate Organization
|
||||
|
||||
The Ratatui project is now organized as a Cargo workspace containing the following crates:
|
||||
|
||||
### Core Crates
|
||||
|
||||
#### `ratatui` (Main Crate)
|
||||
|
||||
- **Purpose**: The main entry point that most applications should use
|
||||
- **Contents**: Re-exports everything from other crates for convenience, plus experimental features
|
||||
- **Target Users**: Application developers building terminal UIs
|
||||
- **Key Features**:
|
||||
- Complete widget ecosystem
|
||||
- Backend implementations
|
||||
- Layout system
|
||||
- Terminal management
|
||||
- Experimental `WidgetRef` and `StatefulWidgetRef` traits
|
||||
|
||||
#### `ratatui-core`
|
||||
|
||||
- **Purpose**: Foundational types and traits for the Ratatui ecosystem
|
||||
- **Contents**: Core widget traits, text types, buffer, layout, style, and symbols
|
||||
- **Target Users**: Widget library authors, minimalist projects
|
||||
- **Key Features**:
|
||||
- `Widget` and `StatefulWidget` traits
|
||||
- Text rendering (`Text`, `Line`, `Span`)
|
||||
- Buffer management
|
||||
- Layout system
|
||||
- Style and color definitions
|
||||
- Symbol collections
|
||||
|
||||
#### `ratatui-widgets`
|
||||
|
||||
- **Purpose**: Built-in widget implementations
|
||||
- **Contents**: All standard widgets like `Block`, `Paragraph`, `List`, `Chart`, etc.
|
||||
- **Target Users**: Applications needing standard widgets, widget library authors
|
||||
- **Key Features**:
|
||||
- Complete set of built-in widgets
|
||||
- Optimized implementations
|
||||
- Comprehensive documentation and examples
|
||||
|
||||
### Backend Crates
|
||||
|
||||
#### `ratatui-crossterm`
|
||||
|
||||
- **Purpose**: Crossterm backend implementation
|
||||
- **Contents**: Cross-platform terminal backend using the `crossterm` crate
|
||||
- **Target Users**: Applications targeting multiple platforms
|
||||
|
||||
#### `ratatui-termion`
|
||||
|
||||
- **Purpose**: Termion backend implementation
|
||||
- **Contents**: Unix-specific terminal backend using the `termion` crate
|
||||
- **Target Users**: Unix-specific applications requiring low-level control
|
||||
|
||||
#### `ratatui-termwiz`
|
||||
|
||||
- **Purpose**: Termwiz backend implementation
|
||||
- **Contents**: Terminal backend using the `termwiz` crate
|
||||
- **Target Users**: Applications needing advanced terminal features
|
||||
|
||||
### Utility Crates
|
||||
|
||||
#### `ratatui-macros`
|
||||
|
||||
- **Purpose**: Declarative macros for common patterns and boilerplate reduction
|
||||
- **Contents**: Macros for common patterns and boilerplate reduction
|
||||
- **Target Users**: Applications and libraries wanting macro support
|
||||
|
||||
## Dependency Relationships
|
||||
|
||||
```text
|
||||
ratatui
|
||||
├── ratatui-core
|
||||
├── ratatui-widgets → ratatui-core
|
||||
├── ratatui-crossterm → ratatui-core
|
||||
├── ratatui-termion → ratatui-core
|
||||
├── ratatui-termwiz → ratatui-core
|
||||
└── ratatui-macros
|
||||
```
|
||||
|
||||
### Key Dependencies
|
||||
|
||||
- **ratatui-core**: Foundation for all other crates
|
||||
- **ratatui-widgets**: Depends on `ratatui-core` for widget traits and types
|
||||
- **Backend crates**: Each depends on `ratatui-core` for backend traits and types
|
||||
- **ratatui**: Depends on all other crates and re-exports their public APIs
|
||||
|
||||
## Design Principles
|
||||
|
||||
### Stability and Compatibility
|
||||
|
||||
The modular architecture provides different levels of API stability:
|
||||
|
||||
- **ratatui-core**: Designed for maximum stability to minimize breaking changes for widget
|
||||
libraries
|
||||
- **ratatui-widgets**: Focused on widget implementations with moderate stability requirements
|
||||
- **Backend crates**: Isolated from core changes, allowing backend-specific updates
|
||||
- **ratatui**: Main crate that can evolve more freely while maintaining backward compatibility
|
||||
through re-exports
|
||||
|
||||
### Compilation Performance
|
||||
|
||||
The split architecture enables:
|
||||
|
||||
- **Reduced compilation times**: Widget libraries only need to compile core types
|
||||
- **Parallel compilation**: Different crates can be compiled in parallel
|
||||
- **Selective compilation**: Applications can exclude unused backends or widgets
|
||||
|
||||
### Ecosystem Benefits
|
||||
|
||||
- **Widget Library Authors**: Can depend on stable `ratatui-core` without frequent updates
|
||||
- **Application Developers**: Can use the convenient `ratatui` crate with everything included
|
||||
- **Minimalist Projects**: Can use only `ratatui-core` for lightweight applications
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### For Application Developers
|
||||
|
||||
Most applications should continue using the main `ratatui` crate with minimal changes:
|
||||
|
||||
```rust
|
||||
// No changes needed - everything is re-exported
|
||||
use ratatui::{
|
||||
widgets::{Block, Paragraph},
|
||||
layout::{Layout, Constraint},
|
||||
Terminal,
|
||||
};
|
||||
```
|
||||
|
||||
### For Widget Library Authors
|
||||
|
||||
Consider migrating to `ratatui-core` for better stability:
|
||||
|
||||
```rust
|
||||
// Before (0.29.x and earlier)
|
||||
use ratatui::{
|
||||
widgets::{Widget, StatefulWidget},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
};
|
||||
|
||||
// After (0.30.0+)
|
||||
use ratatui_core::{
|
||||
widgets::{Widget, StatefulWidget},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
};
|
||||
```
|
||||
|
||||
### Backwards Compatibility
|
||||
|
||||
All existing code using the `ratatui` crate will continue to work unchanged, as the main crate
|
||||
re-exports all public APIs from the specialized crates.
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Potential Enhancements
|
||||
|
||||
- **Widget-specific crates**: Further split widgets into individual crates for even more
|
||||
granular dependencies
|
||||
- **Plugin system**: Enable dynamic widget loading and third-party widget ecosystems
|
||||
- **Feature flags**: More granular feature flags for compile-time customization
|
||||
|
||||
### Version Synchronization
|
||||
|
||||
Currently, all crates are versioned together for simplicity. Future versions may adopt
|
||||
independent versioning once the API stabilizes further.
|
||||
|
||||
## Related Issues and PRs
|
||||
|
||||
This architecture was developed through extensive discussion and implementation across multiple
|
||||
PRs:
|
||||
|
||||
- [Issue #1388](https://github.com/ratatui/ratatui/issues/1388): Original RFC for modularization
|
||||
- [PR #1459](https://github.com/ratatui/ratatui/pull/1459): Move ratatui crate into workspace
|
||||
folder
|
||||
- [PR #1460](https://github.com/ratatui/ratatui/pull/1460): Move core types to ratatui-core
|
||||
- [PR #1474](https://github.com/ratatui/ratatui/pull/1474): Move widgets into ratatui-widgets
|
||||
crate
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing to the Ratatui project, please consider:
|
||||
|
||||
- **Core changes**: Submit PRs against `ratatui-core` for fundamental improvements
|
||||
- **Widget changes**: Submit PRs against `ratatui-widgets` for widget-specific improvements
|
||||
- **Backend changes**: Submit PRs against the appropriate backend crate
|
||||
- **Integration changes**: Submit PRs against the main `ratatui` crate
|
||||
|
||||
See the [CONTRIBUTING.md](CONTRIBUTING.md) guide for more details on the contribution process.
|
||||
@@ -11,18 +11,25 @@ GitHub with a [breaking change] label.
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [v0.30.0 Unreleased](#v0300-unreleased)
|
||||
- `Flex::SpaceAround` now mirrors flexbox: space between items is twice the size of the outer gaps
|
||||
are twice the size of first and last elements
|
||||
- `block::Title` no longer exists
|
||||
- The `From` impls for backend types are now replaced with more specific traits
|
||||
- `FrameExt` trait for `unstable-widget-ref` feature
|
||||
- `List::highlight_symbol` now accepts `Into<Line>` instead of `&str`
|
||||
- 'layout::Alignment' is renamed to 'layout::HorizontalAlignment'
|
||||
- The MSRV is now 1.81.0
|
||||
- The MSRV is now 1.85.0
|
||||
- `Backend` now requires an associated `Error` type and `clear_region` method
|
||||
- `TestBackend` now uses `core::convert::Infallible` for error handling instead of `std::io::Error`
|
||||
- Disabling `default-features` will now disable layout cache, which can have a negative impact on performance
|
||||
- `Layout::init_cache` and `Layout::DEFAULT_CACHE_SIZE` are now only available if `layout-cache` feature is enabled
|
||||
- Disabling `default-features` suppresses the error message if `show_cursor()` fails when dropping `Terminal`
|
||||
- `Layout::init_cache` and `Layout::DEFAULT_CACHE_SIZE` are now only available if `layout-cache`
|
||||
feature is enabled
|
||||
- Disabling `default-features` suppresses the error message if `show_cursor()` fails when dropping
|
||||
`Terminal`
|
||||
- Support a broader range for `unicode-width` version
|
||||
- [v0.29.0](#v0290)
|
||||
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
|
||||
- `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`
|
||||
@@ -85,6 +92,60 @@ This is a quick summary of the sections below:
|
||||
|
||||
## v0.30.0 Unreleased
|
||||
|
||||
### `Flex::SpaceAround` now mirrors flexbox: space between items is twice the size of the outer gaps ([#1952])
|
||||
|
||||
[#1952]: https://github.com/ratatui/ratatui/pull/1952
|
||||
|
||||
The old `Flex::SpaceAround` behavior has been changed to distribute space evenly around each
|
||||
element, with the middle spacers being twice the size of the first and last one. The old
|
||||
behavior can be achieved by using `Flex::SpaceEvenly` instead.
|
||||
|
||||
```diff
|
||||
- let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::SpaceAround).split(area);
|
||||
+ let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::SpaceEvenly).split(area);
|
||||
```
|
||||
|
||||
### `block::Title` no longer exists ([#1926])
|
||||
|
||||
[#1926]: https://github.com/ratatui/ratatui/pull/1926
|
||||
|
||||
The title alignment is better expressed in the `Line` as this fits more coherently with the rest of
|
||||
the library.
|
||||
|
||||
- `widgets::block` is no longer exported
|
||||
- `widgets::block::Title` no longer exists
|
||||
- `widgets::block::Position` is now `widgets::TitlePosition`
|
||||
- `Block::title()` now accepts `Into::<Line>` instead of `Into<Title>`
|
||||
- `BlockExt` is now exported at widgets::`BlockExt` instead of `widgets::block::BlockExt`
|
||||
|
||||
```diff
|
||||
- use ratatui::widgets::{Block, block::{Title, Position}};
|
||||
+ use ratatui::widgets::{Block, TitlePosition};
|
||||
|
||||
let block = Block::default()
|
||||
- .title(Title::from("Hello"))
|
||||
- .title(Title::from("Hello").position(Position::Bottom).alignment(Alignment::Center))
|
||||
- .title_position(Position::Bottom);
|
||||
+ .title(Line::from("Hello"))
|
||||
+ .title_bottom(Line::from("Hello").centered());
|
||||
+ .title_position(TitlePosition::Bottom);
|
||||
|
||||
- use ratatui::widgets::block::BlockExt;
|
||||
+ use ratatui::widgets::BlockExt;
|
||||
|
||||
struct MyWidget {
|
||||
block: Option<Block>,
|
||||
}
|
||||
|
||||
impl Widget for &MyWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.block.as_ref().render(area, buf);
|
||||
let area = self.block.inner_if_some();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `Style` no longer implements `Styled` ([#1572])
|
||||
|
||||
[#1572]: https://github.com/ratatui/ratatui/pull/1572
|
||||
@@ -148,12 +209,37 @@ and implement the `clear_region` method, which no longer has a default implement
|
||||
This change was made to provide greater flexibility for custom backends, particularly to remove the
|
||||
explicit dependency on `std::io` for backends that want to support `no_std` targets.
|
||||
|
||||
### The MSRV is now 1.81.0 ([#1786])
|
||||
If your app or library uses the `Backend` trait directly - for example, by providing a generic
|
||||
implementation for many backends - you may need to update the referenced error type.
|
||||
|
||||
[#1786]: https://github.com/ratatui/ratatui/pull/1786
|
||||
```diff
|
||||
- fn run<B: Backend>(mut terminal: Terminal<B>) -> io::Result<()> {
|
||||
+ fn run<B: Backend>(mut terminal: Terminal<B>) -> Result<(), B::Error> {
|
||||
```
|
||||
|
||||
The minimum supported Rust version (MSRV) is now 1.81.0. This is due to the use of `#[expect]` in
|
||||
the codebase, which is only available in Rust 1.81.0 and later.
|
||||
Alternatively, you can explicitly require the associated error to be `std::io::Error`. This approach
|
||||
may require fewer changes in user code but is generally not recommended, as it limits compatibility
|
||||
with third-party backends. Additionally, the error type used by built-in backends may or may not
|
||||
change in the future, making this approach less future-proof compared to the previous one.
|
||||
|
||||
```diff
|
||||
- fn run<B: Backend>(mut terminal: Terminal<B>) -> io::Result<()> {
|
||||
+ fn run<B: Backend<Error = io::Error>>(mut terminal: Terminal<B>) -> io::Result<()> {
|
||||
```
|
||||
|
||||
If your application uses a concrete backend implementation, prefer specifying it explicitly
|
||||
instead.
|
||||
|
||||
```diff
|
||||
- fn run<B: Backend>(mut terminal: Terminal<B>) -> io::Result<()> {
|
||||
+ fn run(mut terminal: DefaultTerminal) -> io::Result<()> {
|
||||
```
|
||||
|
||||
### The MSRV is now 1.85.0 ([#1860])
|
||||
|
||||
[#1860]: https://github.com/ratatui/ratatui/pull/1860
|
||||
|
||||
The minimum supported Rust version (MSRV) is now 1.85.0.
|
||||
|
||||
### `layout::Alignment` is renamed to `layout::HorizontalAlignment` ([#1735])
|
||||
|
||||
@@ -292,6 +378,17 @@ The `termwiz` backend is upgraded from 0.22.0 to 0.23.0.
|
||||
This release has a few fixes for hyperlinks and input handling, plus some dependency updates.
|
||||
See the [commits](https://github.com/wezterm/wezterm/commits/main/termwiz) for more details.
|
||||
|
||||
### Support a broader range for `unicode-width` version ([#1999])
|
||||
|
||||
[#1999]: https://github.com/ratatui/ratatui/pull/1999
|
||||
|
||||
Ratatui's dependency on `unicode-width`, previously pinned to 0.2.0, has
|
||||
expanded to allow version 0.2.1. This comes with 2 behavior changes described in
|
||||
[unicode-width#61] and [unicode-width#74].
|
||||
|
||||
[unicode-width#61]: https://github.com/unicode-rs/unicode-width/pull/61
|
||||
[unicode-width#74]: https://github.com/unicode-rs/unicode-width/pull/74
|
||||
|
||||
## [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])
|
||||
|
||||
1553
CHANGELOG.md
1553
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -22,13 +22,33 @@ on the crate or its users should ideally be discussed in a "Feature Request" iss
|
||||
|
||||
### 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
|
||||
Try to do one pull request per change. The time taken to review a PR grows exponentially with the size
|
||||
of the change. Small focused PRs will generally be much 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.
|
||||
|
||||
Guidelines for PR size:
|
||||
|
||||
- Aim for PRs under 500 lines of changes when possible.
|
||||
- Split large features into incremental PRs that build on each other.
|
||||
- Separate refactoring, formatting, and functional changes into different PRs.
|
||||
- If a large PR is unavoidable, clearly explain why in the PR description.
|
||||
|
||||
### Breaking changes and backwards compatibility
|
||||
|
||||
We prioritize maintaining backwards compatibility and minimizing disruption to users:
|
||||
|
||||
- **Prefer deprecation over removal**: Add deprecation warnings rather than immediately removing
|
||||
public APIs
|
||||
- **Provide migration paths**: Include clear upgrade instructions for any breaking changes
|
||||
- **Follow our deprecation policy**: Wait at least two versions before removing deprecated items
|
||||
- **Consider feature flags**: Use feature flags for experimental or potentially disruptive changes
|
||||
- **Document breaking changes**: Clearly mark breaking changes in commit messages and PR descriptions
|
||||
|
||||
See our [deprecation notice policy](#deprecation-notice) for more details.
|
||||
|
||||
### Code formatting
|
||||
|
||||
Run `cargo xtask format` before committing to ensure that code is consistently formatted with
|
||||
@@ -41,6 +61,10 @@ to be installed when running rustfmt. You can install the nightly version of Rus
|
||||
rustup install nightly
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Do not modify formatting configuration (`rustfmt.toml`, `.clippy.toml`) without
|
||||
> prior discussion. These changes affect all contributors and should be carefully considered.
|
||||
|
||||
### Search `tui-rs` for similar work
|
||||
|
||||
The original fork of Ratatui, [`tui-rs`](https://github.com/fdehau/tui-rs/), has a large amount of
|
||||
@@ -67,6 +91,15 @@ Running `cargo xtask ci` before pushing will perform the same checks that we do
|
||||
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.
|
||||
|
||||
Available xtask commands:
|
||||
|
||||
- `cargo xtask ci` - Run all CI checks
|
||||
- `cargo xtask format` - Format code
|
||||
- `cargo xtask lint` - Run linting checks
|
||||
- `cargo xtask test` - Run all tests
|
||||
|
||||
Run `cargo xtask --help` to see all available commands.
|
||||
|
||||
### Sign your commits
|
||||
|
||||
We use commit signature verification, which will block commits from being merged via the UI unless
|
||||
@@ -74,6 +107,17 @@ they are signed. To set up your machine to sign commits, see [managing commit si
|
||||
verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification)
|
||||
in GitHub docs.
|
||||
|
||||
### Configuration and build system changes
|
||||
|
||||
Changes to project configuration files require special consideration:
|
||||
|
||||
- Linting configuration (`.clippy.toml`, `rustfmt.toml`): Affects all contributors.
|
||||
- CI configuration (`.github/workflows/`): Affects build and deployment.
|
||||
- Build system (`xtask/`, `Cargo.toml` workspace config): Affects development workflow.
|
||||
- Dependencies: Consider MSRV compatibility and licensing.
|
||||
|
||||
Please discuss these changes in an issue before implementing them.
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Setup
|
||||
@@ -91,6 +135,20 @@ cd ratatui
|
||||
cargo xtask build
|
||||
```
|
||||
|
||||
### Architecture
|
||||
|
||||
For an understanding of the crate organization and design decisions, see [ARCHITECTURE.md]. This
|
||||
document explains the modular workspace structure introduced in version 0.30.0 and provides
|
||||
guidance on which crate to use for different use cases.
|
||||
|
||||
When making changes, consider:
|
||||
|
||||
- Which crate should contain your changes per the modular structure,
|
||||
- Whether your changes affect the public API of `ratatui-core` (requires extra care),
|
||||
- And how your changes fit into the overall architecture.
|
||||
|
||||
[ARCHITECTURE.md]: https://github.com/ratatui/ratatui/blob/main/ARCHITECTURE.md
|
||||
|
||||
### Tests
|
||||
|
||||
The [test coverage](https://app.codecov.io/gh/ratatui/ratatui) of the crate is reasonably
|
||||
@@ -107,6 +165,10 @@ If an area that you're making a change in is not tested, write tests to characte
|
||||
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`).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Do not remove existing tests without clear justification. If tests need to be
|
||||
> modified due to API changes, explain why in your PR description.
|
||||
|
||||
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
|
||||
@@ -174,6 +236,14 @@ We generally want to wait at least two versions before removing deprecated items
|
||||
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.
|
||||
|
||||
Deprecation process:
|
||||
|
||||
1. Add `#[deprecated]` attribute with a clear message.
|
||||
2. Update documentation to point to the replacement.
|
||||
3. Add an entry to `BREAKING-CHANGES.md` if applicable.
|
||||
4. Wait at least two versions before removal.
|
||||
5. Consider the impact on the ecosystem before removing.
|
||||
|
||||
### 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
|
||||
|
||||
673
Cargo.lock
generated
673
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["ratatui", "ratatui-*", "xtask", "examples/apps/*"]
|
||||
members = ["ratatui", "ratatui-*", "xtask", "examples/apps/*", "examples/concepts/*"]
|
||||
default-members = [
|
||||
"ratatui",
|
||||
"ratatui-core",
|
||||
@@ -11,6 +11,7 @@ default-members = [
|
||||
"ratatui-termwiz",
|
||||
"ratatui-widgets",
|
||||
"examples/apps/*",
|
||||
"examples/concepts/*",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -40,20 +41,20 @@ hashbrown = "0.15"
|
||||
indoc = "2"
|
||||
instability = "0.3"
|
||||
itertools = { version = "0.14", default-features = false, features = ["use_alloc"] }
|
||||
kasuari = { version = "0.4", default-features = false }
|
||||
kasuari = { git = "https://github.com/j-g00da/kasuari.git", branch = "js/cycle-prevention", default-features = false }
|
||||
line-clipping = "0.3"
|
||||
lru = "0.14"
|
||||
palette = "0.7"
|
||||
pretty_assertions = "1"
|
||||
rand = "0.9"
|
||||
rand_chacha = "0.9"
|
||||
ratatui = { path = "ratatui", version = "0.30.0-alpha.4" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.5" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.4" }
|
||||
ratatui-macros = { path = "ratatui-macros", version = "0.7.0-alpha.3" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.4" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.4" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.4" }
|
||||
ratatui = { path = "ratatui", version = "0.30.0-alpha.5" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.6" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.5" }
|
||||
ratatui-macros = { path = "ratatui-macros", version = "0.7.0-alpha.4" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.5" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.5" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.5" }
|
||||
rstest = "0.25"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -70,7 +71,7 @@ trybuild = "1"
|
||||
unicode-segmentation = "1"
|
||||
unicode-truncate = { version = "2", default-features = false }
|
||||
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
|
||||
unicode-width = "=0.2.0"
|
||||
unicode-width = ">=0.2.0, <=0.2.1"
|
||||
|
||||
# Improve benchmark consistency
|
||||
[profile.bench]
|
||||
|
||||
10
README.md
10
README.md
@@ -73,6 +73,7 @@ fn render(frame: &mut Frame) {
|
||||
- [Ratatui Forum] - a place to ask questions and discuss the library.
|
||||
- [Widget Examples] - a collection of examples that demonstrate how to use the library.
|
||||
- [App Examples] - a collection of more complex examples that demonstrate how to build apps.
|
||||
- [ARCHITECTURE.md] - explains the crate organization and modular workspace structure.
|
||||
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
- [Breaking Changes] - a list of breaking changes in the library.
|
||||
|
||||
@@ -114,6 +115,14 @@ We rely on GitHub for [bugs][Report a bug] and [feature requests][Request a Feat
|
||||
Please make sure you read the [contributing](./CONTRIBUTING.md) guidelines before [creating a pull
|
||||
request][Create a Pull Request].
|
||||
|
||||
If you'd like to show your support, you can add the Ratatui badge to your project's README:
|
||||
|
||||
```md
|
||||
[](https://ratatui.rs/)
|
||||
```
|
||||
|
||||
[](https://ratatui.rs/)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development. None of
|
||||
@@ -133,6 +142,7 @@ This project is licensed under the [MIT License][License].
|
||||
[Docs]: https://docs.rs/ratatui
|
||||
[Widget Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui-widgets/examples
|
||||
[App Examples]: https://github.com/ratatui/ratatui/tree/main/examples
|
||||
[ARCHITECTURE.md]: https://github.com/ratatui/ratatui/blob/main/ARCHITECTURE.md
|
||||
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
|
||||
[git-cliff]: https://git-cliff.org
|
||||
[Conventional Commits]: https://www.conventionalcommits.org
|
||||
|
||||
1
assets/logo-simple.svg
Normal file
1
assets/logo-simple.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><title>Ratatui</title><path d="M17 29h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1H5v-1H4v-1H3v-1H2v-1H1v-1H0v-2h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h2zm-2 14h1v1h1v1h1v-2h-1v-1h-1v-1h-1zm0-6h-3v1h1v1h4v-4h-1v-1h-1zm35-21h-1v4h-1v1h-1v1h-1v1h-2v1h-1v1h-1v1h-1v1h-2v9h5v1h1v9h-1v1h-1v2h-1v1h-1v-1h-1v-1h-1v-1h1v-1h1v-1h1v-5h-1v1h-3v1h-1v3h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-2h1v-2h1v-1h-2v1h-4v-1h-1v-1h-1v-1h-1v-2h1v-1h1v-1h2v-1h7v-1h1v-1h1v-1h1v-1h1v-1h1v-1h4v-1h3v-1h10zm-17 2h-1v2h1v1h2v-1h1v-2h-1v-1h-2zM29 1h1v9h1v1h1v2h1v2h-1v1h-1v1h-1v1h-1v1h-1v1h-3v-1h-1v-1h-2v-1h-1v-1h-1v-1h-6v-1h-1V9h1V8h1V7h1V6h1V5h1V4h1V3h1V2h1V1h2V0h6z"/></svg>
|
||||
|
After Width: | Height: | Size: 831 B |
1
assets/logo.svg
Normal file
1
assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><title>Ratatui</title><path d="M50 16h-1v4h-1v1h-1v1h-1v1h-2v1h-1v1h-1v1h-1v1h-2v9h5v1h1v9h-1v1h-1v2h-1v1h-1v-1h-1v1h-2v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-2v1h-1v1h-1v1h-1v1h-1v1h-1v1h-1v1H9v1H8v1H7v1H6v1H5v1H4v1H3v1H2v2h1v1h1v1h1v1h1v1h1v1H5v-1H4v-1H3v-1H2v-1H1v-1H0v-2h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h2v1h1v-1h-1v-1h-1v-2h1v-1h1v-1h2v-1h7v-1h1v-1h1v-1h1v-1h1v-1h1v-1h4v-1h3v-1h10zM39 49h1v-1h-1zm2-8h-3v1h-1v3h-1v-1h-1v1h1v1h1v1h1v1h1v-1h1v-1h1v-1h1v-5h-1zm-7 3h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-5-5h1v1h1v1h1v1h1v1h1v-2h1v-2h1v-1h-2v1h-4v-1h-1zm14-11h-1v2h1v1h2v-1h1v-2h-1v-1h-2zM18 40h1v1h1v2h-1v-1h-1v-1h-1v-2h1zm0-7h1v4h-4v-1h-1v-1h3v-3h1zM29 1h1v9h1v1h1v2h1v2h-1v1h-1v1h-1v1h-1v1h-1v1h-3v-1h-1v-1h-2v-1h-1v-1h-1v-1h-6v-1h-1V9h1V8h1V7h1V6h1V5h1V4h1V3h1V2h1V1h2V0h6z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
18
bacon.toml
18
bacon.toml
@@ -60,6 +60,22 @@ command = ["cargo", "xtask", "hack"]
|
||||
[jobs.format]
|
||||
command = ["cargo", "xtask", "format"]
|
||||
|
||||
[jobs.zizmor-offline]
|
||||
# zizmor checks the workflow files for security issues. The offline version is generally faster, but
|
||||
# checks for fewer issues.
|
||||
command = ["zizmor", "--color", "always", ".github/workflows", "--offline"]
|
||||
need_stdout = true
|
||||
default_watch = false
|
||||
watch = [".github/workflows/"]
|
||||
|
||||
[jobs.zizmor-online]
|
||||
# zizmor checks the workflow files for security issues. The online version is a bit slower, but it
|
||||
# checks for more issues
|
||||
command = ["zizmor", "--color", "always", ".github/workflows"]
|
||||
need_stdout = true
|
||||
default_watch = false
|
||||
watch = [".github/workflows/"]
|
||||
|
||||
# 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.)
|
||||
@@ -74,3 +90,5 @@ ctrl-v = "job:coverage-unit-tests-only"
|
||||
u = "job:test-unit"
|
||||
n = "job:nextest"
|
||||
f = "job:format"
|
||||
z = "job:zizmor-offline"
|
||||
shift-z = "job:zizmor-online"
|
||||
|
||||
@@ -115,6 +115,7 @@ commit_parsers = [
|
||||
{ message = "^chore\\(changelog\\)", skip = true },
|
||||
{ message = "^[cC]hore", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^build\\(deps\\)", skip = true },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
|
||||
@@ -9,7 +9,6 @@ allow = [
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
|
||||
@@ -21,10 +21,7 @@ use ratatui::widgets::{Widget, WidgetRef};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -36,15 +33,15 @@ struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
while !self.should_quit {
|
||||
self.draw(&mut terminal)?;
|
||||
self.render(terminal)?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self, tui: &mut DefaultTerminal) -> Result<()> {
|
||||
fn render(&mut self, tui: &mut DefaultTerminal) -> Result<()> {
|
||||
tui.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -71,7 +68,7 @@ impl App {
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let constraints = Constraint::from_lengths([1, 1, 2, 1]);
|
||||
let [greeting, timer, squares, position] = Layout::vertical(constraints).areas(area);
|
||||
let [greeting, timer, squares, position] = area.layout(&Layout::vertical(constraints));
|
||||
|
||||
// render an ephemeral greeting widget
|
||||
Greeting::new("Ratatui!").render(greeting, buf);
|
||||
@@ -177,9 +174,9 @@ struct BlueSquare;
|
||||
impl Widget for &BoxedSquares {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let constraints = vec![Constraint::Length(4); self.squares.len()];
|
||||
let areas = Layout::horizontal(constraints).split(area);
|
||||
for (widget, area) in self.squares.iter().zip(areas.iter()) {
|
||||
widget.render_ref(*area, buf);
|
||||
let areas = area.layout_vec(&Layout::horizontal(constraints));
|
||||
for (widget, area) in self.squares.iter().zip(areas) {
|
||||
widget.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.6.3"
|
||||
color-eyre = "0.6.5"
|
||||
crossterm = { workspace = true, features = ["event-stream"] }
|
||||
octocrab = "0.44.0"
|
||||
ratatui.workspace = true
|
||||
tokio = { version = "1.44.2", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.46.1", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1.17"
|
||||
|
||||
@@ -78,8 +78,8 @@ impl App {
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
|
||||
let [title_area, body_area] = vertical.areas(frame.area());
|
||||
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
|
||||
let [title_area, body_area] = frame.area().layout(&layout);
|
||||
let title = Line::from("Ratatui async example").centered().bold();
|
||||
frame.render_widget(title, title_area);
|
||||
frame.render_widget(&self.pull_requests, body_area);
|
||||
|
||||
@@ -22,21 +22,18 @@ use time::{Date, Month, OffsetDateTime};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
ratatui::run(run)
|
||||
}
|
||||
|
||||
/// Run the application.
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
let mut selected_date = OffsetDateTime::now_local()?.date();
|
||||
let mut calendar_style = StyledCalendar::Default;
|
||||
loop {
|
||||
terminal.draw(|frame| render(frame, calendar_style, selected_date))?;
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
||||
KeyCode::Char('q') => break Ok(()),
|
||||
KeyCode::Char('s') => calendar_style = calendar_style.next(),
|
||||
KeyCode::Char('n') | KeyCode::Tab => selected_date = next_month(selected_date),
|
||||
KeyCode::Char('p') | KeyCode::BackTab => selected_date = prev_month(selected_date),
|
||||
@@ -84,11 +81,10 @@ fn render(frame: &mut Frame, calendar_style: StyledCalendar, selected_date: Date
|
||||
)),
|
||||
]);
|
||||
|
||||
let vertical = Layout::vertical([
|
||||
let [text_area, area] = frame.area().layout(&Layout::vertical([
|
||||
Constraint::Length(header.height() as u16),
|
||||
Constraint::Fill(1),
|
||||
]);
|
||||
let [text_area, area] = vertical.areas(frame.area());
|
||||
]));
|
||||
frame.render_widget(header.centered(), text_area);
|
||||
calendar_style
|
||||
.render_year(frame, area, selected_date)
|
||||
@@ -136,16 +132,13 @@ impl StyledCalendar {
|
||||
fn render_year(self, frame: &mut Frame, area: Rect, date: Date) -> Result<()> {
|
||||
let events = events(date)?;
|
||||
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 1,
|
||||
});
|
||||
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area);
|
||||
let areas = rows.iter().flat_map(|row| {
|
||||
Layout::horizontal([Constraint::Ratio(1, 4); 4])
|
||||
.split(*row)
|
||||
.to_vec()
|
||||
});
|
||||
let vertical = Layout::vertical([Constraint::Ratio(1, 3); 3]);
|
||||
let horizontal = &Layout::horizontal([Constraint::Ratio(1, 4); 4]);
|
||||
let areas = area
|
||||
.inner(Margin::new(1, 1))
|
||||
.layout_vec(&vertical)
|
||||
.into_iter()
|
||||
.flat_map(|row| row.layout_vec(horizontal));
|
||||
for (i, area) in areas.enumerate() {
|
||||
let month = date
|
||||
.replace_day(1)
|
||||
|
||||
@@ -153,16 +153,15 @@ impl App {
|
||||
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(header.height() as u16),
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(1),
|
||||
]);
|
||||
let [text_area, up, down] = vertical.areas(frame.area());
|
||||
let [text_area, up, down] = frame.area().layout(&vertical);
|
||||
frame.render_widget(header.centered(), text_area);
|
||||
|
||||
let horizontal =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [draw, pong] = horizontal.areas(up);
|
||||
let [map, boxes] = horizontal.areas(down);
|
||||
let horizontal = Layout::horizontal([Constraint::Fill(1); 2]);
|
||||
let [draw, pong] = up.layout(&horizontal);
|
||||
let [map, boxes] = down.layout(&horizontal);
|
||||
|
||||
frame.render_widget(self.map_canvas(), map);
|
||||
frame.render_widget(self.draw_canvas(draw), draw);
|
||||
|
||||
@@ -21,10 +21,7 @@ use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
ratatui::run(|terminal| App::new().run(terminal))
|
||||
}
|
||||
|
||||
struct App {
|
||||
@@ -78,7 +75,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
@@ -111,10 +108,11 @@ impl App {
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area());
|
||||
let [animated_chart, bar_chart] =
|
||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
|
||||
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
|
||||
let vertical = Layout::vertical([Constraint::Fill(1); 2]);
|
||||
let [top, bottom] = frame.area().layout(&vertical);
|
||||
let horizontal = Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]);
|
||||
let [animated_chart, bar_chart] = top.layout(&horizontal);
|
||||
let [line_chart, scatter] = bottom.layout(&Layout::horizontal([Constraint::Fill(1); 2]));
|
||||
|
||||
self.render_animated_chart(frame, animated_chart);
|
||||
render_barchart(frame, bar_chart);
|
||||
|
||||
@@ -11,40 +11,35 @@
|
||||
use color_eyre::Result;
|
||||
use crossterm::event;
|
||||
use itertools::Itertools;
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
return Ok(());
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
let layout = Layout::vertical([
|
||||
let [named, indexed_colors, indexed_greys] = Layout::vertical([
|
||||
Constraint::Length(30),
|
||||
Constraint::Length(17),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(frame.area());
|
||||
.areas(frame.area());
|
||||
|
||||
render_named_colors(frame, layout[0]);
|
||||
render_indexed_colors(frame, layout[1]);
|
||||
render_indexed_grayscale(frame, layout[2]);
|
||||
render_named_colors(frame, named);
|
||||
render_indexed_colors(frame, indexed_colors);
|
||||
render_indexed_grayscale(frame, indexed_greys);
|
||||
}
|
||||
|
||||
const NAMED_COLORS: [Color; 16] = [
|
||||
@@ -87,12 +82,12 @@ fn render_fg_named_colors(frame: &mut Frame, bg: Color, area: Rect) {
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
|
||||
let areas = vertical.iter().flat_map(|area| {
|
||||
Layout::horizontal([Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
});
|
||||
let vertical = Layout::vertical([Constraint::Length(1); 2]);
|
||||
let horizontal = Layout::horizontal([Constraint::Ratio(1, 8); 8]);
|
||||
let areas = inner
|
||||
.layout_vec(&vertical)
|
||||
.into_iter()
|
||||
.flat_map(|area| area.layout_vec(&horizontal));
|
||||
for (fg, area) in NAMED_COLORS.into_iter().zip(areas) {
|
||||
let color_name = fg.to_string();
|
||||
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
|
||||
@@ -105,12 +100,12 @@ fn render_bg_named_colors(frame: &mut Frame, fg: Color, area: Rect) {
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
|
||||
let areas = vertical.iter().flat_map(|area| {
|
||||
Layout::horizontal([Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
});
|
||||
let vertical = Layout::vertical([Constraint::Length(1); 2]);
|
||||
let horizontal = Layout::horizontal([Constraint::Ratio(1, 8); 8]);
|
||||
let areas = inner
|
||||
.layout_vec(&vertical)
|
||||
.into_iter()
|
||||
.flat_map(|area| area.layout_vec(&horizontal));
|
||||
for (bg, area) in NAMED_COLORS.into_iter().zip(areas) {
|
||||
let color_name = bg.to_string();
|
||||
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
|
||||
|
||||
@@ -31,10 +31,7 @@ use ratatui::widgets::Widget;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -91,7 +88,7 @@ impl App {
|
||||
/// Run the app
|
||||
///
|
||||
/// This is the main event loop for the app.
|
||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
pub fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
while self.is_running() {
|
||||
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
@@ -127,8 +124,8 @@ impl App {
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min};
|
||||
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
|
||||
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
|
||||
let [top, colors] = area.layout(&Layout::vertical([Length(1), Min(0)]));
|
||||
let [title, fps] = top.layout(&Layout::horizontal([Min(0), Length(8)]));
|
||||
Text::from("colors_rgb example. Press q to quit")
|
||||
.centered()
|
||||
.render(title, buf);
|
||||
|
||||
@@ -24,10 +24,7 @@ use strum::{Display, EnumIter, FromRepr};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -82,7 +79,7 @@ struct SpacerBlock;
|
||||
|
||||
// App behaviour
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
self.insert_test_defaults();
|
||||
|
||||
while self.is_running() {
|
||||
@@ -245,14 +242,13 @@ impl Widget for &App {
|
||||
swap_legend_area,
|
||||
_,
|
||||
blocks_area,
|
||||
] = Layout::vertical([
|
||||
] = area.layout(&Layout::vertical([
|
||||
Length(2), // header
|
||||
Length(2), // instructions
|
||||
Length(1), // swap key legend
|
||||
Length(1), // gap
|
||||
Fill(1), // blocks
|
||||
])
|
||||
.areas(area);
|
||||
]));
|
||||
|
||||
App::header().render(header_area, buf);
|
||||
App::instructions().render(instructions_area, buf);
|
||||
@@ -322,20 +318,26 @@ impl App {
|
||||
}
|
||||
|
||||
fn render_layout_blocks(&self, area: Rect, buf: &mut Buffer) {
|
||||
let [user_constraints, area] = Layout::vertical([Length(3), Fill(1)])
|
||||
.spacing(1)
|
||||
.areas(area);
|
||||
let main_layout = Layout::vertical([Length(3), Fill(1)]).spacing(1);
|
||||
let [user_constraints, area] = area.layout(&main_layout);
|
||||
|
||||
self.render_user_constraints_legend(user_constraints, buf);
|
||||
|
||||
let [start, center, end, space_around, space_between] =
|
||||
Layout::vertical([Length(7); 5]).areas(area);
|
||||
let [
|
||||
start,
|
||||
center,
|
||||
end,
|
||||
space_between,
|
||||
space_around,
|
||||
space_evenly,
|
||||
] = area.layout(&Layout::vertical([Length(7); 6]));
|
||||
|
||||
self.render_layout_block(Flex::Start, start, buf);
|
||||
self.render_layout_block(Flex::Center, center, buf);
|
||||
self.render_layout_block(Flex::End, end, buf);
|
||||
self.render_layout_block(Flex::SpaceAround, space_around, buf);
|
||||
self.render_layout_block(Flex::SpaceBetween, space_between, buf);
|
||||
self.render_layout_block(Flex::SpaceAround, space_around, buf);
|
||||
self.render_layout_block(Flex::SpaceEvenly, space_evenly, buf);
|
||||
}
|
||||
|
||||
fn render_user_constraints_legend(&self, area: Rect, buf: &mut Buffer) {
|
||||
@@ -349,8 +351,8 @@ impl App {
|
||||
}
|
||||
|
||||
fn render_layout_block(&self, flex: Flex, area: Rect, buf: &mut Buffer) {
|
||||
let [label_area, axis_area, blocks_area] =
|
||||
Layout::vertical([Length(1), Max(1), Length(4)]).areas(area);
|
||||
let layout = Layout::vertical([Length(1), Max(1), Length(4)]);
|
||||
let [label_area, axis_area, blocks_area] = area.layout(&layout);
|
||||
|
||||
if label_area.height > 0 {
|
||||
format!("Flex::{flex:?}").bold().render(label_area, buf);
|
||||
|
||||
@@ -36,10 +36,7 @@ const FILL_COLOR: Color = tailwind::SLATE.c950;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
@@ -72,7 +69,7 @@ enum AppState {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
self.update_max_scroll_offset();
|
||||
while self.is_running() {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
@@ -143,7 +140,7 @@ impl App {
|
||||
|
||||
impl Widget for App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let [tabs, axis, demo] = Layout::vertical([Length(3), Length(3), Fill(0)]).areas(area);
|
||||
let [tabs, axis, demo] = area.layout(&Layout::vertical([Length(3), Length(3), Fill(0)]));
|
||||
|
||||
self.render_tabs(tabs, buf);
|
||||
Self::render_axis(axis, buf);
|
||||
@@ -284,8 +281,8 @@ impl Widget for SelectedTab {
|
||||
|
||||
impl SelectedTab {
|
||||
fn render_length_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 4]).areas(area);
|
||||
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 4]);
|
||||
let [example1, example2, example3, _] = area.layout(&layout);
|
||||
|
||||
Example::new(&[Length(20), Length(20)]).render(example1, buf);
|
||||
Example::new(&[Length(20), Min(20)]).render(example2, buf);
|
||||
@@ -293,8 +290,8 @@ impl SelectedTab {
|
||||
}
|
||||
|
||||
fn render_percentage_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 6]);
|
||||
let [example1, example2, example3, example4, example5, _] = area.layout(&layout);
|
||||
|
||||
Example::new(&[Percentage(75), Fill(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(25), Fill(0)]).render(example2, buf);
|
||||
@@ -304,8 +301,8 @@ impl SelectedTab {
|
||||
}
|
||||
|
||||
fn render_ratio_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 5]).areas(area);
|
||||
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 5]);
|
||||
let [example1, example2, example3, example4, _] = area.layout(&layout);
|
||||
|
||||
Example::new(&[Ratio(1, 2); 2]).render(example1, buf);
|
||||
Example::new(&[Ratio(1, 4); 4]).render(example2, buf);
|
||||
@@ -314,15 +311,15 @@ impl SelectedTab {
|
||||
}
|
||||
|
||||
fn render_fill_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, _] = Layout::vertical([Length(EXAMPLE_HEIGHT); 3]).areas(area);
|
||||
let [example1, example2, _] = area.layout(&Layout::vertical([Length(EXAMPLE_HEIGHT); 3]));
|
||||
|
||||
Example::new(&[Fill(1), Fill(2), Fill(3)]).render(example1, buf);
|
||||
Example::new(&[Fill(1), Percentage(50), Fill(1)]).render(example2, buf);
|
||||
}
|
||||
|
||||
fn render_min_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 6]);
|
||||
let [example1, example2, example3, example4, example5, _] = area.layout(&layout);
|
||||
|
||||
Example::new(&[Percentage(100), Min(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(100), Min(20)]).render(example2, buf);
|
||||
@@ -332,8 +329,8 @@ impl SelectedTab {
|
||||
}
|
||||
|
||||
fn render_max_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 6]);
|
||||
let [example1, example2, example3, example4, example5, _] = area.layout(&layout);
|
||||
|
||||
Example::new(&[Percentage(0), Max(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(0), Max(20)]).render(example2, buf);
|
||||
@@ -357,9 +354,10 @@ impl Example {
|
||||
|
||||
impl Widget for Example {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let [area, _] =
|
||||
Layout::vertical([Length(ILLUSTRATION_HEIGHT), Length(SPACER_HEIGHT)]).areas(area);
|
||||
let blocks = Layout::horizontal(&self.constraints).split(area);
|
||||
let vertical = Layout::vertical([Length(ILLUSTRATION_HEIGHT), Length(SPACER_HEIGHT)]);
|
||||
let horizontal = Layout::horizontal(&self.constraints);
|
||||
let [area, _] = area.layout(&vertical);
|
||||
let blocks = area.layout_vec(&horizontal);
|
||||
|
||||
for (block, constraint) in blocks.iter().zip(&self.constraints) {
|
||||
Self::illustration(*constraint, block.width).render(*block, buf);
|
||||
|
||||
@@ -15,7 +15,7 @@ use crossterm::event::{
|
||||
};
|
||||
use crossterm::execute;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::layout::{Constraint, Flex, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Paragraph, Widget};
|
||||
@@ -167,13 +167,13 @@ fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame, states: [State; 3]) {
|
||||
let vertical = Layout::vertical([
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Max(3),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0), // ignore remaining space
|
||||
]);
|
||||
let [title, buttons, help, _] = vertical.areas(frame.area());
|
||||
let [title, buttons, help, _] = frame.area().layout(&layout);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new("Custom Widget Example (mouse enabled)"),
|
||||
@@ -184,13 +184,8 @@ fn render(frame: &mut Frame, states: [State; 3]) {
|
||||
}
|
||||
|
||||
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: [State; 3]) {
|
||||
let horizontal = Layout::horizontal([
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Min(0), // ignore remaining space
|
||||
]);
|
||||
let [red, green, blue, _] = horizontal.areas(area);
|
||||
let layout = Layout::horizontal([Constraint::Length(15); 3]).flex(Flex::Start);
|
||||
let [red, green, blue] = area.layout(&layout);
|
||||
|
||||
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
|
||||
frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);
|
||||
|
||||
@@ -12,9 +12,9 @@ termion = ["ratatui/termion", "dep:termion"]
|
||||
termwiz = ["ratatui/termwiz", "dep:termwiz"]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
clap = { version = "4.5.41", features = ["derive"] }
|
||||
crossterm = { workspace = true, optional = true }
|
||||
rand = "0.9.1"
|
||||
rand = "0.9.2"
|
||||
ratatui.workspace = true
|
||||
termwiz = { workspace = true, optional = true }
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ where
|
||||
{
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|frame| ui::draw(frame, &mut app))?;
|
||||
terminal.draw(|frame| ui::render(frame, &mut app))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if !event::poll(timeout)? {
|
||||
|
||||
@@ -42,7 +42,7 @@ where
|
||||
{
|
||||
let events = events(tick_rate);
|
||||
loop {
|
||||
terminal.draw(|frame| ui::draw(frame, &mut app))?;
|
||||
terminal.draw(|frame| ui::render(frame, &mut app))?;
|
||||
|
||||
match events.recv()? {
|
||||
Event::Input(key) => match key {
|
||||
|
||||
@@ -36,7 +36,7 @@ fn run_app(
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|frame| ui::draw(frame, &mut app))?;
|
||||
terminal.draw(|frame| ui::render(frame, &mut app))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if let Some(input) = terminal
|
||||
|
||||
@@ -10,7 +10,7 @@ use ratatui::{Frame, symbols};
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
pub fn draw(frame: &mut Frame, app: &mut App) {
|
||||
pub fn render(frame: &mut Frame, app: &mut App) {
|
||||
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area());
|
||||
let tabs = app
|
||||
.tabs
|
||||
|
||||
@@ -6,12 +6,12 @@ edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.6.3"
|
||||
color-eyre = "0.6.5"
|
||||
crossterm.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
palette = "0.7.6"
|
||||
rand = "0.9.1"
|
||||
rand = "0.9.2"
|
||||
rand_chacha = "0.9.0"
|
||||
ratatui = { workspace = true, features = ["all-widgets"] }
|
||||
strum.workspace = true
|
||||
|
||||
@@ -129,12 +129,12 @@ impl App {
|
||||
/// matter, but for larger apps this can be a significant performance improvement.
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let vertical = Layout::vertical([
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
]);
|
||||
let [title_bar, tab, bottom_bar] = vertical.areas(area);
|
||||
let [title_bar, tab, bottom_bar] = area.layout(&layout);
|
||||
|
||||
Block::new().style(THEME.root).render(area, buf);
|
||||
self.render_title_bar(title_bar, buf);
|
||||
@@ -146,7 +146,7 @@ impl Widget for &App {
|
||||
impl App {
|
||||
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::horizontal([Constraint::Min(0), Constraint::Length(43)]);
|
||||
let [title, tabs] = layout.areas(area);
|
||||
let [title, tabs] = area.layout(&layout);
|
||||
|
||||
Span::styled("Ratatui", THEME.app_title).render(title, buf);
|
||||
let titles = Tab::iter().map(Tab::title);
|
||||
|
||||
@@ -134,7 +134,7 @@ fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
|
||||
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
|
||||
let horizontal = Layout::horizontal([width]).flex(Flex::Center);
|
||||
let vertical = Layout::vertical([height]).flex(Flex::Center);
|
||||
let [area] = vertical.areas(area);
|
||||
let [area] = horizontal.areas(area);
|
||||
let [area] = area.layout(&vertical);
|
||||
let [area] = area.layout(&horizontal);
|
||||
area
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ impl AboutTab {
|
||||
impl Widget for AboutTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
|
||||
let [logo_area, description] = horizontal.areas(area);
|
||||
let layout = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
|
||||
let [logo_area, description] = area.layout(&layout);
|
||||
render_crate_description(description, buf);
|
||||
let eye_state = if self.row_index % 2 == 0 {
|
||||
MascotEyeColor::Default
|
||||
|
||||
@@ -71,15 +71,15 @@ impl Widget for EmailTab {
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf);
|
||||
let vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
|
||||
let [inbox, email] = vertical.areas(area);
|
||||
let layout = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
|
||||
let [inbox, email] = area.layout(&layout);
|
||||
render_inbox(self.row_index, inbox, buf);
|
||||
render_email(self.row_index, email, buf);
|
||||
}
|
||||
}
|
||||
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let [tabs, inbox] = vertical.areas(area);
|
||||
let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let [tabs, inbox] = area.layout(&layout);
|
||||
let theme = THEME.email;
|
||||
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
|
||||
.style(theme.tabs)
|
||||
@@ -130,8 +130,8 @@ fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
if let Some(email) = email {
|
||||
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
|
||||
let [headers_area, body_area] = vertical.areas(inner);
|
||||
let layout = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
|
||||
let [headers_area, body_area] = inner.layout(&layout);
|
||||
let headers = vec![
|
||||
Line::from(vec![
|
||||
"From: ".set_style(theme.header),
|
||||
|
||||
@@ -135,8 +135,8 @@ impl Widget for RecipeTab {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
let [recipe, ingredients] =
|
||||
Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]).areas(area);
|
||||
let layout = Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]);
|
||||
let [recipe, ingredients] = area.layout(&layout);
|
||||
|
||||
render_recipe(recipe, buf);
|
||||
render_ingredients(self.row_index, ingredients, buf);
|
||||
|
||||
@@ -39,8 +39,8 @@ impl Widget for TracerouteTab {
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
|
||||
let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]);
|
||||
let [left, map] = horizontal.areas(area);
|
||||
let [hops, pings] = vertical.areas(left);
|
||||
let [left, map] = area.layout(&horizontal);
|
||||
let [hops, pings] = left.layout(&vertical);
|
||||
|
||||
render_hops(self.row_index, hops, buf);
|
||||
render_ping(self.row_index, pings, buf);
|
||||
|
||||
@@ -41,16 +41,16 @@ impl Widget for WeatherTab {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
let [main, _, gauges] = Layout::vertical([
|
||||
let tab_layout = Layout::vertical([
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(area);
|
||||
let [calendar, charts] =
|
||||
Layout::horizontal([Constraint::Length(23), Constraint::Min(0)]).areas(main);
|
||||
let [simple, horizontal] =
|
||||
Layout::vertical([Constraint::Length(29), Constraint::Min(0)]).areas(charts);
|
||||
]);
|
||||
let [main, _, gauges] = area.layout(&tab_layout);
|
||||
let main_layout = Layout::horizontal([Constraint::Length(23), Constraint::Min(0)]);
|
||||
let [calendar, charts] = main.layout(&main_layout);
|
||||
let charts_layout = Layout::vertical([Constraint::Length(29), Constraint::Min(0)]);
|
||||
let [simple, horizontal] = charts.layout(&charts_layout);
|
||||
|
||||
render_calendar(calendar, buf);
|
||||
render_simple_barchart(simple, buf);
|
||||
|
||||
@@ -27,10 +27,7 @@ use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
|
||||
@@ -156,11 +153,12 @@ enum SelectedTab {
|
||||
Center,
|
||||
End,
|
||||
SpaceAround,
|
||||
SpaceEvenly,
|
||||
SpaceBetween,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
// increase the layout cache to account for the number of layout events. This ensures that
|
||||
// layout is not generally reprocessed on every frame (which would lead to possible janky
|
||||
// results when there are more than one possible solution to the requested layout). This
|
||||
@@ -258,7 +256,7 @@ fn example_height() -> u16 {
|
||||
impl Widget for App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::vertical([Length(3), Length(1), Fill(0)]);
|
||||
let [tabs, axis, demo] = layout.areas(area);
|
||||
let [tabs, axis, demo] = area.layout(&layout);
|
||||
self.tabs().render(tabs, buf);
|
||||
let scroll_needed = self.render_demo(demo, buf);
|
||||
let axis_width = if scroll_needed {
|
||||
@@ -372,8 +370,9 @@ impl SelectedTab {
|
||||
Self::Start => SKY.c400,
|
||||
Self::Center => SKY.c300,
|
||||
Self::End => SKY.c200,
|
||||
Self::SpaceAround => INDIGO.c400,
|
||||
Self::SpaceEvenly => INDIGO.c400,
|
||||
Self::SpaceBetween => INDIGO.c300,
|
||||
Self::SpaceAround => INDIGO.c500,
|
||||
};
|
||||
format!(" {text} ").fg(color).bg(Color::Black).into()
|
||||
}
|
||||
@@ -388,8 +387,9 @@ impl StatefulWidget for SelectedTab {
|
||||
Self::Start => Self::render_examples(area, buf, Flex::Start, spacing),
|
||||
Self::Center => Self::render_examples(area, buf, Flex::Center, spacing),
|
||||
Self::End => Self::render_examples(area, buf, Flex::End, spacing),
|
||||
Self::SpaceAround => Self::render_examples(area, buf, Flex::SpaceAround, spacing),
|
||||
Self::SpaceEvenly => Self::render_examples(area, buf, Flex::SpaceEvenly, spacing),
|
||||
Self::SpaceBetween => Self::render_examples(area, buf, Flex::SpaceBetween, spacing),
|
||||
Self::SpaceAround => Self::render_examples(area, buf, Flex::SpaceAround, spacing),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,7 +421,7 @@ impl Widget for Example {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let title_height = get_description_height(&self.description);
|
||||
let layout = Layout::vertical([Length(title_height), Fill(0)]);
|
||||
let [title, illustrations] = layout.areas(area);
|
||||
let [title, illustrations] = area.layout(&layout);
|
||||
|
||||
let (blocks, spacers) = Layout::horizontal(&self.constraints)
|
||||
.flex(self.flex)
|
||||
|
||||
@@ -43,14 +43,11 @@ enum AppState {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
while self.state != AppState::Quitting {
|
||||
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
@@ -106,10 +103,10 @@ impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min, Ratio};
|
||||
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
|
||||
let [header_area, gauge_area, footer_area] = layout.areas(area);
|
||||
let [header_area, gauge_area, footer_area] = area.layout(&layout);
|
||||
|
||||
let layout = Layout::vertical([Ratio(1, 4); 4]);
|
||||
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = layout.areas(gauge_area);
|
||||
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = gauge_area.layout(&layout);
|
||||
|
||||
render_header(header_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
@@ -185,7 +182,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn title_block(title: &str) -> Block {
|
||||
fn title_block(title: &str) -> Block<'_> {
|
||||
let title = Line::from(title).centered();
|
||||
Block::new()
|
||||
.borders(Borders::NONE)
|
||||
|
||||
@@ -21,17 +21,14 @@ use ratatui::{DefaultTerminal, Frame};
|
||||
/// and exits when the user presses 'q'.
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?; // augment errors / panics with easy to read messages
|
||||
let terminal = ratatui::init();
|
||||
let app_result = run(terminal).context("app loop failed");
|
||||
ratatui::restore();
|
||||
app_result
|
||||
ratatui::run(run).context("failed to run app")
|
||||
}
|
||||
|
||||
/// Run the application loop. This is where you would handle events and update the application
|
||||
/// state. This example exits when the user presses 'q'. Other styles of application loops are
|
||||
/// possible, for example, you could have multiple application states and switch between them based
|
||||
/// on events, or you could have a single application state and update it based on events.
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if should_quit()? {
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
/// [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use crossterm::event;
|
||||
use itertools::Itertools;
|
||||
use ratatui::DefaultTerminal;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Stylize;
|
||||
@@ -18,35 +17,18 @@ use ratatui::widgets::Widget;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
struct App {
|
||||
hyperlink: Hyperlink<'static>,
|
||||
}
|
||||
let text = Line::from(vec!["Example ".into(), "hyperlink".blue()]);
|
||||
let hyperlink = Hyperlink::new(text, "https://example.com");
|
||||
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
let text = Line::from(vec!["Example ".into(), "hyperlink".blue()]);
|
||||
let hyperlink = Hyperlink::new(text, "https://example.com");
|
||||
Self { hyperlink }
|
||||
}
|
||||
|
||||
fn run(self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(&self.hyperlink, frame.area()))?;
|
||||
if event::read()?
|
||||
.as_key_press_event()
|
||||
.is_some_and(|key| matches!(key.code, KeyCode::Char('q') | KeyCode::Esc))
|
||||
{
|
||||
break;
|
||||
terminal.draw(|frame| frame.render_widget(&hyperlink, frame.area()))?;
|
||||
if event::read()?.is_key_press() {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A hyperlink widget that renders a hyperlink in the terminal using [OSC 8].
|
||||
|
||||
@@ -8,7 +8,7 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
rand = "0.9.1"
|
||||
rand = "0.9.2"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -230,8 +230,8 @@ fn render(frame: &mut Frame, downloads: &Downloads) {
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
||||
let [progress_area, main] = vertical.areas(area);
|
||||
let [list_area, gauge_area] = horizontal.areas(main);
|
||||
let [progress_area, main] = area.layout(&vertical);
|
||||
let [list_area, gauge_area] = main.layout(&horizontal);
|
||||
|
||||
// total progress
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
|
||||
@@ -23,5 +23,5 @@ Some more ideas for handling focus can be found in:
|
||||
- [`focusable`](https://crates.io/crates/focusable) (see also [Ratatui forum
|
||||
post](https://forum.ratatui.rs/t/focusable-crate-manage-focus-state-for-your-widgets/73))
|
||||
- [`rat-focus`](https://crates.io/crates/rat-focus)
|
||||
- A useful [`Bevy` discssion](https://github.com/bevyengine/bevy/discussions/15374) about focus
|
||||
- A useful [`Bevy` discussion](https://github.com/bevyengine/bevy/discussions/15374) about focus
|
||||
more generally.
|
||||
|
||||
@@ -26,12 +26,8 @@ use serde::Serialize;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
|
||||
// serialize the form to JSON if the user submitted it, otherwise print "Canceled"
|
||||
match result {
|
||||
match ratatui::run(|terminal| App::default().run(terminal)) {
|
||||
Ok(Some(form)) => println!("{}", serde_json::to_string_pretty(&form)?),
|
||||
Ok(None) => println!("Canceled"),
|
||||
Err(err) => eprintln!("{err}"),
|
||||
@@ -54,7 +50,7 @@ enum AppState {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<Option<InputForm>> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<Option<InputForm>> {
|
||||
while self.state == AppState::Running {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
self.handle_events()?;
|
||||
@@ -119,8 +115,8 @@ impl InputForm {
|
||||
///
|
||||
/// The cursor is placed at the end of the focused field.
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let [first_name_area, last_name_area, age_area] =
|
||||
Layout::vertical(Constraint::from_lengths([1, 1, 1])).areas(frame.area());
|
||||
let layout = Layout::vertical(Constraint::from_lengths([1, 1, 1]));
|
||||
let [first_name_area, last_name_area, age_area] = frame.area().layout(&layout);
|
||||
|
||||
frame.render_widget(&self.first_name, first_name_area);
|
||||
frame.render_widget(&self.last_name, last_name_area);
|
||||
@@ -189,11 +185,11 @@ impl StringField {
|
||||
|
||||
impl Widget for &StringField {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let constraints = [
|
||||
let layout = Layout::horizontal([
|
||||
Constraint::Length(self.label.len() as u16 + 2),
|
||||
Constraint::Fill(1),
|
||||
];
|
||||
let [label_area, value_area] = Layout::horizontal(constraints).areas(area);
|
||||
]);
|
||||
let [label_area, value_area] = area.layout(&layout);
|
||||
let label = Line::from_iter([self.label, ": "]).bold();
|
||||
label.render(label_area, buf);
|
||||
self.value.clone().render(value_area, buf);
|
||||
@@ -254,11 +250,11 @@ impl AgeField {
|
||||
|
||||
impl Widget for &AgeField {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let constraints = [
|
||||
let layout = Layout::horizontal([
|
||||
Constraint::Length(self.label.len() as u16 + 2),
|
||||
Constraint::Fill(1),
|
||||
];
|
||||
let [label_area, value_area] = Layout::horizontal(constraints).areas(area);
|
||||
]);
|
||||
let [label_area, value_area] = area.layout(&layout);
|
||||
let label = Line::from_iter([self.label, ": "]).bold();
|
||||
let value = self.value.to_string();
|
||||
label.render(label_area, buf);
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
/// A minimal example of a Ratatui application.
|
||||
///
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop,
|
||||
/// so this is not meant to be prescriptive. See the [examples] folder for more complete
|
||||
/// examples. In particular, the [hello-world] example is a good starting point.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
/// [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||
/// [hello-world]: https://github.com/ratatui/ratatui/blob/main/examples/apps/hello-world
|
||||
use crossterm::event;
|
||||
use ratatui::text::Text;
|
||||
|
||||
fn main() {
|
||||
let mut terminal = ratatui::init();
|
||||
loop {
|
||||
terminal
|
||||
.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
|
||||
.expect("failed to draw frame");
|
||||
if event::read().expect("failed to read event").is_key_press() {
|
||||
break;
|
||||
//! A minimal example of a Ratatui application.
|
||||
//!
|
||||
//! This is a bare minimum example. There are many approaches to running an application loop,
|
||||
//! so this is not meant to be prescriptive. See the [examples] folder for more complete
|
||||
//! examples. In particular, the [hello-world] example is a good starting point.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||
//! [hello-world]: https://github.com/ratatui/ratatui/blob/main/examples/apps/hello-world
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget("Hello World!", frame.area()))?;
|
||||
if crossterm::event::read()?.is_key_press() {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
ratatui::restore();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,34 +12,29 @@ use std::{error::Error, iter::once, result};
|
||||
|
||||
use crossterm::event;
|
||||
use itertools::Itertools;
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Constraint, Layout};
|
||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
return Ok(());
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let [text_area, main_area] = vertical.areas(frame.area());
|
||||
let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let [text_area, main_area] = frame.area().layout(&layout);
|
||||
frame.render_widget(
|
||||
Paragraph::new("Note: not all terminals support all modifiers")
|
||||
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
|
||||
@@ -10,7 +10,7 @@ color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
## a collection of line drawing algorithms (e.g. Bresenham's line algorithm)
|
||||
line_drawing = "1.0.1"
|
||||
rand = "0.9.1"
|
||||
rand = "0.9.2"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -21,10 +21,7 @@ use ratatui::{DefaultTerminal, Frame, symbols};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = MouseDrawingApp::default().run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
ratatui::run(|terminal| MouseDrawingApp::default().run(terminal))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -40,7 +37,7 @@ struct MouseDrawingApp {
|
||||
}
|
||||
|
||||
impl MouseDrawingApp {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
execute!(std::io::stdout(), EnableMouseCapture)?;
|
||||
while !self.should_exit {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
|
||||
@@ -31,69 +31,58 @@
|
||||
/// [Color Eyre recipe]: https://ratatui.rs/recipes/apps/color-eyre
|
||||
use color_eyre::{Result, eyre::bail};
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::Frame;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Paragraph};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PanicHandlerState {
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
struct App {
|
||||
hook_enabled: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self { hook_enabled: true }
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
let mut panic_hook_state = PanicHandlerState::Enabled;
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
|
||||
terminal.draw(|frame| render(frame, &panic_hook_state))?;
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('p') => panic!("intentional demo panic"),
|
||||
KeyCode::Char('e') => bail!("intentional demo error"),
|
||||
KeyCode::Char('h') => {
|
||||
let _ = std::panic::take_hook();
|
||||
self.hook_enabled = false;
|
||||
panic_hook_state = PanicHandlerState::Disabled;
|
||||
}
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let text = vec![
|
||||
if self.hook_enabled {
|
||||
Line::from("HOOK IS CURRENTLY **ENABLED**")
|
||||
} else {
|
||||
Line::from("HOOK IS CURRENTLY **DISABLED**")
|
||||
},
|
||||
Line::from(""),
|
||||
Line::from("Press `p` to cause a panic"),
|
||||
Line::from("Press `e` to cause an error"),
|
||||
Line::from("Press `h` to disable the panic hook"),
|
||||
Line::from("Press `q` to quit"),
|
||||
Line::from(""),
|
||||
Line::from("When your app panics without a panic hook, you will likely have to"),
|
||||
Line::from("reset your terminal afterwards with the `reset` command"),
|
||||
Line::from(""),
|
||||
Line::from("Try first with the panic handler enabled, and then with it disabled"),
|
||||
Line::from("to see the difference"),
|
||||
];
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered().title("Panic Handler Demo"))
|
||||
.centered();
|
||||
|
||||
frame.render_widget(paragraph, frame.area());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame, state: &PanicHandlerState) {
|
||||
let text = vec![
|
||||
Line::from(format!("Panic hook is currently: {state:?}")),
|
||||
Line::from(""),
|
||||
Line::from("Press `p` to cause a panic"),
|
||||
Line::from("Press `e` to cause an error"),
|
||||
Line::from("Press `h` to disable the panic hook"),
|
||||
Line::from("Press `q` to quit"),
|
||||
Line::from(""),
|
||||
Line::from("When your app panics without a panic hook, you will likely have to"),
|
||||
Line::from("reset your terminal afterwards with the `reset` command"),
|
||||
Line::from(""),
|
||||
Line::from("Try first with the panic handler enabled, and then with it disabled"),
|
||||
Line::from("to see the difference"),
|
||||
];
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered().title("Panic Handler Demo"))
|
||||
.centered();
|
||||
|
||||
frame.render_widget(paragraph, frame.area());
|
||||
}
|
||||
|
||||
@@ -1,80 +1,70 @@
|
||||
/// A Ratatui example that demonstrates how to handle popups.
|
||||
// See also https://github.com/joshka/tui-popup and
|
||||
// https://github.com/sephiroth74/tui-confirm-dialog
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//! A Ratatui example that demonstrates how to handle popups.
|
||||
//! See also:
|
||||
//! - <https://github.com/joshka/tui-popup> and
|
||||
//! - <https://github.com/sephiroth74/tui-confirm-dialog>
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Constraint, Flex, Layout, Rect};
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::widgets::{Block, Clear, Paragraph, Wrap};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Clear};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
show_popup: bool,
|
||||
}
|
||||
// This flag will be toggled when the user presses 'p'. This could be stored in an app struct
|
||||
// if you have more state to manage than just this flag.
|
||||
let mut show_popup = false;
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
terminal.draw(|frame| render(frame, show_popup))?;
|
||||
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => self.show_popup = !self.show_popup,
|
||||
KeyCode::Char('p') => show_popup = !show_popup,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let area = frame.area();
|
||||
fn render(frame: &mut Frame, show_popup: bool) {
|
||||
let area = frame.area();
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
||||
let [instructions, content] = vertical.areas(area);
|
||||
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
|
||||
let [instructions, content] = area.layout(&layout);
|
||||
|
||||
let text = if self.show_popup {
|
||||
"Press p to close the popup"
|
||||
} else {
|
||||
"Press p to show the popup"
|
||||
};
|
||||
let paragraph = Paragraph::new(text.slow_blink())
|
||||
.centered()
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, instructions);
|
||||
frame.render_widget(
|
||||
Line::from("Press 'p' to toggle popup, 'q' to quit").centered(),
|
||||
instructions,
|
||||
);
|
||||
|
||||
let block = Block::bordered().title("Content").on_blue();
|
||||
frame.render_widget(block, content);
|
||||
frame.render_widget(Block::bordered().title("Content").on_blue(), content);
|
||||
|
||||
if self.show_popup {
|
||||
let block = Block::bordered().title("Popup");
|
||||
let area = popup_area(area, 60, 20);
|
||||
frame.render_widget(Clear, area); //this clears out the background
|
||||
frame.render_widget(block, area);
|
||||
}
|
||||
if show_popup {
|
||||
let popup = Block::bordered().title("Popup");
|
||||
let popup_area = centered_area(area, 60, 20);
|
||||
// clears out any background in the area before rendering the popup
|
||||
frame.render_widget(Clear, popup_area);
|
||||
frame.render_widget(popup, popup_area);
|
||||
}
|
||||
}
|
||||
|
||||
/// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||
fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
|
||||
/// Create a centered rect using up certain percentage of the available rect
|
||||
fn centered_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
|
||||
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
|
||||
let [area] = vertical.areas(area);
|
||||
let [area] = horizontal.areas(area);
|
||||
let [area] = area.layout(&vertical);
|
||||
let [area] = area.layout(&horizontal);
|
||||
area
|
||||
}
|
||||
|
||||
@@ -29,14 +29,11 @@ struct App {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
|
||||
@@ -34,10 +34,7 @@ const ITEM_HEIGHT: usize = 4;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
ratatui::run(|terminal| App::new().run(terminal))
|
||||
}
|
||||
struct TableColors {
|
||||
buffer_bg: Color,
|
||||
@@ -80,20 +77,14 @@ impl Data {
|
||||
[&self.name, &self.address, &self.email]
|
||||
}
|
||||
|
||||
// https://github.com/rust-lang/rust/issues/139338
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
// https://github.com/rust-lang/rust/issues/139338
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
fn address(&self) -> &str {
|
||||
&self.address
|
||||
}
|
||||
|
||||
// https://github.com/rust-lang/rust/issues/139338
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
fn email(&self) -> &str {
|
||||
&self.email
|
||||
}
|
||||
@@ -120,6 +111,7 @@ impl App {
|
||||
items: data_vec,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_row(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
@@ -171,7 +163,7 @@ impl App {
|
||||
self.colors = TableColors::new(&PALETTES[self.color_index]);
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
|
||||
@@ -194,8 +186,8 @@ impl App {
|
||||
}
|
||||
|
||||
fn render(&mut self, frame: &mut Frame) {
|
||||
let vertical = &Layout::vertical([Constraint::Min(5), Constraint::Length(4)]);
|
||||
let rects = vertical.split(frame.area());
|
||||
let layout = Layout::vertical([Constraint::Min(5), Constraint::Length(4)]);
|
||||
let rects = frame.area().layout_vec(&layout);
|
||||
|
||||
self.set_colors();
|
||||
|
||||
|
||||
@@ -27,10 +27,7 @@ const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `todo_list` field
|
||||
@@ -124,7 +121,7 @@ impl TodoItem {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
while !self.should_exit {
|
||||
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
@@ -181,15 +178,15 @@ impl App {
|
||||
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let [header_area, main_area, footer_area] = Layout::vertical([
|
||||
let main_layout = Layout::vertical([
|
||||
Constraint::Length(2),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(area);
|
||||
]);
|
||||
let [header_area, content_area, footer_area] = area.layout(&main_layout);
|
||||
|
||||
let [list_area, item_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area);
|
||||
let content_layout = Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]);
|
||||
let [list_area, item_area] = content_area.layout(&content_layout);
|
||||
|
||||
App::render_header(header_area, buf);
|
||||
App::render_footer(footer_area, buf);
|
||||
|
||||
@@ -30,10 +30,7 @@ use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
ratatui::run(|terminal| App::new().run(terminal))
|
||||
}
|
||||
|
||||
/// App holds the state of the application
|
||||
@@ -127,7 +124,7 @@ impl App {
|
||||
self.reset_cursor();
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
|
||||
@@ -158,12 +155,12 @@ impl App {
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]);
|
||||
let [help_area, input_area, messages_area] = vertical.areas(frame.area());
|
||||
let [help_area, input_area, messages_area] = frame.area().layout(&layout);
|
||||
|
||||
let (msg, style) = match self.input_mode {
|
||||
InputMode::Normal => (
|
||||
|
||||
@@ -8,7 +8,7 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
rand = "0.9.1"
|
||||
rand = "0.9.2"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -9,67 +9,38 @@
|
||||
//! [`BarChart`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use crossterm::event;
|
||||
use rand::{Rng, rng};
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Constraint, Layout};
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Bar, BarChart, BarGroup};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
|
||||
let temperatures: Vec<u8> = (0..24).map(|_| rng().random_range(50..90)).collect();
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(|frame| render(frame, &temperatures))?;
|
||||
if event::read()?.is_key_press() {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
struct App {
|
||||
should_exit: bool,
|
||||
temperatures: Vec<u8>,
|
||||
}
|
||||
fn render(frame: &mut Frame, temperatures: &[u8]) {
|
||||
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
|
||||
let [title, main] = frame.area().layout(&layout);
|
||||
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
let mut rng = rng();
|
||||
let temperatures = (0..24).map(|_| rng.random_range(50..90)).collect();
|
||||
Self {
|
||||
should_exit: false,
|
||||
temperatures,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while !self.should_exit {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if event::read()?
|
||||
.as_key_press_event()
|
||||
.is_some_and(|key| key.code == KeyCode::Char('q'))
|
||||
{
|
||||
self.should_exit = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let [title, main] = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)])
|
||||
.spacing(1)
|
||||
.areas(frame.area());
|
||||
|
||||
frame.render_widget("Weather demo".bold().into_centered_line(), title);
|
||||
frame.render_widget(vertical_barchart(&self.temperatures), main);
|
||||
}
|
||||
frame.render_widget("Weather demo".bold().into_centered_line(), title);
|
||||
frame.render_widget(vertical_barchart(temperatures), main);
|
||||
}
|
||||
|
||||
/// Create a vertical bar chart from the temperatures data.
|
||||
fn vertical_barchart(temperatures: &[u8]) -> BarChart {
|
||||
fn vertical_barchart(temperatures: &[u8]) -> BarChart<'_> {
|
||||
let bars: Vec<Bar> = temperatures
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -80,7 +51,7 @@ fn vertical_barchart(temperatures: &[u8]) -> BarChart {
|
||||
.bar_width(5)
|
||||
}
|
||||
|
||||
fn vertical_bar(hour: usize, temperature: &u8) -> Bar {
|
||||
fn vertical_bar(hour: usize, temperature: &u8) -> Bar<'_> {
|
||||
Bar::default()
|
||||
.value(u64::from(*temperature))
|
||||
.label(Line::from(format!("{hour:>02}:00")))
|
||||
|
||||
@@ -1,39 +1,34 @@
|
||||
/// An example of how to use [`WidgetRef`] to store heterogeneous widgets in a container.
|
||||
///
|
||||
/// This example creates a `StackContainer` widget that can hold any number of widgets of
|
||||
/// different types. It creates two widgets, `Greeting` and `Farewell`, and stores them in a
|
||||
/// `StackContainer` with a vertical layout. The `StackContainer` widget renders each of its
|
||||
/// child widgets in the order they were added.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//! An example of how to use [`WidgetRef`] to store heterogeneous widgets in a container.
|
||||
//!
|
||||
//! This example creates a `StackContainer` widget that can hold any number of widgets of
|
||||
//! different types. It creates two widgets, `Greeting` and `Farewell`, and stores them in a
|
||||
//! `StackContainer` with a vertical layout. The `StackContainer` widget renders each of its
|
||||
//! child widgets in the order they were added.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::iter::zip;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event;
|
||||
use ratatui::Frame;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::widgets::{Block, Paragraph, Widget, WidgetRef};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
return Ok(());
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
|
||||
11
examples/concepts/state/Cargo.toml
Normal file
11
examples/concepts/state/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "ratatui-state-examples"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.6"
|
||||
crossterm = "0.29"
|
||||
ratatui = { workspace = true, features = ["unstable-widget-ref"] }
|
||||
179
examples/concepts/state/README.md
Normal file
179
examples/concepts/state/README.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Ratatui State Management Examples
|
||||
|
||||
This collection demonstrates various patterns for handling both mutable and immutable state in
|
||||
Ratatui applications. Each example solves a counter problem - incrementing a counter value - but
|
||||
uses different architectural approaches. These patterns represent common solutions to state
|
||||
management challenges you'll encounter when building TUI applications.
|
||||
|
||||
For more information about widgets in Ratatui, see the [widgets module documentation](https://docs.rs/ratatui/latest/ratatui/widgets/index.html).
|
||||
|
||||
## When to Use Each Pattern
|
||||
|
||||
Choose the pattern that best fits your application's architecture and complexity:
|
||||
|
||||
- **Simple applications**: Use `render-function` or `mutable-widget` patterns
|
||||
- **Clean separation**: Consider `stateful-widget` or `component-trait` patterns
|
||||
- **Complex hierarchies**: Use `nested-*` patterns for parent-child relationships
|
||||
- **Shared state**: Use `refcell` when multiple widgets need access to the same state
|
||||
- **Advanced scenarios**: Use `widget-with-mutable-ref` when you understand Rust lifetimes well
|
||||
|
||||
## Running the Examples
|
||||
|
||||
To run any example, use:
|
||||
|
||||
```bash
|
||||
cargo run --bin example-name
|
||||
```
|
||||
|
||||
Press any key (or resize the terminal) to increment the counter. Press `<Esc>` or `q` to exit.
|
||||
|
||||
## Examples
|
||||
|
||||
### Immutable State Patterns
|
||||
|
||||
These patterns keep widget state immutable during rendering, with state updates happening outside
|
||||
the render cycle. They're generally easier to reason about and less prone to borrowing issues.
|
||||
|
||||
#### [`immutable-function.rs`] - Function-Based Immutable State
|
||||
|
||||
**Best for**: Simple applications with pure rendering functions
|
||||
**Pros**: Pure functions, easy to test, clear separation of concerns
|
||||
**Cons**: Verbose parameter passing, limited integration with Ratatui ecosystem
|
||||
|
||||
Uses standalone functions that take immutable references to state. State updates happen in the
|
||||
application loop outside of rendering.
|
||||
|
||||
#### [`immutable-shared-ref.rs`] - Shared Reference Pattern (Recommended)
|
||||
|
||||
**Best for**: Most modern Ratatui applications
|
||||
**Pros**: Reusable widgets, efficient, integrates with Ratatui ecosystem, modern best practice
|
||||
**Cons**: Requires external state management for dynamic behavior
|
||||
|
||||
Implements [`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) for
|
||||
`&T`, allowing widgets to be rendered multiple times by reference without being consumed.
|
||||
|
||||
#### [`immutable-consuming.rs`] - Consuming Widget Pattern
|
||||
|
||||
**Best for**: Compatibility with older code, simple widgets created fresh each frame
|
||||
**Pros**: Simple implementation, widely compatible, familiar pattern
|
||||
**Cons**: Widget consumed on each render, requires reconstruction for reuse
|
||||
|
||||
Implements [`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) directly
|
||||
on the owned type, consuming the widget when rendered.
|
||||
|
||||
### Mutable State Patterns
|
||||
|
||||
These patterns allow widgets to modify their state during rendering, useful for widgets that need
|
||||
to update state as part of their rendering behavior.
|
||||
|
||||
#### [`mutable-function.rs`] - Function-Based Mutable State
|
||||
|
||||
**Best for**: Simple applications with minimal mutable state
|
||||
**Pros**: Easy to understand, no traits to implement, direct control
|
||||
**Cons**: State gets passed around as function parameters, harder to organize as complexity grows
|
||||
|
||||
Uses simple functions that accept mutable state references. State is managed at the application
|
||||
level and passed down to render functions.
|
||||
|
||||
#### [`mutable-widget.rs`] - Mutable Widget Pattern
|
||||
|
||||
**Best for**: Self-contained widgets with their own mutable state
|
||||
**Pros**: Encapsulates state within the widget, familiar OOP-style approach
|
||||
**Cons**: Requires `&mut` references, can be challenging with complex borrowing scenarios
|
||||
|
||||
Implements [`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) for
|
||||
`&mut T`, allowing the widget to mutate its own state during rendering.
|
||||
|
||||
### Intermediate Patterns
|
||||
|
||||
#### [`stateful-widget.rs`] - Stateful Widget Pattern
|
||||
|
||||
**Best for**: Clean separation of widget logic from state
|
||||
**Pros**: Separates widget logic from state, reusable, idiomatic Ratatui pattern
|
||||
**Cons**: State must be managed externally
|
||||
|
||||
Uses [`StatefulWidget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.StatefulWidget.html)
|
||||
to keep rendering logic separate from state management.
|
||||
|
||||
#### [`component-trait.rs`] - Custom Component Trait
|
||||
|
||||
**Best for**: Implementing consistent behavior across multiple widget types
|
||||
**Pros**: Flexible, allows custom render signatures, good for widget frameworks
|
||||
**Cons**: Non-standard, requires users to learn your custom API
|
||||
|
||||
Creates a custom trait similar to [`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html)
|
||||
but with a `&mut self` render method for direct mutation.
|
||||
|
||||
### Advanced Patterns
|
||||
|
||||
#### [`nested-mutable-widget.rs`] - Nested Mutable Widgets
|
||||
|
||||
**Best for**: Parent-child widget relationships with mutable state
|
||||
**Pros**: Hierarchical organization, each widget manages its own state
|
||||
**Cons**: Complex borrowing, requires careful lifetime management
|
||||
|
||||
Demonstrates how to nest widgets that both need mutable access to their state.
|
||||
|
||||
#### [`nested-stateful-widget.rs`] - Nested Stateful Widgets
|
||||
|
||||
**Best for**: Complex applications with hierarchical state management
|
||||
**Pros**: Clean separation, composable, scales well with application complexity
|
||||
**Cons**: More boilerplate, requires understanding of nested state patterns
|
||||
|
||||
Shows how to compose multiple [`StatefulWidget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.StatefulWidget.html)s
|
||||
in a parent-child hierarchy.
|
||||
|
||||
#### [`refcell.rs`] - Interior Mutability Pattern
|
||||
|
||||
**Best for**: Shared state across multiple widgets, complex state sharing scenarios
|
||||
**Pros**: Allows shared mutable access, works with immutable widget references
|
||||
**Cons**: Runtime borrow checking, potential panics, harder to debug
|
||||
|
||||
Uses [`Rc<RefCell<T>>`](https://doc.rust-lang.org/std/rc/struct.Rc.html) for interior mutability
|
||||
when multiple widgets need access to the same state.
|
||||
|
||||
#### [`widget-with-mutable-ref.rs`] - Lifetime-Based Mutable References
|
||||
|
||||
**Best for**: Advanced users who need precise control over state lifetime
|
||||
**Pros**: Zero-cost abstraction, explicit lifetime management
|
||||
**Cons**: Complex lifetimes, requires deep Rust knowledge, easy to get wrong
|
||||
|
||||
Stores mutable references directly in widget structs using explicit lifetimes.
|
||||
|
||||
## Choosing the Right Pattern
|
||||
|
||||
**For most applications, start with immutable patterns:**
|
||||
|
||||
1. **Simple apps**: Use `immutable-function` for basic rendering with external state management
|
||||
2. **Modern Ratatui**: Use `immutable-shared-ref` for reusable, efficient widgets (recommended)
|
||||
3. **Legacy compatibility**: Use `immutable-consuming` when working with older code patterns
|
||||
|
||||
**Use mutable patterns when widgets need to update state during rendering:**
|
||||
|
||||
1. **Simple mutable state**: Begin with `mutable-function` or `mutable-widget` for prototypes
|
||||
2. **Clean separation**: Use `stateful-widget` when you want to separate widget logic from state
|
||||
3. **Hierarchical widgets**: Use `nested-*` patterns for complex widget relationships
|
||||
4. **Shared state**: Use `refcell` when multiple widgets need the same state
|
||||
5. **Performance critical**: Consider `widget-with-mutable-ref` for advanced lifetime management
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Borrowing issues**: The borrow checker can be challenging with mutable state.
|
||||
[`StatefulWidget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.StatefulWidget.html)
|
||||
often provides the cleanest solution.
|
||||
- **Overengineering**: Don't use complex patterns like `refcell` or `widget-with-mutable-ref`
|
||||
unless you actually need them.
|
||||
- **State organization**: Keep state close to where it's used. Don't pass state through many
|
||||
layers unnecessarily.
|
||||
|
||||
[`component-trait.rs`]: ./src/bin/component-trait.rs
|
||||
[`immutable-consuming.rs`]: ./src/bin/immutable-consuming.rs
|
||||
[`immutable-function.rs`]: ./src/bin/immutable-function.rs
|
||||
[`immutable-shared-ref.rs`]: ./src/bin/immutable-shared-ref.rs
|
||||
[`mutable-widget.rs`]: ./src/bin/mutable-widget.rs
|
||||
[`nested-mutable-widget.rs`]: ./src/bin/nested-mutable-widget.rs
|
||||
[`nested-stateful-widget.rs`]: ./src/bin/nested-stateful-widget.rs
|
||||
[`refcell.rs`]: ./src/bin/refcell.rs
|
||||
[`mutable-function.rs`]: ./src/bin/mutable-function.rs
|
||||
[`stateful-widget.rs`]: ./src/bin/stateful-widget.rs
|
||||
[`widget-with-mutable-ref.rs`]: ./src/bin/widget-with-mutable-ref.rs
|
||||
83
examples/concepts/state/src/bin/component-trait.rs
Normal file
83
examples/concepts/state/src/bin/component-trait.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! # Custom Component Trait Pattern
|
||||
//!
|
||||
//! This example demonstrates using a custom trait instead of the standard `Widget` trait for
|
||||
//! handling mutable state during rendering. This pattern is useful when you want to implement
|
||||
//! consistent behavior across multiple widget types without implementing the `Widget` trait for
|
||||
//! each one.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You're building a widget framework or library with custom behavior
|
||||
//! - You want a consistent API across multiple widget types
|
||||
//! - You need more control over the render method signature
|
||||
//! - You're prototyping widget behavior before standardizing on `Widget` or `StatefulWidget`
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Flexible - you can define custom method signatures
|
||||
//! - Consistent - enforces the same behavior across widget types
|
||||
//! - Simple - no need to understand `StatefulWidget` complexity
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Non-standard - users must learn your custom API instead of Ratatui's standard traits
|
||||
//! - Less discoverable - doesn't integrate with Ratatui's widget ecosystem
|
||||
//! - Limited reuse - can't be used with existing Ratatui functions expecting `Widget`
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The custom `Component` trait allows widgets to mutate their state directly during rendering
|
||||
//! by taking `&mut self` instead of `self`. This is similar to the mutable widget pattern but
|
||||
//! with a custom trait interface.
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the custom component trait pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter widget using a custom `Component` trait and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter = Counter::default();
|
||||
loop {
|
||||
terminal.draw(|frame| counter.render(frame, frame.area()))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A custom trait for components that can render themselves while mutating their state.
|
||||
///
|
||||
/// This trait provides an alternative to the standard `Widget` trait by allowing components to
|
||||
/// take `&mut self`, enabling direct state mutation during rendering.
|
||||
trait Component {
|
||||
/// Render the component to the given area of the frame.
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect);
|
||||
}
|
||||
|
||||
/// A simple counter component that increments its value each time it's rendered.
|
||||
///
|
||||
/// Demonstrates how the custom `Component` trait allows widgets to maintain and mutate
|
||||
/// their own state during the rendering process.
|
||||
#[derive(Default)]
|
||||
struct Counter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Component for Counter {
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
self.count += 1;
|
||||
frame.render_widget(format!("Counter: {count}", count = self.count), area);
|
||||
}
|
||||
}
|
||||
86
examples/concepts/state/src/bin/immutable-consuming.rs
Normal file
86
examples/concepts/state/src/bin/immutable-consuming.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! # Consuming Widget Pattern with Immutable State
|
||||
//!
|
||||
//! This example demonstrates implementing the `Widget` trait directly on the widget type,
|
||||
//! causing it to be consumed when rendered. This was the original pattern in Ratatui and
|
||||
//! is still commonly used, especially for simple widgets that are created fresh each frame.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You're working with existing code that uses this pattern
|
||||
//! - Your widgets are simple and created fresh each frame
|
||||
//! - You want maximum compatibility with older Ratatui code
|
||||
//! - You don't need to reuse widget instances
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Simple - straightforward implementation
|
||||
//! - Compatible - works with all Ratatui versions
|
||||
//! - Familiar - widely used pattern in existing code
|
||||
//! - No borrowing - no need to manage references
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Consuming - widget is destroyed after each render
|
||||
//! - Inefficient - requires reconstruction for repeated use
|
||||
//! - Limited reuse - cannot store and reuse widget instances
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget implements `Widget` directly on the owned type, meaning it's consumed
|
||||
//! when rendered and must be recreated for subsequent renders.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the consuming widget pattern for immutable state rendering.
|
||||
///
|
||||
/// Creates a new counter widget instance each frame, showing how the consuming
|
||||
/// pattern works with immutable state that's managed externally.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut count = 0;
|
||||
loop {
|
||||
terminal.draw(|frame| {
|
||||
// Widget is created fresh each time and consumed when rendered
|
||||
let counter = Counter::new(count);
|
||||
frame.render_widget(counter, frame.area());
|
||||
})?;
|
||||
// State updates happen outside of widget lifecycle
|
||||
count += 1;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A simple counter widget that displays a count value.
|
||||
///
|
||||
/// Implements `Widget` directly on the owned type, meaning the widget is consumed
|
||||
/// when rendered. The count state is managed externally and passed in during construction.
|
||||
struct Counter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
/// Create a new counter widget with the given count.
|
||||
fn new(count: usize) -> Self {
|
||||
Self { count }
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Counter {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// Widget is consumed here - self is moved, not borrowed
|
||||
format!("Counter: {}", self.count).render(area, buf);
|
||||
}
|
||||
}
|
||||
82
examples/concepts/state/src/bin/immutable-function.rs
Normal file
82
examples/concepts/state/src/bin/immutable-function.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
//! # Function-Based Pattern with Immutable State
|
||||
//!
|
||||
//! This example demonstrates using standalone functions for rendering widgets with immutable
|
||||
//! state. This pattern keeps state management completely separate from widget rendering logic,
|
||||
//! making it easy to test and reason about.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You prefer functional programming approaches
|
||||
//! - Your rendering logic is simple and doesn't need complex widget hierarchies
|
||||
//! - You want clear separation between state and rendering
|
||||
//! - You're building simple UIs or prototyping quickly
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Simple - easy to understand and test
|
||||
//! - Pure functions - no side effects in rendering
|
||||
//! - Flexible - can easily compose multiple render functions
|
||||
//! - Clear separation - state management is completely separate from rendering
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Limited - doesn't integrate with Ratatui's widget ecosystem
|
||||
//! - Verbose - requires passing state explicitly to every function
|
||||
//! - No reuse - can't be used with existing Ratatui widget infrastructure
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The function takes immutable references to both the frame and state, ensuring that
|
||||
//! rendering is a pure operation that doesn't modify state.
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the function-based pattern for immutable state rendering.
|
||||
///
|
||||
/// Creates a counter state and renders it using a pure function, incrementing the counter
|
||||
/// in the application loop rather than during rendering.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter_state = Counter::default();
|
||||
loop {
|
||||
terminal.draw(|frame| render_counter(frame, frame.area(), &counter_state))?;
|
||||
// State updates happen outside of rendering
|
||||
counter_state.increment();
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// State for the counter.
|
||||
///
|
||||
/// This state is managed externally and passed to render functions as an immutable reference.
|
||||
#[derive(Default)]
|
||||
struct Counter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
/// Increment the counter value.
|
||||
fn increment(&mut self) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure render function that displays the counter state.
|
||||
///
|
||||
/// Takes immutable references to ensure rendering has no side effects on state.
|
||||
/// This function can be easily tested and composed with other render functions.
|
||||
fn render_counter(frame: &mut Frame, area: Rect, state: &Counter) {
|
||||
frame.render_widget(format!("Counter: {}", state.count), area);
|
||||
}
|
||||
92
examples/concepts/state/src/bin/immutable-shared-ref.rs
Normal file
92
examples/concepts/state/src/bin/immutable-shared-ref.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
//! # Shared Reference Pattern with Immutable State
|
||||
//!
|
||||
//! This example demonstrates implementing the `Widget` trait on a shared reference (`&Widget`)
|
||||
//! with immutable state. This is the recommended pattern for most widgets in modern Ratatui
|
||||
//! applications, as it allows widgets to be reused without being consumed.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You want to reuse widgets across multiple renders
|
||||
//! - Your widget doesn't need to modify its state during rendering
|
||||
//! - You want the benefits of Ratatui's widget ecosystem
|
||||
//! - You're building modern, efficient Ratatui applications
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Reusable - widget can be rendered multiple times without reconstruction
|
||||
//! - Efficient - no cloning or reconstruction needed
|
||||
//! - Standard - integrates with Ratatui's widget ecosystem
|
||||
//! - Modern - follows current Ratatui best practices
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Immutable - cannot modify widget state during rendering
|
||||
//! - External state - requires external state management for dynamic behavior
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget implements `Widget for &Counter`, allowing it to be rendered by reference
|
||||
//! while keeping its internal data immutable during rendering.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the shared reference pattern for immutable widget rendering.
|
||||
///
|
||||
/// Creates a counter widget that can be rendered multiple times by reference,
|
||||
/// with state updates happening outside the widget's render method.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter = Counter::new();
|
||||
loop {
|
||||
terminal.draw(|frame| {
|
||||
// Widget is rendered by reference, can be reused
|
||||
frame.render_widget(&counter, frame.area());
|
||||
})?;
|
||||
// State updates happen outside of rendering
|
||||
counter.increment();
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A counter widget with immutable rendering behavior.
|
||||
///
|
||||
/// Implements `Widget` on a shared reference, allowing the widget to be rendered
|
||||
/// multiple times without being consumed while keeping its data immutable during rendering.
|
||||
struct Counter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
/// Create a new counter.
|
||||
fn new() -> Self {
|
||||
Self { count: 0 }
|
||||
}
|
||||
|
||||
/// Increment the counter value.
|
||||
///
|
||||
/// This method modifies the counter's state outside of the rendering process,
|
||||
/// maintaining the separation between state updates and rendering.
|
||||
fn increment(&mut self) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &Counter {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// Rendering is immutable - no state changes occur here
|
||||
format!("Counter: {}", self.count).render(area, buf);
|
||||
}
|
||||
}
|
||||
67
examples/concepts/state/src/bin/mutable-function.rs
Normal file
67
examples/concepts/state/src/bin/mutable-function.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
//! # Render Function Pattern
|
||||
//!
|
||||
//! This example demonstrates the simplest approach to handling mutable state - using regular
|
||||
//! functions that accept mutable state references. This pattern works well for simple applications
|
||||
//! and prototypes where you don't need the complexity of widget traits.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - Simple applications with minimal state management needs
|
||||
//! - Prototypes and quick experiments
|
||||
//! - When you prefer functional programming over object-oriented approaches
|
||||
//! - Applications where state is naturally managed at the top level
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Extremely simple - no traits to implement or understand
|
||||
//! - Direct and explicit - state flow is obvious
|
||||
//! - Flexible - easy to modify without interface constraints
|
||||
//! - Beginner-friendly - uses basic Rust concepts
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - State must be passed through function parameters
|
||||
//! - Harder to organize as application complexity grows
|
||||
//! - No encapsulation - state management is scattered
|
||||
//! - Less reusable than widget-based approaches
|
||||
//! - Can lead to parameter passing through many layers
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! State is managed at the application level and passed to render functions as needed.
|
||||
//! This approach works well when state is simple and doesn't need complex organization.
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the render function pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter using simple functions and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
terminal.draw(|frame| render(frame, &mut counter))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Renders a counter using a simple function-based approach.
|
||||
///
|
||||
/// Demonstrates the functional approach to state management where state is managed externally
|
||||
/// and passed in as a parameter.
|
||||
fn render(frame: &mut Frame, counter: &mut usize) {
|
||||
*counter += 1;
|
||||
frame.render_widget(format!("Counter: {counter}"), frame.area());
|
||||
}
|
||||
74
examples/concepts/state/src/bin/mutable-widget.rs
Normal file
74
examples/concepts/state/src/bin/mutable-widget.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! # Mutable Widget Pattern
|
||||
//!
|
||||
//! This example demonstrates implementing the `Widget` trait on a mutable reference (`&mut T`)
|
||||
//! to allow direct state mutation during rendering. This is one of the simplest approaches for
|
||||
//! widgets that need to maintain their own state.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You have self-contained widgets with their own state
|
||||
//! - You prefer an object-oriented approach to widget design
|
||||
//! - Your widget's state is simple and doesn't need complex sharing
|
||||
//! - You want to encapsulate state within the widget itself
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Simple and intuitive - state is encapsulated within the widget
|
||||
//! - Familiar pattern for developers coming from OOP backgrounds
|
||||
//! - Direct state access without external state management
|
||||
//! - Works well with Rust's ownership system for simple cases
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Can lead to borrowing challenges in complex scenarios
|
||||
//! - Requires mutable access to the widget, which may not always be available
|
||||
//! - Less flexible than `StatefulWidget` for shared or complex state patterns
|
||||
//! - May require careful lifetime management in nested scenarios
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget implements `Widget` for `&mut Self`, allowing it to mutate its internal state
|
||||
//! during the render call. Each render increments a counter, demonstrating state mutation.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the mutable widget pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter widget using `Widget` for `&mut Self` and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter = Counter::default();
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(&mut counter, frame.area()))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A counter widget that maintains its own state and increments on each render.
|
||||
///
|
||||
/// Demonstrates the mutable widget pattern by implementing `Widget` for `&mut Self`.
|
||||
#[derive(Default)]
|
||||
struct Counter {
|
||||
counter: usize,
|
||||
}
|
||||
|
||||
impl Widget for &mut Counter {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.counter += 1;
|
||||
format!("Counter: {counter}", counter = self.counter).render(area, buf);
|
||||
}
|
||||
}
|
||||
96
examples/concepts/state/src/bin/nested-mutable-widget.rs
Normal file
96
examples/concepts/state/src/bin/nested-mutable-widget.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
//! # Nested Mutable Widget Pattern
|
||||
//!
|
||||
//! This example demonstrates nesting widgets that both need mutable access to their state.
|
||||
//! This pattern is useful when you have a parent-child widget relationship where both widgets
|
||||
//! need to maintain and mutate their own state during rendering.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You have hierarchical widget relationships (parent-child)
|
||||
//! - Each widget needs to maintain its own distinct state
|
||||
//! - You prefer the mutable widget pattern over StatefulWidget
|
||||
//! - Widgets have clear ownership of their state
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Clear hierarchical organization
|
||||
//! - Each widget encapsulates its own state
|
||||
//! - Intuitive parent-child relationships
|
||||
//! - State ownership is explicit
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Complex borrowing scenarios can arise
|
||||
//! - Requires careful lifetime management
|
||||
//! - May lead to borrow checker issues in complex hierarchies
|
||||
//! - Less flexible than StatefulWidget for state sharing
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The parent `App` widget contains a child `Counter` widget. Both implement `Widget` for
|
||||
//! `&mut Self`, allowing them to mutate their respective states during rendering. The parent
|
||||
//! delegates rendering to the child while maintaining its own state structure.
|
||||
|
||||
use ratatui::DefaultTerminal;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the nested mutable widget pattern for mutable state management.
|
||||
///
|
||||
/// Creates a parent-child widget hierarchy using mutable widgets and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
let app = App::default();
|
||||
ratatui::run(|terminal| app.run(terminal))
|
||||
}
|
||||
|
||||
/// The main application widget that contains and manages child widgets.
|
||||
///
|
||||
/// Demonstrates the parent widget in a nested mutable widget hierarchy.
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
counter: Counter,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Run the application with the given terminal.
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> color_eyre::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.counter.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// A counter widget that maintains its own state within a nested hierarchy.
|
||||
///
|
||||
/// Can be used standalone or as a child within other widgets, demonstrating
|
||||
/// how mutable widgets can be composed together.
|
||||
#[derive(Default)]
|
||||
struct Counter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Widget for &mut Counter {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.count += 1;
|
||||
format!("Counter: {count}", count = self.count).render(area, buf);
|
||||
}
|
||||
}
|
||||
103
examples/concepts/state/src/bin/nested-stateful-widget.rs
Normal file
103
examples/concepts/state/src/bin/nested-stateful-widget.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
//! # Nested StatefulWidget Pattern
|
||||
//!
|
||||
//! This example demonstrates composing multiple `StatefulWidget`s in a parent-child hierarchy.
|
||||
//! This pattern is ideal for complex applications where you need clean separation of concerns
|
||||
//! and want to leverage the benefits of the StatefulWidget pattern at multiple levels.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - Complex applications with hierarchical state management needs
|
||||
//! - When you want clean separation between widgets and their state
|
||||
//! - Building composable widget systems
|
||||
//! - Applications that need testable, reusable widget components
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Excellent separation of concerns
|
||||
//! - Highly composable and reusable widgets
|
||||
//! - Easy to test individual widgets and their state
|
||||
//! - Scales well with application complexity
|
||||
//! - Follows idiomatic Ratatui patterns
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - More boilerplate code than simpler patterns
|
||||
//! - Requires understanding of nested state management
|
||||
//! - State structures can become complex
|
||||
//! - May be overkill for simple applications
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The parent `App` widget manages application-level state while delegating specific
|
||||
//! functionality to child widgets like `Counter`. Each widget is responsible for its own
|
||||
//! state type and rendering logic, making the system highly modular.
|
||||
|
||||
use ratatui::DefaultTerminal;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::{StatefulWidget, Widget};
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the nested StatefulWidget pattern for mutable state management.
|
||||
///
|
||||
/// Creates a parent-child widget hierarchy using StatefulWidgets and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(App::run)
|
||||
}
|
||||
|
||||
/// The main application widget using the StatefulWidget pattern.
|
||||
///
|
||||
/// Demonstrates how to compose multiple StatefulWidgets together while coordinating
|
||||
/// between different child widgets.
|
||||
struct App;
|
||||
|
||||
impl App {
|
||||
/// Run the application with the given terminal.
|
||||
fn run(terminal: &mut DefaultTerminal) -> color_eyre::Result<()> {
|
||||
let mut state = AppState { counter: 0 };
|
||||
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_stateful_widget(App, frame.area(), &mut state))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Application state that contains all the state needed by the app and its child widgets.
|
||||
///
|
||||
/// Demonstrates how to organize hierarchical state in the StatefulWidget pattern.
|
||||
struct AppState {
|
||||
counter: usize,
|
||||
}
|
||||
|
||||
impl StatefulWidget for App {
|
||||
type State = AppState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
Counter.render(area, buf, &mut state.counter);
|
||||
}
|
||||
}
|
||||
|
||||
/// A counter widget that uses StatefulWidget for clean state separation.
|
||||
///
|
||||
/// Focuses purely on rendering logic and can be reused with different state instances.
|
||||
struct Counter;
|
||||
|
||||
impl StatefulWidget for Counter {
|
||||
type State = usize;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
*state += 1;
|
||||
format!("Counter: {state}").render(area, buf);
|
||||
}
|
||||
}
|
||||
86
examples/concepts/state/src/bin/refcell.rs
Normal file
86
examples/concepts/state/src/bin/refcell.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! # Interior Mutability Pattern (RefCell)
|
||||
//!
|
||||
//! This example demonstrates using `Rc<RefCell<T>>` for interior mutability, allowing multiple
|
||||
//! widgets to share and mutate the same state. This pattern is useful when you need shared
|
||||
//! mutable state but can't use mutable references due to borrowing constraints.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - Multiple widgets need to access and modify the same state
|
||||
//! - You can't use mutable references due to borrowing constraints
|
||||
//! - You need shared ownership of mutable data
|
||||
//! - Complex widget hierarchies where state needs to be accessed from multiple locations
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Allows shared mutable access to state
|
||||
//! - Works with immutable widget references
|
||||
//! - Enables complex state sharing patterns
|
||||
//! - Can be cloned cheaply (reference counting)
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Runtime borrow checking - potential for panics if you violate borrowing rules
|
||||
//! - Less efficient than compile-time borrow checking
|
||||
//! - Harder to debug when borrow violations occur
|
||||
//! - More complex than simpler state management patterns
|
||||
//! - Can lead to subtle bugs if not used carefully
|
||||
//!
|
||||
//! ## Important Safety Notes
|
||||
//!
|
||||
//! - Only one mutable borrow can exist at a time
|
||||
//! - Violating this rule will cause a panic at runtime
|
||||
//! - Always minimize the scope of borrows to avoid conflicts
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget wraps its state in `Rc<RefCell<T>>`, allowing the state to be shared and mutated
|
||||
//! even when the widget itself is used by value (as required by the `Widget` trait).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::ops::AddAssign;
|
||||
use std::rc::Rc;
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the interior mutability pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter widget using `Rc<RefCell<T>>` and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let counter = Counter::default();
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(counter.clone(), frame.area()))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A counter widget that uses interior mutability for shared state management.
|
||||
///
|
||||
/// Demonstrates how `Rc<RefCell<T>>` enables mutable state access even when the
|
||||
/// widget itself is used by value.
|
||||
#[derive(Default, Clone)]
|
||||
struct Counter {
|
||||
count: Rc<RefCell<usize>>,
|
||||
}
|
||||
|
||||
impl Widget for Counter {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.count.borrow_mut().add_assign(1);
|
||||
format!("Counter: {count}", count = self.count.borrow()).render(area, buf);
|
||||
}
|
||||
}
|
||||
78
examples/concepts/state/src/bin/stateful-widget.rs
Normal file
78
examples/concepts/state/src/bin/stateful-widget.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
//! # StatefulWidget Pattern (Recommended)
|
||||
//!
|
||||
//! This example demonstrates the `StatefulWidget` trait, which is the recommended approach for
|
||||
//! handling mutable state in Ratatui applications. This pattern separates the widget's rendering
|
||||
//! logic from its state, making it more flexible and reusable.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - Most Ratatui applications (this is the recommended default)
|
||||
//! - When building reusable widget libraries
|
||||
//! - When you need clean separation between rendering logic and state
|
||||
//! - When multiple widgets might share similar state structures
|
||||
//! - When you want to follow idiomatic Ratatui patterns
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Clean separation of concerns between widget and state
|
||||
//! - Reusable - the same widget can work with different state instances
|
||||
//! - Testable - state and rendering logic can be tested independently
|
||||
//! - Composable - works well with complex application architectures
|
||||
//! - Idiomatic - follows Ratatui's recommended patterns
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Slightly more verbose than direct mutation patterns
|
||||
//! - Requires understanding of the `StatefulWidget` trait
|
||||
//! - State must be managed externally
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget defines its rendering behavior through `StatefulWidget`, while the state is
|
||||
//! managed separately. This allows the same widget to be used with different state instances
|
||||
//! and makes testing easier.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::{StatefulWidget, Widget};
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the StatefulWidget pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter widget using `StatefulWidget` and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
terminal.draw(|frame| {
|
||||
frame.render_stateful_widget(CounterWidget, frame.area(), &mut counter)
|
||||
})?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A counter widget that uses the StatefulWidget pattern for state management.
|
||||
///
|
||||
/// Demonstrates the separation of rendering logic from state, making the widget reusable
|
||||
/// with different state instances and easier to test.
|
||||
struct CounterWidget;
|
||||
|
||||
impl StatefulWidget for CounterWidget {
|
||||
type State = usize;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
*state += 1;
|
||||
format!("Counter: {state}").render(area, buf);
|
||||
}
|
||||
}
|
||||
84
examples/concepts/state/src/bin/widget-with-mutable-ref.rs
Normal file
84
examples/concepts/state/src/bin/widget-with-mutable-ref.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
//! # Lifetime-Based Mutable References Pattern
|
||||
//!
|
||||
//! This example demonstrates storing mutable references directly in widget structs using explicit
|
||||
//! lifetimes. This is an advanced pattern that provides zero-cost state access but requires
|
||||
//! careful lifetime management.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently
|
||||
//! reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
//! release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You need maximum performance with zero runtime overhead
|
||||
//! - You have a good understanding of Rust lifetimes and borrowing
|
||||
//! - State lifetime is clearly defined and relatively simple
|
||||
//! - You're building performance-critical applications
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Zero runtime cost - no reference counting or runtime borrow checking
|
||||
//! - Compile-time safety - borrow checker ensures memory safety
|
||||
//! - Direct access to state without indirection
|
||||
//! - Maximum performance for state access
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Complex lifetime management - requires deep Rust knowledge
|
||||
//! - Easy to create compilation errors that are hard to understand
|
||||
//! - Inflexible - lifetime constraints can make code harder to refactor
|
||||
//! - Not suitable for beginners - requires advanced Rust skills
|
||||
//! - Widget structs become less reusable due to lifetime constraints
|
||||
//!
|
||||
//! ## Important Considerations
|
||||
//!
|
||||
//! - The widget's lifetime is tied to the state's lifetime
|
||||
//! - You must ensure the state outlives the widget
|
||||
//! - Lifetime annotations can become complex in larger applications
|
||||
//! - Consider simpler patterns unless performance is critical
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget stores a mutable reference to external state, allowing direct access without
|
||||
//! runtime overhead. The widget must be recreated for each render call due to the lifetime
|
||||
//! constraints.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the lifetime-based mutable references pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter widget using mutable references with explicit lifetimes and runs the
|
||||
/// application loop, updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut count = 0;
|
||||
loop {
|
||||
let counter = CounterWidget { count: &mut count };
|
||||
terminal.draw(|frame| frame.render_widget(counter, frame.area()))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A counter widget that holds a mutable reference to external state.
|
||||
///
|
||||
/// Demonstrates the lifetime-based pattern where the widget directly stores a
|
||||
/// mutable reference to external state.
|
||||
struct CounterWidget<'a> {
|
||||
count: &'a mut usize,
|
||||
}
|
||||
|
||||
impl Widget for CounterWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
*self.count += 1;
|
||||
format!("Counter: {count}", count = self.count).render(area, buf);
|
||||
}
|
||||
}
|
||||
8
examples/concepts/state/src/lib.rs
Normal file
8
examples/concepts/state/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! Helper functions for checking if exit keys are pressed
|
||||
use crossterm::event::{self, KeyCode};
|
||||
|
||||
pub fn is_exit_key_pressed() -> std::io::Result<bool> {
|
||||
Ok(event::read()?
|
||||
.as_key_press_event()
|
||||
.is_some_and(|key| matches!(key.code, KeyCode::Esc | KeyCode::Char('q'))))
|
||||
}
|
||||
@@ -4,7 +4,7 @@ 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.5"
|
||||
version = "0.1.0-alpha.6"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
@@ -41,7 +41,7 @@ layout-cache = ["std"]
|
||||
anstyle = ["dep:anstyle"]
|
||||
|
||||
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
|
||||
palette = ["dep:palette"]
|
||||
palette = ["std", "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.
|
||||
@@ -53,7 +53,7 @@ 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"]
|
||||
serde = ["std", "dep:serde", "bitflags/serde", "compact_str/serde"]
|
||||
|
||||
[dependencies]
|
||||
anstyle = { workspace = true, optional = true }
|
||||
|
||||
@@ -29,6 +29,32 @@ Add `ratatui-core` to your `Cargo.toml`:
|
||||
cargo add ratatui-core
|
||||
```
|
||||
|
||||
## Crate Organization
|
||||
|
||||
`ratatui-core` is part of the Ratatui workspace that was modularized in version 0.30.0 to
|
||||
improve compilation times, API stability, and dependency management. This crate provides the
|
||||
foundational types and traits that other crates in the workspace depend on.
|
||||
|
||||
**When to use `ratatui-core`:**
|
||||
|
||||
- Building widget libraries that implement [`Widget`] or [`StatefulWidget`]
|
||||
- Creating lightweight applications that don't need built-in widgets
|
||||
- You want minimal dependencies and faster compilation times
|
||||
- You need maximum API stability (core types change less frequently)
|
||||
|
||||
**When to use the main [`ratatui`] crate:**
|
||||
|
||||
- Building applications that use built-in widgets
|
||||
- You want convenience and don't mind slightly longer compilation times
|
||||
- You need backend implementations and terminal management utilities
|
||||
|
||||
For detailed information about the workspace organization, see [ARCHITECTURE.md].
|
||||
|
||||
[`ratatui`]: https://crates.io/crates/ratatui
|
||||
[`Widget`]: widgets::Widget
|
||||
[`StatefulWidget`]: widgets::StatefulWidget
|
||||
[ARCHITECTURE.md]: https://github.com/ratatui/ratatui/blob/main/ARCHITECTURE.md
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions from the community! Please see our [CONTRIBUTING](../CONTRIBUTING.md)
|
||||
|
||||
@@ -104,8 +104,6 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// Returns the content of the buffer as a slice
|
||||
// https://github.com/rust-lang/rust/issues/139338
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
pub fn content(&self) -> &[Cell] {
|
||||
&self.content
|
||||
}
|
||||
@@ -1237,7 +1235,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Emojis normally contain various characters which should stay part of the Emoji.
|
||||
/// This should work fine by utilizing unicode_segmentation but a testcase is probably helpful
|
||||
/// This should work fine by utilizing `unicode_segmentation` but a testcase is probably helpful
|
||||
/// due to the nature of never perfect Unicode implementations and all of its quirks.
|
||||
#[rstest]
|
||||
// Shrug without gender or skintone. Has a width of 2 like all emojis have.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use compact_str::CompactString;
|
||||
|
||||
use crate::style::{Color, Modifier, Style};
|
||||
use crate::symbols::merge::MergeStrategy;
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Cell {
|
||||
/// The string to be drawn in the cell.
|
||||
@@ -14,7 +15,7 @@ pub struct Cell {
|
||||
/// buffer for short strings.
|
||||
///
|
||||
/// See <https://github.com/ratatui/ratatui/pull/601> for more information.
|
||||
symbol: CompactString,
|
||||
symbol: Option<CompactString>,
|
||||
|
||||
/// The foreground color of the cell.
|
||||
pub fg: Color,
|
||||
@@ -35,35 +36,86 @@ pub struct Cell {
|
||||
|
||||
impl Cell {
|
||||
/// An empty `Cell`
|
||||
pub const EMPTY: Self = Self::new(" ");
|
||||
pub const EMPTY: Self = Self {
|
||||
symbol: None,
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
};
|
||||
|
||||
/// 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
|
||||
/// symbol doesn't 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,
|
||||
symbol: Some(CompactString::const_new(symbol)),
|
||||
..Self::EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the symbol of the cell.
|
||||
///
|
||||
/// If the cell has no symbol, returns a single space character.
|
||||
#[must_use]
|
||||
pub fn symbol(&self) -> &str {
|
||||
self.symbol.as_str()
|
||||
self.symbol.as_ref().map_or(" ", |s| s.as_str())
|
||||
}
|
||||
|
||||
/// Merges the symbol of the cell with the one already on the cell, using the provided
|
||||
/// [`MergeStrategy`].
|
||||
///
|
||||
/// Merges [Box Drawing Unicode block] characters to create a single character representing
|
||||
/// their combination, useful for [border collapsing]. Currently limited to box drawing
|
||||
/// characters, with potential future support for others.
|
||||
///
|
||||
/// Merging may not be perfect due to Unicode limitations; some symbol combinations might not
|
||||
/// produce a valid character. [`MergeStrategy`] defines how to handle such cases, e.g.,
|
||||
/// `Exact` for valid merges only, or `Fuzzy` for close matches.
|
||||
///
|
||||
/// If the cell has no symbol set, it will set the symbol to the provided one rather than
|
||||
/// merging.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui_core::buffer::Cell;
|
||||
/// use ratatui_core::symbols::merge::MergeStrategy;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Cell::new("┘")
|
||||
/// .merge_symbol("┏", MergeStrategy::Exact)
|
||||
/// .symbol(),
|
||||
/// "╆",
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Cell::new("╭")
|
||||
/// .merge_symbol("┘", MergeStrategy::Fuzzy)
|
||||
/// .symbol(),
|
||||
/// "┼",
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// [border collapsing]: https://ratatui.rs/recipes/layout/collapse-borders/
|
||||
/// [Box Drawing Unicode block]: https://en.wikipedia.org/wiki/Box_Drawing
|
||||
pub fn merge_symbol(&mut self, symbol: &str, strategy: MergeStrategy) -> &mut Self {
|
||||
let merged_symbol = self
|
||||
.symbol
|
||||
.as_ref()
|
||||
.map_or(symbol, |s| strategy.merge(s, symbol));
|
||||
self.symbol = Some(CompactString::new(merged_symbol));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell.
|
||||
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
|
||||
self.symbol = CompactString::new(symbol);
|
||||
self.symbol = Some(CompactString::new(symbol));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -71,14 +123,14 @@ impl 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.symbol.get_or_insert_default().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.symbol = Some(CompactString::new(ch.encode_utf8(&mut buf)));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -139,21 +191,49 @@ impl Cell {
|
||||
|
||||
/// 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;
|
||||
*self = Self::EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Self {
|
||||
Self::EMPTY
|
||||
impl PartialEq for Cell {
|
||||
/// Compares two `Cell`s for equality.
|
||||
///
|
||||
/// Note that cells with no symbol (i.e., `Cell::EMPTY`) are considered equal to cells with a
|
||||
/// single space symbol. This is to ensure that empty cells are treated uniformly,
|
||||
/// regardless of how they were created
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// Treat None and Some(" ") as equal
|
||||
let symbols_eq = self.symbol() == other.symbol();
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
let underline_color_eq = self.underline_color == other.underline_color;
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
let underline_color_eq = true;
|
||||
|
||||
symbols_eq
|
||||
&& underline_color_eq
|
||||
&& self.fg == other.fg
|
||||
&& self.bg == other.bg
|
||||
&& self.modifier == other.modifier
|
||||
&& self.skip == other.skip
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Cell {}
|
||||
|
||||
impl core::hash::Hash for Cell {
|
||||
/// Hashes the cell.
|
||||
///
|
||||
/// This treats symbols with Some(" ") as equal to None, so that empty cells are
|
||||
/// treated uniformly, regardless of how they were created.
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.symbol().hash(state);
|
||||
self.fg.hash(state);
|
||||
self.bg.hash(state);
|
||||
#[cfg(feature = "underline-color")]
|
||||
self.underline_color.hash(state);
|
||||
self.modifier.hash(state);
|
||||
self.skip.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +255,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
cell,
|
||||
Cell {
|
||||
symbol: CompactString::const_new("あ"),
|
||||
symbol: Some(CompactString::const_new("あ")),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
|
||||
@@ -1,5 +1,314 @@
|
||||
#![warn(clippy::missing_const_for_fn)]
|
||||
//! Provides types and traits for working with layout and positioning in the terminal.
|
||||
//! Layout and positioning in terminal user interfaces.
|
||||
//!
|
||||
//! This module provides a comprehensive set of types and traits for working with layout and
|
||||
//! positioning in terminal applications. It implements a flexible layout system that allows you to
|
||||
//! divide the terminal screen into different areas using constraints, manage positioning and
|
||||
//! sizing, and handle complex UI arrangements.
|
||||
//!
|
||||
//! The layout system in Ratatui is based on the Cassowary constraint solver algorithm, implemented
|
||||
//! through the [`kasuari`] crate. This allows for sophisticated constraint-based layouts where
|
||||
//! multiple requirements can be satisfied simultaneously, with priorities determining which
|
||||
//! constraints take precedence when conflicts arise.
|
||||
//!
|
||||
//! [`kasuari`]: https://crates.io/crates/kasuari
|
||||
//!
|
||||
//! # Core Concepts
|
||||
//!
|
||||
//! ## Coordinate System
|
||||
//!
|
||||
//! The coordinate system runs left to right, top to bottom, with the origin `(0, 0)` in the top
|
||||
//! left corner of the terminal. The x and y coordinates are represented by `u16` values.
|
||||
//!
|
||||
//! ```text
|
||||
//! x (columns)
|
||||
//! ┌─────────────→
|
||||
//! y │ (0,0)
|
||||
//! │
|
||||
//! (rows)
|
||||
//! ↓
|
||||
//! ```
|
||||
//!
|
||||
//! ## Layout Fundamentals
|
||||
//!
|
||||
//! Layouts form the structural foundation of your terminal UI. The [`Layout`] struct divides
|
||||
//! available screen space into rectangular areas using a constraint-based approach. You define
|
||||
//! multiple constraints for how space should be allocated, and the Cassowary solver determines
|
||||
//! the optimal layout that satisfies as many constraints as possible. These areas can then be
|
||||
//! used to render widgets or nested layouts.
|
||||
//!
|
||||
//! Note that the [`Layout`] struct is not required to create layouts - you can also manually
|
||||
//! calculate and create [`Rect`] areas using simple mathematics to divide up the terminal space
|
||||
//! if you prefer direct control over positioning and sizing.
|
||||
//!
|
||||
//! ## Rectangular Areas
|
||||
//!
|
||||
//! All layout operations work with rectangular areas represented by the [`Rect`] type. A [`Rect`]
|
||||
//! defines a position and size in the terminal, specified by its top-left corner coordinates and
|
||||
//! dimensions.
|
||||
//!
|
||||
//! # Available Types
|
||||
//!
|
||||
//! ## Core Layout Types
|
||||
//!
|
||||
//! - [`Layout`] - The primary layout engine that divides space using constraints and direction
|
||||
//! - [`Rect`] - Represents a rectangular area with position and dimensions
|
||||
//! - [`Constraint`] - Defines how space should be allocated (length, percentage, ratio, etc.)
|
||||
//! - [`Direction`] - Specifies layout orientation (horizontal or vertical)
|
||||
//! - [`Flex`] - Controls space distribution when constraints are satisfied
|
||||
//!
|
||||
//! ## Positioning and Sizing
|
||||
//!
|
||||
//! - [`Position`] - Represents a point in the terminal coordinate system
|
||||
//! - [`Size`] - Represents dimensions (width and height)
|
||||
//! - [`Margin`] - Defines spacing around rectangular areas
|
||||
//! - [`Offset`] - Represents relative movement in the coordinate system
|
||||
//! - [`Spacing`] - Controls spacing or overlap between layout segments
|
||||
//!
|
||||
//! ## Alignment
|
||||
//!
|
||||
//! - [`Alignment`] (alias for [`HorizontalAlignment`]) - Horizontal text/content alignment
|
||||
//! - [`HorizontalAlignment`] - Horizontal alignment options (left, center, right)
|
||||
//! - [`VerticalAlignment`] - Vertical alignment options (top, center, bottom)
|
||||
//!
|
||||
//! ## Iteration Support
|
||||
//!
|
||||
//! - [`Rows`] - Iterator over horizontal rows within a rectangular area
|
||||
//! - [`Columns`] - Iterator over vertical columns within a rectangular area
|
||||
//! - [`Positions`] - Iterator over all positions within a rectangular area
|
||||
//!
|
||||
//! # Quick Start
|
||||
//!
|
||||
//! Here's a simple example of creating a basic layout using the [`Layout`] struct:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui_core::layout::{Constraint, Direction, Layout, Rect};
|
||||
//!
|
||||
//! // Create a terminal area
|
||||
//! let area = Rect::new(0, 0, 80, 24);
|
||||
//!
|
||||
//! // Divide it vertically into two equal parts using Layout
|
||||
//! let layout = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
//! let [top, bottom] = layout.areas(area);
|
||||
//!
|
||||
//! // Now you have two areas: top and bottom
|
||||
//! ```
|
||||
//!
|
||||
//! **Note**: When the number of layout areas is known at compile time, use destructuring
|
||||
//! assignment with descriptive variable names for better readability:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
//!
|
||||
//! let area = Rect::new(0, 0, 80, 24);
|
||||
//! let [header, content, footer] = Layout::vertical([
|
||||
//! Constraint::Length(3),
|
||||
//! Constraint::Fill(1),
|
||||
//! Constraint::Length(1),
|
||||
//! ])
|
||||
//! .areas(area);
|
||||
//! ```
|
||||
//!
|
||||
//! Use [`Layout::split`] when the number of areas is only known at runtime.
|
||||
//!
|
||||
//! Alternatively, you can create layouts manually using mathematics:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui_core::layout::Rect;
|
||||
//!
|
||||
//! // Create a terminal area
|
||||
//! let area = Rect::new(0, 0, 80, 24);
|
||||
//!
|
||||
//! // Manually divide into two equal parts
|
||||
//! let top_half = Rect::new(area.x, area.y, area.width, area.height / 2);
|
||||
//! let bottom_half = Rect::new(
|
||||
//! area.x,
|
||||
//! area.y + area.height / 2,
|
||||
//! area.width,
|
||||
//! area.height / 2,
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! # Layout Examples
|
||||
//!
|
||||
//! ## Basic Vertical Split
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
//!
|
||||
//! let area = Rect::new(0, 0, 80, 24);
|
||||
//! let [header, content, footer] = Layout::vertical([
|
||||
//! Constraint::Length(3), // Header: fixed height
|
||||
//! Constraint::Fill(1), // Content: flexible
|
||||
//! Constraint::Length(1), // Footer: fixed height
|
||||
//! ])
|
||||
//! .areas(area);
|
||||
//! ```
|
||||
//!
|
||||
//! ## Horizontal Sidebar Layout
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
//!
|
||||
//! let area = Rect::new(0, 0, 80, 24);
|
||||
//! let [sidebar, main] = Layout::horizontal([
|
||||
//! Constraint::Length(20), // Sidebar: fixed width
|
||||
//! Constraint::Fill(1), // Main content: flexible
|
||||
//! ])
|
||||
//! .areas(area);
|
||||
//! ```
|
||||
//!
|
||||
//! ## Complex Nested Layout
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
//!
|
||||
//! fn create_complex_layout(area: Rect) -> [Rect; 4] {
|
||||
//! // First, split vertically
|
||||
//! let [header, body, footer] = Layout::vertical([
|
||||
//! Constraint::Length(3), // Header
|
||||
//! Constraint::Fill(1), // Body
|
||||
//! Constraint::Length(1), // Footer
|
||||
//! ])
|
||||
//! .areas(area);
|
||||
//!
|
||||
//! // Then split the body horizontally
|
||||
//! let [sidebar, main] = Layout::horizontal([
|
||||
//! Constraint::Length(20), // Sidebar
|
||||
//! Constraint::Fill(1), // Main
|
||||
//! ])
|
||||
//! .areas(body);
|
||||
//!
|
||||
//! [header, sidebar, main, footer]
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Working with Constraints
|
||||
//!
|
||||
//! [`Constraint`]s define how space is allocated within a layout using the Cassowary constraint
|
||||
//! solver algorithm. The constraint solver attempts to satisfy all constraints simultaneously,
|
||||
//! with priorities determining which constraints take precedence when conflicts arise. Different
|
||||
//! constraint types serve different purposes:
|
||||
//!
|
||||
//! - [`Constraint::Min`] - Minimum size constraint
|
||||
//! - [`Constraint::Max`] - Maximum size constraint
|
||||
//! - [`Constraint::Length`] - Fixed size in character cells
|
||||
//! - [`Constraint::Percentage`] - Relative size as a percentage of available space
|
||||
//! - [`Constraint::Ratio`] - Proportional size using ratios
|
||||
//! - [`Constraint::Fill`] - Proportional fill of remaining space
|
||||
//!
|
||||
//! Constraints are resolved in priority order, with [`Constraint::Min`] having the highest
|
||||
//! priority and [`Constraint::Fill`] having the lowest. The constraint solver will satisfy as
|
||||
//! many constraints as possible while respecting these priorities.
|
||||
//!
|
||||
//! # Flexible Space Distribution
|
||||
//!
|
||||
//! The [`Flex`] enum controls how extra space is distributed when constraints are satisfied:
|
||||
//!
|
||||
//! - [`Flex::Start`] - Align content to the start, leaving excess space at the end
|
||||
//! - [`Flex::End`] - Align content to the end, leaving excess space at the start
|
||||
//! - [`Flex::Center`] - Center content, distributing excess space equally on both sides
|
||||
//! - [`Flex::SpaceBetween`] - Distribute excess space evenly *between* elements, none at the ends
|
||||
//! - [`Flex::SpaceAround`] - Distribute space *around* elements: equal padding on both sides of
|
||||
//! each element; gaps between elements are twice the edge spacing
|
||||
//! - [`Flex::SpaceEvenly`] - Distribute space *evenly*: equal spacing between all elements,
|
||||
//! including before the first and after the last.
|
||||
//! - [`Flex::Legacy`] - Legacy behavior (puts excess space in the last element)
|
||||
//!
|
||||
//! # Positioning and Alignment
|
||||
//!
|
||||
//! Use [`Position`] to represent specific points in the terminal, [`Size`] for dimensions, and the
|
||||
//! alignment types for controlling content positioning within areas:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui_core::layout::{Alignment, Position, Rect, Size};
|
||||
//!
|
||||
//! let pos = Position::new(10, 5);
|
||||
//! let size = Size::new(80, 24);
|
||||
//! let rect = Rect::new(pos.x, pos.y, size.width, size.height);
|
||||
//!
|
||||
//! // Alignment for content within areas
|
||||
//! let center = Alignment::Center;
|
||||
//! ```
|
||||
//!
|
||||
//! # Advanced Features
|
||||
//!
|
||||
//! ## Margins and Spacing
|
||||
//!
|
||||
//! Add spacing around areas using uniform margins or between layout segments using [`Spacing`]:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui_core::layout::{Constraint, Layout, Margin, Rect, Spacing};
|
||||
//!
|
||||
//! let layout = Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)])
|
||||
//! .margin(2) // 2-cell margin on all sides
|
||||
//! .spacing(Spacing::Space(1)); // 1-cell spacing between segments
|
||||
//!
|
||||
//! // For asymmetric margins, use the Rect inner method directly
|
||||
//! let area = Rect::new(0, 0, 80, 24).inner(Margin::new(2, 1));
|
||||
//! ```
|
||||
//!
|
||||
//! ## Area Iteration
|
||||
//!
|
||||
//! Iterate over rows, columns, or all positions within a rectangular area. The `rows()` and
|
||||
//! `columns()` iterators return full [`Rect`] regions that can be used to render widgets or
|
||||
//! passed to other layout methods for more complex nested layouts. The `positions()` iterator
|
||||
//! returns [`Position`] values representing individual cell coordinates:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui_core::buffer::Buffer;
|
||||
//! use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
//! use ratatui_core::widgets::Widget;
|
||||
//!
|
||||
//! let area = Rect::new(0, 0, 20, 10);
|
||||
//! let mut buffer = Buffer::empty(area);
|
||||
//!
|
||||
//! // Renders "Row 0", "Row 1", etc. in each horizontal row
|
||||
//! for (i, row) in area.rows().enumerate() {
|
||||
//! format!("Row {i}").render(row, &mut buffer);
|
||||
//! }
|
||||
//!
|
||||
//! // Renders column indices (0-9 repeating) in each vertical column
|
||||
//! for (i, col) in area.columns().enumerate() {
|
||||
//! format!("{}", i % 10).render(col, &mut buffer);
|
||||
//! }
|
||||
//!
|
||||
//! // Renders position indices (0-9 repeating) at each cell position
|
||||
//! for (i, pos) in area.positions().enumerate() {
|
||||
//! buffer[pos].set_symbol(&format!("{}", i % 10));
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Performance Considerations
|
||||
//!
|
||||
//! The layout system includes optional caching to improve performance for repeated layout
|
||||
//! calculations. Layout caching is enabled by default in the main `ratatui` crate, but requires
|
||||
//! explicitly enabling the `layout-cache` feature when using `ratatui-core` directly. When
|
||||
//! enabled, layout results are cached based on the area and layout configuration.
|
||||
//!
|
||||
//! # Related Documentation
|
||||
//!
|
||||
//! For more detailed information and practical examples:
|
||||
//!
|
||||
//! - [Layout Concepts](https://ratatui.rs/concepts/layout/) - Comprehensive guide to layout
|
||||
//! concepts
|
||||
//! - [Layout Recipes](https://ratatui.rs/recipes/layout/) - Practical layout examples and patterns
|
||||
//! - [Grid Layout Recipe](https://ratatui.rs/recipes/layout/grid/) - Creating grid-based layouts
|
||||
//! - [Center a Widget Recipe](https://ratatui.rs/recipes/layout/center-a-widget/) - Centering
|
||||
//! content
|
||||
//! - [Dynamic Layouts Recipe](https://ratatui.rs/recipes/layout/dynamic/) - Creating responsive
|
||||
//! layouts
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! See the Ratatui repository for complete examples:
|
||||
//!
|
||||
//! - [`constraints`](https://github.com/ratatui/ratatui/blob/main/examples/apps/constraints/) -
|
||||
//! Demonstrates different constraint types
|
||||
//! - [`flex`](https://github.com/ratatui/ratatui/blob/main/examples/apps/flex/) - Shows flex space
|
||||
//! distribution
|
||||
//! - [`layout`](https://github.com/ratatui/ratatui/blob/main/examples/apps/layout/) - Basic layout
|
||||
//! examples
|
||||
|
||||
mod alignment;
|
||||
mod constraint;
|
||||
|
||||
@@ -7,12 +7,19 @@ use strum::{Display, EnumString};
|
||||
/// and libraries, it's unlikely that this alias will be removed in the future.
|
||||
pub type Alignment = HorizontalAlignment;
|
||||
|
||||
/// A type representing horizontal alignment.
|
||||
/// Horizontal content alignment within a layout area.
|
||||
///
|
||||
/// Prior to Ratatui 0.30.0, this type was named `Alignment`. In Ratatui 0.30.0, the name was
|
||||
/// changed to `HorizontalAlignment` to make it more descriptive. The old name is still available as
|
||||
/// an alias for backwards compatibility.
|
||||
///
|
||||
/// This type is used throughout Ratatui to control how content is positioned horizontally within
|
||||
/// available space. It's commonly used with widgets to control text alignment, but can also be
|
||||
/// used in layout calculations.
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum HorizontalAlignment {
|
||||
#[default]
|
||||
Left,
|
||||
@@ -20,8 +27,14 @@ pub enum HorizontalAlignment {
|
||||
Right,
|
||||
}
|
||||
|
||||
/// A type representing vertical alignment.
|
||||
/// Vertical content alignment within a layout area.
|
||||
///
|
||||
/// This type is used to control how content is positioned vertically within available space.
|
||||
/// It complements [`HorizontalAlignment`] to provide full 2D positioning control.
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum VerticalAlignment {
|
||||
#[default]
|
||||
Top,
|
||||
|
||||
@@ -5,10 +5,9 @@ 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.
|
||||
///
|
||||
/// Constraints are the core mechanism for defining how space should be allocated within a
|
||||
/// [`Layout`](crate::layout::Layout). They can specify fixed sizes (length), proportional sizes
|
||||
/// (percentage, ratio), size limits (min, max), or proportional fill values for layout elements.
|
||||
/// 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).
|
||||
@@ -22,6 +21,27 @@ use strum::EnumIs;
|
||||
/// 5. [`Constraint::Ratio`]
|
||||
/// 6. [`Constraint::Fill`]
|
||||
///
|
||||
/// # Size Calculation
|
||||
///
|
||||
/// - [`apply`](Self::apply) - Apply the constraint to a length and return the resulting size
|
||||
///
|
||||
/// # Collection Creation
|
||||
///
|
||||
/// - [`from_lengths`](Self::from_lengths) - Create a collection of length constraints
|
||||
/// - [`from_ratios`](Self::from_ratios) - Create a collection of ratio constraints
|
||||
/// - [`from_percentages`](Self::from_percentages) - Create a collection of percentage constraints
|
||||
/// - [`from_maxes`](Self::from_maxes) - Create a collection of maximum constraints
|
||||
/// - [`from_mins`](Self::from_mins) - Create a collection of minimum constraints
|
||||
/// - [`from_fills`](Self::from_fills) - Create a collection of fill constraints
|
||||
///
|
||||
/// # Conversion and Construction
|
||||
///
|
||||
/// - [`from(u16)`](Self::from) - Create a [`Length`](Self::Length) constraint from `u16`
|
||||
/// - [`from(&Constraint)`](Self::from) - Create from `&Constraint` (copy)
|
||||
/// - [`as_ref()`](Self::as_ref) - Get a reference to self
|
||||
/// - [`default()`](Self::default) - Create default constraint
|
||||
/// ([`Percentage(100)`](Self::Percentage))
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `Constraint` provides helper methods to create lists of constraints from various input formats.
|
||||
@@ -45,7 +65,10 @@ use strum::EnumIs;
|
||||
/// // Create a layout with fill proportional sizes for each element
|
||||
/// let constraints = Constraint::from_fills([1, 2, 1]);
|
||||
/// ```
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, EnumIs)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Constraint {
|
||||
/// Applies a minimum size constraint to the element
|
||||
///
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
/// Defines the direction of a layout.
|
||||
///
|
||||
/// This enumeration is used with [`Layout`](crate::layout::Layout) to specify whether layout
|
||||
/// segments should be arranged horizontally or vertically.
|
||||
///
|
||||
/// - `Horizontal`: Layout segments are arranged side by side (left to right)
|
||||
/// - `Vertical`: Layout segments are arranged top to bottom (default)
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Direction {
|
||||
Horizontal,
|
||||
#[default]
|
||||
|
||||
@@ -5,7 +5,12 @@ 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.
|
||||
/// This enumeration controls the distribution of space when layout constraints are met and there
|
||||
/// is excess space available. The `Flex` option is used with [`Layout`](crate::layout::Layout) to
|
||||
/// control how extra space is distributed among layout segments, which is particularly useful for
|
||||
/// creating responsive layouts that adapt to different terminal sizes.
|
||||
///
|
||||
/// Available options:
|
||||
///
|
||||
/// - `Legacy`: Fills the available space within the container, putting excess space into the last
|
||||
/// element.
|
||||
@@ -14,7 +19,10 @@ use crate::layout::Constraint;
|
||||
/// - `Center`: Centers items within the container.
|
||||
/// - `SpaceBetween`: Adds excess space between each element.
|
||||
/// - `SpaceAround`: Adds excess space around each element.
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash, EnumIs)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
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
|
||||
@@ -155,7 +163,8 @@ pub enum Flex {
|
||||
/// ```
|
||||
SpaceBetween,
|
||||
|
||||
/// Adds excess space around each element.
|
||||
/// Evenly distributes excess space between all elements, including before the first and after
|
||||
/// the last.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -175,6 +184,28 @@ pub enum Flex {
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────┘
|
||||
/// ```
|
||||
SpaceEvenly,
|
||||
|
||||
/// 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)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use alloc::rc::Rc;
|
||||
use alloc::vec::Vec;
|
||||
use core::array::TryFromSliceError;
|
||||
use core::iter;
|
||||
#[cfg(feature = "layout-cache")]
|
||||
use core::num::NonZeroUsize;
|
||||
@@ -75,6 +76,7 @@ std::thread_local! {
|
||||
///
|
||||
/// See the [`Layout::spacing`] method for details on how to use this enum.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Spacing {
|
||||
Space(u16),
|
||||
Overlap(u16),
|
||||
@@ -108,47 +110,57 @@ impl From<i16> for Spacing {
|
||||
}
|
||||
}
|
||||
|
||||
/// The primary layout engine for dividing terminal space using constraints and direction.
|
||||
///
|
||||
/// A layout is a set of constraints that can be applied to a given area to split it into smaller
|
||||
/// ones.
|
||||
/// rectangular areas. This is the core building block for creating structured user interfaces in
|
||||
/// terminal applications.
|
||||
///
|
||||
/// A layout is composed of:
|
||||
/// - a direction (horizontal or vertical)
|
||||
/// - a set of constraints (length, ratio, percentage, fill, min, max)
|
||||
/// - a margin (horizontal and vertical), the space between the edge of the main area and the split
|
||||
/// areas
|
||||
/// - a flex option
|
||||
/// - a spacing option
|
||||
/// - a flex option that controls space distribution
|
||||
/// - a spacing option that controls gaps between segments
|
||||
///
|
||||
/// The algorithm used to compute the layout is based on the [`kasuari`] solver. It is a simple
|
||||
/// linear solver that can be used to solve linear equations and inequalities. In our case, we
|
||||
/// define a set of constraints that are applied to split the provided area into Rects aligned in a
|
||||
/// single direction, and the solver computes the values of the position and sizes that satisfy as
|
||||
/// many of the constraints in order of their priorities.
|
||||
/// The algorithm used to compute the layout is based on the [`kasuari`] solver, a linear constraint
|
||||
/// solver that computes positions and sizes to satisfy as many constraints as possible in order of
|
||||
/// their priorities.
|
||||
///
|
||||
/// When the layout is computed, the result is cached in a thread-local cache, so that subsequent
|
||||
/// calls with the same parameters are faster. The cache is a `LruCache`, and the size of the cache
|
||||
/// can be configured using [`Layout::init_cache()`].
|
||||
///
|
||||
/// # Constructors
|
||||
/// # Construction
|
||||
///
|
||||
/// There are four ways to create a new layout:
|
||||
/// - [`default`](Default::default) - Create a layout with default values (vertical direction, no
|
||||
/// constraints, no margin)
|
||||
/// - [`new`](Self::new) - Create a new layout with a given direction and constraints
|
||||
/// - [`vertical`](Self::vertical) - Create a new vertical layout with the given constraints
|
||||
/// - [`horizontal`](Self::horizontal) - Create a new horizontal layout with the given constraints
|
||||
///
|
||||
/// - [`Layout::default`]: create a new layout with default values
|
||||
/// - [`Layout::new`]: create a new layout with a given direction and constraints
|
||||
/// - [`Layout::vertical`]: create a new vertical layout with the given constraints
|
||||
/// - [`Layout::horizontal`]: create a new horizontal layout with the given constraints
|
||||
/// # Configuration
|
||||
///
|
||||
/// # Setters
|
||||
/// - [`direction`](Self::direction) - Set the direction of the layout
|
||||
/// - [`constraints`](Self::constraints) - Set the constraints of the layout
|
||||
/// - [`margin`](Self::margin) - Set uniform margin on all sides
|
||||
/// - [`horizontal_margin`](Self::horizontal_margin) - Set the horizontal margin of the layout
|
||||
/// - [`vertical_margin`](Self::vertical_margin) - Set the vertical margin of the layout
|
||||
/// - [`flex`](Self::flex) - Set the way space is distributed when constraints are satisfied
|
||||
/// - [`spacing`](Self::spacing) - Set the gap between the constraints of the layout
|
||||
///
|
||||
/// There are several setters to modify the layout:
|
||||
/// # Layout Operations
|
||||
///
|
||||
/// - [`Layout::direction`]: set the direction of the layout
|
||||
/// - [`Layout::constraints`]: set the constraints of the layout
|
||||
/// - [`Layout::margin`]: set the margin of the layout
|
||||
/// - [`Layout::horizontal_margin`]: set the horizontal margin of the layout
|
||||
/// - [`Layout::vertical_margin`]: set the vertical margin of the layout
|
||||
/// - [`Layout::flex`]: set the way the space is distributed when the constraints are satisfied
|
||||
/// - [`Layout::spacing`]: sets the gap between the constraints of the layout
|
||||
/// - [`areas`](Self::areas) - Split area into fixed number of rectangles (compile-time known)
|
||||
/// - [`spacers`](Self::spacers) - Get spacer rectangles between layout areas
|
||||
/// - [`split`](Self::split) - Split area into rectangles (runtime determined count)
|
||||
/// - [`split_with_spacers`](Self::split_with_spacers) - Split area and return both areas and
|
||||
/// spacers
|
||||
///
|
||||
/// # Cache Management
|
||||
///
|
||||
/// - [`init_cache`](Self::init_cache) - Initialize layout cache with custom size
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
@@ -159,22 +171,25 @@ impl From<i16> for Spacing {
|
||||
/// use ratatui_core::widgets::Widget;
|
||||
///
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let layout = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
|
||||
/// let [left, right] = layout.areas(area);
|
||||
/// Text::from("foo").render(left, buf);
|
||||
/// Text::from("bar").render(right, buf);
|
||||
/// let layout = Layout::vertical([Constraint::Length(5), Constraint::Fill(1)]);
|
||||
/// let [top, bottom] = layout.areas(area);
|
||||
/// Text::from("foo").render(top, buf);
|
||||
/// Text::from("bar").render(bottom, buf);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See the `layout`, `flex`, and `constraints` examples in the [Examples] folder for more details
|
||||
/// about how to use layouts.
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// [`kasuari`]: https://crates.io/crates/kasuari
|
||||
/// [Examples]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Layout {
|
||||
direction: Direction,
|
||||
constraints: Vec<Constraint>,
|
||||
@@ -213,7 +228,7 @@ impl Layout {
|
||||
///
|
||||
/// Layout::new(
|
||||
/// Direction::Horizontal,
|
||||
/// [Constraint::Length(5), Constraint::Min(0)],
|
||||
/// [Constraint::Length(5), Constraint::Fill(1)],
|
||||
/// );
|
||||
///
|
||||
/// Layout::new(
|
||||
@@ -245,7 +260,7 @@ impl Layout {
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Constraint, Layout};
|
||||
///
|
||||
/// let layout = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
|
||||
/// let layout = Layout::vertical([Constraint::Length(5), Constraint::Fill(1)]);
|
||||
/// ```
|
||||
pub fn vertical<I>(constraints: I) -> Self
|
||||
where
|
||||
@@ -265,7 +280,7 @@ impl Layout {
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Constraint, Layout};
|
||||
///
|
||||
/// let layout = Layout::horizontal([Constraint::Length(5), Constraint::Min(0)]);
|
||||
/// let layout = Layout::horizontal([Constraint::Length(5), Constraint::Fill(1)]);
|
||||
/// ```
|
||||
pub fn horizontal<I>(constraints: I) -> Self
|
||||
where
|
||||
@@ -297,13 +312,13 @@ impl Layout {
|
||||
///
|
||||
/// let layout = Layout::default()
|
||||
/// .direction(Direction::Horizontal)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Length(5), Constraint::Fill(1)])
|
||||
/// .split(Rect::new(0, 0, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(0, 0, 5, 10), Rect::new(5, 0, 5, 10)]);
|
||||
///
|
||||
/// let layout = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Length(5), Constraint::Fill(1)])
|
||||
/// .split(Rect::new(0, 0, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(0, 0, 10, 5), Rect::new(0, 5, 10, 5)]);
|
||||
/// ```
|
||||
@@ -350,10 +365,10 @@ impl Layout {
|
||||
/// ]
|
||||
/// );
|
||||
///
|
||||
/// Layout::default().constraints([Constraint::Min(0)]);
|
||||
/// Layout::default().constraints(&[Constraint::Min(0)]);
|
||||
/// Layout::default().constraints(vec![Constraint::Min(0)]);
|
||||
/// Layout::default().constraints([Constraint::Min(0)].iter().filter(|_| true));
|
||||
/// Layout::default().constraints([Constraint::Fill(1)]);
|
||||
/// Layout::default().constraints(&[Constraint::Fill(1)]);
|
||||
/// Layout::default().constraints(vec![Constraint::Fill(1)]);
|
||||
/// Layout::default().constraints([Constraint::Fill(1)].iter().filter(|_| true));
|
||||
/// Layout::default().constraints([1, 2, 3].iter().map(|&c| Constraint::Length(c)));
|
||||
/// Layout::default().constraints([1, 2, 3]);
|
||||
/// Layout::default().constraints(vec![1, 2, 3]);
|
||||
@@ -376,7 +391,7 @@ impl Layout {
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// let layout = Layout::default()
|
||||
/// .constraints([Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Fill(1)])
|
||||
/// .margin(2)
|
||||
/// .split(Rect::new(0, 0, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(2, 2, 6, 6)]);
|
||||
@@ -398,7 +413,7 @@ impl Layout {
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// let layout = Layout::default()
|
||||
/// .constraints([Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Fill(1)])
|
||||
/// .horizontal_margin(2)
|
||||
/// .split(Rect::new(0, 0, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(2, 0, 6, 10)]);
|
||||
@@ -417,7 +432,7 @@ impl Layout {
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// let layout = Layout::default()
|
||||
/// .constraints([Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Fill(1)])
|
||||
/// .vertical_margin(2)
|
||||
/// .split(Rect::new(0, 0, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(0, 2, 10, 6)]);
|
||||
@@ -438,8 +453,10 @@ impl Layout {
|
||||
/// - [`Flex::Start`]: The items are aligned to the start of the layout.
|
||||
/// - [`Flex::Center`]: The items are aligned to the center of the layout.
|
||||
/// - [`Flex::End`]: The items are aligned to the end of the layout.
|
||||
/// - [`Flex::SpaceAround`]: The items are evenly distributed with equal space around them.
|
||||
/// - [`Flex::SpaceBetween`]: The items are evenly distributed with equal space between them.
|
||||
/// - [`Flex::SpaceAround`]: The items are evenly distributed with equal space around them,
|
||||
/// except the first and last items, which have half the space on their sides.
|
||||
/// - [`Flex::SpaceEvenly`]: The items are evenly distributed with equal space around them.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -478,7 +495,8 @@ impl Layout {
|
||||
/// passed to this function. See the documentation of the [`Spacing`] enum for more information.
|
||||
///
|
||||
/// Note that if the layout has only one segment, the spacing will not be applied.
|
||||
/// Also, spacing will not be applied for [`Flex::SpaceAround`] and [`Flex::SpaceBetween`]
|
||||
/// Also, spacing will not be applied for [`Flex::SpaceAround`], [`Flex::SpaceEvenly`] and
|
||||
/// [`Flex::SpaceBetween`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -523,17 +541,53 @@ impl Layout {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Layout, Constraint, Rect};
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// let area = Rect::new(0, 0, 10, 10);
|
||||
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
|
||||
/// let [top, main] = layout.areas(area);
|
||||
///
|
||||
/// // or explicitly specify the number of constraints:
|
||||
/// let areas = layout.areas::<2>(area);
|
||||
/// ```
|
||||
pub fn areas<const N: usize>(&self, area: Rect) -> [Rect; N] {
|
||||
let (areas, _) = self.split_with_spacers(area);
|
||||
areas.as_ref().try_into().expect("invalid number of rects")
|
||||
let areas = self.split(area);
|
||||
areas.as_ref().try_into().unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"invalid number of rects: expected {N}, found {}",
|
||||
areas.len()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Split the rect into a number of sub-rects according to the given [`Layout`].
|
||||
///
|
||||
/// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
|
||||
/// `Rc<[Rect]>`.
|
||||
///
|
||||
/// This method requires the number of constraints to be known at compile time. If you don't
|
||||
/// know the number of constraints at compile time, use [`Layout::split`] instead.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the number of constraints is not equal to the length of the returned
|
||||
/// array.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// let area = Rect::new(0, 0, 10, 10);
|
||||
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
/// let [top, main] = layout.try_areas(area)?;
|
||||
///
|
||||
/// // or explicitly specify the number of constraints:
|
||||
/// let areas = layout.try_areas::<2>(area)?;
|
||||
/// # Ok::<(), core::array::TryFromSliceError>(())
|
||||
/// ```
|
||||
pub fn try_areas<const N: usize>(&self, area: Rect) -> Result<[Rect; N], TryFromSliceError> {
|
||||
self.split(area).as_ref().try_into()
|
||||
}
|
||||
|
||||
/// Split the rect into a number of sub-rects according to the given [`Layout`] and return just
|
||||
@@ -557,7 +611,7 @@ impl Layout {
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// let area = Rect::new(0, 0, 10, 10);
|
||||
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
|
||||
/// let [top, main] = layout.areas(area);
|
||||
/// let [before, inbetween, after] = layout.spacers(area);
|
||||
///
|
||||
@@ -572,7 +626,7 @@ impl Layout {
|
||||
.expect("invalid number of rects")
|
||||
}
|
||||
|
||||
/// Wrapper function around the cassowary solver to be able to split a given area into
|
||||
/// Wrapper function around the [`kasuari`] solver to be able to split a given area into
|
||||
/// smaller ones based on the preferred widths or heights and the direction.
|
||||
///
|
||||
/// Note that the constraints are applied to the whole area that is to be split, so using
|
||||
@@ -596,7 +650,7 @@ impl Layout {
|
||||
/// use ratatui_core::layout::{Constraint, Direction, Layout, Rect};
|
||||
/// let layout = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Length(5), Constraint::Fill(1)])
|
||||
/// .split(Rect::new(2, 2, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(2, 2, 10, 5), Rect::new(2, 7, 10, 5)]);
|
||||
///
|
||||
@@ -610,7 +664,7 @@ impl Layout {
|
||||
self.split_with_spacers(area).0
|
||||
}
|
||||
|
||||
/// Wrapper function around the cassowary solver that splits the given area into smaller ones
|
||||
/// Wrapper function around the [`kasuari`] solver that splits the given area into smaller ones
|
||||
/// based on the preferred widths or heights and the direction, with the ability to include
|
||||
/// spacers between the areas.
|
||||
///
|
||||
@@ -629,7 +683,7 @@ impl Layout {
|
||||
///
|
||||
/// let (areas, spacers) = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Length(5), Constraint::Fill(1)])
|
||||
/// .split_with_spacers(Rect::new(2, 2, 10, 10));
|
||||
/// assert_eq!(areas[..], [Rect::new(2, 2, 10, 5), Rect::new(2, 7, 10, 5)]);
|
||||
/// assert_eq!(
|
||||
@@ -672,7 +726,7 @@ impl Layout {
|
||||
}
|
||||
|
||||
fn try_split(&self, area: Rect) -> Result<(Segments, Spacers), AddConstraintError> {
|
||||
// To take advantage of all of cassowary features, we would want to store the `Solver` in
|
||||
// To take advantage of all of [`kasuari`] features, we would want to store the `Solver` in
|
||||
// one of the fields of the Layout struct. And we would want to set it up such that we could
|
||||
// add or remove constraints as and when needed.
|
||||
// The advantage of doing it as described above is that it would allow users to
|
||||
@@ -881,9 +935,48 @@ fn configure_flex_constraints(
|
||||
solver.add_constraint(last.is_empty())?;
|
||||
}
|
||||
}
|
||||
// all spacers are the same size and will grow to fill any remaining space after the
|
||||
// constraints are satisfied
|
||||
|
||||
// All spacers excluding first and last are the same size and will grow to fill
|
||||
// any remaining space after the constraints are satisfied.
|
||||
// All spacers excluding first and last are also twice the size of the first and last
|
||||
// spacers
|
||||
Flex::SpaceAround => {
|
||||
if spacers.len() <= 2 {
|
||||
// If there are two or less spacers, fallback to Flex::SpaceEvenly
|
||||
for (left, right) in spacers.iter().tuple_combinations() {
|
||||
solver.add_constraint(left.has_size(right, SPACER_SIZE_EQ))?;
|
||||
}
|
||||
for spacer in spacers {
|
||||
solver.add_constraint(spacer.has_min_size(spacing, SPACER_SIZE_EQ))?;
|
||||
solver.add_constraint(spacer.has_size(area, SPACE_GROW))?;
|
||||
}
|
||||
} else {
|
||||
// Separate the first and last spacer from the middle ones
|
||||
let (first, rest) = spacers.split_first().unwrap();
|
||||
let (last, middle) = rest.split_last().unwrap();
|
||||
|
||||
// All middle spacers should be equal in size
|
||||
for (left, right) in middle.iter().tuple_combinations() {
|
||||
solver.add_constraint(left.has_size(right, SPACER_SIZE_EQ))?;
|
||||
}
|
||||
|
||||
// First and last spacers should be half the size of any middle spacer
|
||||
if let Some(first_middle) = middle.first() {
|
||||
solver.add_constraint(first_middle.has_double_size(first, SPACER_SIZE_EQ))?;
|
||||
solver.add_constraint(first_middle.has_double_size(last, SPACER_SIZE_EQ))?;
|
||||
}
|
||||
|
||||
// Apply minimum size and growth constraints
|
||||
for spacer in spacers {
|
||||
solver.add_constraint(spacer.has_min_size(spacing, SPACER_SIZE_EQ))?;
|
||||
solver.add_constraint(spacer.has_size(area, SPACE_GROW))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All spacers are the same size and will grow to fill any remaining space after the
|
||||
// constraints are satisfied
|
||||
Flex::SpaceEvenly => {
|
||||
for (left, right) in spacers.iter().tuple_combinations() {
|
||||
solver.add_constraint(left.has_size(right, SPACER_SIZE_EQ))?;
|
||||
}
|
||||
@@ -893,8 +986,9 @@ fn configure_flex_constraints(
|
||||
}
|
||||
}
|
||||
|
||||
// all spacers are the same size and will grow to fill any remaining space after the
|
||||
// constraints are satisfied, but the first and last spacers are zero size
|
||||
// All spacers excluding first and last are the same size and will grow to fill
|
||||
// any remaining space after the constraints are satisfied.
|
||||
// The first and last spacers are zero size.
|
||||
Flex::SpaceBetween => {
|
||||
for (left, right) in spacers_except_first_and_last.iter().tuple_combinations() {
|
||||
solver.add_constraint(left.has_size(right.size(), SPACER_SIZE_EQ))?;
|
||||
@@ -908,6 +1002,7 @@ fn configure_flex_constraints(
|
||||
solver.add_constraint(last.is_empty())?;
|
||||
}
|
||||
}
|
||||
|
||||
Flex::Start => {
|
||||
for spacer in spacers_except_first_and_last {
|
||||
solver.add_constraint(spacer.has_size(spacing_f64, SPACER_SIZE_EQ))?;
|
||||
@@ -948,7 +1043,7 @@ fn configure_flex_constraints(
|
||||
/// │abcdef││abcdef│
|
||||
/// └──────┘└──────┘
|
||||
///
|
||||
/// [Min(0), Fill(2)]
|
||||
/// [Fill(1), Fill(2)]
|
||||
/// ┌──────┐┌────────────┐
|
||||
/// │abcdef││abcdefabcdef│
|
||||
/// └──────┘└────────────┘
|
||||
@@ -1092,6 +1187,14 @@ impl Element {
|
||||
self.size() | EQ(strength) | size.into()
|
||||
}
|
||||
|
||||
fn has_double_size<E: Into<Expression>>(
|
||||
&self,
|
||||
size: E,
|
||||
strength: Strength,
|
||||
) -> kasuari::Constraint {
|
||||
self.size() | EQ(strength) | (size.into() * 2.0)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> kasuari::Constraint {
|
||||
self.size() | EQ(Strength::REQUIRED - Strength::WEAK) | 0.0
|
||||
}
|
||||
@@ -2169,6 +2272,7 @@ mod tests {
|
||||
Flex::End,
|
||||
Flex::Center,
|
||||
Flex::SpaceAround,
|
||||
Flex::SpaceEvenly,
|
||||
Flex::SpaceBetween,
|
||||
] {
|
||||
let widths = Layout::horizontal(&constraints)
|
||||
@@ -2306,26 +2410,37 @@ mod tests {
|
||||
#[case::length_center2(vec![Length(25), Length(25)], vec![25..50, 50..75], Flex::Center)]
|
||||
#[case::length_end2(vec![Length(25), Length(25)], vec![50..75, 75..100], Flex::End)]
|
||||
#[case::length_spacebetween(vec![Length(25), Length(25)], vec![0..25, 75..100], Flex::SpaceBetween)]
|
||||
#[case::length_spacearound(vec![Length(25), Length(25)], vec![17..42, 58..83], Flex::SpaceAround)]
|
||||
#[case::length_spaceevenly(vec![Length(25), Length(25)], vec![17..42, 58..83], Flex::SpaceEvenly)]
|
||||
#[case::length_spacearound(vec![Length(25), Length(25)], vec![13..38, 63..88], Flex::SpaceAround)]
|
||||
#[case::percentage_legacy(vec![Percentage(25), Percentage(25)], vec![0..25, 25..100], Flex::Legacy)]
|
||||
#[case::percentage_start(vec![Percentage(25), Percentage(25)], vec![0..25, 25..50], Flex::Start)]
|
||||
#[case::percentage_center(vec![Percentage(25), Percentage(25)], vec![25..50, 50..75], Flex::Center)]
|
||||
#[case::percentage_end(vec![Percentage(25), Percentage(25)], vec![50..75, 75..100], Flex::End)]
|
||||
#[case::percentage_spacebetween(vec![Percentage(25), Percentage(25)], vec![0..25, 75..100], Flex::SpaceBetween)]
|
||||
#[case::percentage_spacearound(vec![Percentage(25), Percentage(25)], vec![17..42, 58..83], Flex::SpaceAround)]
|
||||
#[case::percentage_spaceevenly(vec![Percentage(25), Percentage(25)], vec![17..42, 58..83], Flex::SpaceEvenly)]
|
||||
#[case::percentage_spacearound(vec![Percentage(25), Percentage(25)], vec![13..38, 63..88], Flex::SpaceAround)]
|
||||
#[case::min_legacy2(vec![Min(25), Min(25)], vec![0..25, 25..100], Flex::Legacy)]
|
||||
#[case::min_start2(vec![Min(25), Min(25)], vec![0..50, 50..100], Flex::Start)]
|
||||
#[case::min_center2(vec![Min(25), Min(25)], vec![0..50, 50..100], Flex::Center)]
|
||||
#[case::min_end2(vec![Min(25), Min(25)], vec![0..50, 50..100], Flex::End)]
|
||||
#[case::min_spacebetween(vec![Min(25), Min(25)], vec![0..50, 50..100], Flex::SpaceBetween)]
|
||||
#[case::min_spaceevenly(vec![Min(25), Min(25)], vec![0..50, 50..100], Flex::SpaceEvenly)]
|
||||
#[case::min_spacearound(vec![Min(25), Min(25)], vec![0..50, 50..100], Flex::SpaceAround)]
|
||||
#[case::max_legacy2(vec![Max(25), Max(25)], vec![0..25, 25..100], Flex::Legacy)]
|
||||
#[case::max_start2(vec![Max(25), Max(25)], vec![0..25, 25..50], Flex::Start)]
|
||||
#[case::max_center2(vec![Max(25), Max(25)], vec![25..50, 50..75], Flex::Center)]
|
||||
#[case::max_end2(vec![Max(25), Max(25)], vec![50..75, 75..100], Flex::End)]
|
||||
#[case::max_spacebetween(vec![Max(25), Max(25)], vec![0..25, 75..100], Flex::SpaceBetween)]
|
||||
#[case::max_spacearound(vec![Max(25), Max(25)], vec![17..42, 58..83], Flex::SpaceAround)]
|
||||
#[case::max_spaceevenly(vec![Max(25), Max(25)], vec![17..42, 58..83], Flex::SpaceEvenly)]
|
||||
#[case::max_spacearound(vec![Max(25), Max(25)], vec![13..38, 63..88], Flex::SpaceAround)]
|
||||
#[case::length_spaced_around(vec![Length(25), Length(25), Length(25)], vec![0..25, 38..63, 75..100], Flex::SpaceBetween)]
|
||||
#[case::one_segment_legacy(vec![Length(50)], vec![0..100], Flex::Legacy)]
|
||||
#[case::one_segment_start(vec![Length(50)], vec![0..50], Flex::Start)]
|
||||
#[case::one_segment_end(vec![Length(50)], vec![50..100], Flex::End)]
|
||||
#[case::one_segment_center(vec![Length(50)], vec![25..75], Flex::Center)]
|
||||
#[case::one_segment_spacebetween(vec![Length(50)], vec![0..100], Flex::SpaceBetween)]
|
||||
#[case::one_segment_spaceevenly(vec![Length(50)], vec![25..75], Flex::SpaceEvenly)]
|
||||
#[case::one_segment_spacearound(vec![Length(50)], vec![25..75], Flex::SpaceAround)]
|
||||
fn flex_constraint(
|
||||
#[case] constraints: Vec<Constraint>,
|
||||
#[case] expected: Vec<Range<u16>>,
|
||||
@@ -2348,7 +2463,8 @@ mod tests {
|
||||
#[case::length_overlap4(vec![(42 , 20) , (61 , 20) , (80 , 20)] , vec![Length(20) , Length(20) , Length(20)] , Flex::End , -1)]
|
||||
#[case::length_overlap5(vec![(0 , 20) , (19 , 20) , (38 , 62)] , vec![Length(20) , Length(20) , Length(20)] , Flex::Legacy , -1)]
|
||||
#[case::length_overlap6(vec![(0 , 20) , (40 , 20) , (80 , 20)] , vec![Length(20) , Length(20) , Length(20)] , Flex::SpaceBetween , -1)]
|
||||
#[case::length_overlap7(vec![(10 , 20) , (40 , 20) , (70 , 20)] , vec![Length(20) , Length(20) , Length(20)] , Flex::SpaceAround , -1)]
|
||||
#[case::length_overlap7(vec![(10 , 20) , (40 , 20) , (70 , 20)] , vec![Length(20) , Length(20) , Length(20)] , Flex::SpaceEvenly , -1)]
|
||||
#[case::length_overlap7(vec![(7 , 20) , (40 , 20) , (73 , 20)] , vec![Length(20) , Length(20) , Length(20)] , Flex::SpaceAround , -1)]
|
||||
fn flex_overlap(
|
||||
#[case] expected: Vec<(u16, u16)>,
|
||||
#[case] constraints: Vec<Constraint>,
|
||||
@@ -2375,7 +2491,8 @@ mod tests {
|
||||
#[case::length_spacing(vec![(36, 20), (58, 20) , (80, 20)], vec![Length(20), Length(20), Length(20)], Flex::End , 2)]
|
||||
#[case::length_spacing(vec![(0 , 20), (22, 20) , (44, 56)], vec![Length(20), Length(20), Length(20)], Flex::Legacy , 2)]
|
||||
#[case::length_spacing(vec![(0 , 20), (40, 20) , (80, 20)], vec![Length(20), Length(20), Length(20)], Flex::SpaceBetween, 2)]
|
||||
#[case::length_spacing(vec![(10, 20), (40, 20) , (70, 20)], vec![Length(20), Length(20), Length(20)], Flex::SpaceAround, 2)]
|
||||
#[case::length_spacing(vec![(10, 20), (40, 20) , (70, 20)], vec![Length(20), Length(20), Length(20)], Flex::SpaceEvenly, 2)]
|
||||
#[case::length_spacing(vec![(7, 20), (40, 20) , (73, 20)], vec![Length(20), Length(20), Length(20)], Flex::SpaceAround, 2)]
|
||||
fn flex_spacing(
|
||||
#[case] expected: Vec<(u16, u16)>,
|
||||
#[case] constraints: Vec<Constraint>,
|
||||
@@ -2432,7 +2549,8 @@ mod tests {
|
||||
#[case::c(vec![(36, 20), (58, 20), (80, 20)], vec![Length(20), Length(20), Length(20)], Flex::End, 2)]
|
||||
#[case::d(vec![(0, 20), (22, 20), (44, 56)], vec![Length(20), Length(20), Length(20)], Flex::Legacy, 2)]
|
||||
#[case::e(vec![(0, 20), (22, 20), (44, 56)], vec![Length(20), Length(20), Length(20)], Flex::Legacy, 2)]
|
||||
#[case::f(vec![(10, 20), (40, 20), (70, 20)], vec![Length(20), Length(20), Length(20)], Flex::SpaceAround, 2)]
|
||||
#[case::f(vec![(10, 20), (40, 20), (70, 20)], vec![Length(20), Length(20), Length(20)], Flex::SpaceEvenly, 2)]
|
||||
#[case::f(vec![(7, 20), (40, 20), (73, 20)], vec![Length(20), Length(20), Length(20)], Flex::SpaceAround, 2)]
|
||||
fn constraint_specification_tests_for_priority_with_spacing(
|
||||
#[case] expected: Vec<(u16, u16)>,
|
||||
#[case] constraints: Vec<Constraint>,
|
||||
@@ -2454,13 +2572,14 @@ mod tests {
|
||||
#[case::prop(vec![(0 , 10), (10, 80), (90 , 10)] , vec![Length(10), Fill(1), Length(10)], Flex::Legacy)]
|
||||
#[case::flex(vec![(0 , 10), (90 , 10)] , vec![Length(10), Length(10)], Flex::SpaceBetween)]
|
||||
#[case::prop(vec![(0 , 27), (27, 10), (37, 26), (63, 10), (73, 27)] , vec![Fill(1), Length(10), Fill(1), Length(10), Fill(1)], Flex::Legacy)]
|
||||
#[case::flex(vec![(27 , 10), (63, 10)] , vec![Length(10), Length(10)], Flex::SpaceAround)]
|
||||
#[case::flex(vec![(27 , 10), (63, 10)] , vec![Length(10), Length(10)], Flex::SpaceEvenly)]
|
||||
#[case::prop(vec![(0 , 10), (10, 10), (20 , 80)] , vec![Length(10), Length(10), Fill(1)], Flex::Legacy)]
|
||||
#[case::flex(vec![(0 , 10), (10, 10)] , vec![Length(10), Length(10)], Flex::Start)]
|
||||
#[case::prop(vec![(0 , 80), (80 , 10), (90, 10)] , vec![Fill(1), Length(10), Length(10)], Flex::Legacy)]
|
||||
#[case::flex(vec![(80 , 10), (90, 10)] , vec![Length(10), Length(10)], Flex::End)]
|
||||
#[case::prop(vec![(0 , 40), (40, 10), (50, 10), (60, 40)] , vec![Fill(1), Length(10), Length(10), Fill(1)], Flex::Legacy)]
|
||||
#[case::flex(vec![(40 , 10), (50, 10)] , vec![Length(10), Length(10)], Flex::Center)]
|
||||
#[case::flex(vec![(20 , 10), (70, 10)] , vec![Length(10), Length(10)], Flex::SpaceAround)]
|
||||
fn fill_vs_flex(
|
||||
#[case] expected: Vec<(u16, u16)>,
|
||||
#[case] constraints: Vec<Constraint>,
|
||||
@@ -2477,8 +2596,9 @@ mod tests {
|
||||
|
||||
#[rstest]
|
||||
#[case::flex0(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::Legacy , 0)]
|
||||
#[case::flex0(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::SpaceAround , 0)]
|
||||
#[case::flex0(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::SpaceEvenly , 0)]
|
||||
#[case::flex0(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::SpaceBetween , 0)]
|
||||
#[case::flex0(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::SpaceAround , 0)]
|
||||
#[case::flex0(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::Start , 0)]
|
||||
#[case::flex0(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::Center , 0)]
|
||||
#[case::flex0(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::End , 0)]
|
||||
@@ -2486,11 +2606,13 @@ mod tests {
|
||||
#[case::flex10(vec![(0 , 45), (55 , 45)] , vec![Fill(1), Fill(1)], Flex::Start , 10)]
|
||||
#[case::flex10(vec![(0 , 45), (55 , 45)] , vec![Fill(1), Fill(1)], Flex::Center , 10)]
|
||||
#[case::flex10(vec![(0 , 45), (55 , 45)] , vec![Fill(1), Fill(1)], Flex::End , 10)]
|
||||
#[case::flex10(vec![(10 , 35), (55 , 35)] , vec![Fill(1), Fill(1)], Flex::SpaceAround , 10)]
|
||||
#[case::flex10(vec![(10 , 35), (55 , 35)] , vec![Fill(1), Fill(1)], Flex::SpaceEvenly , 10)]
|
||||
#[case::flex10(vec![(0 , 45), (55 , 45)] , vec![Fill(1), Fill(1)], Flex::SpaceBetween , 10)]
|
||||
#[case::flex10(vec![(10 , 30), (60 , 30)] , vec![Fill(1), Fill(1)], Flex::SpaceAround , 10)]
|
||||
#[case::flex_length0(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::Legacy , 0)]
|
||||
#[case::flex_length0(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceAround , 0)]
|
||||
#[case::flex_length0(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceEvenly , 0)]
|
||||
#[case::flex_length0(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceBetween , 0)]
|
||||
#[case::flex_length0(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceAround , 0)]
|
||||
#[case::flex_length0(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::Start , 0)]
|
||||
#[case::flex_length0(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::Center , 0)]
|
||||
#[case::flex_length0(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::End , 0)]
|
||||
@@ -2498,8 +2620,9 @@ mod tests {
|
||||
#[case::flex_length10(vec![(0 , 35), (45, 10), (65 , 35)] , vec![Fill(1), Length(10), Fill(1)], Flex::Start , 10)]
|
||||
#[case::flex_length10(vec![(0 , 35), (45, 10), (65 , 35)] , vec![Fill(1), Length(10), Fill(1)], Flex::Center , 10)]
|
||||
#[case::flex_length10(vec![(0 , 35), (45, 10), (65 , 35)] , vec![Fill(1), Length(10), Fill(1)], Flex::End , 10)]
|
||||
#[case::flex_length10(vec![(10 , 25), (45, 10), (65 , 25)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceAround , 10)]
|
||||
#[case::flex_length10(vec![(10 , 25), (45, 10), (65 , 25)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceEvenly , 10)]
|
||||
#[case::flex_length10(vec![(0 , 35), (45, 10), (65 , 35)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceBetween , 10)]
|
||||
#[case::flex_length10(vec![(10 , 15), (45, 10), (75 , 15)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceAround , 10)]
|
||||
fn fill_spacing(
|
||||
#[case] expected: Vec<(u16, u16)>,
|
||||
#[case] constraints: Vec<Constraint>,
|
||||
@@ -2525,24 +2648,28 @@ mod tests {
|
||||
#[case::flex0_4(vec![(0 , 55), (45 , 55)] , vec![Fill(1), Fill(1)], Flex::Start , -10)]
|
||||
#[case::flex0_5(vec![(0 , 55), (45 , 55)] , vec![Fill(1), Fill(1)], Flex::Center , -10)]
|
||||
#[case::flex0_6(vec![(0 , 55), (45 , 55)] , vec![Fill(1), Fill(1)], Flex::End , -10)]
|
||||
#[case::flex0_7(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::SpaceEvenly , -10)]
|
||||
#[case::flex10_1(vec![(0 , 51), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::Legacy , -1)]
|
||||
#[case::flex10_2(vec![(0 , 51), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::Start , -1)]
|
||||
#[case::flex10_3(vec![(0 , 51), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::Center , -1)]
|
||||
#[case::flex10_4(vec![(0 , 51), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::End , -1)]
|
||||
#[case::flex10_5(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::SpaceAround , -1)]
|
||||
#[case::flex10_6(vec![(0 , 51), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::SpaceBetween , -1)]
|
||||
#[case::flex10_7(vec![(0 , 50), (50 , 50)] , vec![Fill(1), Fill(1)], Flex::SpaceEvenly , -1)]
|
||||
#[case::flex_length0_1(vec![(0 , 55), (45, 10), (45 , 55)] , vec![Fill(1), Length(10), Fill(1)], Flex::Legacy , -10)]
|
||||
#[case::flex_length0_2(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceAround , -10)]
|
||||
#[case::flex_length0_3(vec![(0 , 55), (45, 10), (45 , 55)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceBetween , -10)]
|
||||
#[case::flex_length0_4(vec![(0 , 55), (45, 10), (45 , 55)] , vec![Fill(1), Length(10), Fill(1)], Flex::Start , -10)]
|
||||
#[case::flex_length0_5(vec![(0 , 55), (45, 10), (45 , 55)] , vec![Fill(1), Length(10), Fill(1)], Flex::Center , -10)]
|
||||
#[case::flex_length0_6(vec![(0 , 55), (45, 10), (45 , 55)] , vec![Fill(1), Length(10), Fill(1)], Flex::End , -10)]
|
||||
#[case::flex_length0_7(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceEvenly , -10)]
|
||||
#[case::flex_length10_1(vec![(0 , 46), (45, 10), (54 , 46)] , vec![Fill(1), Length(10), Fill(1)], Flex::Legacy , -1)]
|
||||
#[case::flex_length10_2(vec![(0 , 46), (45, 10), (54 , 46)] , vec![Fill(1), Length(10), Fill(1)], Flex::Start , -1)]
|
||||
#[case::flex_length10_3(vec![(0 , 46), (45, 10), (54 , 46)] , vec![Fill(1), Length(10), Fill(1)], Flex::Center , -1)]
|
||||
#[case::flex_length10_4(vec![(0 , 46), (45, 10), (54 , 46)] , vec![Fill(1), Length(10), Fill(1)], Flex::End , -1)]
|
||||
#[case::flex_length10_5(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceAround , -1)]
|
||||
#[case::flex_length10_6(vec![(0 , 46), (45, 10), (54 , 46)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceBetween , -1)]
|
||||
#[case::flex_length10_7(vec![(0 , 45), (45, 10), (55 , 45)] , vec![Fill(1), Length(10), Fill(1)], Flex::SpaceEvenly , -1)]
|
||||
fn fill_overlap(
|
||||
#[case] expected: Vec<(u16, u16)>,
|
||||
#[case] constraints: Vec<Constraint>,
|
||||
@@ -2584,7 +2711,8 @@ mod tests {
|
||||
#[rstest]
|
||||
#[case::spacers(vec![(0, 0), (10, 0), (100, 0)], vec![Length(10), Length(10)], Flex::Legacy)]
|
||||
#[case::spacers(vec![(0, 0), (10, 80), (100, 0)], vec![Length(10), Length(10)], Flex::SpaceBetween)]
|
||||
#[case::spacers(vec![(0, 27), (37, 26), (73, 27)], vec![Length(10), Length(10)], Flex::SpaceAround)]
|
||||
#[case::spacers(vec![(0, 27), (37, 26), (73, 27)], vec![Length(10), Length(10)], Flex::SpaceEvenly)]
|
||||
#[case::spacers(vec![(0, 20), (30, 40), (80, 20)], vec![Length(10), Length(10)], Flex::SpaceAround)]
|
||||
#[case::spacers(vec![(0, 0), (10, 0), (20, 80)], vec![Length(10), Length(10)], Flex::Start)]
|
||||
#[case::spacers(vec![(0, 40), (50, 0), (60, 40)], vec![Length(10), Length(10)], Flex::Center)]
|
||||
#[case::spacers(vec![(0, 80), (90, 0), (100, 0)], vec![Length(10), Length(10)], Flex::End)]
|
||||
@@ -2608,7 +2736,8 @@ mod tests {
|
||||
#[rstest]
|
||||
#[case::spacers(vec![(0, 0), (10, 5), (100, 0)], vec![Length(10), Length(10)], Flex::Legacy, 5)]
|
||||
#[case::spacers(vec![(0, 0), (10, 80), (100, 0)], vec![Length(10), Length(10)], Flex::SpaceBetween, 5)]
|
||||
#[case::spacers(vec![(0, 27), (37, 26), (73, 27)], vec![Length(10), Length(10)], Flex::SpaceAround, 5)]
|
||||
#[case::spacers(vec![(0, 27), (37, 26), (73, 27)], vec![Length(10), Length(10)], Flex::SpaceEvenly, 5)]
|
||||
#[case::spacers(vec![(0, 20), (30, 40), (80, 20)], vec![Length(10), Length(10)], Flex::SpaceAround, 5)]
|
||||
#[case::spacers(vec![(0, 0), (10, 5), (25, 75)], vec![Length(10), Length(10)], Flex::Start, 5)]
|
||||
#[case::spacers(vec![(0, 38), (48, 5), (63, 37)], vec![Length(10), Length(10)], Flex::Center, 5)]
|
||||
#[case::spacers(vec![(0, 75), (85, 5), (100, 0)], vec![Length(10), Length(10)], Flex::End, 5)]
|
||||
@@ -2634,7 +2763,8 @@ mod tests {
|
||||
#[rstest]
|
||||
#[case::spacers_1(vec![(0, 0), (10, 0), (100, 0)], vec![Length(10), Length(10)], Flex::Legacy, -1)]
|
||||
#[case::spacers_2(vec![(0, 0), (10, 80), (100, 0)], vec![Length(10), Length(10)], Flex::SpaceBetween, -1)]
|
||||
#[case::spacers_3(vec![(0, 27), (37, 26), (73, 27)], vec![Length(10), Length(10)], Flex::SpaceAround, -1)]
|
||||
#[case::spacers_3(vec![(0, 27), (37, 26), (73, 27)], vec![Length(10), Length(10)], Flex::SpaceEvenly, -1)]
|
||||
#[case::spacers_3(vec![(0, 20), (30, 40), (80, 20)], vec![Length(10), Length(10)], Flex::SpaceAround, -1)]
|
||||
#[case::spacers_4(vec![(0, 0), (10, 0), (19, 81)], vec![Length(10), Length(10)], Flex::Start, -1)]
|
||||
#[case::spacers_5(vec![(0, 41), (51, 0), (60, 40)], vec![Length(10), Length(10)], Flex::Center, -1)]
|
||||
#[case::spacers_6(vec![(0, 81), (91, 0), (100, 0)], vec![Length(10), Length(10)], Flex::End, -1)]
|
||||
@@ -2660,7 +2790,8 @@ mod tests {
|
||||
#[rstest]
|
||||
#[case::spacers(vec![(0, 0), (0, 100), (100, 0)], vec![Length(10), Length(10)], Flex::Legacy, 200)]
|
||||
#[case::spacers(vec![(0, 0), (0, 100), (100, 0)], vec![Length(10), Length(10)], Flex::SpaceBetween, 200)]
|
||||
#[case::spacers(vec![(0, 33), (33, 34), (67, 33)], vec![Length(10), Length(10)], Flex::SpaceAround, 200)]
|
||||
#[case::spacers(vec![(0, 33), (33, 34), (67, 33)], vec![Length(10), Length(10)], Flex::SpaceEvenly, 200)]
|
||||
#[case::spacers(vec![(0, 25), (25, 50), (75, 25)], vec![Length(10), Length(10)], Flex::SpaceAround, 200)]
|
||||
#[case::spacers(vec![(0, 0), (0, 100), (100, 0)], vec![Length(10), Length(10)], Flex::Start, 200)]
|
||||
#[case::spacers(vec![(0, 0), (0, 100), (100, 0)], vec![Length(10), Length(10)], Flex::Center, 200)]
|
||||
#[case::spacers(vec![(0, 0), (0, 100), (100, 0)], vec![Length(10), Length(10)], Flex::End, 200)]
|
||||
@@ -2703,5 +2834,11 @@ mod tests {
|
||||
.collect::<Vec<(u16, u16)>>();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[timeout(core::time::Duration::from_secs(10))]
|
||||
fn regression_1855() {
|
||||
Layout::horizontal(vec![Min(0); 40]).split(Rect::new(0, 0, 40, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,38 @@
|
||||
use core::fmt;
|
||||
|
||||
/// Represents spacing around rectangular areas.
|
||||
///
|
||||
/// `Margin` defines the horizontal and vertical spacing that should be applied around a rectangular
|
||||
/// area. It's commonly used with [`Layout`](crate::layout::Layout) to add space between the
|
||||
/// layout's boundaries and its contents, or with [`Rect::inner`](crate::layout::Rect::inner) and
|
||||
/// [`Rect::outer`](crate::layout::Rect::outer) to create padded areas.
|
||||
///
|
||||
/// The margin values represent the number of character cells to add on each side. For horizontal
|
||||
/// margin, the space is applied to both the left and right sides. For vertical margin, the space
|
||||
/// is applied to both the top and bottom sides.
|
||||
///
|
||||
/// # Construction
|
||||
///
|
||||
/// - [`new`](Self::new) - Create a new margin with horizontal and vertical spacing
|
||||
/// - [`default`](Default::default) - Create with zero margin
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Margin, Rect};
|
||||
///
|
||||
/// // Create a margin of 2 cells horizontally and 1 cell vertically
|
||||
/// let margin = Margin::new(2, 1);
|
||||
///
|
||||
/// // Apply directly to a rectangle
|
||||
/// let area = Rect::new(0, 0, 80, 24);
|
||||
/// let inner_area = area.inner(margin);
|
||||
///
|
||||
/// // Or use with a layout (which only accepts uniform margins)
|
||||
/// let layout = Layout::vertical([Constraint::Fill(1)]).margin(2);
|
||||
/// ```
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Margin {
|
||||
|
||||
@@ -3,12 +3,26 @@ use core::fmt;
|
||||
|
||||
use crate::layout::Rect;
|
||||
|
||||
/// Position in the terminal
|
||||
/// Position in the terminal coordinate system.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// `Position` is used throughout the layout system to represent specific points in the terminal.
|
||||
/// It can be created from coordinates, tuples, or extracted from rectangular areas.
|
||||
///
|
||||
/// # Construction
|
||||
///
|
||||
/// - [`new`](Self::new) - Create a new position from x and y coordinates
|
||||
/// - [`default`](Default::default) - Create at origin (0, 0)
|
||||
///
|
||||
/// # Conversion
|
||||
///
|
||||
/// - [`from((u16, u16))`](Self::from) - Create from `(u16, u16)` tuple
|
||||
/// - [`from(Rect)`](Self::from) - Create from [`Rect`] (uses top-left corner)
|
||||
/// - [`into((u16, u16))`] - Convert to `(u16, u16)` tuple
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
@@ -23,6 +37,8 @@ use crate::layout::Rect;
|
||||
/// // position can be converted back into the components when needed
|
||||
/// let (x, y) = position.into();
|
||||
/// ```
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Position {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#![warn(missing_docs)]
|
||||
use core::array::TryFromSliceError;
|
||||
use core::cmp::{max, min};
|
||||
use core::fmt;
|
||||
|
||||
@@ -9,10 +10,77 @@ pub use iter::*;
|
||||
|
||||
use super::{Constraint, Flex, Layout};
|
||||
|
||||
/// A Rectangular area.
|
||||
/// A rectangular area in the terminal.
|
||||
///
|
||||
/// 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.
|
||||
/// A `Rect` represents a rectangular region in the terminal coordinate system, defined by its
|
||||
/// top-left corner position and dimensions. This is the fundamental building block for all layout
|
||||
/// operations and widget rendering in Ratatui.
|
||||
///
|
||||
/// Rectangles are used throughout the layout system to define areas where widgets can be rendered.
|
||||
/// They are typically created by [`Layout`] operations that divide terminal space, but can also be
|
||||
/// manually constructed for specific positioning needs.
|
||||
///
|
||||
/// The coordinate system uses the top-left corner as the origin (0, 0), with x increasing to the
|
||||
/// right and y increasing downward. All measurements are in character cells.
|
||||
///
|
||||
/// # Construction and Conversion
|
||||
///
|
||||
/// - [`new`](Self::new) - Create a new rectangle from coordinates and dimensions
|
||||
/// - [`as_position`](Self::as_position) - Convert to a position at the top-left corner
|
||||
/// - [`as_size`](Self::as_size) - Convert to a size representing the dimensions
|
||||
/// - [`from((Position, Size))`](Self::from) - Create from `(Position, Size)` tuple
|
||||
/// - [`from(((u16, u16), (u16, u16)))`](Self::from) - Create from `((u16, u16), (u16, u16))`
|
||||
/// coordinate and dimension tuples
|
||||
/// - [`into((Position, Size))`] - Convert to `(Position, Size)` tuple
|
||||
/// - [`default`](Self::default) - Create a zero-sized rectangle at origin
|
||||
///
|
||||
/// # Geometry and Properties
|
||||
///
|
||||
/// - [`area`](Self::area) - Calculate the total area in character cells
|
||||
/// - [`is_empty`](Self::is_empty) - Check if the rectangle has zero area
|
||||
/// - [`left`](Self::left), [`right`](Self::right), [`top`](Self::top), [`bottom`](Self::bottom) -
|
||||
/// Get edge coordinates
|
||||
///
|
||||
/// # Spatial Operations
|
||||
///
|
||||
/// - [`inner`](Self::inner), [`outer`](Self::outer) - Apply margins to shrink or expand
|
||||
/// - [`offset`](Self::offset) - Move the rectangle by a relative amount
|
||||
/// - [`union`](Self::union) - Combine with another rectangle to create a bounding box
|
||||
/// - [`intersection`](Self::intersection) - Find the overlapping area with another rectangle
|
||||
/// - [`clamp`](Self::clamp) - Constrain the rectangle to fit within another
|
||||
///
|
||||
/// # Positioning and Centering
|
||||
///
|
||||
/// - [`centered_horizontally`](Self::centered_horizontally) - Center horizontally within a
|
||||
/// constraint
|
||||
/// - [`centered_vertically`](Self::centered_vertically) - Center vertically within a constraint
|
||||
/// - [`centered`](Self::centered) - Center both horizontally and vertically
|
||||
///
|
||||
/// # Testing and Iteration
|
||||
///
|
||||
/// - [`contains`](Self::contains) - Check if a position is within the rectangle
|
||||
/// - [`intersects`](Self::intersects) - Check if it overlaps with another rectangle
|
||||
/// - [`rows`](Self::rows) - Iterate over horizontal rows within the rectangle
|
||||
/// - [`columns`](Self::columns) - Iterate over vertical columns within the rectangle
|
||||
/// - [`positions`](Self::positions) - Iterate over all positions within the rectangle
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Position, Rect, Size};
|
||||
///
|
||||
/// // Create a rectangle manually
|
||||
/// let rect = Rect::new(10, 5, 80, 20);
|
||||
/// assert_eq!(rect.x, 10);
|
||||
/// assert_eq!(rect.y, 5);
|
||||
/// assert_eq!(rect.width, 80);
|
||||
/// assert_eq!(rect.height, 20);
|
||||
///
|
||||
/// // Create from position and size
|
||||
/// let rect = Rect::from((Position::new(10, 5), Size::new(80, 20)));
|
||||
/// ```
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rect {
|
||||
@@ -155,6 +223,34 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new `Rect` outside the current one, with the given margin applied on each side.
|
||||
///
|
||||
/// If the margin causes the `Rect`'s bounds to outsdie the range of a `u16`, the `Rect` will
|
||||
/// be truncated to keep the bounds within `u16`. This will cause the size of the `Rect` to
|
||||
/// change.
|
||||
///
|
||||
/// The generated `Rect` may not fit inside the buffer or containing area, so it consider
|
||||
/// constraining the resulting `Rect` with [`Rect::clamp`] before using it.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub const fn outer(self, margin: Margin) -> Self {
|
||||
let x = self.x.saturating_sub(margin.horizontal);
|
||||
let y = self.y.saturating_sub(margin.vertical);
|
||||
let width = self
|
||||
.right()
|
||||
.saturating_add(margin.horizontal)
|
||||
.saturating_sub(x);
|
||||
let height = self
|
||||
.bottom()
|
||||
.saturating_add(margin.vertical)
|
||||
.saturating_sub(y);
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the `Rect` without modifying its size.
|
||||
///
|
||||
/// Moves the `Rect` according to the given offset without modifying its [`width`](Rect::width)
|
||||
@@ -270,17 +366,31 @@ impl Rect {
|
||||
|
||||
/// An iterator over rows within the `Rect`.
|
||||
///
|
||||
/// Each row is a full `Rect` region with height 1 that can be used for rendering widgets
|
||||
/// or as input to further layout methods.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui_core::buffer::Buffer;
|
||||
/// use ratatui_core::layout::Rect;
|
||||
/// use ratatui_core::text::Line;
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
/// use ratatui_core::widgets::Widget;
|
||||
///
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for row in area.rows() {
|
||||
/// Line::raw("Hello, world!").render(row, buf);
|
||||
/// fn render_list(area: Rect, buf: &mut Buffer) {
|
||||
/// // Renders "Item 0", "Item 1", etc. in each row
|
||||
/// for (i, row) in area.rows().enumerate() {
|
||||
/// format!("Item {i}").render(row, buf);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn render_with_nested_layout(area: Rect, buf: &mut Buffer) {
|
||||
/// // Splits each row into left/right areas and renders labels and content
|
||||
/// for (i, row) in area.rows().take(3).enumerate() {
|
||||
/// let [left, right] =
|
||||
/// Layout::horizontal([Constraint::Percentage(30), Constraint::Fill(1)]).areas(row);
|
||||
///
|
||||
/// format!("{i}:").render(left, buf);
|
||||
/// "Content".render(right, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@@ -290,17 +400,20 @@ impl Rect {
|
||||
|
||||
/// An iterator over columns within the `Rect`.
|
||||
///
|
||||
/// Each column is a full `Rect` region with width 1 that can be used for rendering widgets
|
||||
/// or as input to further layout methods.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui_core::buffer::Buffer;
|
||||
/// use ratatui_core::layout::Rect;
|
||||
/// use ratatui_core::text::Text;
|
||||
/// use ratatui_core::widgets::Widget;
|
||||
///
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// fn render_columns(area: Rect, buf: &mut Buffer) {
|
||||
/// // Renders column indices (0-9 repeating) in each column
|
||||
/// for (i, column) in area.columns().enumerate() {
|
||||
/// Text::from(format!("{}", i)).render(column, buf);
|
||||
/// format!("{}", i % 10).render(column, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@@ -311,16 +424,19 @@ impl Rect {
|
||||
/// An iterator over the positions within the `Rect`.
|
||||
///
|
||||
/// The positions are returned in a row-major order (left-to-right, top-to-bottom).
|
||||
/// Each position is a `Position` that represents a single cell coordinate.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui_core::buffer::Buffer;
|
||||
/// use ratatui_core::layout::Rect;
|
||||
/// use ratatui_core::layout::{Position, Rect};
|
||||
/// use ratatui_core::widgets::Widget;
|
||||
///
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for position in area.positions() {
|
||||
/// buf[(position.x, position.y)].set_symbol("x");
|
||||
/// fn render_positions(area: Rect, buf: &mut Buffer) {
|
||||
/// // Renders position indices (0-9 repeating) at each cell position
|
||||
/// for (i, position) in area.positions().enumerate() {
|
||||
/// buf[position].set_symbol(&format!("{}", i % 10));
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@@ -367,9 +483,7 @@ impl Rect {
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn centered_horizontally(self, constraint: Constraint) -> Self {
|
||||
let [area] = Layout::horizontal([constraint])
|
||||
.flex(Flex::Center)
|
||||
.areas(self);
|
||||
let [area] = self.layout(&Layout::horizontal([constraint]).flex(Flex::Center));
|
||||
area
|
||||
}
|
||||
|
||||
@@ -387,9 +501,7 @@ impl Rect {
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn centered_vertically(self, constraint: Constraint) -> Self {
|
||||
let [area] = Layout::vertical([constraint])
|
||||
.flex(Flex::Center)
|
||||
.areas(self);
|
||||
let [area] = self.layout(&Layout::vertical([constraint]).flex(Flex::Center));
|
||||
area
|
||||
}
|
||||
|
||||
@@ -417,6 +529,99 @@ impl Rect {
|
||||
.centered_vertically(vertical_constraint)
|
||||
}
|
||||
|
||||
/// Split the rect into a number of sub-rects according to the given [`Layout`].
|
||||
///
|
||||
/// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
|
||||
/// `Rc<[Rect]>`.
|
||||
///
|
||||
/// This method requires the number of constraints to be known at compile time. If you don't
|
||||
/// know the number of constraints at compile time, use [`Layout::split`] instead.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the number of constraints is not equal to the length of the returned array.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// let area = Rect::new(0, 0, 10, 10);
|
||||
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
/// let [top, main] = area.layout(&layout);
|
||||
/// assert_eq!(top, Rect::new(0, 0, 10, 1));
|
||||
/// assert_eq!(main, Rect::new(0, 1, 10, 9));
|
||||
///
|
||||
/// // or explicitly specify the number of constraints:
|
||||
/// let areas = area.layout::<2>(&layout);
|
||||
/// assert_eq!(areas, [Rect::new(0, 0, 10, 1), Rect::new(0, 1, 10, 9),]);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn layout<const N: usize>(self, layout: &Layout) -> [Self; N] {
|
||||
let areas = layout.split(self);
|
||||
areas.as_ref().try_into().unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"invalid number of rects: expected {N}, found {}",
|
||||
areas.len()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Split the rect into a number of sub-rects according to the given [`Layout`].
|
||||
///
|
||||
/// An ergonomic wrapper around [`Layout::split`] that returns a [`Vec`] of `Rect`s instead of
|
||||
/// `Rc<[Rect]>`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// let area = Rect::new(0, 0, 10, 10);
|
||||
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
/// let areas = area.layout_vec(&layout);
|
||||
/// assert_eq!(areas, vec![Rect::new(0, 0, 10, 1), Rect::new(0, 1, 10, 9),]);
|
||||
/// ```
|
||||
///
|
||||
/// [`Vec`]: alloc::vec::Vec
|
||||
#[must_use]
|
||||
pub fn layout_vec(self, layout: &Layout) -> alloc::vec::Vec<Self> {
|
||||
layout.split(self).as_ref().to_vec()
|
||||
}
|
||||
|
||||
/// Try to split the rect into a number of sub-rects according to the given [`Layout`].
|
||||
///
|
||||
/// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
|
||||
/// `Rc<[Rect]>`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the number of constraints is not equal to the length of the returned
|
||||
/// array.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// let area = Rect::new(0, 0, 10, 10);
|
||||
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
/// let [top, main] = area.try_layout(&layout)?;
|
||||
/// assert_eq!(top, Rect::new(0, 0, 10, 1));
|
||||
/// assert_eq!(main, Rect::new(0, 1, 10, 9));
|
||||
///
|
||||
/// // or explicitly specify the number of constraints:
|
||||
/// let areas = area.try_layout::<2>(&layout)?;
|
||||
/// assert_eq!(areas, [Rect::new(0, 0, 10, 1), Rect::new(0, 1, 10, 9),]);
|
||||
/// # Ok::<(), core::array::TryFromSliceError>(())
|
||||
/// ``````
|
||||
pub fn try_layout<const N: usize>(
|
||||
self,
|
||||
layout: &Layout,
|
||||
) -> Result<[Self; N], TryFromSliceError> {
|
||||
layout.split(self).as_ref().try_into()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -511,6 +716,27 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outer() {
|
||||
// enough space to grow on all sides
|
||||
assert_eq!(
|
||||
Rect::new(100, 200, 10, 20).outer(Margin::new(20, 30)),
|
||||
Rect::new(80, 170, 50, 80)
|
||||
);
|
||||
|
||||
// left / top saturation should truncate the size (10 less on left / top)
|
||||
assert_eq!(
|
||||
Rect::new(10, 20, 10, 20).outer(Margin::new(20, 30)),
|
||||
Rect::new(0, 0, 40, 70),
|
||||
);
|
||||
|
||||
// right / bottom saturation should truncate the size (10 less on bottom / right)
|
||||
assert_eq!(
|
||||
Rect::new(u16::MAX - 20, u16::MAX - 40, 10, 20).outer(Margin::new(20, 30)),
|
||||
Rect::new(u16::MAX - 40, u16::MAX - 70, 40, 70),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset() {
|
||||
assert_eq!(
|
||||
@@ -756,4 +982,54 @@ mod tests {
|
||||
Rect::new(1, 2, 3, 1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout() {
|
||||
let layout = Layout::horizontal([Constraint::Length(3), Constraint::Min(0)]);
|
||||
|
||||
let [a, b] = Rect::new(0, 0, 10, 10).layout(&layout);
|
||||
assert_eq!(a, Rect::new(0, 0, 3, 10));
|
||||
assert_eq!(b, Rect::new(3, 0, 7, 10));
|
||||
|
||||
let areas = Rect::new(0, 0, 10, 10).layout::<2>(&layout);
|
||||
assert_eq!(areas[0], Rect::new(0, 0, 3, 10));
|
||||
assert_eq!(areas[1], Rect::new(3, 0, 7, 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "invalid number of rects: expected 3, found 1")]
|
||||
fn layout_invalid_number_of_rects() {
|
||||
let layout = Layout::horizontal([Constraint::Length(1)]);
|
||||
let [_, _, _] = Rect::new(0, 0, 10, 10).layout(&layout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_vec() {
|
||||
let layout = Layout::horizontal([Constraint::Length(3), Constraint::Min(0)]);
|
||||
|
||||
let areas = Rect::new(0, 0, 10, 10).layout_vec(&layout);
|
||||
assert_eq!(areas[0], Rect::new(0, 0, 3, 10));
|
||||
assert_eq!(areas[1], Rect::new(3, 0, 7, 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_layout() {
|
||||
let layout = Layout::horizontal([Constraint::Length(3), Constraint::Min(0)]);
|
||||
|
||||
let [a, b] = Rect::new(0, 0, 10, 10).try_layout(&layout).unwrap();
|
||||
assert_eq!(a, Rect::new(0, 0, 3, 10));
|
||||
assert_eq!(b, Rect::new(3, 0, 7, 10));
|
||||
|
||||
let areas = Rect::new(0, 0, 10, 10).try_layout::<2>(&layout).unwrap();
|
||||
assert_eq!(areas[0], Rect::new(0, 0, 3, 10));
|
||||
assert_eq!(areas[1], Rect::new(3, 0, 7, 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_layout_invalid_number_of_rects() {
|
||||
let layout = Layout::horizontal([Constraint::Length(1)]);
|
||||
Rect::new(0, 0, 10, 10)
|
||||
.try_layout::<3>(&layout)
|
||||
.unwrap_err();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,38 @@ use core::fmt;
|
||||
|
||||
use crate::layout::Rect;
|
||||
|
||||
/// A simple size struct
|
||||
/// A simple size struct for representing dimensions in the terminal.
|
||||
///
|
||||
/// The width and height are stored as `u16` values and represent the number of columns and rows
|
||||
/// respectively.
|
||||
/// respectively. This is used throughout the layout system to represent dimensions of rectangular
|
||||
/// areas and other layout elements.
|
||||
///
|
||||
/// Size can be created from tuples, extracted from rectangular areas, or constructed directly.
|
||||
/// It's commonly used in conjunction with [`Position`](crate::layout::Position) to define
|
||||
/// rectangular areas.
|
||||
///
|
||||
/// # Construction
|
||||
///
|
||||
/// - [`new`](Self::new) - Create a new size from width and height
|
||||
/// - [`default`](Default::default) - Create with zero dimensions
|
||||
///
|
||||
/// # Conversion
|
||||
///
|
||||
/// - [`from((u16, u16))`](Self::from) - Create from `(u16, u16)` tuple
|
||||
/// - [`from(Rect)`](Self::from) - Create from [`Rect`] (uses width and height)
|
||||
/// - [`into((u16, u16))`] - Convert to `(u16, u16)` tuple
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Rect, Size};
|
||||
///
|
||||
/// let size = Size::new(80, 24);
|
||||
/// let size = Size::from((80, 24));
|
||||
/// let size = Size::from(Rect::new(0, 0, 80, 24));
|
||||
/// ```
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Size {
|
||||
|
||||
@@ -27,6 +27,32 @@
|
||||
//! ```shell
|
||||
//! cargo add ratatui-core
|
||||
//! ```
|
||||
//!
|
||||
//! # Crate Organization
|
||||
//!
|
||||
//! `ratatui-core` is part of the Ratatui workspace that was modularized in version 0.30.0 to
|
||||
//! improve compilation times, API stability, and dependency management. This crate provides the
|
||||
//! foundational types and traits that other crates in the workspace depend on.
|
||||
//!
|
||||
//! **When to use `ratatui-core`:**
|
||||
//!
|
||||
//! - Building widget libraries that implement [`Widget`] or [`StatefulWidget`]
|
||||
//! - Creating lightweight applications that don't need built-in widgets
|
||||
//! - You want minimal dependencies and faster compilation times
|
||||
//! - You need maximum API stability (core types change less frequently)
|
||||
//!
|
||||
//! **When to use the main [`ratatui`] crate:**
|
||||
//!
|
||||
//! - Building applications that use built-in widgets
|
||||
//! - You want convenience and don't mind slightly longer compilation times
|
||||
//! - You need backend implementations and terminal management utilities
|
||||
//!
|
||||
//! For detailed information about the workspace organization, see [ARCHITECTURE.md].
|
||||
//!
|
||||
//! [`ratatui`]: https://crates.io/crates/ratatui
|
||||
//! [`Widget`]: widgets::Widget
|
||||
//! [`StatefulWidget`]: widgets::StatefulWidget
|
||||
//! [ARCHITECTURE.md]: https://github.com/ratatui/ratatui/blob/main/ARCHITECTURE.md
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
@@ -190,6 +190,8 @@ impl From<anstyle::Style> for Style {
|
||||
Self {
|
||||
fg: style.get_fg_color().map(Color::from),
|
||||
bg: style.get_bg_color().map(Color::from),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: style.get_underline_color().map(Color::from),
|
||||
add_modifier: style.get_effects().into(),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -207,6 +209,11 @@ impl From<Style> for anstyle::Style {
|
||||
let bg = anstyle::Color::from(bg);
|
||||
anstyle_style = anstyle_style.bg_color(Some(bg));
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if let Some(underline) = style.underline_color {
|
||||
let underline = anstyle::Color::from(underline);
|
||||
anstyle_style = anstyle_style.underline_color(Some(underline));
|
||||
}
|
||||
anstyle_style = anstyle_style.effects(style.add_modifier.into());
|
||||
anstyle_style
|
||||
}
|
||||
@@ -300,10 +307,13 @@ mod tests {
|
||||
let anstyle_style = anstyle::Style::new()
|
||||
.fg_color(Some(anstyle::Color::Ansi(AnsiColor::Red)))
|
||||
.bg_color(Some(anstyle::Color::Ansi(AnsiColor::Blue)))
|
||||
.underline_color(Some(anstyle::Color::Ansi(AnsiColor::Green)))
|
||||
.effects(Effects::BOLD | Effects::ITALIC);
|
||||
let style = Style::from(anstyle_style);
|
||||
assert_eq!(style.fg, Some(Color::Red));
|
||||
assert_eq!(style.bg, Some(Color::Blue));
|
||||
#[cfg(feature = "underline-color")]
|
||||
assert_eq!(style.underline_color, Some(Color::Green));
|
||||
assert!(style.add_modifier.contains(Modifier::BOLD));
|
||||
assert!(style.add_modifier.contains(Modifier::ITALIC));
|
||||
}
|
||||
@@ -313,6 +323,8 @@ mod tests {
|
||||
let style = Style {
|
||||
fg: Some(Color::Red),
|
||||
bg: Some(Color::Blue),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Some(Color::Green),
|
||||
add_modifier: Modifier::BOLD | Modifier::ITALIC,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -325,6 +337,11 @@ mod tests {
|
||||
anstyle_style.get_bg_color(),
|
||||
Some(anstyle::Color::Ansi(AnsiColor::Blue))
|
||||
);
|
||||
#[cfg(feature = "underline-color")]
|
||||
assert_eq!(
|
||||
anstyle_style.get_underline_color(),
|
||||
Some(anstyle::Color::Ansi(AnsiColor::Green))
|
||||
);
|
||||
assert!(anstyle_style.get_effects().contains(Effects::BOLD));
|
||||
assert!(anstyle_style.get_effects().contains(Effects::ITALIC));
|
||||
}
|
||||
|
||||
@@ -419,6 +419,7 @@ use crate::style::Color;
|
||||
/// 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)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct AccentedPalette {
|
||||
pub c50: Color,
|
||||
pub c100: Color,
|
||||
@@ -441,6 +442,7 @@ pub struct AccentedPalette {
|
||||
/// 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)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct NonAccentedPalette {
|
||||
pub c50: Color,
|
||||
pub c100: Color,
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
|
||||
use crate::style::Color;
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Palette {
|
||||
pub c50: Color,
|
||||
pub c100: Color,
|
||||
|
||||
@@ -9,5 +9,6 @@ pub mod braille;
|
||||
pub mod half_block;
|
||||
pub mod line;
|
||||
pub mod marker;
|
||||
pub mod merge;
|
||||
pub mod scrollbar;
|
||||
pub mod shade;
|
||||
|
||||
@@ -8,19 +8,19 @@ 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,
|
||||
pub struct Set<'a> {
|
||||
pub full: &'a str,
|
||||
pub seven_eighths: &'a str,
|
||||
pub three_quarters: &'a str,
|
||||
pub five_eighths: &'a str,
|
||||
pub half: &'a str,
|
||||
pub three_eighths: &'a str,
|
||||
pub one_quarter: &'a str,
|
||||
pub one_eighth: &'a str,
|
||||
pub empty: &'a str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
impl Default for Set<'_> {
|
||||
fn default() -> Self {
|
||||
NINE_LEVELS
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user