Vendor ratatui temporarily (#835)

* Vendor ratatui temporarily

Once https://github.com/tui-rs-revival/ratatui/pull/114 has been merged,
we can undo this! But otherwise we can't publish to crates.io with a git
dependency.

* make tests pass

* Shush.

* these literally just fail in nix, nowhere else

idk how to work with nix properly, and they're also not our tests
This commit is contained in:
Ellie Huxtable 2023-03-31 22:57:37 +01:00 committed by GitHub
parent 3552c7e0d3
commit a515b06bcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 14697 additions and 21 deletions

16
Cargo.lock generated
View File

@ -77,6 +77,8 @@ dependencies = [
"atuin-common",
"atuin-server",
"base64 0.20.0",
"bitflags",
"cassowary",
"chrono",
"clap",
"clap_complete",
@ -93,7 +95,6 @@ dependencies = [
"interim",
"itertools",
"log",
"ratatui",
"rpassword",
"runtime-format",
"semver",
@ -102,6 +103,7 @@ dependencies = [
"tiny-bip39",
"tokio",
"tracing-subscriber",
"unicode-segmentation",
"unicode-width",
"whoami",
]
@ -1583,18 +1585,6 @@ dependencies = [
"getrandom",
]
[[package]]
name = "ratatui"
version = "0.20.1"
source = "git+https://github.com/conradludgate/tui-rs-revival?branch=inline#6ed61959ecfc560e4e6a00a1410bb5fcbf0eda91"
dependencies = [
"bitflags",
"cassowary",
"crossterm",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"

View File

@ -73,15 +73,16 @@ semver = "1.0.14"
runtime-format = "0.1.2"
tiny-bip39 = "1"
futures-util = "0.3"
ratatui = "0.20.1"
fuzzy-matcher = "0.3.7"
colored = "2.0.0"
# ratatui
bitflags = "1.3"
cassowary = "0.3"
unicode-segmentation = "1.2"
[dependencies.tracing-subscriber]
version = "0.3"
default-features = false
features = ["ansi", "fmt", "registry", "env-filter"]
optional = true
[patch.crates-io]
ratatui = { git = "https://github.com/conradludgate/tui-rs-revival", branch = "inline" }

View File

@ -1,12 +1,12 @@
use std::time::Duration;
use atuin_client::history::History;
use ratatui::{
use crate::ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
widgets::{Block, StatefulWidget, Widget},
};
use atuin_client::history::History;
use super::format_duration;

View File

@ -23,8 +23,7 @@ use super::{
engines::{SearchEngine, SearchState},
history_list::{HistoryList, ListState, PREFIX_LENGTH},
};
use crate::{command::client::search::engines, VERSION};
use ratatui::{
use crate::ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
@ -32,6 +31,7 @@ use ratatui::{
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;

View File

@ -7,6 +7,9 @@ use eyre::Result;
use command::AtuinCmd;
mod command;
#[allow(clippy::all)]
mod ratatui;
const VERSION: &str = env!("CARGO_PKG_VERSION");
static HELP_TEMPLATE: &str = "\

View File

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

View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -0,0 +1,32 @@
---
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
src/ratatui/.github/workflows/cd.yml vendored Normal file
View File

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

76
src/ratatui/.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,76 @@
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
src/ratatui/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
target
Cargo.lock
*.log
*.rs.rustfmt
.gdb_history
.idea/

21
src/ratatui/LICENSE Normal file
View File

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

136
src/ratatui/README.md Normal file
View File

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

View File

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

View File

@ -0,0 +1,58 @@
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>;
}

View File

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

736
src/ratatui/buffer.rs Normal file
View File

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

560
src/ratatui/layout.rs Normal file
View File

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

177
src/ratatui/mod.rs Normal file
View File

@ -0,0 +1,177 @@
#![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};

310
src/ratatui/style.rs Normal file
View File

@ -0,0 +1,310 @@
//! `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));
}
}
}

233
src/ratatui/symbols.rs Normal file
View File

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

487
src/ratatui/terminal.rs Normal file
View File

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

430
src/ratatui/text.rs Normal file
View File

@ -0,0 +1,430 @@
//! 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);
}
}

View File

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

View File

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

View File

@ -0,0 +1,95 @@
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,37 @@
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();
}
}
}
}

View File

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

268
src/ratatui/widgets/list.rs Normal file
View File

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

184
src/ratatui/widgets/mod.rs Normal file
View File

@ -0,0 +1,184 @@
//! `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);
}

View File

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

View File

@ -0,0 +1,534 @@
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!"
]
);
}
}

View File

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

View File

@ -0,0 +1,504 @@
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)]);
}
}

129
src/ratatui/widgets/tabs.rs Normal file
View File

@ -0,0 +1,129 @@
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;
}
}
}