mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-22 16:23:54 +01:00
Unvendor ratatui (#1101)
This commit is contained in:
parent
465faca6d1
commit
f3e707542c
17
Cargo.lock
generated
17
Cargo.lock
generated
@ -99,8 +99,6 @@ dependencies = [
|
||||
"atuin-server",
|
||||
"atuin-server-postgres",
|
||||
"base64 0.21.0",
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
@ -116,6 +114,7 @@ dependencies = [
|
||||
"interim",
|
||||
"itertools",
|
||||
"log",
|
||||
"ratatui",
|
||||
"rpassword",
|
||||
"runtime-format",
|
||||
"semver",
|
||||
@ -124,7 +123,6 @@ dependencies = [
|
||||
"tiny-bip39",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"whoami",
|
||||
]
|
||||
@ -1974,6 +1972,19 @@ dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce841e0486e7c2412c3740168ede33adeba8e154a15107b879d8162d77c7174e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"crossterm",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
|
@ -70,11 +70,7 @@ tiny-bip39 = "1"
|
||||
futures-util = "0.3"
|
||||
fuzzy-matcher = "0.3.7"
|
||||
colored = "2.0.0"
|
||||
|
||||
# ratatui
|
||||
bitflags = "1.3"
|
||||
cassowary = "0.3"
|
||||
unicode-segmentation = "1.2"
|
||||
ratatui = "0.21"
|
||||
|
||||
[dependencies.tracing-subscriber]
|
||||
version = "0.3"
|
||||
|
@ -1,12 +1,12 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::ratatui::{
|
||||
use atuin_client::history::History;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
};
|
||||
use atuin_client::history::History;
|
||||
|
||||
use super::format_duration;
|
||||
|
||||
|
@ -23,15 +23,15 @@ use super::{
|
||||
engines::{SearchEngine, SearchState},
|
||||
history_list::{HistoryList, ListState, PREFIX_LENGTH},
|
||||
};
|
||||
use crate::ratatui::{
|
||||
use crate::{command::client::search::engines, VERSION};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, BorderType, Borders, Paragraph},
|
||||
Frame, Terminal, TerminalOptions, Viewport,
|
||||
};
|
||||
use crate::{command::client::search::engines, VERSION};
|
||||
|
||||
const RETURN_ORIGINAL: usize = usize::MAX;
|
||||
const RETURN_QUERY: usize = usize::MAX - 1;
|
||||
@ -402,7 +402,7 @@ impl State {
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
fn build_help(&mut self) -> Paragraph {
|
||||
let help = Paragraph::new(Text::from(Spans::from(vec![
|
||||
let help = Paragraph::new(Text::from(Line::from(vec![
|
||||
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to exit"),
|
||||
])))
|
||||
|
@ -7,9 +7,6 @@ use eyre::Result;
|
||||
use command::AtuinCmd;
|
||||
mod command;
|
||||
|
||||
#[allow(clippy::all)]
|
||||
mod ratatui;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
static HELP_TEMPLATE: &str = "\
|
||||
|
@ -1,60 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create an issue about a bug you encountered
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi there, sorry `ratatui` is not working as expected.
|
||||
Please fill this bug report conscientiously.
|
||||
A detailed and complete issue is more likely to be processed quickly.
|
||||
-->
|
||||
|
||||
## Description
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
|
||||
## To Reproduce
|
||||
<!--
|
||||
Try to reduce the issue to a simple code sample exhibiting the problem.
|
||||
Ideally, fork the project and add a test or an example.
|
||||
-->
|
||||
|
||||
|
||||
## Expected behavior
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If applicable, add screenshots, gifs or videos to help explain your problem.
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
<!--
|
||||
Add a description of the systems where you are observing the issue. For example:
|
||||
- OS: Linux
|
||||
- Terminal Emulator: xterm
|
||||
- Font: Inconsolata (Patched)
|
||||
- Crate version: 0.7
|
||||
- Backend: termion
|
||||
-->
|
||||
|
||||
- OS:
|
||||
- Terminal Emulator:
|
||||
- Font:
|
||||
- Crate version:
|
||||
- Backend:
|
||||
|
||||
## Additional context
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
If you already looked into the issue, include all the leads you have explored.
|
||||
-->
|
@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
@ -1,32 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
<!--
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
-->
|
||||
|
||||
## Solution
|
||||
<!--
|
||||
A clear and concise description of what you want to happen.
|
||||
Things to consider:
|
||||
- backward compatibility
|
||||
- ease of use of the API (https://rust-lang.github.io/api-guidelines/)
|
||||
- consistency with the rest of the crate
|
||||
-->
|
||||
|
||||
## Alternatives
|
||||
<!--
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
## Additional context
|
||||
<!--
|
||||
Add any other context or screenshots about the feature request here.
|
||||
-->
|
19
atuin/src/ratatui/.github/workflows/cd.yml
vendored
19
atuin/src/ratatui/.github/workflows/cd.yml
vendored
@ -1,19 +0,0 @@
|
||||
name: Continuous Deployment
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish on crates.io
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Publish
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: publish
|
||||
args: --token ${{ secrets.CARGO_TOKEN }}
|
76
atuin/src/ratatui/.github/workflows/ci.yml
vendored
76
atuin/src/ratatui/.github/workflows/ci.yml
vendored
@ -1,76 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
name: CI
|
||||
|
||||
env:
|
||||
CI_CARGO_MAKE_VERSION: 0.35.16
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
rust: ["1.59.0", "stable"]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
triple: x86_64-unknown-linux-musl
|
||||
- os: windows-latest
|
||||
triple: x86_64-pc-windows-msvc
|
||||
- os: macos-latest
|
||||
triple: x86_64-apple-darwin
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: hecrj/setup-rust-action@50a120e4d34903c2c1383dec0e9b1d349a9cc2b1
|
||||
with:
|
||||
rust-version: ${{ matrix.rust }}
|
||||
components: rustfmt,clippy
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install cargo-make on Linux or macOS
|
||||
if: ${{ runner.os != 'windows' }}
|
||||
shell: bash
|
||||
run: |
|
||||
curl -LO 'https://github.com/sagiegurari/cargo-make/releases/download/${{ env.CI_CARGO_MAKE_VERSION }}/cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
|
||||
unzip 'cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
|
||||
cp 'cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}/cargo-make' ~/.cargo/bin/
|
||||
cargo make --version
|
||||
- name: Install cargo-make on Windows
|
||||
if: ${{ runner.os == 'windows' }}
|
||||
shell: bash
|
||||
run: |
|
||||
# `cargo-make-v0.35.16-{target}/` directory is created on Linux and macOS, but it is not creatd on Windows.
|
||||
mkdir cargo-make-temporary
|
||||
cd cargo-make-temporary
|
||||
curl -LO 'https://github.com/sagiegurari/cargo-make/releases/download/${{ env.CI_CARGO_MAKE_VERSION }}/cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
|
||||
unzip 'cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
|
||||
cp cargo-make.exe ~/.cargo/bin/
|
||||
cd ..
|
||||
cargo make --version
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: "Check conventional commits"
|
||||
uses: crate-ci/committed@master
|
||||
with:
|
||||
args: "-vv"
|
||||
commits: "HEAD"
|
||||
- name: "Check typos"
|
||||
uses: crate-ci/typos@master
|
6
atuin/src/ratatui/.gitignore
vendored
6
atuin/src/ratatui/.gitignore
vendored
@ -1,6 +0,0 @@
|
||||
target
|
||||
Cargo.lock
|
||||
*.log
|
||||
*.rs.rustfmt
|
||||
.gdb_history
|
||||
.idea/
|
@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Florian Dehau
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -1,136 +0,0 @@
|
||||
# ratatui
|
||||
|
||||
An actively maintained `tui`-rs fork.
|
||||
|
||||
[![Build Status](https://github.com/tui-rs-revival/ratatui/workflows/CI/badge.svg)](https://github.com/tui-rs-revival/ratatui/actions?query=workflow%3ACI+)
|
||||
[![Crate Status](https://img.shields.io/crates/v/ratatui.svg)](https://crates.io/crates/ratatui)
|
||||
[![Docs Status](https://docs.rs/ratatui/badge.svg)](https://docs.rs/crate/ratatui/)
|
||||
|
||||
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
|
||||
|
||||
# Install
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tui = { package = "ratatui" }
|
||||
```
|
||||
|
||||
# What is this fork?
|
||||
|
||||
This fork was created to continue maintenance on the original TUI project. The original maintainer had created an [issue](https://github.com/fdehau/tui-rs/issues/654) explaining how he couldn't find time to continue development, which led to us creating this fork.
|
||||
|
||||
With that in mind, **we the community** look forward to continuing the work started by [**Florian Dehau.**](https://github.com/fdehau) :rocket:
|
||||
|
||||
In order to organize ourselves, we currently use a [discord server](https://discord.gg/pMCEU9hNEj), feel free to join and come chat ! There are also plans to implement a [matrix](https://matrix.org/) bridge in the near future.
|
||||
**Discord is not a MUST to contribute,** we follow a pretty standard github centered open source workflow keeping the most important conversations on github, open an issue or PR and it will be addressed. :smile:
|
||||
|
||||
Please make sure you read the updated contributing guidelines, especially if you are interested in working on a PR or issue opened in the previous repository.
|
||||
|
||||
# Introduction
|
||||
|
||||
`ratatui`-rs is a [Rust](https://www.rust-lang.org) library to build rich terminal
|
||||
user interfaces and dashboards. It is heavily inspired by the `Javascript`
|
||||
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
|
||||
`Go` library [termui](https://github.com/gizak/termui).
|
||||
|
||||
The library supports multiple backends:
|
||||
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||
- [termion](https://github.com/ticki/termion)
|
||||
|
||||
The library is based on the principle of immediate rendering with intermediate
|
||||
buffers. This means that at each new frame you should build all widgets that are
|
||||
supposed to be part of the UI. While providing a great flexibility for rich and
|
||||
interactive UI, this may introduce overhead for highly dynamic content. So, the
|
||||
implementation try to minimize the number of ansi escapes sequences generated to
|
||||
draw the updated UI. In practice, given the speed of `Rust` the overhead rather
|
||||
comes from the terminal emulator than the library itself.
|
||||
|
||||
Moreover, the library does not provide any input handling nor any event system and
|
||||
you may rely on the previously cited libraries to achieve such features.
|
||||
|
||||
## Rust version requirements
|
||||
|
||||
Since version 0.17.0, `ratatui` requires **rustc version 1.59.0 or greater**.
|
||||
|
||||
# Documentation
|
||||
|
||||
The documentation can be found on [docs.rs.](https://docs.rs/ratatui)
|
||||
|
||||
# Demo
|
||||
|
||||
The demo shown in the gif can be run with all available backends.
|
||||
|
||||
```
|
||||
# crossterm
|
||||
cargo run --example demo --release -- --tick-rate 200
|
||||
# termion
|
||||
cargo run --example demo --no-default-features --features=termion --release -- --tick-rate 200
|
||||
```
|
||||
|
||||
where `tick-rate` is the UI refresh rate in ms.
|
||||
|
||||
The UI code is in [examples/demo/ui.rs](https://github.com/tui-rs-revival/ratatui/blob/main/examples/demo/ui.rs) while the
|
||||
application state is in [examples/demo/app.rs](https://github.com/tui-rs-revival/ratatui/blob/main/examples/demo/app.rs).
|
||||
|
||||
If the user interface contains glyphs that are not displayed correctly by your terminal, you may want to run
|
||||
the demo without those symbols:
|
||||
|
||||
```
|
||||
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
|
||||
```
|
||||
|
||||
# Widgets
|
||||
|
||||
## Built in
|
||||
|
||||
The library comes with the following list of widgets:
|
||||
|
||||
- [Block](https://github.com/tui-rs-revival/ratatui/blob/main/examples/block.rs)
|
||||
- [Gauge](https://github.com/tui-rs-revival/ratatui/blob/main/examples/gauge.rs)
|
||||
- [Sparkline](https://github.com/tui-rs-revival/ratatui/blob/main/examples/sparkline.rs)
|
||||
- [Chart](https://github.com/tui-rs-revival/ratatui/blob/main/examples/chart.rs)
|
||||
- [BarChart](https://github.com/tui-rs-revival/ratatui/blob/main/examples/barchart.rs)
|
||||
- [List](https://github.com/tui-rs-revival/ratatui/blob/main/examples/list.rs)
|
||||
- [Table](https://github.com/tui-rs-revival/ratatui/blob/main/examples/table.rs)
|
||||
- [Paragraph](https://github.com/tui-rs-revival/ratatui/blob/main/examples/paragraph.rs)
|
||||
- [Canvas (with line, point cloud, map)](https://github.com/tui-rs-revival/ratatui/blob/main/examples/canvas.rs)
|
||||
- [Tabs](https://github.com/tui-rs-revival/ratatui/blob/main/examples/tabs.rs)
|
||||
|
||||
Click on each item to see the source of the example. Run the examples with with
|
||||
cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
|
||||
|
||||
You can run all examples by running `cargo make run-examples` (require
|
||||
`cargo-make` that can be installed with `cargo install cargo-make`).
|
||||
|
||||
### Third-party libraries, bootstrapping templates and widgets
|
||||
|
||||
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to `tui::text::Text`
|
||||
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to `tui::style::Color`
|
||||
- [rust-tui-template](https://github.com/orhun/rust-tui-template) — A template for bootstrapping a Rust TUI application with Tui-rs & crossterm
|
||||
- [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
|
||||
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for Tui-rs + Crossterm apps
|
||||
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
|
||||
- [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
|
||||
- [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
|
||||
- [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications with a React/Elm inspired approach
|
||||
- [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for Tui-realm
|
||||
- [tui tree widget](https://github.com/EdJoPaTo/tui-rs-tree-widget) — Tree Widget for Tui-rs
|
||||
- [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple windows and their rendering
|
||||
- [tui-textarea](https://github.com/rhysd/tui-textarea): Simple yet powerful multi-line text editor widget supporting several key shortcuts, undo/redo, text search, etc.
|
||||
- [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget): Widget for tree data structures.
|
||||
- [tui-input](https://github.com/sayanarijit/tui-input): TUI input library supporting multiple backends and tui-rs.
|
||||
|
||||
# Apps
|
||||
|
||||
Check out the list of [close to 40 apps](./APPS.md) using `ratatui`!
|
||||
|
||||
# Alternatives
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an
|
||||
alternative solution to build text user interfaces in Rust.
|
||||
|
||||
# License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
@ -1,241 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
buffer: W,
|
||||
}
|
||||
|
||||
impl<W> CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
pub fn new(buffer: W) -> CrosstermBackend<W> {
|
||||
CrosstermBackend { buffer }
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.buffer.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Backend for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
map_error(queue!(self.buffer, MoveTo(x, y)))?;
|
||||
}
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
let diff = ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier,
|
||||
};
|
||||
diff.queue(&mut self.buffer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg {
|
||||
let color = CColor::from(cell.fg);
|
||||
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
let color = CColor::from(cell.bg);
|
||||
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
|
||||
bg = cell.bg;
|
||||
}
|
||||
|
||||
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
|
||||
}
|
||||
|
||||
map_error(queue!(
|
||||
self.buffer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset)
|
||||
))
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Hide))
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Show))
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
crossterm::cursor::position()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, MoveTo(x, y)))
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
map_error(execute!(
|
||||
self.buffer,
|
||||
Clear(match clear_type {
|
||||
ClearType::All => crossterm::terminal::ClearType::All,
|
||||
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
|
||||
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
|
||||
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
|
||||
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
map_error(queue!(self.buffer, Print("\n")))?;
|
||||
}
|
||||
self.buffer.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let (width, height) =
|
||||
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
Ok(Rect::new(0, 0, width, height))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
|
||||
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
impl From<Color> for CColor {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
Color::Reset => CColor::Reset,
|
||||
Color::Black => CColor::Black,
|
||||
Color::Red => CColor::DarkRed,
|
||||
Color::Green => CColor::DarkGreen,
|
||||
Color::Yellow => CColor::DarkYellow,
|
||||
Color::Blue => CColor::DarkBlue,
|
||||
Color::Magenta => CColor::DarkMagenta,
|
||||
Color::Cyan => CColor::DarkCyan,
|
||||
Color::Gray => CColor::Grey,
|
||||
Color::DarkGray => CColor::DarkGrey,
|
||||
Color::LightRed => CColor::Red,
|
||||
Color::LightGreen => CColor::Green,
|
||||
Color::LightBlue => CColor::Blue,
|
||||
Color::LightYellow => CColor::Yellow,
|
||||
Color::LightMagenta => CColor::Magenta,
|
||||
Color::LightCyan => CColor::Cyan,
|
||||
Color::White => CColor::White,
|
||||
Color::Indexed(i) => CColor::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
}
|
||||
|
||||
impl ModifierDiff {
|
||||
fn queue<W>(&self, mut w: W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
//use crossterm::Attribute;
|
||||
let removed = self.from - self.to;
|
||||
if removed.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
|
||||
}
|
||||
if removed.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
}
|
||||
}
|
||||
if removed.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
|
||||
}
|
||||
if removed.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
|
||||
}
|
||||
if removed.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
}
|
||||
if removed.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
|
||||
}
|
||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
|
||||
}
|
||||
|
||||
let added = self.to - self.from;
|
||||
if added.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
|
||||
}
|
||||
if added.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
|
||||
}
|
||||
if added.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
|
||||
}
|
||||
if added.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
|
||||
}
|
||||
if added.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
}
|
||||
if added.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
|
||||
}
|
||||
if added.contains(Modifier::SLOW_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
|
||||
}
|
||||
if added.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
use crate::ratatui::buffer::Cell;
|
||||
use crate::ratatui::layout::Rect;
|
||||
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termion")]
|
||||
pub use self::termion::TermionBackend;
|
||||
|
||||
mod crossterm;
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ClearType {
|
||||
All,
|
||||
AfterCursor,
|
||||
BeforeCursor,
|
||||
CurrentLine,
|
||||
UntilNewLine,
|
||||
}
|
||||
|
||||
pub trait Backend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||
|
||||
/// Insert `n` line breaks to the terminal screen
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
// to get around the unused warning
|
||||
let _n = n;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error>;
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
||||
|
||||
/// Clears the whole terminal screen
|
||||
fn clear(&mut self) -> Result<(), io::Error>;
|
||||
|
||||
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), io::Error> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear(),
|
||||
ClearType::AfterCursor
|
||||
| ClearType::BeforeCursor
|
||||
| ClearType::CurrentLine
|
||||
| ClearType::UntilNewLine => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("clear_type [{clear_type:?}] not supported with this backend"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
}
|
@ -1,275 +0,0 @@
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
pub struct TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
stdout: W,
|
||||
}
|
||||
|
||||
impl<W> TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
pub fn new(stdout: W) -> TermionBackend<W> {
|
||||
TermionBackend { stdout }
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.stdout.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.stdout.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Backend for TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => write!(self.stdout, "{}", termion::clear::All)?,
|
||||
ClearType::AfterCursor => write!(self.stdout, "{}", termion::clear::AfterCursor)?,
|
||||
ClearType::BeforeCursor => write!(self.stdout, "{}", termion::clear::BeforeCursor)?,
|
||||
ClearType::CurrentLine => write!(self.stdout, "{}", termion::clear::CurrentLine)?,
|
||||
ClearType::UntilNewLine => write!(self.stdout, "{}", termion::clear::UntilNewline)?,
|
||||
};
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
writeln!(self.stdout)?;
|
||||
}
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
/// Hides cursor
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Hide)?;
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
/// Shows cursor
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Show)?;
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
/// Gets cursor position (0-based index)
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
termion::cursor::DetectCursorPos::cursor_pos(&mut self.stdout).map(|(x, y)| (x - 1, y - 1))
|
||||
}
|
||||
|
||||
/// Sets cursor position (0-based index)
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Goto(x + 1, y + 1))?;
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut string = String::with_capacity(content.size_hint().0 * 3);
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap();
|
||||
}
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
write!(
|
||||
string,
|
||||
"{}",
|
||||
ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier
|
||||
}
|
||||
)
|
||||
.unwrap();
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg {
|
||||
write!(string, "{}", Fg(cell.fg)).unwrap();
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
write!(string, "{}", Bg(cell.bg)).unwrap();
|
||||
bg = cell.bg;
|
||||
}
|
||||
string.push_str(&cell.symbol);
|
||||
}
|
||||
write!(
|
||||
self.stdout,
|
||||
"{}{}{}{}",
|
||||
string,
|
||||
Fg(Color::Reset),
|
||||
Bg(Color::Reset),
|
||||
termion::style::Reset,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the size of the terminal
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let terminal = termion::terminal_size()?;
|
||||
Ok(Rect::new(0, 0, terminal.0, terminal.1))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.stdout.flush()
|
||||
}
|
||||
}
|
||||
|
||||
struct Fg(Color);
|
||||
|
||||
struct Bg(Color);
|
||||
|
||||
struct ModifierDiff {
|
||||
from: Modifier,
|
||||
to: Modifier,
|
||||
}
|
||||
|
||||
impl fmt::Display for Fg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_fg(f),
|
||||
Color::Black => termion::color::Black.write_fg(f),
|
||||
Color::Red => termion::color::Red.write_fg(f),
|
||||
Color::Green => termion::color::Green.write_fg(f),
|
||||
Color::Yellow => termion::color::Yellow.write_fg(f),
|
||||
Color::Blue => termion::color::Blue.write_fg(f),
|
||||
Color::Magenta => termion::color::Magenta.write_fg(f),
|
||||
Color::Cyan => termion::color::Cyan.write_fg(f),
|
||||
Color::Gray => termion::color::White.write_fg(f),
|
||||
Color::DarkGray => termion::color::LightBlack.write_fg(f),
|
||||
Color::LightRed => termion::color::LightRed.write_fg(f),
|
||||
Color::LightGreen => termion::color::LightGreen.write_fg(f),
|
||||
Color::LightBlue => termion::color::LightBlue.write_fg(f),
|
||||
Color::LightYellow => termion::color::LightYellow.write_fg(f),
|
||||
Color::LightMagenta => termion::color::LightMagenta.write_fg(f),
|
||||
Color::LightCyan => termion::color::LightCyan.write_fg(f),
|
||||
Color::White => termion::color::LightWhite.write_fg(f),
|
||||
Color::Indexed(i) => termion::color::AnsiValue(i).write_fg(f),
|
||||
Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_fg(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl fmt::Display for Bg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_bg(f),
|
||||
Color::Black => termion::color::Black.write_bg(f),
|
||||
Color::Red => termion::color::Red.write_bg(f),
|
||||
Color::Green => termion::color::Green.write_bg(f),
|
||||
Color::Yellow => termion::color::Yellow.write_bg(f),
|
||||
Color::Blue => termion::color::Blue.write_bg(f),
|
||||
Color::Magenta => termion::color::Magenta.write_bg(f),
|
||||
Color::Cyan => termion::color::Cyan.write_bg(f),
|
||||
Color::Gray => termion::color::White.write_bg(f),
|
||||
Color::DarkGray => termion::color::LightBlack.write_bg(f),
|
||||
Color::LightRed => termion::color::LightRed.write_bg(f),
|
||||
Color::LightGreen => termion::color::LightGreen.write_bg(f),
|
||||
Color::LightBlue => termion::color::LightBlue.write_bg(f),
|
||||
Color::LightYellow => termion::color::LightYellow.write_bg(f),
|
||||
Color::LightMagenta => termion::color::LightMagenta.write_bg(f),
|
||||
Color::LightCyan => termion::color::LightCyan.write_bg(f),
|
||||
Color::White => termion::color::LightWhite.write_bg(f),
|
||||
Color::Indexed(i) => termion::color::AnsiValue(i).write_bg(f),
|
||||
Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_bg(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ModifierDiff {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let remove = self.from - self.to;
|
||||
if remove.contains(Modifier::REVERSED) {
|
||||
write!(f, "{}", termion::style::NoInvert)?;
|
||||
}
|
||||
if remove.contains(Modifier::BOLD) {
|
||||
// XXX: the termion NoBold flag actually enables double-underline on ECMA-48 compliant
|
||||
// terminals, and NoFaint additionally disables bold... so we use this trick to get
|
||||
// the right semantics.
|
||||
write!(f, "{}", termion::style::NoFaint)?;
|
||||
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::Faint)?;
|
||||
}
|
||||
}
|
||||
if remove.contains(Modifier::ITALIC) {
|
||||
write!(f, "{}", termion::style::NoItalic)?;
|
||||
}
|
||||
if remove.contains(Modifier::UNDERLINED) {
|
||||
write!(f, "{}", termion::style::NoUnderline)?;
|
||||
}
|
||||
if remove.contains(Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::NoFaint)?;
|
||||
|
||||
// XXX: the NoFaint flag additionally disables bold as well, so we need to re-enable it
|
||||
// here if we want it.
|
||||
if self.to.contains(Modifier::BOLD) {
|
||||
write!(f, "{}", termion::style::Bold)?;
|
||||
}
|
||||
}
|
||||
if remove.contains(Modifier::CROSSED_OUT) {
|
||||
write!(f, "{}", termion::style::NoCrossedOut)?;
|
||||
}
|
||||
if remove.contains(Modifier::SLOW_BLINK) || remove.contains(Modifier::RAPID_BLINK) {
|
||||
write!(f, "{}", termion::style::NoBlink)?;
|
||||
}
|
||||
|
||||
let add = self.to - self.from;
|
||||
if add.contains(Modifier::REVERSED) {
|
||||
write!(f, "{}", termion::style::Invert)?;
|
||||
}
|
||||
if add.contains(Modifier::BOLD) {
|
||||
write!(f, "{}", termion::style::Bold)?;
|
||||
}
|
||||
if add.contains(Modifier::ITALIC) {
|
||||
write!(f, "{}", termion::style::Italic)?;
|
||||
}
|
||||
if add.contains(Modifier::UNDERLINED) {
|
||||
write!(f, "{}", termion::style::Underline)?;
|
||||
}
|
||||
if add.contains(Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::Faint)?;
|
||||
}
|
||||
if add.contains(Modifier::CROSSED_OUT) {
|
||||
write!(f, "{}", termion::style::CrossedOut)?;
|
||||
}
|
||||
if add.contains(Modifier::SLOW_BLINK) || add.contains(Modifier::RAPID_BLINK) {
|
||||
write!(f, "{}", termion::style::Blink)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,736 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
};
|
||||
use std::cmp::min;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Cell {
|
||||
pub symbol: String,
|
||||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
pub modifier: Modifier,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
|
||||
self.symbol.clear();
|
||||
self.symbol.push_str(symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_char(&mut self, ch: char) -> &mut Cell {
|
||||
self.symbol.clear();
|
||||
self.symbol.push(ch);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
|
||||
self.fg = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
|
||||
self.bg = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, style: Style) -> &mut Cell {
|
||||
if let Some(c) = style.fg {
|
||||
self.fg = c;
|
||||
}
|
||||
if let Some(c) = style.bg {
|
||||
self.bg = c;
|
||||
}
|
||||
self.modifier.insert(style.add_modifier);
|
||||
self.modifier.remove(style.sub_modifier);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(&self) -> Style {
|
||||
Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.add_modifier(self.modifier)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol.clear();
|
||||
self.symbol.push(' ');
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
self.modifier = Modifier::empty();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Cell {
|
||||
Cell {
|
||||
symbol: " ".into(),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A buffer that maps to the desired content of the terminal after the draw call
|
||||
///
|
||||
/// No widget in the library interacts directly with the terminal. Instead each of them is required
|
||||
/// to draw their state to an intermediate buffer. It is basically a grid where each cell contains
|
||||
/// a grapheme, a foreground color and a background color. This grid will then be used to output
|
||||
/// the appropriate escape sequences and characters to draw the UI as the user has defined it.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::buffer::{Buffer, Cell};
|
||||
/// use ratatui::layout::Rect;
|
||||
/// use ratatui::style::{Color, Style, Modifier};
|
||||
///
|
||||
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
|
||||
/// buf.get_mut(0, 2).set_symbol("x");
|
||||
/// assert_eq!(buf.get(0, 2).symbol, "x");
|
||||
/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
|
||||
/// assert_eq!(buf.get(5, 0), &Cell{
|
||||
/// symbol: String::from("r"),
|
||||
/// fg: Color::Red,
|
||||
/// bg: Color::White,
|
||||
/// modifier: Modifier::empty()
|
||||
/// });
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol, "x");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
/// The content of the buffer. The length of this Vec should always be equal to area.width *
|
||||
/// area.height
|
||||
pub content: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
let cell: Cell = Default::default();
|
||||
Buffer::filled(area, &cell)
|
||||
}
|
||||
|
||||
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
|
||||
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
|
||||
let size = area.area() as usize;
|
||||
let mut content = Vec::with_capacity(size);
|
||||
for _ in 0..size {
|
||||
content.push(cell.clone());
|
||||
}
|
||||
Buffer { area, content }
|
||||
}
|
||||
|
||||
/// Returns a Buffer containing the given lines
|
||||
pub fn with_lines<S>(lines: Vec<S>) -> Buffer
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let height = lines.len() as u16;
|
||||
let width = lines
|
||||
.iter()
|
||||
.map(|i| i.as_ref().width() as u16)
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let mut buffer = Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
for (y, line) in lines.iter().enumerate() {
|
||||
buffer.set_string(0, y as u16, line, Style::default());
|
||||
}
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Returns the content of the buffer as a slice
|
||||
pub fn content(&self) -> &[Cell] {
|
||||
&self.content
|
||||
}
|
||||
|
||||
/// Returns the area covered by this buffer
|
||||
pub fn area(&self) -> &Rect {
|
||||
&self.area
|
||||
}
|
||||
|
||||
/// Returns a reference to Cell at the given coordinates
|
||||
pub fn get(&self, x: u16, y: u16) -> &Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&self.content[i]
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to Cell at the given coordinates
|
||||
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&mut self.content[i]
|
||||
}
|
||||
|
||||
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Global coordinates to the top corner of this buffer's area
|
||||
/// assert_eq!(buffer.index_of(200, 100), 0);
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
||||
/// // starts at (200, 100).
|
||||
/// buffer.index_of(0, 0); // Panics
|
||||
/// ```
|
||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||
debug_assert!(
|
||||
x >= self.area.left()
|
||||
&& x < self.area.right()
|
||||
&& y >= self.area.top()
|
||||
&& y < self.area.bottom(),
|
||||
"Trying to access position outside the buffer: x={}, y={}, area={:?}",
|
||||
x,
|
||||
y,
|
||||
self.area
|
||||
);
|
||||
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
|
||||
}
|
||||
|
||||
/// Returns the (global) coordinates of a cell given its index
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
/// assert_eq!(buffer.pos_of(14), (204, 101));
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an index that is outside the Buffer's content.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
||||
/// buffer.pos_of(100); // Panics
|
||||
/// ```
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={} len={}",
|
||||
i,
|
||||
self.content.len()
|
||||
);
|
||||
(
|
||||
self.area.x + i as u16 % self.area.width,
|
||||
self.area.y + i as u16 / self.area.width,
|
||||
)
|
||||
}
|
||||
|
||||
/// Print a string, starting at the position (x, y)
|
||||
pub fn set_string<S>(&mut self, x: u16, y: u16, string: S, style: Style)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
self.set_stringn(x, y, string, usize::MAX, style);
|
||||
}
|
||||
|
||||
/// Print at most the first n characters of a string if enough space is available
|
||||
/// until the end of the line
|
||||
pub fn set_stringn<S>(
|
||||
&mut self,
|
||||
x: u16,
|
||||
y: u16,
|
||||
string: S,
|
||||
width: usize,
|
||||
style: Style,
|
||||
) -> (u16, u16)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let mut index = self.index_of(x, y);
|
||||
let mut x_offset = x as usize;
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
||||
for s in graphemes {
|
||||
let width = s.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
||||
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
if width > max_offset.saturating_sub(x_offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.content[index].set_symbol(s);
|
||||
self.content[index].set_style(style);
|
||||
// Reset following cells if multi-width (they would be hidden by the grapheme),
|
||||
for i in index + 1..index + width {
|
||||
self.content[i].reset();
|
||||
}
|
||||
index += width;
|
||||
x_offset += width;
|
||||
}
|
||||
(x_offset as u16, y)
|
||||
}
|
||||
|
||||
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &spans.0 {
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = self.set_stringn(
|
||||
x,
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
remaining_width = remaining_width.saturating_sub(w);
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
|
||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
|
||||
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
|
||||
}
|
||||
|
||||
#[deprecated(
|
||||
since = "0.10.0",
|
||||
note = "You should use styling capabilities of `Buffer::set_style`"
|
||||
)]
|
||||
pub fn set_background(&mut self, area: Rect, color: Color) {
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
self.get_mut(x, y).set_bg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, area: Rect, style: Style) {
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
self.get_mut(x, y).set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize the buffer so that the mapped area matches the given area and that the buffer
|
||||
/// length is equal to area.width * area.height
|
||||
pub fn resize(&mut self, area: Rect) {
|
||||
let length = area.area() as usize;
|
||||
if self.content.len() > length {
|
||||
self.content.truncate(length);
|
||||
} else {
|
||||
self.content.resize(length, Default::default());
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Reset all cells in the buffer
|
||||
pub fn reset(&mut self) {
|
||||
for c in &mut self.content {
|
||||
c.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge an other buffer into this one
|
||||
pub fn merge(&mut self, other: &Buffer) {
|
||||
let area = self.area.union(other.area);
|
||||
let cell: Cell = Default::default();
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
|
||||
// Move original content to the appropriate space
|
||||
let size = self.area.area() as usize;
|
||||
for i in (0..size).rev() {
|
||||
let (x, y) = self.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
if i != k {
|
||||
self.content[k] = self.content[i].clone();
|
||||
self.content[i] = cell.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Push content of the other buffer into this one (may erase previous
|
||||
// data)
|
||||
let size = other.area.area() as usize;
|
||||
for i in 0..size {
|
||||
let (x, y) = other.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
self.content[k] = other.content[i].clone();
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
|
||||
/// self to other.
|
||||
///
|
||||
/// We're assuming that buffers are well-formed, that is no double-width cell is followed by
|
||||
/// a non-blank cell.
|
||||
///
|
||||
/// # Multi-width characters handling:
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `コ`
|
||||
/// Next: `aa`
|
||||
/// Updates: `0: a, 1: a'
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `a `
|
||||
/// Next: `コ`
|
||||
/// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `012`
|
||||
/// Prev: `aaa`
|
||||
/// Next: `aコ`
|
||||
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
|
||||
/// ```
|
||||
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
|
||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||
// Cells invalidated by drawing/replacing preceding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceding multi-width characters taking their
|
||||
// place (the skipped cells should be blank anyway):
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||
if (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
|
||||
to_skip = current.symbol.width().saturating_sub(1);
|
||||
|
||||
let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
|
||||
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
|
||||
}
|
||||
updates
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cell(s: &str) -> Cell {
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol(s);
|
||||
cell
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_translates_to_and_from_coordinates() {
|
||||
let rect = Rect::new(200, 100, 50, 80);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// First cell is at the upper left corner.
|
||||
assert_eq!(buf.pos_of(0), (200, 100));
|
||||
assert_eq!(buf.index_of(200, 100), 0);
|
||||
|
||||
// Last cell is in the lower right.
|
||||
assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
|
||||
assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
fn pos_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
|
||||
buf.pos_of(100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
fn index_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// width is 10; zero-indexed means that 10 would be the 11th cell.
|
||||
buf.index_of(10, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
|
||||
// Width limit:
|
||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// Width truncation:
|
||||
buffer.set_string(0, 0, "123456", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string_zero_width() {
|
||||
let area = Rect::new(0, 0, 1, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Leading grapheme with zero width
|
||||
let s = "\u{1}a";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
|
||||
// Trailing grapheme with zero with
|
||||
let s = "a\u{1}";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string_double_width() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer.set_string(0, 0, "コン", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_with_lines() {
|
||||
let buffer =
|
||||
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
|
||||
assert_eq!(buffer.area.x, 0);
|
||||
assert_eq!(buffer.area.y, 0);
|
||||
assert_eq!(buffer.area.width, 10);
|
||||
assert_eq!(buffer.area.height, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_empty_empty() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::empty(area);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_empty_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff.len(), 40 * 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_filled_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_single_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌TITLE─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(2, 1, &cell("I")),
|
||||
(3, 1, &cell("T")),
|
||||
(4, 1, &cell("L")),
|
||||
(5, 1, &cell("E")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn buffer_diffing_multi_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
"┌Title─┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
"┌称号──┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(1, 0, &cell("称")),
|
||||
// Skipped "i"
|
||||
(3, 0, &cell("号")),
|
||||
// Skipped "l"
|
||||
(5, 0, &cell("─")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_multi_width_offset() {
|
||||
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
|
||||
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_eq!(
|
||||
one,
|
||||
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge3() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 3,
|
||||
y: 3,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 3,
|
||||
height: 4,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
|
||||
merged.area = Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_eq!(one, merged);
|
||||
}
|
||||
}
|
@ -1,560 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use cassowary::strength::{MEDIUM, REQUIRED, WEAK};
|
||||
use cassowary::WeightedRelation::*;
|
||||
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
|
||||
|
||||
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Corner {
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomRight,
|
||||
BottomLeft,
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Constraint {
|
||||
// TODO: enforce range 0 - 100
|
||||
Percentage(u16),
|
||||
Ratio(u32, u32),
|
||||
Length(u16),
|
||||
Max(u16),
|
||||
Min(u16),
|
||||
}
|
||||
|
||||
impl Constraint {
|
||||
pub fn apply(&self, length: u16) -> u16 {
|
||||
match *self {
|
||||
Constraint::Percentage(p) => length * p / 100,
|
||||
Constraint::Ratio(num, den) => {
|
||||
let r = num * u32::from(length) / den;
|
||||
r as u16
|
||||
}
|
||||
Constraint::Length(l) => length.min(l),
|
||||
Constraint::Max(m) => length.min(m),
|
||||
Constraint::Min(m) => length.max(m),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Margin {
|
||||
pub vertical: u16,
|
||||
pub horizontal: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Layout {
|
||||
direction: Direction,
|
||||
margin: Margin,
|
||||
constraints: Vec<Constraint>,
|
||||
/// Whether the last chunk of the computed layout should be expanded to fill the available
|
||||
/// space.
|
||||
expand_to_fill: bool,
|
||||
}
|
||||
|
||||
type Cache = HashMap<(Rect, Layout), Rc<[Rect]>>;
|
||||
thread_local! {
|
||||
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
fn default() -> Layout {
|
||||
Layout {
|
||||
direction: Direction::Vertical,
|
||||
margin: Margin {
|
||||
horizontal: 0,
|
||||
vertical: 0,
|
||||
},
|
||||
constraints: Vec::new(),
|
||||
expand_to_fill: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn constraints<C>(mut self, constraints: C) -> Layout
|
||||
where
|
||||
C: Into<Vec<Constraint>>,
|
||||
{
|
||||
self.constraints = constraints.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn margin(mut self, margin: u16) -> Layout {
|
||||
self.margin = Margin {
|
||||
horizontal: margin,
|
||||
vertical: margin,
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn horizontal_margin(mut self, horizontal: u16) -> Layout {
|
||||
self.margin.horizontal = horizontal;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn vertical_margin(mut self, vertical: u16) -> Layout {
|
||||
self.margin.vertical = vertical;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn direction(mut self, direction: Direction) -> Layout {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
|
||||
self.expand_to_fill = expand_to_fill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wrapper function around the cassowary-rs solver to be able to split a given
|
||||
/// area into smaller ones based on the preferred widths or heights and the direction.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use ratatui::layout::{Rect, Constraint, Direction, Layout};
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
|
||||
/// .split(Rect {
|
||||
/// x: 2,
|
||||
/// y: 2,
|
||||
/// width: 10,
|
||||
/// height: 10,
|
||||
/// });
|
||||
/// assert_eq!(
|
||||
/// chunks[..],
|
||||
/// [
|
||||
/// Rect {
|
||||
/// x: 2,
|
||||
/// y: 2,
|
||||
/// width: 10,
|
||||
/// height: 5
|
||||
/// },
|
||||
/// Rect {
|
||||
/// x: 2,
|
||||
/// y: 7,
|
||||
/// width: 10,
|
||||
/// height: 5
|
||||
/// }
|
||||
/// ]
|
||||
/// );
|
||||
///
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Horizontal)
|
||||
/// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
|
||||
/// .split(Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 9,
|
||||
/// height: 2,
|
||||
/// });
|
||||
/// assert_eq!(
|
||||
/// chunks[..],
|
||||
/// [
|
||||
/// Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 3,
|
||||
/// height: 2
|
||||
/// },
|
||||
/// Rect {
|
||||
/// x: 3,
|
||||
/// y: 0,
|
||||
/// width: 6,
|
||||
/// height: 2
|
||||
/// }
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn split(&self, area: Rect) -> Rc<[Rect]> {
|
||||
// TODO: Maybe use a fixed size cache ?
|
||||
LAYOUT_CACHE.with(|c| {
|
||||
c.borrow_mut()
|
||||
.entry((area, self.clone()))
|
||||
.or_insert_with(|| split(area, self))
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
|
||||
let mut solver = Solver::new();
|
||||
let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
|
||||
let elements = layout
|
||||
.constraints
|
||||
.iter()
|
||||
.map(|_| Element::new())
|
||||
.collect::<Vec<Element>>();
|
||||
let mut res = layout
|
||||
.constraints
|
||||
.iter()
|
||||
.map(|_| Rect::default())
|
||||
.collect::<Rc<[Rect]>>();
|
||||
|
||||
let mut results = Rc::get_mut(&mut res).expect("newly created Rc should have no shared refs");
|
||||
|
||||
let dest_area = area.inner(&layout.margin);
|
||||
for (i, e) in elements.iter().enumerate() {
|
||||
vars.insert(e.x, (i, 0));
|
||||
vars.insert(e.y, (i, 1));
|
||||
vars.insert(e.width, (i, 2));
|
||||
vars.insert(e.height, (i, 3));
|
||||
}
|
||||
let mut ccs: Vec<CassowaryConstraint> =
|
||||
Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6);
|
||||
for elt in &elements {
|
||||
ccs.push(elt.width | GE(REQUIRED) | 0f64);
|
||||
ccs.push(elt.height | GE(REQUIRED) | 0f64);
|
||||
ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
|
||||
ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
|
||||
ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
|
||||
ccs.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom()));
|
||||
}
|
||||
if let Some(first) = elements.first() {
|
||||
ccs.push(match layout.direction {
|
||||
Direction::Horizontal => first.left() | EQ(REQUIRED) | f64::from(dest_area.left()),
|
||||
Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
|
||||
});
|
||||
}
|
||||
if layout.expand_to_fill {
|
||||
if let Some(last) = elements.last() {
|
||||
ccs.push(match layout.direction {
|
||||
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
|
||||
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
|
||||
});
|
||||
}
|
||||
}
|
||||
match layout.direction {
|
||||
Direction::Horizontal => {
|
||||
for pair in elements.windows(2) {
|
||||
ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
|
||||
}
|
||||
for (i, size) in layout.constraints.iter().enumerate() {
|
||||
ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
|
||||
ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
|
||||
ccs.push(match *size {
|
||||
Constraint::Length(v) => elements[i].width | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => {
|
||||
elements[i].width | EQ(MEDIUM) | (f64::from(v * dest_area.width) / 100.0)
|
||||
}
|
||||
Constraint::Ratio(n, d) => {
|
||||
elements[i].width
|
||||
| EQ(MEDIUM)
|
||||
| (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => elements[i].width | GE(MEDIUM) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].width | LE(MEDIUM) | f64::from(v),
|
||||
});
|
||||
|
||||
match *size {
|
||||
Constraint::Min(v) => {
|
||||
ccs.push(elements[i].width | EQ(WEAK) | f64::from(v));
|
||||
}
|
||||
Constraint::Max(v) => {
|
||||
ccs.push(elements[i].width | EQ(WEAK) | f64::from(v));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Direction::Vertical => {
|
||||
for pair in elements.windows(2) {
|
||||
ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
|
||||
}
|
||||
for (i, size) in layout.constraints.iter().enumerate() {
|
||||
ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
|
||||
ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
|
||||
ccs.push(match *size {
|
||||
Constraint::Length(v) => elements[i].height | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => {
|
||||
elements[i].height | EQ(MEDIUM) | (f64::from(v * dest_area.height) / 100.0)
|
||||
}
|
||||
Constraint::Ratio(n, d) => {
|
||||
elements[i].height
|
||||
| EQ(MEDIUM)
|
||||
| (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => elements[i].height | GE(MEDIUM) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].height | LE(MEDIUM) | f64::from(v),
|
||||
});
|
||||
|
||||
match *size {
|
||||
Constraint::Min(v) => {
|
||||
ccs.push(elements[i].height | EQ(WEAK) | f64::from(v));
|
||||
}
|
||||
Constraint::Max(v) => {
|
||||
ccs.push(elements[i].height | EQ(WEAK) | f64::from(v));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
solver.add_constraints(&ccs).unwrap();
|
||||
for &(var, value) in solver.fetch_changes() {
|
||||
let (index, attr) = vars[&var];
|
||||
let value = if value.is_sign_negative() {
|
||||
0
|
||||
} else {
|
||||
value as u16
|
||||
};
|
||||
match attr {
|
||||
0 => {
|
||||
results[index].x = value;
|
||||
}
|
||||
1 => {
|
||||
results[index].y = value;
|
||||
}
|
||||
2 => {
|
||||
results[index].width = value;
|
||||
}
|
||||
3 => {
|
||||
results[index].height = value;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if layout.expand_to_fill {
|
||||
// Fix imprecision by extending the last item a bit if necessary
|
||||
if let Some(last) = results.last_mut() {
|
||||
match layout.direction {
|
||||
Direction::Vertical => {
|
||||
last.height = dest_area.bottom() - last.y;
|
||||
}
|
||||
Direction::Horizontal => {
|
||||
last.width = dest_area.right() - last.x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// A container used by the solver inside split
|
||||
struct Element {
|
||||
x: Variable,
|
||||
y: Variable,
|
||||
width: Variable,
|
||||
height: Variable,
|
||||
}
|
||||
|
||||
impl Element {
|
||||
fn new() -> Element {
|
||||
Element {
|
||||
x: Variable::new(),
|
||||
y: Variable::new(),
|
||||
width: Variable::new(),
|
||||
height: Variable::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn left(&self) -> Variable {
|
||||
self.x
|
||||
}
|
||||
|
||||
fn top(&self) -> Variable {
|
||||
self.y
|
||||
}
|
||||
|
||||
fn right(&self) -> Expression {
|
||||
self.x + self.width
|
||||
}
|
||||
|
||||
fn bottom(&self) -> Expression {
|
||||
self.y + self.height
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
|
||||
pub struct Rect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Creates a new rect, with width and height limited to keep the area under max u16.
|
||||
/// If clipped, aspect ratio will be preserved.
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
|
||||
let max_area = u16::max_value();
|
||||
let (clipped_width, clipped_height) =
|
||||
if u32::from(width) * u32::from(height) > u32::from(max_area) {
|
||||
let aspect_ratio = f64::from(width) / f64::from(height);
|
||||
let max_area_f = f64::from(max_area);
|
||||
let height_f = (max_area_f / aspect_ratio).sqrt();
|
||||
let width_f = height_f * aspect_ratio;
|
||||
(width_f as u16, height_f as u16)
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width: clipped_width,
|
||||
height: clipped_height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn area(self) -> u16 {
|
||||
self.width * self.height
|
||||
}
|
||||
|
||||
pub fn left(self) -> u16 {
|
||||
self.x
|
||||
}
|
||||
|
||||
pub fn right(self) -> u16 {
|
||||
self.x.saturating_add(self.width)
|
||||
}
|
||||
|
||||
pub fn top(self) -> u16 {
|
||||
self.y
|
||||
}
|
||||
|
||||
pub fn bottom(self) -> u16 {
|
||||
self.y.saturating_add(self.height)
|
||||
}
|
||||
|
||||
pub fn inner(self, margin: &Margin) -> Rect {
|
||||
if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
|
||||
Rect::default()
|
||||
} else {
|
||||
Rect {
|
||||
x: self.x + margin.horizontal,
|
||||
y: self.y + margin.vertical,
|
||||
width: self.width - 2 * margin.horizontal,
|
||||
height: self.height - 2 * margin.vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union(self, other: Rect) -> Rect {
|
||||
let x1 = min(self.x, other.x);
|
||||
let y1 = min(self.y, other.y);
|
||||
let x2 = max(self.x + self.width, other.x + other.width);
|
||||
let y2 = max(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersection(self, other: Rect) -> Rect {
|
||||
let x1 = max(self.x, other.x);
|
||||
let y1 = max(self.y, other.y);
|
||||
let x2 = min(self.x + self.width, other.x + other.width);
|
||||
let y2 = min(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersects(self, other: Rect) -> bool {
|
||||
self.x < other.x + other.width
|
||||
&& self.x + self.width > other.x
|
||||
&& self.y < other.y + other.height
|
||||
&& self.y + self.height > other.y
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_vertical_split_by_height() {
|
||||
let target = Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
};
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Max(5),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(target);
|
||||
|
||||
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
|
||||
chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rect_size_truncation() {
|
||||
for width in 256u16..300u16 {
|
||||
for height in 256u16..300u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
// The target dimensions are rounded down so the math will not be too precise
|
||||
// but let's make sure the ratios don't diverge crazily.
|
||||
assert!(
|
||||
(f64::from(rect.width) / f64::from(rect.height)
|
||||
- f64::from(width) / f64::from(height))
|
||||
.abs()
|
||||
< 1.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area above max u16.
|
||||
let width = 900;
|
||||
let height = 100;
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
assert_ne!(rect.width, 900);
|
||||
assert_ne!(rect.height, 100);
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rect_size_preservation() {
|
||||
for width in 0..256u16 {
|
||||
for height in 0..256u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert_eq!(rect.width, width);
|
||||
assert_eq!(rect.height, height);
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area below max u16.
|
||||
let rect = Rect::new(0, 0, 300, 100);
|
||||
assert_eq!(rect.width, 300);
|
||||
assert_eq!(rect.height, 100);
|
||||
}
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
#![allow(clippy::all)]
|
||||
#![allow(warnings)]
|
||||
|
||||
//! [ratatui](https://github.com/tui-rs-revival/ratatui) is a library used to build rich
|
||||
//! terminal users interfaces and dashboards.
|
||||
//!
|
||||
//! ![](https://raw.githubusercontent.com/tui-rs-revival/ratatui/master/assets/demo.gif)
|
||||
//!
|
||||
//! # Get started
|
||||
//!
|
||||
//! ## Adding `ratatui` as a dependency
|
||||
//!
|
||||
//! Add the following to your `Cargo.toml`:
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! crossterm = "0.26"
|
||||
//! ratatui = "0.20"
|
||||
//! ```
|
||||
//!
|
||||
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
|
||||
//! example you want to use the `termion` backend instead. This can be done by changing your
|
||||
//! dependencies specification to the following:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! termion = "1.5"
|
||||
//! ratatui = { version = "0.20", default-features = false, features = ['termion'] }
|
||||
//!
|
||||
//! ```
|
||||
//!
|
||||
//! The same logic applies for all other available backends.
|
||||
//!
|
||||
//! ## Creating a `Terminal`
|
||||
//!
|
||||
//! Every application using `ratatui` should start by instantiating a `Terminal`. It is a light
|
||||
//! abstraction over available backends that provides basic functionalities such as clearing the
|
||||
//! screen, hiding the cursor, etc.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout();
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If you had previously chosen `termion` as a backend, the terminal can be created in a similar
|
||||
//! way:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use std::io;
|
||||
//! use ratatui::{backend::TermionBackend, Terminal};
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! You may also refer to the examples to find out how to create a `Terminal` for each available
|
||||
//! backend.
|
||||
//!
|
||||
//! ## Building a User Interface (UI)
|
||||
//!
|
||||
//! Every component of your interface will be implementing the `Widget` trait. The library comes
|
||||
//! with a predefined set of widgets that should meet most of your use cases. You are also free to
|
||||
//! implement your own.
|
||||
//!
|
||||
//! Each widget follows a builder pattern API providing a default configuration along with methods
|
||||
//! to customize them. The widget is then rendered using [`Frame::render_widget`] which takes
|
||||
//! your widget instance and an area to draw to.
|
||||
//!
|
||||
//! The following example renders a block of the size of the terminal:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::{io, thread, time::Duration};
|
||||
//! use ratatui::{
|
||||
//! backend::CrosstermBackend,
|
||||
//! widgets::{Widget, Block, Borders},
|
||||
//! layout::{Layout, Constraint, Direction},
|
||||
//! Terminal
|
||||
//! };
|
||||
//! use crossterm::{
|
||||
//! event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
//! execute,
|
||||
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
//! };
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! // setup terminal
|
||||
//! enable_raw_mode()?;
|
||||
//! let mut stdout = io::stdout();
|
||||
//! execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//!
|
||||
//! terminal.draw(|f| {
|
||||
//! let size = f.size();
|
||||
//! let block = Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, size);
|
||||
//! })?;
|
||||
//!
|
||||
//! thread::sleep(Duration::from_millis(5000));
|
||||
//!
|
||||
//! // restore terminal
|
||||
//! disable_raw_mode()?;
|
||||
//! execute!(
|
||||
//! terminal.backend_mut(),
|
||||
//! LeaveAlternateScreen,
|
||||
//! DisableMouseCapture
|
||||
//! )?;
|
||||
//! terminal.show_cursor()?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Layout
|
||||
//!
|
||||
//! The library comes with a basic yet useful layout management object called `Layout`. As you may
|
||||
//! see below and in the examples, the library makes heavy use of the builder pattern to provide
|
||||
//! full customization. And `Layout` is no exception:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::{
|
||||
//! backend::Backend,
|
||||
//! layout::{Constraint, Direction, Layout},
|
||||
//! widgets::{Block, Borders},
|
||||
//! Frame,
|
||||
//! };
|
||||
//! fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
//! let chunks = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
//! .margin(1)
|
||||
//! .constraints(
|
||||
//! [
|
||||
//! Constraint::Percentage(10),
|
||||
//! Constraint::Percentage(80),
|
||||
//! Constraint::Percentage(10)
|
||||
//! ].as_ref()
|
||||
//! )
|
||||
//! .split(f.size());
|
||||
//! let block = Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[0]);
|
||||
//! let block = Block::default()
|
||||
//! .title("Block 2")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[1]);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This let you describe responsive terminal UI by nesting layouts. You should note that by
|
||||
//! default the computed layout tries to fill the available space completely. So if for any reason
|
||||
//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
|
||||
//! corresponding area.
|
||||
|
||||
pub mod backend;
|
||||
pub mod buffer;
|
||||
pub mod layout;
|
||||
pub mod style;
|
||||
pub mod symbols;
|
||||
pub mod terminal;
|
||||
pub mod text;
|
||||
pub mod widgets;
|
||||
|
||||
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};
|
@ -1,310 +0,0 @@
|
||||
//! `style` contains the primitives used to control how your user interface will look.
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Color {
|
||||
Reset,
|
||||
Black,
|
||||
Red,
|
||||
Green,
|
||||
Yellow,
|
||||
Blue,
|
||||
Magenta,
|
||||
Cyan,
|
||||
Gray,
|
||||
DarkGray,
|
||||
LightRed,
|
||||
LightGreen,
|
||||
LightYellow,
|
||||
LightBlue,
|
||||
LightMagenta,
|
||||
LightCyan,
|
||||
White,
|
||||
Rgb(u8, u8, u8),
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Modifier changes the way a piece of text is displayed.
|
||||
///
|
||||
/// They are bitflags so they can easily be composed.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::Modifier;
|
||||
///
|
||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
||||
/// ```
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Modifier: u16 {
|
||||
const BOLD = 0b0000_0000_0001;
|
||||
const DIM = 0b0000_0000_0010;
|
||||
const ITALIC = 0b0000_0000_0100;
|
||||
const UNDERLINED = 0b0000_0000_1000;
|
||||
const SLOW_BLINK = 0b0000_0001_0000;
|
||||
const RAPID_BLINK = 0b0000_0010_0000;
|
||||
const REVERSED = 0b0000_0100_0000;
|
||||
const HIDDEN = 0b0000_1000_0000;
|
||||
const CROSSED_OUT = 0b0001_0000_0000;
|
||||
}
|
||||
}
|
||||
|
||||
/// Style let you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
/// .bg(Color::Green)
|
||||
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
/// ```
|
||||
///
|
||||
/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
|
||||
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default().bg(Color::Red),
|
||||
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Red),
|
||||
/// add_modifier: Modifier::BOLD,
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// The default implementation returns a `Style` that does not modify anything. If you wish to
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::reset().fg(Color::Yellow),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Reset),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
pub fg: Option<Color>,
|
||||
pub bg: Option<Color>,
|
||||
pub add_modifier: Modifier,
|
||||
pub sub_modifier: Modifier,
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Style {
|
||||
Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Returns a `Style` resetting all properties.
|
||||
pub fn reset() -> Style {
|
||||
Style {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::all(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the foreground color.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let style = Style::default().fg(Color::Blue);
|
||||
/// let diff = Style::default().fg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
||||
/// ```
|
||||
pub fn fg(mut self, color: Color) -> Style {
|
||||
self.fg = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the background color.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let style = Style::default().bg(Color::Blue);
|
||||
/// let diff = Style::default().bg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
||||
/// ```
|
||||
pub fn bg(mut self, color: Color) -> Style {
|
||||
self.bg = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the text emphasis.
|
||||
///
|
||||
/// When applied, it adds the given modifier to the `Style` modifiers.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD);
|
||||
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::empty());
|
||||
/// ```
|
||||
pub fn add_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.sub_modifier.remove(modifier);
|
||||
self.add_modifier.insert(modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the text emphasis.
|
||||
///
|
||||
/// When applied, it removes the given modifier from the `Style` modifiers.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
||||
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
|
||||
/// ```
|
||||
pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.add_modifier.remove(modifier);
|
||||
self.sub_modifier.insert(modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Results in a combined style that is equivalent to applying the two individual styles to
|
||||
/// a style one after the other.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style_1 = Style::default().fg(Color::Yellow);
|
||||
/// let style_2 = Style::default().bg(Color::Red);
|
||||
/// let combined = style_1.patch(style_2);
|
||||
/// assert_eq!(
|
||||
/// Style::default().patch(style_1).patch(style_2),
|
||||
/// Style::default().patch(combined));
|
||||
/// ```
|
||||
pub fn patch(mut self, other: Style) -> Style {
|
||||
self.fg = other.fg.or(self.fg);
|
||||
self.bg = other.bg.or(self.bg);
|
||||
|
||||
self.add_modifier.remove(other.sub_modifier);
|
||||
self.add_modifier.insert(other.add_modifier);
|
||||
self.sub_modifier.remove(other.add_modifier);
|
||||
self.sub_modifier.insert(other.sub_modifier);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn styles() -> Vec<Style> {
|
||||
vec![
|
||||
Style::default(),
|
||||
Style::default().fg(Color::Yellow),
|
||||
Style::default().bg(Color::Yellow),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
Style::default().remove_modifier(Modifier::BOLD),
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
Style::default().remove_modifier(Modifier::ITALIC),
|
||||
Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_patch_gives_same_result_as_individual_patch() {
|
||||
let styles = styles();
|
||||
for &a in &styles {
|
||||
for &b in &styles {
|
||||
for &c in &styles {
|
||||
for &d in &styles {
|
||||
let combined = a.patch(b.patch(c.patch(d)));
|
||||
|
||||
assert_eq!(
|
||||
Style::default().patch(a).patch(b).patch(c).patch(d),
|
||||
Style::default().patch(combined)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_individual_modifiers() {
|
||||
use crate::ratatui::{buffer::Buffer, layout::Rect};
|
||||
|
||||
let mods = vec![
|
||||
Modifier::BOLD,
|
||||
Modifier::DIM,
|
||||
Modifier::ITALIC,
|
||||
Modifier::UNDERLINED,
|
||||
Modifier::SLOW_BLINK,
|
||||
Modifier::RAPID_BLINK,
|
||||
Modifier::REVERSED,
|
||||
Modifier::HIDDEN,
|
||||
Modifier::CROSSED_OUT,
|
||||
];
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
|
||||
for m in &mods {
|
||||
buffer.get_mut(0, 0).set_style(Style::reset());
|
||||
buffer
|
||||
.get_mut(0, 0)
|
||||
.set_style(Style::default().add_modifier(*m));
|
||||
let style = buffer.get(0, 0).style();
|
||||
assert!(style.add_modifier.contains(*m));
|
||||
assert!(!style.sub_modifier.contains(*m));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
pub mod block {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▉";
|
||||
pub const THREE_QUARTERS: &str = "▊";
|
||||
pub const FIVE_EIGHTHS: &str = "▋";
|
||||
pub const HALF: &str = "▌";
|
||||
pub const THREE_EIGHTHS: &str = "▍";
|
||||
pub const ONE_QUARTER: &str = "▎";
|
||||
pub const ONE_EIGHTH: &str = "▏";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
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 const THREE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: FULL,
|
||||
three_quarters: HALF,
|
||||
five_eighths: HALF,
|
||||
half: HALF,
|
||||
three_eighths: HALF,
|
||||
one_quarter: HALF,
|
||||
one_eighth: " ",
|
||||
empty: " ",
|
||||
};
|
||||
|
||||
pub const NINE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: SEVEN_EIGHTHS,
|
||||
three_quarters: THREE_QUARTERS,
|
||||
five_eighths: FIVE_EIGHTHS,
|
||||
half: HALF,
|
||||
three_eighths: THREE_EIGHTHS,
|
||||
one_quarter: ONE_QUARTER,
|
||||
one_eighth: ONE_EIGHTH,
|
||||
empty: " ",
|
||||
};
|
||||
}
|
||||
|
||||
pub mod bar {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▇";
|
||||
pub const THREE_QUARTERS: &str = "▆";
|
||||
pub const FIVE_EIGHTHS: &str = "▅";
|
||||
pub const HALF: &str = "▄";
|
||||
pub const THREE_EIGHTHS: &str = "▃";
|
||||
pub const ONE_QUARTER: &str = "▂";
|
||||
pub const ONE_EIGHTH: &str = "▁";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
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 const THREE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: FULL,
|
||||
three_quarters: HALF,
|
||||
five_eighths: HALF,
|
||||
half: HALF,
|
||||
three_eighths: HALF,
|
||||
one_quarter: HALF,
|
||||
one_eighth: " ",
|
||||
empty: " ",
|
||||
};
|
||||
|
||||
pub const NINE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: SEVEN_EIGHTHS,
|
||||
three_quarters: THREE_QUARTERS,
|
||||
five_eighths: FIVE_EIGHTHS,
|
||||
half: HALF,
|
||||
three_eighths: THREE_EIGHTHS,
|
||||
one_quarter: ONE_QUARTER,
|
||||
one_eighth: ONE_EIGHTH,
|
||||
empty: " ",
|
||||
};
|
||||
}
|
||||
|
||||
pub mod line {
|
||||
pub const VERTICAL: &str = "│";
|
||||
pub const DOUBLE_VERTICAL: &str = "║";
|
||||
pub const THICK_VERTICAL: &str = "┃";
|
||||
|
||||
pub const HORIZONTAL: &str = "─";
|
||||
pub const DOUBLE_HORIZONTAL: &str = "═";
|
||||
pub const THICK_HORIZONTAL: &str = "━";
|
||||
|
||||
pub const TOP_RIGHT: &str = "┐";
|
||||
pub const ROUNDED_TOP_RIGHT: &str = "╮";
|
||||
pub const DOUBLE_TOP_RIGHT: &str = "╗";
|
||||
pub const THICK_TOP_RIGHT: &str = "┓";
|
||||
|
||||
pub const TOP_LEFT: &str = "┌";
|
||||
pub const ROUNDED_TOP_LEFT: &str = "╭";
|
||||
pub const DOUBLE_TOP_LEFT: &str = "╔";
|
||||
pub const THICK_TOP_LEFT: &str = "┏";
|
||||
|
||||
pub const BOTTOM_RIGHT: &str = "┘";
|
||||
pub const ROUNDED_BOTTOM_RIGHT: &str = "╯";
|
||||
pub const DOUBLE_BOTTOM_RIGHT: &str = "╝";
|
||||
pub const THICK_BOTTOM_RIGHT: &str = "┛";
|
||||
|
||||
pub const BOTTOM_LEFT: &str = "└";
|
||||
pub const ROUNDED_BOTTOM_LEFT: &str = "╰";
|
||||
pub const DOUBLE_BOTTOM_LEFT: &str = "╚";
|
||||
pub const THICK_BOTTOM_LEFT: &str = "┗";
|
||||
|
||||
pub const VERTICAL_LEFT: &str = "┤";
|
||||
pub const DOUBLE_VERTICAL_LEFT: &str = "╣";
|
||||
pub const THICK_VERTICAL_LEFT: &str = "┫";
|
||||
|
||||
pub const VERTICAL_RIGHT: &str = "├";
|
||||
pub const DOUBLE_VERTICAL_RIGHT: &str = "╠";
|
||||
pub const THICK_VERTICAL_RIGHT: &str = "┣";
|
||||
|
||||
pub const HORIZONTAL_DOWN: &str = "┬";
|
||||
pub const DOUBLE_HORIZONTAL_DOWN: &str = "╦";
|
||||
pub const THICK_HORIZONTAL_DOWN: &str = "┳";
|
||||
|
||||
pub const HORIZONTAL_UP: &str = "┴";
|
||||
pub const DOUBLE_HORIZONTAL_UP: &str = "╩";
|
||||
pub const THICK_HORIZONTAL_UP: &str = "┻";
|
||||
|
||||
pub const CROSS: &str = "┼";
|
||||
pub const DOUBLE_CROSS: &str = "╬";
|
||||
pub const THICK_CROSS: &str = "╋";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Set {
|
||||
pub vertical: &'static str,
|
||||
pub horizontal: &'static str,
|
||||
pub top_right: &'static str,
|
||||
pub top_left: &'static str,
|
||||
pub bottom_right: &'static str,
|
||||
pub bottom_left: &'static str,
|
||||
pub vertical_left: &'static str,
|
||||
pub vertical_right: &'static str,
|
||||
pub horizontal_down: &'static str,
|
||||
pub horizontal_up: &'static str,
|
||||
pub cross: &'static str,
|
||||
}
|
||||
|
||||
pub const NORMAL: Set = Set {
|
||||
vertical: VERTICAL,
|
||||
horizontal: HORIZONTAL,
|
||||
top_right: TOP_RIGHT,
|
||||
top_left: TOP_LEFT,
|
||||
bottom_right: BOTTOM_RIGHT,
|
||||
bottom_left: BOTTOM_LEFT,
|
||||
vertical_left: VERTICAL_LEFT,
|
||||
vertical_right: VERTICAL_RIGHT,
|
||||
horizontal_down: HORIZONTAL_DOWN,
|
||||
horizontal_up: HORIZONTAL_UP,
|
||||
cross: CROSS,
|
||||
};
|
||||
|
||||
pub const ROUNDED: Set = Set {
|
||||
top_right: ROUNDED_TOP_RIGHT,
|
||||
top_left: ROUNDED_TOP_LEFT,
|
||||
bottom_right: ROUNDED_BOTTOM_RIGHT,
|
||||
bottom_left: ROUNDED_BOTTOM_LEFT,
|
||||
..NORMAL
|
||||
};
|
||||
|
||||
pub const DOUBLE: Set = Set {
|
||||
vertical: DOUBLE_VERTICAL,
|
||||
horizontal: DOUBLE_HORIZONTAL,
|
||||
top_right: DOUBLE_TOP_RIGHT,
|
||||
top_left: DOUBLE_TOP_LEFT,
|
||||
bottom_right: DOUBLE_BOTTOM_RIGHT,
|
||||
bottom_left: DOUBLE_BOTTOM_LEFT,
|
||||
vertical_left: DOUBLE_VERTICAL_LEFT,
|
||||
vertical_right: DOUBLE_VERTICAL_RIGHT,
|
||||
horizontal_down: DOUBLE_HORIZONTAL_DOWN,
|
||||
horizontal_up: DOUBLE_HORIZONTAL_UP,
|
||||
cross: DOUBLE_CROSS,
|
||||
};
|
||||
|
||||
pub const THICK: Set = Set {
|
||||
vertical: THICK_VERTICAL,
|
||||
horizontal: THICK_HORIZONTAL,
|
||||
top_right: THICK_TOP_RIGHT,
|
||||
top_left: THICK_TOP_LEFT,
|
||||
bottom_right: THICK_BOTTOM_RIGHT,
|
||||
bottom_left: THICK_BOTTOM_LEFT,
|
||||
vertical_left: THICK_VERTICAL_LEFT,
|
||||
vertical_right: THICK_VERTICAL_RIGHT,
|
||||
horizontal_down: THICK_HORIZONTAL_DOWN,
|
||||
horizontal_up: THICK_HORIZONTAL_UP,
|
||||
cross: THICK_CROSS,
|
||||
};
|
||||
}
|
||||
|
||||
pub const DOT: &str = "•";
|
||||
|
||||
pub mod braille {
|
||||
pub const BLANK: u16 = 0x2800;
|
||||
pub const DOTS: [[u16; 2]; 4] = [
|
||||
[0x0001, 0x0008],
|
||||
[0x0002, 0x0010],
|
||||
[0x0004, 0x0020],
|
||||
[0x0040, 0x0080],
|
||||
];
|
||||
}
|
||||
|
||||
/// Marker to use when plotting data points
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Marker {
|
||||
/// One point per cell in shape of dot
|
||||
Dot,
|
||||
/// One point per cell in shape of a block
|
||||
Block,
|
||||
/// Up to 8 points per cell
|
||||
Braille,
|
||||
}
|
@ -1,487 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Viewport {
|
||||
Fullscreen,
|
||||
Inline(u16),
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
impl Viewport {
|
||||
pub fn fixed(area: Rect) -> Viewport {
|
||||
Self::Fixed(area)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
pub struct TerminalOptions {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
/// Interface to the terminal backed by Termion
|
||||
#[derive(Debug)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
backend: B,
|
||||
/// Holds the results of the current and previous draw calls. The two are compared at the end
|
||||
/// of each draw pass to output the necessary updates to the terminal
|
||||
buffers: [Buffer; 2],
|
||||
/// Index of the current buffer in the previous array
|
||||
current: usize,
|
||||
/// Whether the cursor is currently hidden
|
||||
hidden_cursor: bool,
|
||||
/// Viewport
|
||||
viewport: Viewport,
|
||||
viewport_area: Rect,
|
||||
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
|
||||
last_known_size: Rect,
|
||||
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
|
||||
/// and the terminal resized.
|
||||
last_known_cursor_pos: (u16, u16),
|
||||
}
|
||||
|
||||
/// Represents a consistent terminal interface for rendering.
|
||||
pub struct Frame<'a, B: 'a>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
terminal: &'a mut Terminal<B>,
|
||||
|
||||
/// Where should the cursor be after drawing this frame?
|
||||
///
|
||||
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
|
||||
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
|
||||
cursor_position: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
impl<'a, B> Frame<'a, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Frame size, guaranteed not to change when rendering.
|
||||
pub fn size(&self) -> Rect {
|
||||
self.terminal.viewport_area
|
||||
}
|
||||
|
||||
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::Terminal;
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::widgets::Block;
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let block = Block::default();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// let mut frame = terminal.get_frame();
|
||||
/// frame.render_widget(block, area);
|
||||
/// ```
|
||||
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
|
||||
where
|
||||
W: Widget,
|
||||
{
|
||||
widget.render(area, self.terminal.current_buffer_mut());
|
||||
}
|
||||
|
||||
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
|
||||
///
|
||||
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
|
||||
/// given [`StatefulWidget`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::Terminal;
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::widgets::{List, ListItem, ListState};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select(Some(1));
|
||||
/// let items = vec![
|
||||
/// ListItem::new("Item 1"),
|
||||
/// ListItem::new("Item 2"),
|
||||
/// ];
|
||||
/// let list = List::new(items);
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// let mut frame = terminal.get_frame();
|
||||
/// frame.render_stateful_widget(list, area, &mut state);
|
||||
/// ```
|
||||
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
||||
where
|
||||
W: StatefulWidget,
|
||||
{
|
||||
widget.render(area, self.terminal.current_buffer_mut(), state);
|
||||
}
|
||||
|
||||
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
|
||||
/// coordinates. If this method is not called, the cursor will be hidden.
|
||||
///
|
||||
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
|
||||
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
|
||||
/// with it.
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) {
|
||||
self.cursor_position = Some((x, y));
|
||||
}
|
||||
}
|
||||
|
||||
/// CompletedFrame represents the state of the terminal after all changes performed in the last
|
||||
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
|
||||
/// [`Terminal::draw`].
|
||||
pub struct CompletedFrame<'a> {
|
||||
pub buffer: &'a Buffer,
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// Attempt to restore the cursor state
|
||||
if self.hidden_cursor {
|
||||
if let Err(err) = self.show_cursor() {
|
||||
eprintln!("Failed to show the cursor: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
||||
/// default colors for the foreground and the background
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
let size = match options.viewport {
|
||||
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
let (viewport_area, cursor_pos) = match options.viewport {
|
||||
Viewport::Fullscreen => (size, (0, 0)),
|
||||
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
|
||||
Viewport::Fixed(area) => (area, (area.left(), area.top())),
|
||||
};
|
||||
Ok(Terminal {
|
||||
backend,
|
||||
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
viewport: options.viewport,
|
||||
viewport_area,
|
||||
last_known_size: size,
|
||||
last_known_cursor_pos: cursor_pos,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
pub fn get_frame(&mut self) -> Frame<B> {
|
||||
Frame {
|
||||
terminal: self,
|
||||
cursor_position: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffers[self.current]
|
||||
}
|
||||
|
||||
pub fn backend(&self) -> &B {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
pub fn backend_mut(&mut self) -> &mut B {
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// Obtains a difference between the previous and the current buffer and passes it to the
|
||||
/// current backend for drawing.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
if let Some((col, row, _)) = updates.last() {
|
||||
self.last_known_cursor_pos = (*col, *row);
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
|
||||
/// be saved so the size can remain consistent when rendering.
|
||||
/// This leads to a full clear of the screen.
|
||||
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
|
||||
let next_area = match self.viewport {
|
||||
Viewport::Fullscreen => size,
|
||||
Viewport::Inline(height) => {
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.1
|
||||
.saturating_sub(self.viewport_area.top());
|
||||
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
|
||||
}
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
|
||||
self.last_known_size = size;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_viewport_area(&mut self, area: Rect) {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.viewport_area = area;
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
// fixed viewports do not get autoresized
|
||||
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
|
||||
let size = self.size()?;
|
||||
if size != self.last_known_size {
|
||||
self.resize(size)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
|
||||
where
|
||||
F: FnOnce(&mut Frame<B>),
|
||||
{
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// and the terminal (if growing), which may OOB.
|
||||
self.autoresize()?;
|
||||
|
||||
let mut frame = self.get_frame();
|
||||
f(&mut frame);
|
||||
// We can't change the cursor position right away because we have to flush the frame to
|
||||
// stdout first. But we also can't keep the frame around, since it holds a &mut to
|
||||
// Terminal. Thus, we're taking the important data out of the Frame and dropping it.
|
||||
let cursor_position = frame.cursor_position;
|
||||
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
|
||||
match cursor_position {
|
||||
None => self.hide_cursor()?,
|
||||
Some((x, y)) => {
|
||||
self.show_cursor()?;
|
||||
self.set_cursor(x, y)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Swap buffers
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
|
||||
Ok(CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.last_known_size,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()?;
|
||||
self.hidden_cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.backend.get_cursor()
|
||||
}
|
||||
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)?;
|
||||
self.last_known_cursor_pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
match self.viewport {
|
||||
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
|
||||
Viewport::Inline(_) => {
|
||||
self.backend
|
||||
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
Viewport::Fixed(area) => {
|
||||
for row in area.top()..area.bottom() {
|
||||
self.backend.set_cursor(0, row)?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
}
|
||||
|
||||
/// Insert some content before the current inline viewport. This has no effect when the
|
||||
/// viewport is fullscreen.
|
||||
///
|
||||
/// This function scrolls down the current viewport by the given height. The newly freed space is
|
||||
/// then made available to the `draw_fn` closure through a writable `Buffer`.
|
||||
///
|
||||
/// Before:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | buffer |
|
||||
/// +-------------------+
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Insert a single line before the current viewport
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::{Paragraph, Widget};
|
||||
/// # use ratatui::text::{Spans, Span};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// # use ratatui::{Terminal};
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// terminal.insert_before(1, |buf| {
|
||||
/// Paragraph::new(Spans::from(vec![
|
||||
/// Span::raw("This line will be added "),
|
||||
/// Span::styled("before", Style::default().fg(Color::Blue)),
|
||||
/// Span::raw(" the current viewport")
|
||||
/// ])).render(buf.area, buf);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Buffer),
|
||||
{
|
||||
if !matches!(self.viewport, Viewport::Inline(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.clear()?;
|
||||
let height = height.min(self.last_known_size.height);
|
||||
self.backend.append_lines(height)?;
|
||||
let missing_lines =
|
||||
height.saturating_sub(self.last_known_size.bottom() - self.viewport_area.top());
|
||||
let area = Rect {
|
||||
x: self.viewport_area.left(),
|
||||
y: self.viewport_area.top().saturating_sub(missing_lines),
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
draw_fn(&mut buffer);
|
||||
|
||||
let iter = buffer.content.iter().enumerate().map(|(i, c)| {
|
||||
let (x, y) = buffer.pos_of(i);
|
||||
(x, y, c)
|
||||
});
|
||||
self.backend.draw(iter)?;
|
||||
self.backend.flush()?;
|
||||
|
||||
let remaining_lines = self.last_known_size.height - area.bottom();
|
||||
let missing_lines = self.viewport_area.height.saturating_sub(remaining_lines);
|
||||
self.backend.append_lines(self.viewport_area.height)?;
|
||||
|
||||
self.set_viewport_area(Rect {
|
||||
x: area.left(),
|
||||
y: area.bottom().saturating_sub(missing_lines),
|
||||
width: area.width,
|
||||
height: self.viewport_area.height,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inline_size<B: Backend>(
|
||||
backend: &mut B,
|
||||
height: u16,
|
||||
size: Rect,
|
||||
offset_in_previous_viewport: u16,
|
||||
) -> io::Result<(Rect, (u16, u16))> {
|
||||
let pos = backend.get_cursor()?;
|
||||
let mut row = pos.1;
|
||||
|
||||
let max_height = size.height.min(height);
|
||||
|
||||
let lines_after_cursor = height
|
||||
.saturating_sub(offset_in_previous_viewport)
|
||||
.saturating_sub(1);
|
||||
|
||||
backend.append_lines(lines_after_cursor)?;
|
||||
|
||||
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
|
||||
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
row = row.saturating_sub(offset_in_previous_viewport);
|
||||
|
||||
Ok((
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
},
|
||||
pos,
|
||||
))
|
||||
}
|
@ -1,430 +0,0 @@
|
||||
//! Primitives for styled text.
|
||||
//!
|
||||
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
|
||||
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
|
||||
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
|
||||
//! - A single line string where each grapheme may have its own style is represented by [`Spans`].
|
||||
//! - A multiple line string where each grapheme may have its own style is represented by a
|
||||
//! [`Text`].
|
||||
//!
|
||||
//! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`]
|
||||
//! is a [`Spans`].
|
||||
//!
|
||||
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
|
||||
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations so
|
||||
//! that you can start by using simple `String` or `&str` and then promote them to the previous
|
||||
//! primitives when you need additional styling capabilities.
|
||||
//!
|
||||
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
|
||||
//! its `title` property (which is a [`Spans`] under the hood):
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::widgets::Block;
|
||||
//! # use ratatui::text::{Span, Spans};
|
||||
//! # use ratatui::style::{Color, Style};
|
||||
//! // A simple string with no styling.
|
||||
//! // Converted to Spans(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
||||
//! // ])
|
||||
//! let block = Block::default().title("My title");
|
||||
//!
|
||||
//! // A simple string with a unique style.
|
||||
//! // Converted to Spans(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(
|
||||
//! Span::styled("My title", Style::default().fg(Color::Yellow))
|
||||
//! );
|
||||
//!
|
||||
//! // A string with multiple styles.
|
||||
//! // Converted to Spans(vec![
|
||||
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
|
||||
//! // Span { content: Cow::Borrowed(" title"), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(vec![
|
||||
//! Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
//! Span::raw(" title"),
|
||||
//! ]);
|
||||
//! ```
|
||||
use crate::ratatui::style::Style;
|
||||
use std::borrow::Cow;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct StyledGrapheme<'a> {
|
||||
pub symbol: &'a str,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
/// A string where all graphemes have the same style.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Span<'a> {
|
||||
pub content: Cow<'a, str>,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Span<'a> {
|
||||
/// Create a span with no style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// Span::raw("My text");
|
||||
/// Span::raw(String::from("My text"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a span with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Span::styled("My text", style);
|
||||
/// Span::styled(String::from("My text"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the width of the content held by this span.
|
||||
pub fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the graphemes held by this span.
|
||||
///
|
||||
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
|
||||
/// the resulting [`Style`].
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, StyledGrapheme};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use std::iter::Iterator;
|
||||
/// let style = Style::default().fg(Color::Yellow);
|
||||
/// let span = Span::styled("Text", style);
|
||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
/// let styled_graphemes = span.styled_graphemes(style);
|
||||
/// assert_eq!(
|
||||
/// vec![
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "T",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "e",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "x",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "t",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// ],
|
||||
/// styled_graphemes.collect::<Vec<StyledGrapheme>>()
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
UnicodeSegmentation::graphemes(self.content.as_ref(), true)
|
||||
.map(move |g| StyledGrapheme {
|
||||
symbol: g,
|
||||
style: base_style.patch(self.style),
|
||||
})
|
||||
.filter(|s| s.symbol != "\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Span<'a> {
|
||||
fn from(s: String) -> Span<'a> {
|
||||
Span::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Span<'a> {
|
||||
fn from(s: &'a str) -> Span<'a> {
|
||||
Span::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// A string composed of clusters of graphemes, each with their own style.
|
||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
||||
pub struct Spans<'a>(pub Vec<Span<'a>>);
|
||||
|
||||
impl<'a> Spans<'a> {
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let spans = Spans::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// assert_eq!(7, spans.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.iter().map(Span::width).sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Spans<'a> {
|
||||
fn from(s: String) -> Spans<'a> {
|
||||
Spans(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Spans<'a> {
|
||||
fn from(s: &'a str) -> Spans<'a> {
|
||||
Spans(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
|
||||
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
|
||||
Spans(spans)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Spans<'a> {
|
||||
fn from(span: Span<'a>) -> Spans<'a> {
|
||||
Spans(vec![span])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Spans<'a>> for String {
|
||||
fn from(line: Spans<'a>) -> String {
|
||||
line.0.iter().fold(String::new(), |mut acc, s| {
|
||||
acc.push_str(s.content.as_ref());
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
///
|
||||
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
|
||||
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
|
||||
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
/// let mut text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
///
|
||||
/// // Adding two more unstyled lines
|
||||
/// text.extend(Text::raw("These are two\nmore lines!"));
|
||||
/// assert_eq!(4, text.height());
|
||||
///
|
||||
/// // Adding a final two styled lines
|
||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
/// assert_eq!(6, text.height());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
||||
pub struct Text<'a> {
|
||||
pub lines: Vec<Spans<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
/// Create some text (potentially multiple lines) with no style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// Text::raw("The first line\nThe second line");
|
||||
/// Text::raw(String::from("The first line\nThe second line"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let lines: Vec<_> = match content.into() {
|
||||
Cow::Borrowed("") => vec![Spans::from("")],
|
||||
Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
|
||||
Cow::Owned(s) if s.is_empty() => vec![Spans::from("")],
|
||||
Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
|
||||
};
|
||||
|
||||
Text { lines }
|
||||
}
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let mut text = Text::raw(content);
|
||||
text.patch_style(style);
|
||||
text
|
||||
}
|
||||
|
||||
/// Returns the max width of all the lines.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(15, text.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.lines
|
||||
.iter()
|
||||
.map(Spans::width)
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the height.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
/// ```
|
||||
pub fn height(&self) -> usize {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
/// Apply a new style to existing text.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_text = Text::raw("The first line\nThe second line");
|
||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// assert_ne!(raw_text, styled_text);
|
||||
///
|
||||
/// raw_text.patch_style(style);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for line in &mut self.lines {
|
||||
for span in &mut line.0 {
|
||||
span.style = span.style.patch(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Text<'a> {
|
||||
fn from(s: String) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Text<'a> {
|
||||
fn from(s: &'a str) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for Text<'a> {
|
||||
fn from(s: Cow<'a, str>) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Text<'a> {
|
||||
fn from(span: Span<'a>) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![Spans::from(span)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Spans<'a>> for Text<'a> {
|
||||
fn from(spans: Spans<'a>) -> Text<'a> {
|
||||
Text { lines: vec![spans] }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
|
||||
Text { lines }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Text<'a> {
|
||||
type Item = Spans<'a>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.lines.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extend<Spans<'a>> for Text<'a> {
|
||||
fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
|
||||
self.lines.extend(iter);
|
||||
}
|
||||
}
|
@ -1,219 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use std::cmp::min;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// Display multiple bars in a single widgets
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, Borders, BarChart};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// BarChart::default()
|
||||
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
|
||||
/// .bar_width(3)
|
||||
/// .bar_gap(1)
|
||||
/// .bar_style(Style::default().fg(Color::Yellow).bg(Color::Red))
|
||||
/// .value_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
|
||||
/// .label_style(Style::default().fg(Color::White))
|
||||
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
|
||||
/// .max(4);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BarChart<'a> {
|
||||
/// Block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// The width of each bar
|
||||
bar_width: u16,
|
||||
/// The gap between each bar
|
||||
bar_gap: u16,
|
||||
/// Set of symbols used to display the data
|
||||
bar_set: symbols::bar::Set,
|
||||
/// Style of the bars
|
||||
bar_style: Style,
|
||||
/// Style of the values printed at the bottom of each bar
|
||||
value_style: Style,
|
||||
/// Style of the labels printed under each bar
|
||||
label_style: Style,
|
||||
/// Style for the widget
|
||||
style: Style,
|
||||
/// Slice of (label, value) pair to plot on the chart
|
||||
data: &'a [(&'a str, u64)],
|
||||
/// Value necessary for a bar to reach the maximum height (if no value is specified,
|
||||
/// the maximum value in the data is taken as reference)
|
||||
max: Option<u64>,
|
||||
/// Values to display on the bar (computed when the data is passed to the widget)
|
||||
values: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'a> Default for BarChart<'a> {
|
||||
fn default() -> BarChart<'a> {
|
||||
BarChart {
|
||||
block: None,
|
||||
max: None,
|
||||
data: &[],
|
||||
values: Vec::new(),
|
||||
bar_style: Style::default(),
|
||||
bar_width: 1,
|
||||
bar_gap: 1,
|
||||
bar_set: symbols::bar::NINE_LEVELS,
|
||||
value_style: Default::default(),
|
||||
label_style: Default::default(),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BarChart<'a> {
|
||||
pub fn data(mut self, data: &'a [(&'a str, u64)]) -> BarChart<'a> {
|
||||
self.data = data;
|
||||
self.values = Vec::with_capacity(self.data.len());
|
||||
for &(_, v) in self.data {
|
||||
self.values.push(format!("{}", v));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max(mut self, max: u64) -> BarChart<'a> {
|
||||
self.max = Some(max);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.bar_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
|
||||
self.bar_width = width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
|
||||
self.bar_gap = gap;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.value_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.label_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for BarChart<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
let chart_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if chart_area.height < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let max = self
|
||||
.max
|
||||
.unwrap_or_else(|| self.data.iter().map(|t| t.1).max().unwrap_or_default());
|
||||
let max_index = min(
|
||||
(chart_area.width / (self.bar_width + self.bar_gap)) as usize,
|
||||
self.data.len(),
|
||||
);
|
||||
let mut data = self
|
||||
.data
|
||||
.iter()
|
||||
.take(max_index)
|
||||
.map(|&(l, v)| {
|
||||
(
|
||||
l,
|
||||
v * u64::from(chart_area.height - 1) * 8 / std::cmp::max(max, 1),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(&str, u64)>>();
|
||||
for j in (0..chart_area.height - 1).rev() {
|
||||
for (i, d) in data.iter_mut().enumerate() {
|
||||
let symbol = match d.1 {
|
||||
0 => self.bar_set.empty,
|
||||
1 => self.bar_set.one_eighth,
|
||||
2 => self.bar_set.one_quarter,
|
||||
3 => self.bar_set.three_eighths,
|
||||
4 => self.bar_set.half,
|
||||
5 => self.bar_set.five_eighths,
|
||||
6 => self.bar_set.three_quarters,
|
||||
7 => self.bar_set.seven_eighths,
|
||||
_ => self.bar_set.full,
|
||||
};
|
||||
|
||||
for x in 0..self.bar_width {
|
||||
buf.get_mut(
|
||||
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap) + x,
|
||||
chart_area.top() + j,
|
||||
)
|
||||
.set_symbol(symbol)
|
||||
.set_style(self.bar_style);
|
||||
}
|
||||
|
||||
if d.1 > 8 {
|
||||
d.1 -= 8;
|
||||
} else {
|
||||
d.1 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i, &(label, value)) in self.data.iter().take(max_index).enumerate() {
|
||||
if value != 0 {
|
||||
let value_label = &self.values[i];
|
||||
let width = value_label.width() as u16;
|
||||
if width < self.bar_width {
|
||||
buf.set_string(
|
||||
chart_area.left()
|
||||
+ i as u16 * (self.bar_width + self.bar_gap)
|
||||
+ (self.bar_width - width) / 2,
|
||||
chart_area.bottom() - 2,
|
||||
value_label,
|
||||
self.value_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
buf.set_stringn(
|
||||
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap),
|
||||
chart_area.bottom() - 1,
|
||||
label,
|
||||
self.bar_width as usize,
|
||||
self.label_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,573 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::Style,
|
||||
symbols::line,
|
||||
text::{Span, Spans},
|
||||
widgets::{Borders, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BorderType {
|
||||
Plain,
|
||||
Rounded,
|
||||
Double,
|
||||
Thick,
|
||||
}
|
||||
|
||||
impl BorderType {
|
||||
pub fn line_symbols(border_type: BorderType) -> line::Set {
|
||||
match border_type {
|
||||
BorderType::Plain => line::NORMAL,
|
||||
BorderType::Rounded => line::ROUNDED,
|
||||
BorderType::Double => line::DOUBLE,
|
||||
BorderType::Thick => line::THICK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Base widget to be used with all upper level ones. It may be used to display a box border around
|
||||
/// the widget and/or add a title.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, BorderType, Borders};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// Block::default()
|
||||
/// .title("Block")
|
||||
/// .borders(Borders::LEFT | Borders::RIGHT)
|
||||
/// .border_style(Style::default().fg(Color::White))
|
||||
/// .border_type(BorderType::Rounded)
|
||||
/// .style(Style::default().bg(Color::Black));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Block<'a> {
|
||||
/// Optional title place on the upper left of the block
|
||||
title: Option<Spans<'a>>,
|
||||
/// Title alignment. The default is top left of the block, but one can choose to place
|
||||
/// title in the top middle, or top right of the block
|
||||
title_alignment: Alignment,
|
||||
/// Visible borders
|
||||
borders: Borders,
|
||||
/// Border style
|
||||
border_style: Style,
|
||||
/// Type of the border. The default is plain lines but one can choose to have rounded corners
|
||||
/// or doubled lines instead.
|
||||
border_type: BorderType,
|
||||
/// Widget style
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Default for Block<'a> {
|
||||
fn default() -> Block<'a> {
|
||||
Block {
|
||||
title: None,
|
||||
title_alignment: Alignment::Left,
|
||||
borders: Borders::NONE,
|
||||
border_style: Default::default(),
|
||||
border_type: BorderType::Plain,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Block<'a> {
|
||||
pub fn title<T>(mut self, title: T) -> Block<'a>
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
{
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[deprecated(
|
||||
since = "0.10.0",
|
||||
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
|
||||
)]
|
||||
pub fn title_style(mut self, style: Style) -> Block<'a> {
|
||||
if let Some(t) = self.title {
|
||||
let title = String::from(t);
|
||||
self.title = Some(Spans::from(Span::styled(title, style)));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
|
||||
self.title_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn border_style(mut self, style: Style) -> Block<'a> {
|
||||
self.border_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Block<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn borders(mut self, flag: Borders) -> Block<'a> {
|
||||
self.borders = flag;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn border_type(mut self, border_type: BorderType) -> Block<'a> {
|
||||
self.border_type = border_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Compute the inner area of a block based on its border visibility rules.
|
||||
pub fn inner(&self, area: Rect) -> Rect {
|
||||
let mut inner = area;
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
inner.x = inner.x.saturating_add(1).min(inner.right());
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::TOP) || self.title.is_some() {
|
||||
inner.y = inner.y.saturating_add(1).min(inner.bottom());
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::RIGHT) {
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::BOTTOM) {
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
}
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Block<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
let symbols = BorderType::line_symbols(self.border_type);
|
||||
|
||||
// Sides
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(area.left(), y)
|
||||
.set_symbol(symbols.vertical)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
if self.borders.intersects(Borders::TOP) {
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, area.top())
|
||||
.set_symbol(symbols.horizontal)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
if self.borders.intersects(Borders::RIGHT) {
|
||||
let x = area.right() - 1;
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(symbols.vertical)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
if self.borders.intersects(Borders::BOTTOM) {
|
||||
let y = area.bottom() - 1;
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(symbols.horizontal)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
}
|
||||
|
||||
// Corners
|
||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
.set_symbol(symbols.bottom_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
||||
buf.get_mut(area.right() - 1, area.top())
|
||||
.set_symbol(symbols.top_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.left(), area.bottom() - 1)
|
||||
.set_symbol(symbols.bottom_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
buf.get_mut(area.left(), area.top())
|
||||
.set_symbol(symbols.top_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
|
||||
// Title
|
||||
if let Some(title) = self.title {
|
||||
let left_border_dx = if self.borders.intersects(Borders::LEFT) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let right_border_dx = if self.borders.intersects(Borders::RIGHT) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let title_area_width = area
|
||||
.width
|
||||
.saturating_sub(left_border_dx)
|
||||
.saturating_sub(right_border_dx);
|
||||
|
||||
let title_dx = match self.title_alignment {
|
||||
Alignment::Left => left_border_dx,
|
||||
Alignment::Center => area.width.saturating_sub(title.width() as u16) / 2,
|
||||
Alignment::Right => area
|
||||
.width
|
||||
.saturating_sub(title.width() as u16)
|
||||
.saturating_sub(right_border_dx),
|
||||
};
|
||||
|
||||
let title_x = area.left() + title_dx;
|
||||
let title_y = area.top();
|
||||
|
||||
buf.set_spans(title_x, title_y, &title, title_area_width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ratatui::layout::Rect;
|
||||
|
||||
#[test]
|
||||
fn inner_takes_into_account_the_borders() {
|
||||
// No borders
|
||||
assert_eq!(
|
||||
Block::default().inner(Rect::default()),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
"no borders, width=0, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"no borders, width=1, height=1"
|
||||
);
|
||||
|
||||
// Left border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"left, width=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"left, width=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"left, width=2"
|
||||
);
|
||||
|
||||
// Top border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"top, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"top, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 2
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"top, height=2"
|
||||
);
|
||||
|
||||
// Right border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"right, width=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
"right, width=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"right, width=2"
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"bottom, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
"bottom, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 2
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
"bottom, height=2"
|
||||
);
|
||||
|
||||
// All borders
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.inner(Rect::default()),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
"all borders, width=0, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
"all borders, width=1, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
"all borders, width=2, height=2"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 3,
|
||||
height: 3,
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
"all borders, width=3, height=3"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_takes_into_account_the_title() {
|
||||
assert_eq!(
|
||||
Block::default().title("Test").inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.title_alignment(Alignment::Center)
|
||||
.inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.title("Test")
|
||||
.title_alignment(Alignment::Right)
|
||||
.inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
|
||||
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Line {
|
||||
pub x1: f64,
|
||||
pub y1: f64,
|
||||
pub x2: f64,
|
||||
pub y2: f64,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Shape for Line {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
let (x1, y1) = match painter.get_point(self.x1, self.y1) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
let (x2, y2) = match painter.get_point(self.x2, self.y2) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
let (dx, x_range) = if x2 >= x1 {
|
||||
(x2 - x1, x1..=x2)
|
||||
} else {
|
||||
(x1 - x2, x2..=x1)
|
||||
};
|
||||
let (dy, y_range) = if y2 >= y1 {
|
||||
(y2 - y1, y1..=y2)
|
||||
} else {
|
||||
(y1 - y2, y2..=y1)
|
||||
};
|
||||
|
||||
if dx == 0 {
|
||||
for y in y_range {
|
||||
painter.paint(x1, y, self.color);
|
||||
}
|
||||
} else if dy == 0 {
|
||||
for x in x_range {
|
||||
painter.paint(x, y1, self.color);
|
||||
}
|
||||
} else if dy < dx {
|
||||
if x1 > x2 {
|
||||
draw_line_low(painter, x2, y2, x1, y1, self.color);
|
||||
} else {
|
||||
draw_line_low(painter, x1, y1, x2, y2, self.color);
|
||||
}
|
||||
} else if y1 > y2 {
|
||||
draw_line_high(painter, x2, y2, x1, y1, self.color);
|
||||
} else {
|
||||
draw_line_high(painter, x1, y1, x2, y2, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
||||
let dx = (x2 - x1) as isize;
|
||||
let dy = (y2 as isize - y1 as isize).abs();
|
||||
let mut d = 2 * dy - dx;
|
||||
let mut y = y1;
|
||||
for x in x1..=x2 {
|
||||
painter.paint(x, y, color);
|
||||
if d > 0 {
|
||||
y = if y1 > y2 {
|
||||
y.saturating_sub(1)
|
||||
} else {
|
||||
y.saturating_add(1)
|
||||
};
|
||||
d -= 2 * dx;
|
||||
}
|
||||
d += 2 * dy;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
||||
let dx = (x2 as isize - x1 as isize).abs();
|
||||
let dy = (y2 - y1) as isize;
|
||||
let mut d = 2 * dx - dy;
|
||||
let mut x = x1;
|
||||
for y in y1..=y2 {
|
||||
painter.paint(x, y, color);
|
||||
if d > 0 {
|
||||
x = if x1 > x2 {
|
||||
x.saturating_sub(1)
|
||||
} else {
|
||||
x.saturating_add(1)
|
||||
};
|
||||
d -= 2 * dy;
|
||||
}
|
||||
d += 2 * dx;
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
style::Color,
|
||||
widgets::canvas::{
|
||||
world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION},
|
||||
Painter, Shape,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MapResolution {
|
||||
Low,
|
||||
High,
|
||||
}
|
||||
|
||||
impl MapResolution {
|
||||
fn data(self) -> &'static [(f64, f64)] {
|
||||
match self {
|
||||
MapResolution::Low => &WORLD_LOW_RESOLUTION,
|
||||
MapResolution::High => &WORLD_HIGH_RESOLUTION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shape to draw a world map with the given resolution and color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Map {
|
||||
pub resolution: MapResolution,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Default for Map {
|
||||
fn default() -> Map {
|
||||
Map {
|
||||
resolution: MapResolution::Low,
|
||||
color: Color::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape for Map {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
for (x, y) in self.resolution.data() {
|
||||
if let Some((x, y)) = painter.get_point(*x, *y) {
|
||||
painter.paint(x, y, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,510 +0,0 @@
|
||||
mod line;
|
||||
mod map;
|
||||
mod points;
|
||||
mod rectangle;
|
||||
mod world;
|
||||
|
||||
pub use self::line::Line;
|
||||
pub use self::map::{Map, MapResolution};
|
||||
pub use self::points::Points;
|
||||
pub use self::rectangle::Rectangle;
|
||||
|
||||
use crate::ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Spans,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Interface for all shapes that may be drawn on a Canvas widget.
|
||||
pub trait Shape {
|
||||
fn draw(&self, painter: &mut Painter);
|
||||
}
|
||||
|
||||
/// Label to draw some text on the canvas
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Label<'a> {
|
||||
x: f64,
|
||||
y: f64,
|
||||
spans: Spans<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Layer {
|
||||
string: String,
|
||||
colors: Vec<Color>,
|
||||
}
|
||||
|
||||
trait Grid: Debug {
|
||||
fn width(&self) -> u16;
|
||||
fn height(&self) -> u16;
|
||||
fn resolution(&self) -> (f64, f64);
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color);
|
||||
fn save(&self) -> Layer;
|
||||
fn reset(&mut self);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct BrailleGrid {
|
||||
width: u16,
|
||||
height: u16,
|
||||
cells: Vec<u16>,
|
||||
colors: Vec<Color>,
|
||||
}
|
||||
|
||||
impl BrailleGrid {
|
||||
fn new(width: u16, height: u16) -> BrailleGrid {
|
||||
let length = usize::from(width * height);
|
||||
BrailleGrid {
|
||||
width,
|
||||
height,
|
||||
cells: vec![symbols::braille::BLANK; length],
|
||||
colors: vec![Color::Reset; length],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid for BrailleGrid {
|
||||
fn width(&self) -> u16 {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn resolution(&self) -> (f64, f64) {
|
||||
(
|
||||
f64::from(self.width) * 2.0 - 1.0,
|
||||
f64::from(self.height) * 4.0 - 1.0,
|
||||
)
|
||||
}
|
||||
|
||||
fn save(&self) -> Layer {
|
||||
Layer {
|
||||
string: String::from_utf16(&self.cells).unwrap(),
|
||||
colors: self.colors.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
for c in &mut self.cells {
|
||||
*c = symbols::braille::BLANK;
|
||||
}
|
||||
for c in &mut self.colors {
|
||||
*c = Color::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
let index = y / 4 * self.width as usize + x / 2;
|
||||
if let Some(c) = self.cells.get_mut(index) {
|
||||
*c |= symbols::braille::DOTS[y % 4][x % 2];
|
||||
}
|
||||
if let Some(c) = self.colors.get_mut(index) {
|
||||
*c = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CharGrid {
|
||||
width: u16,
|
||||
height: u16,
|
||||
cells: Vec<char>,
|
||||
colors: Vec<Color>,
|
||||
cell_char: char,
|
||||
}
|
||||
|
||||
impl CharGrid {
|
||||
fn new(width: u16, height: u16, cell_char: char) -> CharGrid {
|
||||
let length = usize::from(width * height);
|
||||
CharGrid {
|
||||
width,
|
||||
height,
|
||||
cells: vec![' '; length],
|
||||
colors: vec![Color::Reset; length],
|
||||
cell_char,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid for CharGrid {
|
||||
fn width(&self) -> u16 {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn resolution(&self) -> (f64, f64) {
|
||||
(f64::from(self.width) - 1.0, f64::from(self.height) - 1.0)
|
||||
}
|
||||
|
||||
fn save(&self) -> Layer {
|
||||
Layer {
|
||||
string: self.cells.iter().collect(),
|
||||
colors: self.colors.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
for c in &mut self.cells {
|
||||
*c = ' ';
|
||||
}
|
||||
for c in &mut self.colors {
|
||||
*c = Color::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
let index = y * self.width as usize + x;
|
||||
if let Some(c) = self.cells.get_mut(index) {
|
||||
*c = self.cell_char;
|
||||
}
|
||||
if let Some(c) = self.colors.get_mut(index) {
|
||||
*c = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Painter<'a, 'b> {
|
||||
context: &'a mut Context<'b>,
|
||||
resolution: (f64, f64),
|
||||
}
|
||||
|
||||
impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// Convert the (x, y) coordinates to location of a point on the grid
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use ratatui::{symbols, widgets::canvas::{Painter, Context}};
|
||||
///
|
||||
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
/// let point = painter.get_point(1.0, 0.0);
|
||||
/// assert_eq!(point, Some((0, 7)));
|
||||
/// let point = painter.get_point(1.5, 1.0);
|
||||
/// assert_eq!(point, Some((1, 3)));
|
||||
/// let point = painter.get_point(0.0, 0.0);
|
||||
/// assert_eq!(point, None);
|
||||
/// let point = painter.get_point(2.0, 2.0);
|
||||
/// assert_eq!(point, Some((3, 0)));
|
||||
/// let point = painter.get_point(1.0, 2.0);
|
||||
/// assert_eq!(point, Some((0, 0)));
|
||||
/// ```
|
||||
pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {
|
||||
let left = self.context.x_bounds[0];
|
||||
let right = self.context.x_bounds[1];
|
||||
let top = self.context.y_bounds[1];
|
||||
let bottom = self.context.y_bounds[0];
|
||||
if x < left || x > right || y < bottom || y > top {
|
||||
return None;
|
||||
}
|
||||
let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs();
|
||||
let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs();
|
||||
if width == 0.0 || height == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let x = ((x - left) * self.resolution.0 / width) as usize;
|
||||
let y = ((top - y) * self.resolution.1 / height) as usize;
|
||||
Some((x, y))
|
||||
}
|
||||
|
||||
/// Paint a point of the grid
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use ratatui::{style::Color, symbols, widgets::canvas::{Painter, Context}};
|
||||
///
|
||||
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
/// let cell = painter.paint(1, 3, Color::Red);
|
||||
/// ```
|
||||
pub fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
self.context.grid.paint(x, y, color);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||
fn from(context: &'a mut Context<'b>) -> Painter<'a, 'b> {
|
||||
let resolution = context.grid.resolution();
|
||||
Painter {
|
||||
context,
|
||||
resolution,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the state of the Canvas when painting to it.
|
||||
#[derive(Debug)]
|
||||
pub struct Context<'a> {
|
||||
x_bounds: [f64; 2],
|
||||
y_bounds: [f64; 2],
|
||||
grid: Box<dyn Grid>,
|
||||
dirty: bool,
|
||||
layers: Vec<Layer>,
|
||||
labels: Vec<Label<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
pub fn new(
|
||||
width: u16,
|
||||
height: u16,
|
||||
x_bounds: [f64; 2],
|
||||
y_bounds: [f64; 2],
|
||||
marker: symbols::Marker,
|
||||
) -> Context<'a> {
|
||||
let grid: Box<dyn Grid> = match marker {
|
||||
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, '•')),
|
||||
symbols::Marker::Block => Box::new(CharGrid::new(width, height, '▄')),
|
||||
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
|
||||
};
|
||||
Context {
|
||||
x_bounds,
|
||||
y_bounds,
|
||||
grid,
|
||||
dirty: false,
|
||||
layers: Vec::new(),
|
||||
labels: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw any object that may implement the Shape trait
|
||||
pub fn draw<S>(&mut self, shape: &S)
|
||||
where
|
||||
S: Shape,
|
||||
{
|
||||
self.dirty = true;
|
||||
let mut painter = Painter::from(self);
|
||||
shape.draw(&mut painter);
|
||||
}
|
||||
|
||||
/// Go one layer above in the canvas.
|
||||
pub fn layer(&mut self) {
|
||||
self.layers.push(self.grid.save());
|
||||
self.grid.reset();
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
/// Print a string on the canvas at the given position
|
||||
pub fn print<T>(&mut self, x: f64, y: f64, spans: T)
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
{
|
||||
self.labels.push(Label {
|
||||
x,
|
||||
y,
|
||||
spans: spans.into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Push the last layer if necessary
|
||||
fn finish(&mut self) {
|
||||
if self.dirty {
|
||||
self.layer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Canvas widget may be used to draw more detailed figures using braille patterns (each
|
||||
/// cell can have a braille character in 8 different positions).
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, Borders};
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
|
||||
/// # use ratatui::style::Color;
|
||||
/// Canvas::default()
|
||||
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
|
||||
/// .x_bounds([-180.0, 180.0])
|
||||
/// .y_bounds([-90.0, 90.0])
|
||||
/// .paint(|ctx| {
|
||||
/// ctx.draw(&Map {
|
||||
/// resolution: MapResolution::High,
|
||||
/// color: Color::White
|
||||
/// });
|
||||
/// ctx.layer();
|
||||
/// ctx.draw(&Line {
|
||||
/// x1: 0.0,
|
||||
/// y1: 10.0,
|
||||
/// x2: 10.0,
|
||||
/// y2: 10.0,
|
||||
/// color: Color::White,
|
||||
/// });
|
||||
/// ctx.draw(&Rectangle {
|
||||
/// x: 10.0,
|
||||
/// y: 20.0,
|
||||
/// width: 10.0,
|
||||
/// height: 10.0,
|
||||
/// color: Color::Red
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
pub struct Canvas<'a, F>
|
||||
where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
block: Option<Block<'a>>,
|
||||
x_bounds: [f64; 2],
|
||||
y_bounds: [f64; 2],
|
||||
painter: Option<F>,
|
||||
background_color: Color,
|
||||
marker: symbols::Marker,
|
||||
}
|
||||
|
||||
impl<'a, F> Default for Canvas<'a, F>
|
||||
where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
fn default() -> Canvas<'a, F> {
|
||||
Canvas {
|
||||
block: None,
|
||||
x_bounds: [0.0, 0.0],
|
||||
y_bounds: [0.0, 0.0],
|
||||
painter: None,
|
||||
background_color: Color::Reset,
|
||||
marker: symbols::Marker::Braille,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, F> Canvas<'a, F>
|
||||
where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
pub fn block(mut self, block: Block<'a>) -> Canvas<'a, F> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
/// Define the viewport of the canvas.
|
||||
/// If you were to "zoom" to a certain part of the world you may want to choose different
|
||||
/// bounds.
|
||||
pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
|
||||
self.x_bounds = bounds;
|
||||
self
|
||||
}
|
||||
|
||||
/// Define the viewport of the canvas.
|
||||
///
|
||||
/// If you were to "zoom" to a certain part of the world you may want to choose different
|
||||
/// bounds.
|
||||
pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
|
||||
self.y_bounds = bounds;
|
||||
self
|
||||
}
|
||||
|
||||
/// Store the closure that will be used to draw to the Canvas
|
||||
pub fn paint(mut self, f: F) -> Canvas<'a, F> {
|
||||
self.painter = Some(f);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn background_color(mut self, color: Color) -> Canvas<'a, F> {
|
||||
self.background_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the type of points used to draw the shapes. By default the braille patterns are used
|
||||
/// as they provide a more fine grained result but you might want to use the simple dot or
|
||||
/// block instead if the targeted terminal does not support those symbols.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::canvas::Canvas;
|
||||
/// # use ratatui::symbols;
|
||||
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
|
||||
///
|
||||
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
|
||||
///
|
||||
/// Canvas::default().marker(symbols::Marker::Block).paint(|ctx| {});
|
||||
/// ```
|
||||
pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> {
|
||||
self.marker = marker;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, F> Widget for Canvas<'a, F>
|
||||
where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
let canvas_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
buf.set_style(canvas_area, Style::default().bg(self.background_color));
|
||||
|
||||
let width = canvas_area.width as usize;
|
||||
|
||||
let painter = match self.painter {
|
||||
Some(ref p) => p,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Create a blank context that match the size of the canvas
|
||||
let mut ctx = Context::new(
|
||||
canvas_area.width,
|
||||
canvas_area.height,
|
||||
self.x_bounds,
|
||||
self.y_bounds,
|
||||
self.marker,
|
||||
);
|
||||
// Paint to this context
|
||||
painter(&mut ctx);
|
||||
ctx.finish();
|
||||
|
||||
// Retrieve painted points for each layer
|
||||
for layer in ctx.layers {
|
||||
for (i, (ch, color)) in layer
|
||||
.string
|
||||
.chars()
|
||||
.zip(layer.colors.into_iter())
|
||||
.enumerate()
|
||||
{
|
||||
if ch != ' ' && ch != '\u{2800}' {
|
||||
let (x, y) = (i % width, i / width);
|
||||
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally draw the labels
|
||||
let left = self.x_bounds[0];
|
||||
let right = self.x_bounds[1];
|
||||
let top = self.y_bounds[1];
|
||||
let bottom = self.y_bounds[0];
|
||||
let width = (self.x_bounds[1] - self.x_bounds[0]).abs();
|
||||
let height = (self.y_bounds[1] - self.y_bounds[0]).abs();
|
||||
let resolution = {
|
||||
let width = f64::from(canvas_area.width - 1);
|
||||
let height = f64::from(canvas_area.height - 1);
|
||||
(width, height)
|
||||
};
|
||||
for label in ctx
|
||||
.labels
|
||||
.iter()
|
||||
.filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom)
|
||||
{
|
||||
let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
|
||||
let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
|
||||
buf.set_spans(x, y, &label.spans, canvas_area.right() - x);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
|
||||
/// A shape to draw a group of points with the given color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Points<'a> {
|
||||
pub coords: &'a [(f64, f64)],
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl<'a> Shape for Points<'a> {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
for (x, y) in self.coords {
|
||||
if let Some((x, y)) = painter.get_point(*x, *y) {
|
||||
painter.paint(x, y, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Default for Points<'a> {
|
||||
fn default() -> Points<'a> {
|
||||
Points {
|
||||
coords: &[],
|
||||
color: Color::Reset,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
style::Color,
|
||||
widgets::canvas::{Line, Painter, Shape},
|
||||
};
|
||||
|
||||
/// Shape to draw a rectangle from a `Rect` with the given color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rectangle {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Shape for Rectangle {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
let lines: [Line; 4] = [
|
||||
Line {
|
||||
x1: self.x,
|
||||
y1: self.y,
|
||||
x2: self.x,
|
||||
y2: self.y + self.height,
|
||||
color: self.color,
|
||||
},
|
||||
Line {
|
||||
x1: self.x,
|
||||
y1: self.y + self.height,
|
||||
x2: self.x + self.width,
|
||||
y2: self.y + self.height,
|
||||
color: self.color,
|
||||
},
|
||||
Line {
|
||||
x1: self.x + self.width,
|
||||
y1: self.y,
|
||||
x2: self.x + self.width,
|
||||
y2: self.y + self.height,
|
||||
color: self.color,
|
||||
},
|
||||
Line {
|
||||
x1: self.x,
|
||||
y1: self.y,
|
||||
x2: self.x + self.width,
|
||||
y2: self.y,
|
||||
color: self.color,
|
||||
},
|
||||
];
|
||||
for line in &lines {
|
||||
line.draw(painter);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,660 +0,0 @@
|
||||
use std::{borrow::Cow, cmp::max};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::ratatui::layout::Alignment;
|
||||
use crate::ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::{
|
||||
canvas::{Canvas, Line, Points},
|
||||
Block, Borders, Widget,
|
||||
},
|
||||
};
|
||||
|
||||
/// An X or Y axis for the chart widget
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Axis<'a> {
|
||||
/// Title displayed next to axis end
|
||||
title: Option<Spans<'a>>,
|
||||
/// Bounds for the axis (all data points outside these limits will not be represented)
|
||||
bounds: [f64; 2],
|
||||
/// A list of labels to put to the left or below the axis
|
||||
labels: Option<Vec<Span<'a>>>,
|
||||
/// The style used to draw the axis itself
|
||||
style: Style,
|
||||
/// The alignment of the labels of the Axis
|
||||
labels_alignment: Alignment,
|
||||
}
|
||||
|
||||
impl<'a> Default for Axis<'a> {
|
||||
fn default() -> Axis<'a> {
|
||||
Axis {
|
||||
title: None,
|
||||
bounds: [0.0, 0.0],
|
||||
labels: None,
|
||||
style: Default::default(),
|
||||
labels_alignment: Alignment::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Axis<'a> {
|
||||
pub fn title<T>(mut self, title: T) -> Axis<'a>
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
{
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[deprecated(
|
||||
since = "0.10.0",
|
||||
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
|
||||
)]
|
||||
pub fn title_style(mut self, style: Style) -> Axis<'a> {
|
||||
if let Some(t) = self.title {
|
||||
let title = String::from(t);
|
||||
self.title = Some(Spans::from(Span::styled(title, style)));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
|
||||
self.bounds = bounds;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn labels(mut self, labels: Vec<Span<'a>>) -> Axis<'a> {
|
||||
self.labels = Some(labels);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Axis<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines the alignment of the labels of the axis.
|
||||
/// The alignment behaves differently based on the axis:
|
||||
/// - Y-Axis: The labels are aligned within the area on the left of the axis
|
||||
/// - X-Axis: The first X-axis label is aligned relative to the Y-axis
|
||||
pub fn labels_alignment(mut self, alignment: Alignment) -> Axis<'a> {
|
||||
self.labels_alignment = alignment;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to determine which style of graphing to use
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum GraphType {
|
||||
/// Draw each point
|
||||
Scatter,
|
||||
/// Draw each point and lines between each point using the same marker
|
||||
Line,
|
||||
}
|
||||
|
||||
/// A group of data points
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Dataset<'a> {
|
||||
/// Name of the dataset (used in the legend if shown)
|
||||
name: Cow<'a, str>,
|
||||
/// A reference to the actual data
|
||||
data: &'a [(f64, f64)],
|
||||
/// Symbol used for each points of this dataset
|
||||
marker: symbols::Marker,
|
||||
/// Determines graph type used for drawing points
|
||||
graph_type: GraphType,
|
||||
/// Style used to plot this dataset
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Default for Dataset<'a> {
|
||||
fn default() -> Dataset<'a> {
|
||||
Dataset {
|
||||
name: Cow::from(""),
|
||||
data: &[],
|
||||
marker: symbols::Marker::Dot,
|
||||
graph_type: GraphType::Scatter,
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Dataset<'a> {
|
||||
pub fn name<S>(mut self, name: S) -> Dataset<'a>
|
||||
where
|
||||
S: Into<Cow<'a, str>>,
|
||||
{
|
||||
self.name = name.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
|
||||
self.data = data;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
|
||||
self.marker = marker;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
|
||||
self.graph_type = graph_type;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Dataset<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A container that holds all the infos about where to display each elements of the chart (axis,
|
||||
/// labels, legend, ...).
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
struct ChartLayout {
|
||||
/// Location of the title of the x axis
|
||||
title_x: Option<(u16, u16)>,
|
||||
/// Location of the title of the y axis
|
||||
title_y: Option<(u16, u16)>,
|
||||
/// Location of the first label of the x axis
|
||||
label_x: Option<u16>,
|
||||
/// Location of the first label of the y axis
|
||||
label_y: Option<u16>,
|
||||
/// Y coordinate of the horizontal axis
|
||||
axis_x: Option<u16>,
|
||||
/// X coordinate of the vertical axis
|
||||
axis_y: Option<u16>,
|
||||
/// Area of the legend
|
||||
legend_area: Option<Rect>,
|
||||
/// Area of the graph
|
||||
graph_area: Rect,
|
||||
}
|
||||
|
||||
/// A widget to plot one or more dataset in a cartesian coordinate system
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::symbols;
|
||||
/// # use ratatui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// # use ratatui::text::Span;
|
||||
/// let datasets = vec![
|
||||
/// Dataset::default()
|
||||
/// .name("data1")
|
||||
/// .marker(symbols::Marker::Dot)
|
||||
/// .graph_type(GraphType::Scatter)
|
||||
/// .style(Style::default().fg(Color::Cyan))
|
||||
/// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
|
||||
/// Dataset::default()
|
||||
/// .name("data2")
|
||||
/// .marker(symbols::Marker::Braille)
|
||||
/// .graph_type(GraphType::Line)
|
||||
/// .style(Style::default().fg(Color::Magenta))
|
||||
/// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)]),
|
||||
/// ];
|
||||
/// Chart::new(datasets)
|
||||
/// .block(Block::default().title("Chart"))
|
||||
/// .x_axis(Axis::default()
|
||||
/// .title(Span::styled("X Axis", Style::default().fg(Color::Red)))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .bounds([0.0, 10.0])
|
||||
/// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()))
|
||||
/// .y_axis(Axis::default()
|
||||
/// .title(Span::styled("Y Axis", Style::default().fg(Color::Red)))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .bounds([0.0, 10.0])
|
||||
/// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Chart<'a> {
|
||||
/// A block to display around the widget eventually
|
||||
block: Option<Block<'a>>,
|
||||
/// The horizontal axis
|
||||
x_axis: Axis<'a>,
|
||||
/// The vertical axis
|
||||
y_axis: Axis<'a>,
|
||||
/// A reference to the datasets
|
||||
datasets: Vec<Dataset<'a>>,
|
||||
/// The widget base style
|
||||
style: Style,
|
||||
/// Constraints used to determine whether the legend should be shown or not
|
||||
hidden_legend_constraints: (Constraint, Constraint),
|
||||
}
|
||||
|
||||
impl<'a> Chart<'a> {
|
||||
pub fn new(datasets: Vec<Dataset<'a>>) -> Chart<'a> {
|
||||
Chart {
|
||||
block: None,
|
||||
x_axis: Axis::default(),
|
||||
y_axis: Axis::default(),
|
||||
style: Default::default(),
|
||||
datasets,
|
||||
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> Chart<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Chart<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn x_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
|
||||
self.x_axis = axis;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn y_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
|
||||
self.y_axis = axis;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the constraints used to determine whether the legend should be shown or not.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::Chart;
|
||||
/// # use ratatui::layout::Constraint;
|
||||
/// let constraints = (
|
||||
/// Constraint::Ratio(1, 3),
|
||||
/// Constraint::Ratio(1, 4)
|
||||
/// );
|
||||
/// // Hide the legend when either its width is greater than 33% of the total widget width
|
||||
/// // or if its height is greater than 25% of the total widget height.
|
||||
/// let _chart: Chart = Chart::new(vec![])
|
||||
/// .hidden_legend_constraints(constraints);
|
||||
/// ```
|
||||
pub fn hidden_legend_constraints(mut self, constraints: (Constraint, Constraint)) -> Chart<'a> {
|
||||
self.hidden_legend_constraints = constraints;
|
||||
self
|
||||
}
|
||||
|
||||
/// Compute the internal layout of the chart given the area. If the area is too small some
|
||||
/// elements may be automatically hidden
|
||||
fn layout(&self, area: Rect) -> ChartLayout {
|
||||
let mut layout = ChartLayout::default();
|
||||
if area.height == 0 || area.width == 0 {
|
||||
return layout;
|
||||
}
|
||||
let mut x = area.left();
|
||||
let mut y = area.bottom() - 1;
|
||||
|
||||
if self.x_axis.labels.is_some() && y > area.top() {
|
||||
layout.label_x = Some(y);
|
||||
y -= 1;
|
||||
}
|
||||
|
||||
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
|
||||
x += self.max_width_of_labels_left_of_y_axis(area, self.y_axis.labels.is_some());
|
||||
|
||||
if self.x_axis.labels.is_some() && y > area.top() {
|
||||
layout.axis_x = Some(y);
|
||||
y -= 1;
|
||||
}
|
||||
|
||||
if self.y_axis.labels.is_some() && x + 1 < area.right() {
|
||||
layout.axis_y = Some(x);
|
||||
x += 1;
|
||||
}
|
||||
|
||||
if x < area.right() && y > 1 {
|
||||
layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
|
||||
}
|
||||
|
||||
if let Some(ref title) = self.x_axis.title {
|
||||
let w = title.width() as u16;
|
||||
if w < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||
layout.title_x = Some((x + layout.graph_area.width - w, y));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref title) = self.y_axis.title {
|
||||
let w = title.width() as u16;
|
||||
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||
layout.title_y = Some((x, area.top()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
|
||||
let legend_width = inner_width + 2;
|
||||
let legend_height = self.datasets.len() as u16 + 2;
|
||||
let max_legend_width = self
|
||||
.hidden_legend_constraints
|
||||
.0
|
||||
.apply(layout.graph_area.width);
|
||||
let max_legend_height = self
|
||||
.hidden_legend_constraints
|
||||
.1
|
||||
.apply(layout.graph_area.height);
|
||||
if inner_width > 0
|
||||
&& legend_width < max_legend_width
|
||||
&& legend_height < max_legend_height
|
||||
{
|
||||
layout.legend_area = Some(Rect::new(
|
||||
layout.graph_area.right() - legend_width,
|
||||
layout.graph_area.top(),
|
||||
legend_width,
|
||||
legend_height,
|
||||
));
|
||||
}
|
||||
}
|
||||
layout
|
||||
}
|
||||
|
||||
fn max_width_of_labels_left_of_y_axis(&self, area: Rect, has_y_axis: bool) -> u16 {
|
||||
let mut max_width = self
|
||||
.y_axis
|
||||
.labels
|
||||
.as_ref()
|
||||
.map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(first_x_label) = self.x_axis.labels.as_ref().and_then(|labels| labels.get(0)) {
|
||||
let first_label_width = first_x_label.content.width() as u16;
|
||||
let width_left_of_y_axis = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => {
|
||||
// The last character of the label should be below the Y-Axis when it exists, not on its left
|
||||
let y_axis_offset = if has_y_axis { 1 } else { 0 };
|
||||
first_label_width.saturating_sub(y_axis_offset)
|
||||
}
|
||||
Alignment::Center => first_label_width / 2,
|
||||
Alignment::Right => 0,
|
||||
};
|
||||
max_width = max(max_width, width_left_of_y_axis);
|
||||
}
|
||||
// labels of y axis and first label of x axis can take at most 1/3rd of the total width
|
||||
max_width.min(area.width / 3)
|
||||
}
|
||||
|
||||
fn render_x_labels(
|
||||
&mut self,
|
||||
buf: &mut Buffer,
|
||||
layout: &ChartLayout,
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) {
|
||||
let y = match layout.label_x {
|
||||
Some(y) => y,
|
||||
None => return,
|
||||
};
|
||||
let labels = self.x_axis.labels.as_ref().unwrap();
|
||||
let labels_len = labels.len() as u16;
|
||||
if labels_len < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let width_between_ticks = graph_area.width / labels_len;
|
||||
|
||||
let label_area = self.first_x_label_area(
|
||||
y,
|
||||
labels.first().unwrap().width() as u16,
|
||||
width_between_ticks,
|
||||
chart_area,
|
||||
graph_area,
|
||||
);
|
||||
|
||||
let label_alignment = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => Alignment::Right,
|
||||
Alignment::Center => Alignment::Center,
|
||||
Alignment::Right => Alignment::Left,
|
||||
};
|
||||
|
||||
Self::render_label(buf, labels.first().unwrap(), label_area, label_alignment);
|
||||
|
||||
for (i, label) in labels[1..labels.len() - 1].iter().enumerate() {
|
||||
// We add 1 to x (and width-1 below) to leave at least one space before each intermediate labels
|
||||
let x = graph_area.left() + (i + 1) as u16 * width_between_ticks + 1;
|
||||
let label_area = Rect::new(x, y, width_between_ticks.saturating_sub(1), 1);
|
||||
|
||||
Self::render_label(buf, label, label_area, Alignment::Center);
|
||||
}
|
||||
|
||||
let x = graph_area.right() - width_between_ticks;
|
||||
let label_area = Rect::new(x, y, width_between_ticks, 1);
|
||||
// The last label should be aligned Right to be at the edge of the graph area
|
||||
Self::render_label(buf, labels.last().unwrap(), label_area, Alignment::Right);
|
||||
}
|
||||
|
||||
fn first_x_label_area(
|
||||
&self,
|
||||
y: u16,
|
||||
label_width: u16,
|
||||
max_width_after_y_axis: u16,
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) -> Rect {
|
||||
let (min_x, max_x) = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => (chart_area.left(), graph_area.left()),
|
||||
Alignment::Center => (
|
||||
chart_area.left(),
|
||||
graph_area.left() + max_width_after_y_axis.min(label_width),
|
||||
),
|
||||
Alignment::Right => (
|
||||
graph_area.left().saturating_sub(1),
|
||||
graph_area.left() + max_width_after_y_axis,
|
||||
),
|
||||
};
|
||||
|
||||
Rect::new(min_x, y, max_x - min_x, 1)
|
||||
}
|
||||
|
||||
fn render_label(buf: &mut Buffer, label: &Span, label_area: Rect, alignment: Alignment) {
|
||||
let label_width = label.width() as u16;
|
||||
let bounded_label_width = label_area.width.min(label_width);
|
||||
|
||||
let x = match alignment {
|
||||
Alignment::Left => label_area.left(),
|
||||
Alignment::Center => label_area.left() + label_area.width / 2 - bounded_label_width / 2,
|
||||
Alignment::Right => label_area.right() - bounded_label_width,
|
||||
};
|
||||
|
||||
buf.set_span(x, label_area.top(), label, bounded_label_width);
|
||||
}
|
||||
|
||||
fn render_y_labels(
|
||||
&mut self,
|
||||
buf: &mut Buffer,
|
||||
layout: &ChartLayout,
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) {
|
||||
let x = match layout.label_y {
|
||||
Some(x) => x,
|
||||
None => return,
|
||||
};
|
||||
let labels = self.y_axis.labels.as_ref().unwrap();
|
||||
let labels_len = labels.len() as u16;
|
||||
for (i, label) in labels.iter().enumerate() {
|
||||
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
|
||||
if dy < graph_area.bottom() {
|
||||
let label_area = Rect::new(
|
||||
x,
|
||||
graph_area.bottom().saturating_sub(1) - dy,
|
||||
(graph_area.left() - chart_area.left()).saturating_sub(1),
|
||||
1,
|
||||
);
|
||||
Self::render_label(buf, label, label_area, self.y_axis.labels_alignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Chart<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
// Sample the style of the entire widget. This sample will be used to reset the style of
|
||||
// the cells that are part of the components put on top of the grah area (i.e legend and
|
||||
// axis names).
|
||||
let original_style = buf.get(area.left(), area.top()).style();
|
||||
|
||||
let chart_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
let layout = self.layout(chart_area);
|
||||
let graph_area = layout.graph_area;
|
||||
if graph_area.width < 1 || graph_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.render_x_labels(buf, &layout, chart_area, graph_area);
|
||||
self.render_y_labels(buf, &layout, chart_area, graph_area);
|
||||
|
||||
if let Some(y) = layout.axis_x {
|
||||
for x in graph_area.left()..graph_area.right() {
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(symbols::line::HORIZONTAL)
|
||||
.set_style(self.x_axis.style);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(x) = layout.axis_y {
|
||||
for y in graph_area.top()..graph_area.bottom() {
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(symbols::line::VERTICAL)
|
||||
.set_style(self.y_axis.style);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(y) = layout.axis_x {
|
||||
if let Some(x) = layout.axis_y {
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(symbols::line::BOTTOM_LEFT)
|
||||
.set_style(self.x_axis.style);
|
||||
}
|
||||
}
|
||||
|
||||
for dataset in &self.datasets {
|
||||
Canvas::default()
|
||||
.background_color(self.style.bg.unwrap_or(Color::Reset))
|
||||
.x_bounds(self.x_axis.bounds)
|
||||
.y_bounds(self.y_axis.bounds)
|
||||
.marker(dataset.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Points {
|
||||
coords: dataset.data,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
});
|
||||
if let GraphType::Line = dataset.graph_type {
|
||||
for data in dataset.data.windows(2) {
|
||||
ctx.draw(&Line {
|
||||
x1: data[0].0,
|
||||
y1: data[0].1,
|
||||
x2: data[1].0,
|
||||
y2: data[1].1,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.render(graph_area, buf);
|
||||
}
|
||||
|
||||
if let Some(legend_area) = layout.legend_area {
|
||||
buf.set_style(legend_area, original_style);
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.render(legend_area, buf);
|
||||
for (i, dataset) in self.datasets.iter().enumerate() {
|
||||
buf.set_string(
|
||||
legend_area.x + 1,
|
||||
legend_area.y + 1 + i as u16,
|
||||
&dataset.name,
|
||||
dataset.style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_x {
|
||||
let title = self.x_axis.title.unwrap();
|
||||
let width = graph_area.right().saturating_sub(x);
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height: 1,
|
||||
},
|
||||
original_style,
|
||||
);
|
||||
buf.set_spans(x, y, &title, width);
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_y {
|
||||
let title = self.y_axis.title.unwrap();
|
||||
let width = graph_area.right().saturating_sub(x);
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height: 1,
|
||||
},
|
||||
original_style,
|
||||
);
|
||||
buf.set_spans(x, y, &title, width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct LegendTestCase {
|
||||
chart_area: Rect,
|
||||
hidden_legend_constraints: (Constraint, Constraint),
|
||||
legend_area: Option<Rect>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_hide_the_legend() {
|
||||
let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)];
|
||||
let cases = [
|
||||
LegendTestCase {
|
||||
chart_area: Rect::new(0, 0, 100, 100),
|
||||
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
|
||||
legend_area: Some(Rect::new(88, 0, 12, 12)),
|
||||
},
|
||||
LegendTestCase {
|
||||
chart_area: Rect::new(0, 0, 100, 100),
|
||||
hidden_legend_constraints: (Constraint::Ratio(1, 10), Constraint::Ratio(1, 4)),
|
||||
legend_area: None,
|
||||
},
|
||||
];
|
||||
for case in &cases {
|
||||
let datasets = (0..10)
|
||||
.map(|i| {
|
||||
let name = format!("Dataset #{}", i);
|
||||
Dataset::default().name(name).data(&data)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let chart = Chart::new(datasets)
|
||||
.x_axis(Axis::default().title("X axis"))
|
||||
.y_axis(Axis::default().title("Y axis"))
|
||||
.hidden_legend_constraints(case.hidden_legend_constraints);
|
||||
let layout = chart.layout(case.chart_area);
|
||||
assert_eq!(layout.legend_area, case.legend_area);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
use crate::ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
|
||||
/// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups).
|
||||
///
|
||||
/// This widget **cannot be used to clear the terminal on the first render** as `ratatui` assumes the
|
||||
/// render area is empty. Use [`crate::Terminal::clear`] instead.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Clear, Block, Borders};
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::Frame;
|
||||
/// # use ratatui::backend::Backend;
|
||||
/// fn draw_on_clear<B: Backend>(f: &mut Frame<B>, area: Rect) {
|
||||
/// let block = Block::default().title("Block").borders(Borders::ALL);
|
||||
/// f.render_widget(Clear, area); // <- this will clear/reset the area first
|
||||
/// f.render_widget(block, area); // now render the block widget
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Popup Example
|
||||
///
|
||||
/// For a more complete example how to utilize `Clear` to realize popups see
|
||||
/// the example `examples/popup.rs`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Clear;
|
||||
|
||||
impl Widget for Clear {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
for x in area.left()..area.right() {
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(x, y).reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,313 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// A widget to display a task progress.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Widget, Gauge, Block, Borders};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// Gauge::default()
|
||||
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
|
||||
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::ITALIC))
|
||||
/// .percent(20);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Gauge<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
ratio: f64,
|
||||
label: Option<Span<'a>>,
|
||||
use_unicode: bool,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Default for Gauge<'a> {
|
||||
fn default() -> Gauge<'a> {
|
||||
Gauge {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
use_unicode: false,
|
||||
style: Style::default(),
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Gauge<'a> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Gauge<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn percent(mut self, percent: u16) -> Gauge<'a> {
|
||||
assert!(
|
||||
percent <= 100,
|
||||
"Percentage should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.ratio = f64::from(percent) / 100.0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets ratio ([0.0, 1.0]) directly.
|
||||
pub fn ratio(mut self, ratio: f64) -> Gauge<'a> {
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&ratio),
|
||||
"Ratio should be between 0 and 1 inclusively."
|
||||
);
|
||||
self.ratio = ratio;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label<T>(mut self, label: T) -> Gauge<'a>
|
||||
where
|
||||
T: Into<Span<'a>>,
|
||||
{
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Gauge<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gauge_style(mut self, style: Style) -> Gauge<'a> {
|
||||
self.gauge_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn use_unicode(mut self, unicode: bool) -> Gauge<'a> {
|
||||
self.use_unicode = unicode;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Gauge<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
let gauge_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
buf.set_style(gauge_area, self.gauge_style);
|
||||
if gauge_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
// compute label value and its position
|
||||
// label is put at the center of the gauge_area
|
||||
let label = {
|
||||
let pct = f64::round(self.ratio * 100.0);
|
||||
self.label
|
||||
.unwrap_or_else(|| Span::from(format!("{}%", pct)))
|
||||
};
|
||||
let clamped_label_width = gauge_area.width.min(label.width() as u16);
|
||||
let label_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2;
|
||||
let label_row = gauge_area.top() + gauge_area.height / 2;
|
||||
|
||||
// the gauge will be filled proportionally to the ratio
|
||||
let filled_width = f64::from(gauge_area.width) * self.ratio;
|
||||
let end = if self.use_unicode {
|
||||
gauge_area.left() + filled_width.floor() as u16
|
||||
} else {
|
||||
gauge_area.left() + filled_width.round() as u16
|
||||
};
|
||||
for y in gauge_area.top()..gauge_area.bottom() {
|
||||
// render the filled area (left to end)
|
||||
for x in gauge_area.left()..end {
|
||||
// spaces are needed to apply the background styling
|
||||
buf.get_mut(x, y)
|
||||
.set_symbol(" ")
|
||||
.set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
|
||||
.set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
|
||||
}
|
||||
if self.use_unicode && self.ratio < 1.0 {
|
||||
buf.get_mut(end, y)
|
||||
.set_symbol(get_unicode_block(filled_width % 1.0));
|
||||
}
|
||||
}
|
||||
// set the span
|
||||
buf.set_span(label_col, label_row, &label, clamped_label_width);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_unicode_block<'a>(frac: f64) -> &'a str {
|
||||
match (frac * 8.0).round() as u16 {
|
||||
1 => symbols::block::ONE_EIGHTH,
|
||||
2 => symbols::block::ONE_QUARTER,
|
||||
3 => symbols::block::THREE_EIGHTHS,
|
||||
4 => symbols::block::HALF,
|
||||
5 => symbols::block::FIVE_EIGHTHS,
|
||||
6 => symbols::block::THREE_QUARTERS,
|
||||
7 => symbols::block::SEVEN_EIGHTHS,
|
||||
8 => symbols::block::FULL,
|
||||
_ => " ",
|
||||
}
|
||||
}
|
||||
|
||||
/// A compact widget to display a task progress over a single line.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Widget, LineGauge, Block, Borders};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::symbols;
|
||||
/// LineGauge::default()
|
||||
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
|
||||
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::BOLD))
|
||||
/// .line_set(symbols::line::THICK)
|
||||
/// .ratio(0.4);
|
||||
/// ```
|
||||
pub struct LineGauge<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
ratio: f64,
|
||||
label: Option<Spans<'a>>,
|
||||
line_set: symbols::line::Set,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Default for LineGauge<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
style: Style::default(),
|
||||
line_set: symbols::line::NORMAL,
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LineGauge<'a> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ratio(mut self, ratio: f64) -> Self {
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&ratio),
|
||||
"Ratio should be between 0 and 1 inclusively."
|
||||
);
|
||||
self.ratio = ratio;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn line_set(mut self, set: symbols::line::Set) -> Self {
|
||||
self.line_set = set;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label<T>(mut self, label: T) -> Self
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
{
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gauge_style(mut self, style: Style) -> Self {
|
||||
self.gauge_style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for LineGauge<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
let gauge_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if gauge_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ratio = self.ratio;
|
||||
let label = self
|
||||
.label
|
||||
.unwrap_or_else(move || Spans::from(format!("{:.0}%", ratio * 100.0)));
|
||||
let (col, row) = buf.set_spans(
|
||||
gauge_area.left(),
|
||||
gauge_area.top(),
|
||||
&label,
|
||||
gauge_area.width,
|
||||
);
|
||||
let start = col + 1;
|
||||
if start >= gauge_area.right() {
|
||||
return;
|
||||
}
|
||||
|
||||
let end = start
|
||||
+ (f64::from(gauge_area.right().saturating_sub(start)) * self.ratio).floor() as u16;
|
||||
for col in start..end {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.fg,
|
||||
bg: None,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
}
|
||||
for col in end..gauge_area.right() {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.bg,
|
||||
bg: None,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_percentage() {
|
||||
Gauge::default().percent(110);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_ratio_upper_bound() {
|
||||
Gauge::default().ratio(1.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_ratio_lower_bound() {
|
||||
Gauge::default().ratio(-0.5);
|
||||
}
|
||||
}
|
@ -1,268 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Corner, Rect},
|
||||
style::Style,
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ListState {
|
||||
offset: usize,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl ListState {
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
|
||||
pub fn select(&mut self, index: Option<usize>) {
|
||||
self.selected = index;
|
||||
if index.is_none() {
|
||||
self.offset = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListItem<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> ListItem<'a> {
|
||||
pub fn new<T>(content: T) -> ListItem<'a>
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
ListItem {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> ListItem<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.content.height()
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to display several items among which one can be selected (optional)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, Borders, List, ListItem};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")];
|
||||
/// List::new(items)
|
||||
/// .block(Block::default().title("List").borders(Borders::ALL))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
/// .highlight_symbol(">>");
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct List<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
items: Vec<ListItem<'a>>,
|
||||
/// Style used as a base style for the widget
|
||||
style: Style,
|
||||
start_corner: Corner,
|
||||
/// Style used to render selected item
|
||||
highlight_style: Style,
|
||||
/// Symbol in front of the selected item (Shift all items to the right)
|
||||
highlight_symbol: Option<&'a str>,
|
||||
/// Whether to repeat the highlight symbol for each line of the selected item
|
||||
repeat_highlight_symbol: bool,
|
||||
}
|
||||
|
||||
impl<'a> List<'a> {
|
||||
pub fn new<T>(items: T) -> List<'a>
|
||||
where
|
||||
T: Into<Vec<ListItem<'a>>>,
|
||||
{
|
||||
List {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
items: items.into(),
|
||||
start_corner: Corner::TopLeft,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
repeat_highlight_symbol: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> List<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> List<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> List<'a> {
|
||||
self.highlight_symbol = Some(highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, style: Style) -> List<'a> {
|
||||
self.highlight_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn repeat_highlight_symbol(mut self, repeat: bool) -> List<'a> {
|
||||
self.repeat_highlight_symbol = repeat;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_corner(mut self, corner: Corner) -> List<'a> {
|
||||
self.start_corner = corner;
|
||||
self
|
||||
}
|
||||
|
||||
fn get_items_bounds(
|
||||
&self,
|
||||
selected: Option<usize>,
|
||||
offset: usize,
|
||||
max_height: usize,
|
||||
) -> (usize, usize) {
|
||||
let offset = offset.min(self.items.len().saturating_sub(1));
|
||||
let mut start = offset;
|
||||
let mut end = offset;
|
||||
let mut height = 0;
|
||||
for item in self.items.iter().skip(offset) {
|
||||
if height + item.height() > max_height {
|
||||
break;
|
||||
}
|
||||
height += item.height();
|
||||
end += 1;
|
||||
}
|
||||
|
||||
let selected = selected.unwrap_or(0).min(self.items.len() - 1);
|
||||
while selected >= end {
|
||||
height = height.saturating_add(self.items[end].height());
|
||||
end += 1;
|
||||
while height > max_height {
|
||||
height = height.saturating_sub(self.items[start].height());
|
||||
start += 1;
|
||||
}
|
||||
}
|
||||
while selected < start {
|
||||
start -= 1;
|
||||
height = height.saturating_add(self.items[start].height());
|
||||
while height > max_height {
|
||||
end -= 1;
|
||||
height = height.saturating_sub(self.items[end].height());
|
||||
}
|
||||
}
|
||||
(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StatefulWidget for List<'a> {
|
||||
type State = ListState;
|
||||
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
buf.set_style(area, self.style);
|
||||
let list_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if list_area.width < 1 || list_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
let list_height = list_area.height as usize;
|
||||
|
||||
let (start, end) = self.get_items_bounds(state.selected, state.offset, list_height);
|
||||
state.offset = start;
|
||||
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
let blank_symbol = " ".repeat(highlight_symbol.width());
|
||||
|
||||
let mut current_height = 0;
|
||||
let has_selection = state.selected.is_some();
|
||||
for (i, item) in self
|
||||
.items
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.skip(state.offset)
|
||||
.take(end - start)
|
||||
{
|
||||
let (x, y) = match self.start_corner {
|
||||
Corner::BottomLeft => {
|
||||
current_height += item.height() as u16;
|
||||
(list_area.left(), list_area.bottom() - current_height)
|
||||
}
|
||||
_ => {
|
||||
let pos = (list_area.left(), list_area.top() + current_height);
|
||||
current_height += item.height() as u16;
|
||||
pos
|
||||
}
|
||||
};
|
||||
let area = Rect {
|
||||
x,
|
||||
y,
|
||||
width: list_area.width,
|
||||
height: item.height() as u16,
|
||||
};
|
||||
let item_style = self.style.patch(item.style);
|
||||
buf.set_style(area, item_style);
|
||||
|
||||
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
|
||||
for (j, line) in item.content.lines.iter().enumerate() {
|
||||
// if the item is selected, we need to display the highlight symbol:
|
||||
// - either for the first line of the item only,
|
||||
// - or for each line of the item if the appropriate option is set
|
||||
let symbol = if is_selected && (j == 0 || self.repeat_highlight_symbol) {
|
||||
highlight_symbol
|
||||
} else {
|
||||
&blank_symbol
|
||||
};
|
||||
let (elem_x, max_element_width) = if has_selection {
|
||||
let (elem_x, _) = buf.set_stringn(
|
||||
x,
|
||||
y + j as u16,
|
||||
symbol,
|
||||
list_area.width as usize,
|
||||
item_style,
|
||||
);
|
||||
(elem_x, (list_area.width - (elem_x - x)))
|
||||
} else {
|
||||
(x, list_area.width)
|
||||
};
|
||||
buf.set_spans(elem_x, y + j as u16, line, max_element_width);
|
||||
}
|
||||
if is_selected {
|
||||
buf.set_style(area, self.highlight_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for List<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = ListState::default();
|
||||
StatefulWidget::render(self, area, buf, &mut state);
|
||||
}
|
||||
}
|
@ -1,184 +0,0 @@
|
||||
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
|
||||
//!
|
||||
//! All widgets are implemented using the builder pattern and are consumable objects. They are not
|
||||
//! meant to be stored but used as *commands* to draw common figures in the UI.
|
||||
//!
|
||||
//! The available widgets are:
|
||||
//! - [`Block`]
|
||||
//! - [`Tabs`]
|
||||
//! - [`List`]
|
||||
//! - [`Table`]
|
||||
//! - [`Paragraph`]
|
||||
//! - [`Chart`]
|
||||
//! - [`BarChart`]
|
||||
//! - [`Gauge`]
|
||||
//! - [`Sparkline`]
|
||||
//! - [`Clear`]
|
||||
|
||||
mod barchart;
|
||||
mod block;
|
||||
pub mod canvas;
|
||||
mod chart;
|
||||
mod clear;
|
||||
mod gauge;
|
||||
mod list;
|
||||
mod paragraph;
|
||||
mod reflow;
|
||||
mod sparkline;
|
||||
mod table;
|
||||
mod tabs;
|
||||
|
||||
pub use self::barchart::BarChart;
|
||||
pub use self::block::{Block, BorderType};
|
||||
pub use self::chart::{Axis, Chart, Dataset, GraphType};
|
||||
pub use self::clear::Clear;
|
||||
pub use self::gauge::{Gauge, LineGauge};
|
||||
pub use self::list::{List, ListItem, ListState};
|
||||
pub use self::paragraph::{Paragraph, Wrap};
|
||||
pub use self::sparkline::Sparkline;
|
||||
pub use self::table::{Cell, Row, Table, TableState};
|
||||
pub use self::tabs::Tabs;
|
||||
|
||||
use crate::ratatui::{buffer::Buffer, layout::Rect};
|
||||
use bitflags::bitflags;
|
||||
|
||||
bitflags! {
|
||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
||||
pub struct Borders: u8 {
|
||||
/// Show no border (default)
|
||||
const NONE = 0b0000;
|
||||
/// Show the top border
|
||||
const TOP = 0b0001;
|
||||
/// Show the right border
|
||||
const RIGHT = 0b0010;
|
||||
/// Show the bottom border
|
||||
const BOTTOM = 0b0100;
|
||||
/// Show the left border
|
||||
const LEFT = 0b1000;
|
||||
/// Show all borders
|
||||
const ALL = Self::TOP.bits | Self::RIGHT.bits | Self::BOTTOM.bits | Self::LEFT.bits;
|
||||
}
|
||||
}
|
||||
|
||||
/// Base requirements for a Widget
|
||||
pub trait Widget {
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom widget.
|
||||
fn render(self, area: Rect, buf: &mut Buffer);
|
||||
}
|
||||
|
||||
/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things
|
||||
/// between two draw calls.
|
||||
///
|
||||
/// Most widgets can be drawn directly based on the input parameters. However, some features may
|
||||
/// require some kind of associated state to be implemented.
|
||||
///
|
||||
/// For example, the [`List`] widget can highlight the item currently selected. This can be
|
||||
/// translated in an offset, which is the number of elements to skip in order to have the selected
|
||||
/// item within the viewport currently allocated to this widget. The widget can therefore only
|
||||
/// provide the following behavior: whenever the selected item is out of the viewport scroll to a
|
||||
/// predefined position (making the selected item the last viewable item or the one in the middle
|
||||
/// for example). Nonetheless, if the widget has access to the last computed offset then it can
|
||||
/// implement a natural scrolling experience where the last offset is reused until the selected
|
||||
/// item is out of the viewport.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// # use ratatui::Terminal;
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::widgets::{Widget, List, ListItem, ListState};
|
||||
///
|
||||
/// // Let's say we have some events to display.
|
||||
/// struct Events {
|
||||
/// // `items` is the state managed by your application.
|
||||
/// items: Vec<String>,
|
||||
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
|
||||
/// // item as well as the offset computed during the previous draw call (used to implement
|
||||
/// // natural scrolling).
|
||||
/// state: ListState
|
||||
/// }
|
||||
///
|
||||
/// impl Events {
|
||||
/// fn new(items: Vec<String>) -> Events {
|
||||
/// Events {
|
||||
/// items,
|
||||
/// state: ListState::default(),
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// pub fn set_items(&mut self, items: Vec<String>) {
|
||||
/// self.items = items;
|
||||
/// // We reset the state as the associated items have changed. This effectively reset
|
||||
/// // the selection as well as the stored offset.
|
||||
/// self.state = ListState::default();
|
||||
/// }
|
||||
///
|
||||
/// // Select the next item. This will not be reflected until the widget is drawn in the
|
||||
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
|
||||
/// pub fn next(&mut self) {
|
||||
/// let i = match self.state.selected() {
|
||||
/// Some(i) => {
|
||||
/// if i >= self.items.len() - 1 {
|
||||
/// 0
|
||||
/// } else {
|
||||
/// i + 1
|
||||
/// }
|
||||
/// }
|
||||
/// None => 0,
|
||||
/// };
|
||||
/// self.state.select(Some(i));
|
||||
/// }
|
||||
///
|
||||
/// // Select the previous item. This will not be reflected until the widget is drawn in the
|
||||
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
|
||||
/// pub fn previous(&mut self) {
|
||||
/// let i = match self.state.selected() {
|
||||
/// Some(i) => {
|
||||
/// if i == 0 {
|
||||
/// self.items.len() - 1
|
||||
/// } else {
|
||||
/// i - 1
|
||||
/// }
|
||||
/// }
|
||||
/// None => 0,
|
||||
/// };
|
||||
/// self.state.select(Some(i));
|
||||
/// }
|
||||
///
|
||||
/// // Unselect the currently selected item if any. The implementation of `ListState` makes
|
||||
/// // sure that the stored offset is also reset.
|
||||
/// pub fn unselect(&mut self) {
|
||||
/// self.state.select(None);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// let mut events = Events::new(vec![
|
||||
/// String::from("Item 1"),
|
||||
/// String::from("Item 2")
|
||||
/// ]);
|
||||
///
|
||||
/// loop {
|
||||
/// terminal.draw(|f| {
|
||||
/// // The items managed by the application are transformed to something
|
||||
/// // that is understood by ratatui.
|
||||
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_ref())).collect();
|
||||
/// // The `List` widget is then built with those items.
|
||||
/// let list = List::new(items);
|
||||
/// // Finally the widget is rendered using the associated state. `events.state` is
|
||||
/// // effectively the only thing that we will "remember" from this draw call.
|
||||
/// f.render_stateful_widget(list, f.size(), &mut events.state);
|
||||
/// });
|
||||
///
|
||||
/// // In response to some input events or an external http request or whatever:
|
||||
/// events.next();
|
||||
/// }
|
||||
/// ```
|
||||
pub trait StatefulWidget {
|
||||
type State;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
@ -1,214 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::Style,
|
||||
text::{StyledGrapheme, Text},
|
||||
widgets::{
|
||||
reflow::{LineComposer, LineTruncator, WordWrapper},
|
||||
Block, Widget,
|
||||
},
|
||||
};
|
||||
use std::iter;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
|
||||
match alignment {
|
||||
Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
|
||||
Alignment::Right => text_area_width.saturating_sub(line_width),
|
||||
Alignment::Left => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to display some text.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::text::{Text, Spans, Span};
|
||||
/// # use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::layout::{Alignment};
|
||||
/// let text = vec![
|
||||
/// Spans::from(vec![
|
||||
/// Span::raw("First"),
|
||||
/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)),
|
||||
/// Span::raw("."),
|
||||
/// ]),
|
||||
/// Spans::from(Span::styled("Second line", Style::default().fg(Color::Red))),
|
||||
/// ];
|
||||
/// Paragraph::new(text)
|
||||
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
|
||||
/// .style(Style::default().fg(Color::White).bg(Color::Black))
|
||||
/// .alignment(Alignment::Center)
|
||||
/// .wrap(Wrap { trim: true });
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Paragraph<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Widget style
|
||||
style: Style,
|
||||
/// How to wrap the text
|
||||
wrap: Option<Wrap>,
|
||||
/// The text to display
|
||||
text: Text<'a>,
|
||||
/// Scroll
|
||||
scroll: (u16, u16),
|
||||
/// Alignment of the text
|
||||
alignment: Alignment,
|
||||
}
|
||||
|
||||
/// Describes how to wrap text across lines.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Paragraph, Wrap};
|
||||
/// # use ratatui::text::Text;
|
||||
/// let bullet_points = Text::from(r#"Some indented points:
|
||||
/// - First thing goes here and is long so that it wraps
|
||||
/// - Here is another point that is long enough to wrap"#);
|
||||
///
|
||||
/// // With leading spaces trimmed (window width of 30 chars):
|
||||
/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
|
||||
/// // Some indented points:
|
||||
/// // - First thing goes here and is
|
||||
/// // long so that it wraps
|
||||
/// // - Here is another point that
|
||||
/// // is long enough to wrap
|
||||
///
|
||||
/// // But without trimming, indentation is preserved:
|
||||
/// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
|
||||
/// // Some indented points:
|
||||
/// // - First thing goes here
|
||||
/// // and is long so that it wraps
|
||||
/// // - Here is another point
|
||||
/// // that is long enough to wrap
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Wrap {
|
||||
/// Should leading whitespace be trimmed
|
||||
pub trim: bool,
|
||||
}
|
||||
|
||||
impl<'a> Paragraph<'a> {
|
||||
pub fn new<T>(text: T) -> Paragraph<'a>
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
Paragraph {
|
||||
block: None,
|
||||
style: Default::default(),
|
||||
wrap: None,
|
||||
text: text.into(),
|
||||
scroll: (0, 0),
|
||||
alignment: Alignment::Left,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Paragraph<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
|
||||
self.wrap = Some(wrap);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
|
||||
self.scroll = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> {
|
||||
self.alignment = alignment;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Paragraph<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
let text_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if text_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let style = self.style;
|
||||
let mut styled = self.text.lines.iter().flat_map(|spans| {
|
||||
spans
|
||||
.0
|
||||
.iter()
|
||||
.flat_map(|span| span.styled_graphemes(style))
|
||||
// Required given the way composers work but might be refactored out if we change
|
||||
// composers to operate on lines instead of a stream of graphemes.
|
||||
.chain(iter::once(StyledGrapheme {
|
||||
symbol: "\n",
|
||||
style: self.style,
|
||||
}))
|
||||
});
|
||||
|
||||
let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
|
||||
Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
|
||||
} else {
|
||||
let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
|
||||
if let Alignment::Left = self.alignment {
|
||||
line_composer.set_horizontal_offset(self.scroll.1);
|
||||
}
|
||||
line_composer
|
||||
};
|
||||
let mut y = 0;
|
||||
while let Some((current_line, current_line_width)) = line_composer.next_line() {
|
||||
if y >= self.scroll.0 {
|
||||
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
|
||||
for StyledGrapheme { symbol, style } in current_line {
|
||||
let width = symbol.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
|
||||
.set_symbol(if symbol.is_empty() {
|
||||
// If the symbol is empty, the last char which rendered last time will
|
||||
// leave on the line. It's a quick fix.
|
||||
" "
|
||||
} else {
|
||||
symbol
|
||||
})
|
||||
.set_style(*style);
|
||||
x += width as u16;
|
||||
}
|
||||
}
|
||||
y += 1;
|
||||
if y >= text_area.height + self.scroll.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn zero_width_char_at_end_of_line() {
|
||||
let line = "foo\0";
|
||||
let paragraph = Paragraph::new(line);
|
||||
let mut buf = Buffer::with_lines(vec![line]);
|
||||
paragraph.render(*buf.area(), &mut buf);
|
||||
}
|
||||
}
|
@ -1,534 +0,0 @@
|
||||
use crate::ratatui::text::StyledGrapheme;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
const NBSP: &str = "\u{00a0}";
|
||||
|
||||
/// A state machine to pack styled symbols into lines.
|
||||
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
|
||||
/// iterators for that).
|
||||
pub trait LineComposer<'a> {
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>;
|
||||
}
|
||||
|
||||
/// A state machine that wraps lines on word boundaries.
|
||||
pub struct WordWrapper<'a, 'b> {
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
max_line_width: u16,
|
||||
current_line: Vec<StyledGrapheme<'a>>,
|
||||
next_line: Vec<StyledGrapheme<'a>>,
|
||||
/// Removes the leading whitespace from lines
|
||||
trim: bool,
|
||||
}
|
||||
|
||||
impl<'a, 'b> WordWrapper<'a, 'b> {
|
||||
pub fn new(
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
max_line_width: u16,
|
||||
trim: bool,
|
||||
) -> WordWrapper<'a, 'b> {
|
||||
WordWrapper {
|
||||
symbols,
|
||||
max_line_width,
|
||||
current_line: vec![],
|
||||
next_line: vec![],
|
||||
trim,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
std::mem::swap(&mut self.current_line, &mut self.next_line);
|
||||
self.next_line.truncate(0);
|
||||
|
||||
let mut current_line_width = self
|
||||
.current_line
|
||||
.iter()
|
||||
.map(|StyledGrapheme { symbol, .. }| symbol.width() as u16)
|
||||
.sum();
|
||||
|
||||
let mut symbols_to_last_word_end: usize = 0;
|
||||
let mut width_to_last_word_end: u16 = 0;
|
||||
let mut prev_whitespace = false;
|
||||
let mut symbols_exhausted = true;
|
||||
for StyledGrapheme { symbol, style } in &mut self.symbols {
|
||||
symbols_exhausted = false;
|
||||
let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
|
||||
|
||||
// Ignore characters wider that the total max width.
|
||||
if symbol.width() as u16 > self.max_line_width
|
||||
// Skip leading whitespace when trim is enabled.
|
||||
|| self.trim && symbol_whitespace && symbol != "\n" && current_line_width == 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break on newline and discard it.
|
||||
if symbol == "\n" {
|
||||
if prev_whitespace {
|
||||
current_line_width = width_to_last_word_end;
|
||||
self.current_line.truncate(symbols_to_last_word_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Mark the previous symbol as word end.
|
||||
if symbol_whitespace && !prev_whitespace {
|
||||
symbols_to_last_word_end = self.current_line.len();
|
||||
width_to_last_word_end = current_line_width;
|
||||
}
|
||||
|
||||
self.current_line.push(StyledGrapheme { symbol, style });
|
||||
current_line_width += symbol.width() as u16;
|
||||
|
||||
if current_line_width > self.max_line_width {
|
||||
// If there was no word break in the text, wrap at the end of the line.
|
||||
let (truncate_at, truncated_width) = if symbols_to_last_word_end != 0 {
|
||||
(symbols_to_last_word_end, width_to_last_word_end)
|
||||
} else {
|
||||
(self.current_line.len() - 1, self.max_line_width)
|
||||
};
|
||||
|
||||
// Push the remainder to the next line but strip leading whitespace:
|
||||
{
|
||||
let remainder = &self.current_line[truncate_at..];
|
||||
if let Some(remainder_nonwhite) =
|
||||
remainder.iter().position(|StyledGrapheme { symbol, .. }| {
|
||||
!symbol.chars().all(&char::is_whitespace)
|
||||
})
|
||||
{
|
||||
self.next_line
|
||||
.extend_from_slice(&remainder[remainder_nonwhite..]);
|
||||
}
|
||||
}
|
||||
self.current_line.truncate(truncate_at);
|
||||
current_line_width = truncated_width;
|
||||
break;
|
||||
}
|
||||
|
||||
prev_whitespace = symbol_whitespace;
|
||||
}
|
||||
|
||||
// Even if the iterator is exhausted, pass the previous remainder.
|
||||
if symbols_exhausted && self.current_line.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((&self.current_line[..], current_line_width))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A state machine that truncates overhanging lines.
|
||||
pub struct LineTruncator<'a, 'b> {
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
max_line_width: u16,
|
||||
current_line: Vec<StyledGrapheme<'a>>,
|
||||
/// Record the offset to skip render
|
||||
horizontal_offset: u16,
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineTruncator<'a, 'b> {
|
||||
pub fn new(
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
max_line_width: u16,
|
||||
) -> LineTruncator<'a, 'b> {
|
||||
LineTruncator {
|
||||
symbols,
|
||||
max_line_width,
|
||||
horizontal_offset: 0,
|
||||
current_line: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) {
|
||||
self.horizontal_offset = horizontal_offset;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.current_line.truncate(0);
|
||||
let mut current_line_width = 0;
|
||||
|
||||
let mut skip_rest = false;
|
||||
let mut symbols_exhausted = true;
|
||||
let mut horizontal_offset = self.horizontal_offset as usize;
|
||||
for StyledGrapheme { symbol, style } in &mut self.symbols {
|
||||
symbols_exhausted = false;
|
||||
|
||||
// Ignore characters wider that the total max width.
|
||||
if symbol.width() as u16 > self.max_line_width {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break on newline and discard it.
|
||||
if symbol == "\n" {
|
||||
break;
|
||||
}
|
||||
|
||||
if current_line_width + symbol.width() as u16 > self.max_line_width {
|
||||
// Exhaust the remainder of the line.
|
||||
skip_rest = true;
|
||||
break;
|
||||
}
|
||||
|
||||
let symbol = if horizontal_offset == 0 {
|
||||
symbol
|
||||
} else {
|
||||
let w = symbol.width();
|
||||
if w > horizontal_offset {
|
||||
let t = trim_offset(symbol, horizontal_offset);
|
||||
horizontal_offset = 0;
|
||||
t
|
||||
} else {
|
||||
horizontal_offset -= w;
|
||||
""
|
||||
}
|
||||
};
|
||||
current_line_width += symbol.width() as u16;
|
||||
self.current_line.push(StyledGrapheme { symbol, style });
|
||||
}
|
||||
|
||||
if skip_rest {
|
||||
for StyledGrapheme { symbol, .. } in &mut self.symbols {
|
||||
if symbol == "\n" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if symbols_exhausted && self.current_line.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((&self.current_line[..], current_line_width))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function will return a str slice which start at specified offset.
|
||||
/// As src is a unicode str, start offset has to be calculated with each character.
|
||||
fn trim_offset(src: &str, mut offset: usize) -> &str {
|
||||
let mut start = 0;
|
||||
for c in UnicodeSegmentation::graphemes(src, true) {
|
||||
let w = c.width();
|
||||
if w <= offset {
|
||||
offset -= w;
|
||||
start += c.len();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
&src[start..]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
enum Composer {
|
||||
WordWrapper { trim: bool },
|
||||
LineTruncator,
|
||||
}
|
||||
|
||||
fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
|
||||
let style = Default::default();
|
||||
let mut styled =
|
||||
UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
|
||||
let mut composer: Box<dyn LineComposer> = match which {
|
||||
Composer::WordWrapper { trim } => {
|
||||
Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
|
||||
}
|
||||
Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
|
||||
};
|
||||
let mut lines = vec![];
|
||||
let mut widths = vec![];
|
||||
while let Some((styled, width)) = composer.next_line() {
|
||||
let line = styled
|
||||
.iter()
|
||||
.map(|StyledGrapheme { symbol, .. }| *symbol)
|
||||
.collect::<String>();
|
||||
assert!(width <= text_area_width);
|
||||
lines.push(line);
|
||||
widths.push(width);
|
||||
}
|
||||
(lines, widths)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_one_line() {
|
||||
let width = 40;
|
||||
for i in 1..width {
|
||||
let text = "a".repeat(i);
|
||||
let (word_wrapper, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, &text, width as u16);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
|
||||
let expected = vec![text];
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_short_lines() {
|
||||
let width = 20;
|
||||
let text =
|
||||
"abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let wrapped: Vec<&str> = text.split('\n').collect();
|
||||
assert_eq!(word_wrapper, wrapped);
|
||||
assert_eq!(line_truncator, wrapped);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_long_word() {
|
||||
let width = 20;
|
||||
let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
|
||||
let (word_wrapper, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let wrapped = vec![
|
||||
&text[..width],
|
||||
&text[width..width * 2],
|
||||
&text[width * 2..width * 3],
|
||||
&text[width * 3..],
|
||||
];
|
||||
assert_eq!(
|
||||
word_wrapper, wrapped,
|
||||
"WordWrapper should detect the line cannot be broken on word boundary and \
|
||||
break it at line width limit."
|
||||
);
|
||||
assert_eq!(line_truncator, vec![&text[..width]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_long_sentence() {
|
||||
let width = 20;
|
||||
let text =
|
||||
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
|
||||
let text_multi_space =
|
||||
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
|
||||
m n o";
|
||||
let (word_wrapper_single_space, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
||||
let (word_wrapper_multi_space, _) = run_composer(
|
||||
Composer::WordWrapper { trim: true },
|
||||
text_multi_space,
|
||||
width as u16,
|
||||
);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let word_wrapped = vec![
|
||||
"abcd efghij",
|
||||
"klmnopabcd efgh",
|
||||
"ijklmnopabcdefg",
|
||||
"hijkl mnopab c d e f",
|
||||
"g h i j k l m n o",
|
||||
];
|
||||
assert_eq!(word_wrapper_single_space, word_wrapped);
|
||||
assert_eq!(word_wrapper_multi_space, word_wrapped);
|
||||
|
||||
assert_eq!(line_truncator, vec![&text[..width]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_zero_width() {
|
||||
let width = 0;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = Vec::new();
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_max_line_width_of_1() {
|
||||
let width = 1;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
|
||||
.filter(|g| g.chars().any(|c| !c.is_whitespace()))
|
||||
.collect();
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, vec!["a"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_max_line_width_of_1_double_width_characters() {
|
||||
let width = 1;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
|
||||
両端点では、";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
|
||||
assert_eq!(line_truncator, vec!["", "a"]);
|
||||
}
|
||||
|
||||
/// Tests WordWrapper with words some of which exceed line length and some not.
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_mixed_length() {
|
||||
let width = 20;
|
||||
let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
"abcd efghij",
|
||||
"klmnopabcdefghijklmn",
|
||||
"opabcdefghijkl",
|
||||
"mnopab cdefghi j",
|
||||
"klmno",
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_double_width_chars() {
|
||||
let width = 20;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
|
||||
では、";
|
||||
let (word_wrapper, word_wrapper_width) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
|
||||
let wrapped = vec![
|
||||
"コンピュータ上で文字",
|
||||
"を扱う場合、典型的に",
|
||||
"は文字による通信を行",
|
||||
"う場合にその両端点で",
|
||||
"は、",
|
||||
];
|
||||
assert_eq!(word_wrapper, wrapped);
|
||||
assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_leading_whitespace_removal() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
|
||||
assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
|
||||
}
|
||||
|
||||
/// Tests truncation of leading whitespace.
|
||||
#[test]
|
||||
fn line_composer_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = " ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec![""]);
|
||||
assert_eq!(line_truncator, vec![" "]);
|
||||
}
|
||||
|
||||
/// Tests an input starting with a letter, followed by spaces - some of the behaviour is
|
||||
/// incidental.
|
||||
#[test]
|
||||
fn line_composer_char_plus_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = "a ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
// What's happening below is: the first line gets consumed, trailing spaces discarded,
|
||||
// after 20 of which a word break occurs (probably shouldn't). The second line break
|
||||
// discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
|
||||
// that much.
|
||||
assert_eq!(word_wrapper, vec!["a", ""]);
|
||||
assert_eq!(line_truncator, vec!["a "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
|
||||
let width = 20;
|
||||
// Japanese seems not to use spaces but we should break on spaces anyway... We're using it
|
||||
// to test double-width chars.
|
||||
// You are more than welcome to add word boundary detection based of alterations of
|
||||
// hiragana and katakana...
|
||||
// This happens to also be a test case for mixed width because regular spaces are single width.
|
||||
let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
|
||||
let (word_wrapper, word_wrapper_width) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
"コンピュ",
|
||||
"ータ上で文字を扱う場",
|
||||
"合、 典型的には文",
|
||||
"字による 通信を行",
|
||||
"う場合にその両端点で",
|
||||
"は、",
|
||||
]
|
||||
);
|
||||
// Odd-sized lines have a space in them.
|
||||
assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]);
|
||||
}
|
||||
|
||||
/// Ensure words separated by nbsp are wrapped as if they were a single one.
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_nbsp() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
|
||||
|
||||
// Ensure that if the character was a regular space, it would be wrapped differently.
|
||||
let text_space = text.replace('\u{00a0}', " ");
|
||||
let (word_wrapper_space, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
|
||||
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_preserve_indentation() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
|
||||
let width = 10;
|
||||
let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
|
||||
let width = 10;
|
||||
let text = " 4 Indent\n must wrap!";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
" ",
|
||||
" 4",
|
||||
"Indent",
|
||||
" ",
|
||||
" must",
|
||||
"wrap!"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
@ -1,155 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use std::cmp::min;
|
||||
|
||||
/// Widget to render a sparkline over one or more lines.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, Borders, Sparkline};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// Sparkline::default()
|
||||
/// .block(Block::default().title("Sparkline").borders(Borders::ALL))
|
||||
/// .data(&[0, 2, 3, 4, 1, 4, 10])
|
||||
/// .max(5)
|
||||
/// .style(Style::default().fg(Color::Red).bg(Color::White));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sparkline<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Widget style
|
||||
style: Style,
|
||||
/// A slice of the data to display
|
||||
data: &'a [u64],
|
||||
/// The maximum value to take to compute the maximum bar height (if nothing is specified, the
|
||||
/// widget uses the max of the dataset)
|
||||
max: Option<u64>,
|
||||
/// A set of bar symbols used to represent the give data
|
||||
bar_set: symbols::bar::Set,
|
||||
}
|
||||
|
||||
impl<'a> Default for Sparkline<'a> {
|
||||
fn default() -> Sparkline<'a> {
|
||||
Sparkline {
|
||||
block: None,
|
||||
style: Default::default(),
|
||||
data: &[],
|
||||
max: None,
|
||||
bar_set: symbols::bar::NINE_LEVELS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Sparkline<'a> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Sparkline<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Sparkline<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn data(mut self, data: &'a [u64]) -> Sparkline<'a> {
|
||||
self.data = data;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max(mut self, max: u64) -> Sparkline<'a> {
|
||||
self.max = Some(max);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> Sparkline<'a> {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Sparkline<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
let spark_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if spark_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let max = match self.max {
|
||||
Some(v) => v,
|
||||
None => *self.data.iter().max().unwrap_or(&1u64),
|
||||
};
|
||||
let max_index = min(spark_area.width as usize, self.data.len());
|
||||
let mut data = self
|
||||
.data
|
||||
.iter()
|
||||
.take(max_index)
|
||||
.map(|e| {
|
||||
if max != 0 {
|
||||
e * u64::from(spark_area.height) * 8 / max
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
.collect::<Vec<u64>>();
|
||||
for j in (0..spark_area.height).rev() {
|
||||
for (i, d) in data.iter_mut().enumerate() {
|
||||
let symbol = match *d {
|
||||
0 => self.bar_set.empty,
|
||||
1 => self.bar_set.one_eighth,
|
||||
2 => self.bar_set.one_quarter,
|
||||
3 => self.bar_set.three_eighths,
|
||||
4 => self.bar_set.half,
|
||||
5 => self.bar_set.five_eighths,
|
||||
6 => self.bar_set.three_quarters,
|
||||
7 => self.bar_set.seven_eighths,
|
||||
_ => self.bar_set.full,
|
||||
};
|
||||
buf.get_mut(spark_area.left() + i as u16, spark_area.top() + j)
|
||||
.set_symbol(symbol)
|
||||
.set_style(self.style);
|
||||
|
||||
if *d > 8 {
|
||||
*d -= 8;
|
||||
} else {
|
||||
*d = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_does_not_panic_if_max_is_zero() {
|
||||
let widget = Sparkline::default().data(&[0, 0, 0]);
|
||||
let area = Rect::new(0, 0, 3, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
widget.render(area, &mut buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_does_not_panic_if_max_is_set_to_zero() {
|
||||
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
|
||||
let area = Rect::new(0, 0, 3, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
widget.render(area, &mut buffer);
|
||||
}
|
||||
}
|
@ -1,504 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::Style,
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
|
||||
///
|
||||
/// It can be created from anything that can be converted to a [`Text`].
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::Cell;
|
||||
/// # use ratatui::style::{Style, Modifier};
|
||||
/// # use ratatui::text::{Span, Spans, Text};
|
||||
/// # use std::borrow::Cow;
|
||||
/// Cell::from("simple string");
|
||||
///
|
||||
/// Cell::from(Span::from("span"));
|
||||
///
|
||||
/// Cell::from(Spans::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
|
||||
/// ]));
|
||||
///
|
||||
/// Cell::from(Text::from("a text"));
|
||||
///
|
||||
/// Cell::from(Text::from(Cow::Borrowed("hello")));
|
||||
/// ```
|
||||
///
|
||||
/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
|
||||
/// capabilities of [`Text`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Cell<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Cell<'a> {
|
||||
/// Set the `Style` of this cell.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Cell<'a>
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
fn from(content: T) -> Cell<'a> {
|
||||
Cell {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds data to be displayed in a [`Table`] widget.
|
||||
///
|
||||
/// A [`Row`] is a collection of cells. It can be created from simple strings:
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::Row;
|
||||
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
|
||||
/// ```
|
||||
///
|
||||
/// But if you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::{Row, Cell};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// You can also construct a row from any type that can be converted into [`Text`]:
|
||||
/// ```rust
|
||||
/// # use std::borrow::Cow;
|
||||
/// # use ratatui::widgets::Row;
|
||||
/// Row::new(vec![
|
||||
/// Cow::Borrowed("hello"),
|
||||
/// Cow::Owned("world".to_uppercase()),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Row<'a> {
|
||||
cells: Vec<Cell<'a>>,
|
||||
height: u16,
|
||||
style: Style,
|
||||
bottom_margin: u16,
|
||||
}
|
||||
|
||||
impl<'a> Row<'a> {
|
||||
/// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
|
||||
pub fn new<T>(cells: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<Cell<'a>>,
|
||||
{
|
||||
Self {
|
||||
height: 1,
|
||||
cells: cells.into_iter().map(|c| c.into()).collect(),
|
||||
style: Style::default(),
|
||||
bottom_margin: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
|
||||
/// height will see its content truncated.
|
||||
pub fn height(mut self, height: u16) -> Self {
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Style`] of the entire row. This [`Style`] can be overridden by the [`Style`] of a
|
||||
/// any individual [`Cell`] or event by their [`Text`] content.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bottom margin. By default, the bottom margin is `0`.
|
||||
pub fn bottom_margin(mut self, margin: u16) -> Self {
|
||||
self.bottom_margin = margin;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the total height of the row.
|
||||
fn total_height(&self) -> u16 {
|
||||
self.height.saturating_add(self.bottom_margin)
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to display data in formatted columns.
|
||||
///
|
||||
/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::{Block, Borders, Table, Row, Cell};
|
||||
/// # use ratatui::layout::Constraint;
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::text::{Text, Spans, Span};
|
||||
/// Table::new(vec![
|
||||
/// // Row can be created from simple strings.
|
||||
/// Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
/// // You can style the entire row.
|
||||
/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
|
||||
/// // If you need more control over the styling you may need to create Cells directly
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Row31"),
|
||||
/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
|
||||
/// Cell::from(Spans::from(vec![
|
||||
/// Span::raw("Row"),
|
||||
/// Span::styled("33", Style::default().fg(Color::Green))
|
||||
/// ])),
|
||||
/// ]),
|
||||
/// // If a Row need to display some content over multiple lines, you just have to change
|
||||
/// // its height.
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Row\n41"),
|
||||
/// Cell::from("Row\n42"),
|
||||
/// Cell::from("Row\n43"),
|
||||
/// ]).height(2),
|
||||
/// ])
|
||||
/// // You can set the style of the entire Table.
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// // It has an optional header, which is simply a Row always visible at the top.
|
||||
/// .header(
|
||||
/// Row::new(vec!["Col1", "Col2", "Col3"])
|
||||
/// .style(Style::default().fg(Color::Yellow))
|
||||
/// // If you want some space between the header and the rest of the rows, you can always
|
||||
/// // specify some margin at the bottom.
|
||||
/// .bottom_margin(1)
|
||||
/// )
|
||||
/// // As any other widget, a Table can be wrapped in a Block.
|
||||
/// .block(Block::default().title("Table"))
|
||||
/// // Columns widths are constrained in the same way as Layout...
|
||||
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
|
||||
/// // ...and they can be separated by a fixed spacing.
|
||||
/// .column_spacing(1)
|
||||
/// // If you wish to highlight a row in any specific way when it is selected...
|
||||
/// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
/// // ...and potentially show a symbol in front of the selection.
|
||||
/// .highlight_symbol(">>");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Table<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Base style for the widget
|
||||
style: Style,
|
||||
/// Width constraints for each column
|
||||
widths: &'a [Constraint],
|
||||
/// Space between each column
|
||||
column_spacing: u16,
|
||||
/// Style used to render the selected row
|
||||
highlight_style: Style,
|
||||
/// Symbol in front of the selected rom
|
||||
highlight_symbol: Option<&'a str>,
|
||||
/// Optional header
|
||||
header: Option<Row<'a>>,
|
||||
/// Data to display in each row
|
||||
rows: Vec<Row<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Table<'a> {
|
||||
pub fn new<T>(rows: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = Row<'a>>,
|
||||
{
|
||||
Self {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
header: None,
|
||||
rows: rows.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header(mut self, header: Row<'a>) -> Self {
|
||||
self.header = Some(header);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
|
||||
let between_0_and_100 = |&w| match w {
|
||||
Constraint::Percentage(p) => p <= 100,
|
||||
_ => true,
|
||||
};
|
||||
assert!(
|
||||
widths.iter().all(between_0_and_100),
|
||||
"Percentages should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
|
||||
self.highlight_symbol = Some(highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, highlight_style: Style) -> Self {
|
||||
self.highlight_style = highlight_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Self {
|
||||
self.column_spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
|
||||
let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
|
||||
if has_selection {
|
||||
let highlight_symbol_width =
|
||||
self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
|
||||
constraints.push(Constraint::Length(highlight_symbol_width));
|
||||
}
|
||||
for constraint in self.widths {
|
||||
constraints.push(*constraint);
|
||||
constraints.push(Constraint::Length(self.column_spacing));
|
||||
}
|
||||
if !self.widths.is_empty() {
|
||||
constraints.pop();
|
||||
}
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(constraints)
|
||||
.expand_to_fill(false)
|
||||
.split(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: max_width,
|
||||
height: 1,
|
||||
});
|
||||
let mut chunks = &chunks[..];
|
||||
if has_selection {
|
||||
chunks = &chunks[1..];
|
||||
}
|
||||
chunks.iter().step_by(2).map(|c| c.width).collect()
|
||||
}
|
||||
|
||||
fn get_row_bounds(
|
||||
&self,
|
||||
selected: Option<usize>,
|
||||
offset: usize,
|
||||
max_height: u16,
|
||||
) -> (usize, usize) {
|
||||
let offset = offset.min(self.rows.len().saturating_sub(1));
|
||||
let mut start = offset;
|
||||
let mut end = offset;
|
||||
let mut height = 0;
|
||||
for item in self.rows.iter().skip(offset) {
|
||||
if height + item.height > max_height {
|
||||
break;
|
||||
}
|
||||
height += item.total_height();
|
||||
end += 1;
|
||||
}
|
||||
|
||||
let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
|
||||
while selected >= end {
|
||||
height = height.saturating_add(self.rows[end].total_height());
|
||||
end += 1;
|
||||
while height > max_height {
|
||||
height = height.saturating_sub(self.rows[start].total_height());
|
||||
start += 1;
|
||||
}
|
||||
}
|
||||
while selected < start {
|
||||
start -= 1;
|
||||
height = height.saturating_add(self.rows[start].total_height());
|
||||
while height > max_height {
|
||||
end -= 1;
|
||||
height = height.saturating_sub(self.rows[end].total_height());
|
||||
}
|
||||
}
|
||||
(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TableState {
|
||||
offset: usize,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
|
||||
pub fn select(&mut self, index: Option<usize>) {
|
||||
self.selected = index;
|
||||
if index.is_none() {
|
||||
self.offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a copy of the receiver's scroll offset.
|
||||
///
|
||||
/// This is useful, for example, if you need to "synchronize" the scrolling of a `Table` and a `Paragraph`.
|
||||
pub fn offset(&self) -> usize {
|
||||
self.offset
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StatefulWidget for Table<'a> {
|
||||
type State = TableState;
|
||||
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
let table_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
let has_selection = state.selected.is_some();
|
||||
let columns_widths = self.get_columns_widths(table_area.width, has_selection);
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
let blank_symbol = " ".repeat(highlight_symbol.width());
|
||||
let mut current_height = 0;
|
||||
let mut rows_height = table_area.height;
|
||||
|
||||
// Draw header
|
||||
if let Some(ref header) = self.header {
|
||||
let max_header_height = table_area.height.min(header.total_height());
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x: table_area.left(),
|
||||
y: table_area.top(),
|
||||
width: table_area.width,
|
||||
height: table_area.height.min(header.height),
|
||||
},
|
||||
header.style,
|
||||
);
|
||||
let mut col = table_area.left();
|
||||
if has_selection {
|
||||
col += (highlight_symbol.width() as u16).min(table_area.width);
|
||||
}
|
||||
for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
|
||||
render_cell(
|
||||
buf,
|
||||
cell,
|
||||
Rect {
|
||||
x: col,
|
||||
y: table_area.top(),
|
||||
width: *width,
|
||||
height: max_header_height,
|
||||
},
|
||||
);
|
||||
col += *width + self.column_spacing;
|
||||
}
|
||||
current_height += max_header_height;
|
||||
rows_height = rows_height.saturating_sub(max_header_height);
|
||||
}
|
||||
|
||||
// Draw rows
|
||||
if self.rows.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
|
||||
state.offset = start;
|
||||
for (i, table_row) in self
|
||||
.rows
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.skip(state.offset)
|
||||
.take(end - start)
|
||||
{
|
||||
let (row, col) = (table_area.top() + current_height, table_area.left());
|
||||
current_height += table_row.total_height();
|
||||
let table_row_area = Rect {
|
||||
x: col,
|
||||
y: row,
|
||||
width: table_area.width,
|
||||
height: table_row.height,
|
||||
};
|
||||
buf.set_style(table_row_area, table_row.style);
|
||||
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
|
||||
let table_row_start_col = if has_selection {
|
||||
let symbol = if is_selected {
|
||||
highlight_symbol
|
||||
} else {
|
||||
&blank_symbol
|
||||
};
|
||||
let (col, _) =
|
||||
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
|
||||
col
|
||||
} else {
|
||||
col
|
||||
};
|
||||
let mut col = table_row_start_col;
|
||||
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
|
||||
render_cell(
|
||||
buf,
|
||||
cell,
|
||||
Rect {
|
||||
x: col,
|
||||
y: row,
|
||||
width: *width,
|
||||
height: table_row.height,
|
||||
},
|
||||
);
|
||||
col += *width + self.column_spacing;
|
||||
}
|
||||
if is_selected {
|
||||
buf.set_style(table_row_area, self.highlight_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
|
||||
buf.set_style(area, cell.style);
|
||||
for (i, spans) in cell.content.lines.iter().enumerate() {
|
||||
if i as u16 >= area.height {
|
||||
break;
|
||||
}
|
||||
buf.set_spans(area.x, area.y + i as u16, spans, area.width);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Table<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default();
|
||||
StatefulWidget::render(self, area, buf, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn table_invalid_percentages() {
|
||||
Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
|
||||
}
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
use crate::ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// A widget to display available tabs in a multiple panels context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, Borders, Tabs};
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// # use ratatui::text::{Spans};
|
||||
/// # use ratatui::symbols::{DOT};
|
||||
/// let titles = ["Tab1", "Tab2", "Tab3", "Tab4"].iter().cloned().map(Spans::from).collect();
|
||||
/// Tabs::new(titles)
|
||||
/// .block(Block::default().title("Tabs").borders(Borders::ALL))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .highlight_style(Style::default().fg(Color::Yellow))
|
||||
/// .divider(DOT);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Tabs<'a> {
|
||||
/// A block to wrap this widget in if necessary
|
||||
block: Option<Block<'a>>,
|
||||
/// One title for each tab
|
||||
titles: Vec<Spans<'a>>,
|
||||
/// The index of the selected tabs
|
||||
selected: usize,
|
||||
/// The style used to draw the text
|
||||
style: Style,
|
||||
/// Style to apply to the selected item
|
||||
highlight_style: Style,
|
||||
/// Tab divider
|
||||
divider: Span<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Tabs<'a> {
|
||||
pub fn new(titles: Vec<Spans<'a>>) -> Tabs<'a> {
|
||||
Tabs {
|
||||
block: None,
|
||||
titles,
|
||||
selected: 0,
|
||||
style: Default::default(),
|
||||
highlight_style: Default::default(),
|
||||
divider: Span::raw(symbols::line::VERTICAL),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> Tabs<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn select(mut self, selected: usize) -> Tabs<'a> {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Tabs<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, style: Style) -> Tabs<'a> {
|
||||
self.highlight_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn divider<T>(mut self, divider: T) -> Tabs<'a>
|
||||
where
|
||||
T: Into<Span<'a>>,
|
||||
{
|
||||
self.divider = divider.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Tabs<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
let tabs_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if tabs_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut x = tabs_area.left();
|
||||
let titles_length = self.titles.len();
|
||||
for (i, title) in self.titles.into_iter().enumerate() {
|
||||
let last_title = titles_length - 1 == i;
|
||||
x = x.saturating_add(1);
|
||||
let remaining_width = tabs_area.right().saturating_sub(x);
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = buf.set_spans(x, tabs_area.top(), &title, remaining_width);
|
||||
if i == self.selected {
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x,
|
||||
y: tabs_area.top(),
|
||||
width: pos.0.saturating_sub(x),
|
||||
height: 1,
|
||||
},
|
||||
self.highlight_style,
|
||||
);
|
||||
}
|
||||
x = pos.0.saturating_add(1);
|
||||
let remaining_width = tabs_area.right().saturating_sub(x);
|
||||
if remaining_width == 0 || last_title {
|
||||
break;
|
||||
}
|
||||
let pos = buf.set_span(x, tabs_area.top(), &self.divider, remaining_width);
|
||||
x = pos.0;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user