crossterm support (#331)

* crossterm v2

* patch crossterm

* fix-version

* no more tui dependency

* lints
This commit is contained in:
Conrad Ludgate 2023-02-10 17:25:43 +00:00 committed by GitHub
parent a22ff76be5
commit edda1b741a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 4468 additions and 181 deletions

104
Cargo.lock generated
View file

@ -77,11 +77,14 @@ dependencies = [
"atuin-common",
"atuin-server",
"base64 0.20.0",
"bitflags",
"cassowary",
"chrono",
"clap",
"clap_complete",
"cli-table",
"crossbeam-channel",
"crossterm",
"directories",
"env_logger",
"eyre",
@ -95,11 +98,10 @@ dependencies = [
"semver",
"serde",
"serde_json",
"termion",
"tiny-bip39",
"tokio",
"tracing-subscriber",
"tui",
"unicode-segmentation",
"unicode-width",
"whoami",
]
@ -488,6 +490,32 @@ dependencies = [
"once_cell",
]
[[package]]
name = "crossterm"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77f67c7faacd4db07a939f55d66a983a5355358a1f17d32cc9a8d01d1266b9ce"
dependencies = [
"bitflags",
"crossterm_winapi",
"filedescriptor",
"libc",
"mio",
"parking_lot 0.12.1",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
dependencies = [
"winapi",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -627,6 +655,17 @@ dependencies = [
"once_cell",
]
[[package]]
name = "filedescriptor"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
dependencies = [
"libc",
"thiserror",
"winapi",
]
[[package]]
name = "flume"
version = "0.10.14"
@ -1294,12 +1333,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]]
name = "once_cell"
version = "1.14.0"
@ -1535,15 +1568,6 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_termios"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
dependencies = [
"redox_syscall",
]
[[package]]
name = "redox_users"
version = "0.4.3"
@ -1897,6 +1921,27 @@ dependencies = [
"dirs",
]
[[package]]
name = "signal-hook"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
@ -2152,18 +2197,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "termion"
version = "1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
dependencies = [
"libc",
"numtoa",
"redox_syscall",
"redox_termios",
]
[[package]]
name = "thiserror"
version = "1.0.38"
@ -2417,19 +2450,6 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "tui"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
dependencies = [
"bitflags",
"cassowary",
"termion",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "typenum"
version = "1.15.0"

View file

@ -56,8 +56,7 @@ directories = "4"
indicatif = "0.17.1"
serde = { version = "1.0.145", features = ["derive"] }
serde_json = "1.0.86"
tui = { version = "0.19", default-features = false, features = ["termion"] }
termion = "1.5"
crossterm = { version = "0.26", features = ["use-dev-tty"] }
unicode-width = "0.1"
itertools = "0.10.5"
tokio = { version = "1", features = ["full"] }
@ -75,6 +74,11 @@ semver = "1.0.14"
runtime-format = "0.1.2"
tiny-bip39 = "1"
# from tui
bitflags = "1.3"
cassowary = "0.3"
unicode-segmentation = "1.2"
[dependencies.tracing-subscriber]
version = "0.3"
default-features = false

View file

@ -221,7 +221,7 @@ mod test {
println!("h: {:#?}", histdb.histdb);
println!("counter: {:?}", histdb.histdb.len());
for i in histdb.histdb {
println!("{:?}", i);
println!("{i:?}");
}
}
}

View file

@ -13,7 +13,6 @@ use super::history::ListMode;
mod cursor;
mod duration;
mod event;
mod history_list;
mod interactive;
pub use duration::{format_duration, format_duration_into};

View file

@ -1,70 +0,0 @@
use std::{thread, time::Duration};
use crossbeam_channel::unbounded;
use termion::{event::Event as TermEvent, event::Key, input::TermRead};
pub enum Event<I> {
Input(I),
Tick,
}
/// A small event handler that wrap termion input and tick events. Each event
/// type is handled in its own thread and returned to a common `Receiver`
pub struct Events {
rx: crossbeam_channel::Receiver<Event<TermEvent>>,
}
#[derive(Debug, Clone, Copy)]
pub struct Config {
pub exit_key: Key,
pub tick_rate: Duration,
}
impl Default for Config {
fn default() -> Config {
Config {
exit_key: Key::Char('q'),
tick_rate: Duration::from_millis(250),
}
}
}
impl Events {
pub fn new() -> Events {
Events::with_config(Config::default())
}
pub fn with_config(config: Config) -> Events {
let (tx, rx) = unbounded();
{
let tx = tx.clone();
thread::spawn(move || {
let tty = termion::get_tty().expect("Could not find tty");
for event in tty.events().flatten() {
if let Err(err) = tx.send(Event::Input(event)) {
eprintln!("{err}");
return;
}
}
})
};
thread::spawn(move || loop {
if tx.send(Event::Tick).is_err() {
break;
}
thread::sleep(config.tick_rate);
});
Events { rx }
}
pub fn next(&self) -> Result<Event<TermEvent>, crossbeam_channel::RecvError> {
self.rx.recv()
}
pub fn try_next(&self) -> Result<Event<TermEvent>, crossbeam_channel::TryRecvError> {
self.rx.try_recv()
}
}

View file

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

View file

@ -1,19 +1,22 @@
use std::io::stdout;
use eyre::Result;
use semver::Version;
use termion::{
event::Event as TermEvent, event::Key, event::MouseButton, event::MouseEvent,
input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen,
use std::{
io::{stdout, Write},
time::Duration,
};
use tui::{
backend::{Backend, TermionBackend},
use crate::tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, BorderType, Borders, Paragraph},
Frame, Terminal,
};
use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent},
execute, terminal,
};
use eyre::Result;
use semver::Version;
use unicode_width::UnicodeWidthStr;
use atuin_client::{
@ -26,7 +29,6 @@ use atuin_client::{
use super::{
cursor::Cursor,
event::{Event, Events},
history_list::{HistoryList, ListState, PREFIX_LENGTH},
};
use crate::VERSION;
@ -62,42 +64,69 @@ impl State {
Ok(results)
}
fn handle_input(
fn handle_input(&mut self, settings: &Settings, input: &Event, len: usize) -> Option<usize> {
match input {
Event::Key(k) => self.handle_key_input(settings, k, len),
Event::Mouse(m) => self.handle_mouse_input(*m, len),
_ => None,
}
}
fn handle_mouse_input(&mut self, input: MouseEvent, len: usize) -> Option<usize> {
match input.kind {
event::MouseEventKind::ScrollDown => {
let i = self.results_state.selected().saturating_sub(1);
self.results_state.select(i);
}
event::MouseEventKind::ScrollUp => {
let i = self.results_state.selected() + 1;
self.results_state.select(i.min(len - 1));
}
_ => {}
}
None
}
fn handle_key_input(
&mut self,
settings: &Settings,
input: &TermEvent,
input: &KeyEvent,
len: usize,
) -> Option<usize> {
match input {
TermEvent::Key(Key::Char('\t')) => {}
TermEvent::Key(Key::Ctrl('c' | 'd' | 'g')) => return Some(RETURN_ORIGINAL),
TermEvent::Key(Key::Esc) => {
let ctrl = input.modifiers.contains(KeyModifiers::CONTROL);
let alt = input.modifiers.contains(KeyModifiers::ALT);
match input.code {
KeyCode::Char('c' | 'd' | 'g') if ctrl => return Some(RETURN_ORIGINAL),
KeyCode::Esc => {
return Some(match settings.exit_mode {
ExitMode::ReturnOriginal => RETURN_ORIGINAL,
ExitMode::ReturnQuery => RETURN_QUERY,
})
}
TermEvent::Key(Key::Char('\n')) => {
KeyCode::Enter => {
return Some(self.results_state.selected());
}
TermEvent::Key(Key::Alt(c @ '1'..='9')) => {
KeyCode::Char(c @ '1'..='9') if alt => {
let c = c.to_digit(10)? as usize;
return Some(self.results_state.selected() + c);
}
TermEvent::Key(Key::Left | Key::Ctrl('h')) => {
KeyCode::Left => {
self.input.left();
}
TermEvent::Key(Key::Right | Key::Ctrl('l')) => self.input.right(),
TermEvent::Key(Key::Ctrl('a') | Key::Home) => self.input.start(),
TermEvent::Key(Key::Ctrl('e') | Key::End) => self.input.end(),
TermEvent::Key(Key::Char(c)) => self.input.insert(*c),
TermEvent::Key(Key::Backspace) => {
KeyCode::Char('h') if ctrl => {
self.input.left();
}
KeyCode::Right => self.input.right(),
KeyCode::Char('l') if ctrl => self.input.right(),
KeyCode::Char('a') if ctrl => self.input.start(),
KeyCode::Char('e') if ctrl => self.input.end(),
KeyCode::Backspace => {
self.input.back();
}
TermEvent::Key(Key::Delete) => {
KeyCode::Delete => {
self.input.remove();
}
TermEvent::Key(Key::Ctrl('w')) => {
KeyCode::Char('w') if ctrl => {
// remove the first batch of whitespace
while matches!(self.input.back(), Some(c) if c.is_whitespace()) {}
while self.input.left() {
@ -108,8 +137,8 @@ impl State {
self.input.remove();
}
}
TermEvent::Key(Key::Ctrl('u')) => self.input.clear(),
TermEvent::Key(Key::Ctrl('r')) => {
KeyCode::Char('u') if ctrl => self.input.clear(),
KeyCode::Char('r') if ctrl => {
pub static FILTER_MODES: [FilterMode; 4] = [
FilterMode::Global,
FilterMode::Host,
@ -120,19 +149,24 @@ impl State {
let i = (i + 1) % FILTER_MODES.len();
self.filter_mode = FILTER_MODES[i];
}
TermEvent::Key(Key::Down | Key::Ctrl('n' | 'j'))
| TermEvent::Mouse(MouseEvent::Press(MouseButton::WheelDown, _, _)) => {
if self.results_state.selected() == 0 && input.eq(&TermEvent::Key(Key::Down)) {
return Some(RETURN_ORIGINAL);
}
KeyCode::Down if self.results_state.selected() == 0 => return Some(RETURN_ORIGINAL),
KeyCode::Down => {
let i = self.results_state.selected().saturating_sub(1);
self.results_state.select(i);
}
TermEvent::Key(Key::Up | Key::Ctrl('p' | 'k'))
| TermEvent::Mouse(MouseEvent::Press(MouseButton::WheelUp, _, _)) => {
KeyCode::Char('n' | 'j') if ctrl => {
let i = self.results_state.selected().saturating_sub(1);
self.results_state.select(i);
}
KeyCode::Up => {
let i = self.results_state.selected() + 1;
self.results_state.select(i.min(len - 1));
}
KeyCode::Char('p' | 'k') if ctrl => {
let i = self.results_state.selected() + 1;
self.results_state.select(i.min(len - 1));
}
KeyCode::Char(c) => self.input.insert(c),
_ => {}
};
@ -303,6 +337,45 @@ impl State {
}
}
struct Stdout {
stdout: std::io::Stdout,
}
impl Stdout {
pub fn new() -> std::io::Result<Self> {
terminal::enable_raw_mode()?;
let mut stdout = stdout();
execute!(
stdout,
terminal::EnterAlternateScreen,
event::EnableMouseCapture
)?;
Ok(Self { stdout })
}
}
impl Drop for Stdout {
fn drop(&mut self) {
execute!(
self.stdout,
terminal::LeaveAlternateScreen,
event::DisableMouseCapture
)
.unwrap();
terminal::disable_raw_mode().unwrap();
}
}
impl Write for Stdout {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.stdout.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.stdout.flush()
}
}
// this is a big blob of horrible! clean it up!
// for now, it works. But it'd be great if it were more easily readable, and
// modular. I'd like to add some more stats and stuff at some point
@ -312,15 +385,10 @@ pub async fn history(
settings: &Settings,
db: &mut impl Database,
) -> Result<String> {
let stdout = stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let stdout = Stdout::new()?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// Setup event handlers
let events = Events::new();
let mut input = Cursor::from(query.join(" "));
// Put the cursor at the end of the query by default
input.end();
@ -343,27 +411,6 @@ pub async fn history(
let mut results = app.query_results(settings.search_mode, db).await?;
let index = 'render: loop {
let initial_input = app.input.as_str().to_owned();
let initial_filter_mode = app.filter_mode;
// Handle input
if let Event::Input(input) = events.next()? {
if let Some(i) = app.handle_input(settings, &input, results.len()) {
break 'render i;
}
}
// After we receive input process the whole event channel before query/render.
while let Ok(Event::Input(input)) = events.try_next() {
if let Some(i) = app.handle_input(settings, &input, results.len()) {
break 'render i;
}
}
if initial_input != app.input.as_str() || initial_filter_mode != app.filter_mode {
results = app.query_results(settings.search_mode, db).await?;
}
let compact = match settings.style {
atuin_client::settings::Style::Auto => {
terminal.size().map(|size| size.height < 14).unwrap_or(true)
@ -376,6 +423,24 @@ pub async fn history(
} else {
terminal.draw(|f| app.draw(f, &results))?;
}
let initial_input = app.input.as_str().to_owned();
let initial_filter_mode = app.filter_mode;
if event::poll(Duration::from_millis(250))? {
loop {
if let Some(i) = app.handle_input(settings, &event::read()?, results.len()) {
break 'render i;
}
if !event::poll(Duration::ZERO)? {
break;
}
}
}
if initial_input != app.input.as_str() || initial_filter_mode != app.filter_mode {
results = app.query_results(settings.search_mode, db).await?;
}
};
if index < results.len() {

View file

@ -6,6 +6,7 @@ use eyre::Result;
use command::AtuinCmd;
mod command;
mod tui;
const VERSION: &str = env!("CARGO_PKG_VERSION");

21
src/tui/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.

5
src/tui/README.md Normal file
View file

@ -0,0 +1,5 @@
# tui-rs
A fork of https://crates.io/crates/tui/0.19.0 since it is now unmaintained.
Some parts have been removed or modified for simplicity, but it is currently mostly equivalent.

View file

@ -0,0 +1,221 @@
use crate::tui::{
backend::Backend,
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, ClearType},
};
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<()> {
map_error(execute!(self.buffer, Clear(ClearType::All)))
}
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(())
}
}

20
src/tui/backend/mod.rs Normal file
View file

@ -0,0 +1,20 @@
use std::io;
use crate::tui::buffer::Cell;
use crate::tui::layout::Rect;
mod crossterm;
pub use self::crossterm::CrosstermBackend;
pub trait Backend {
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>;
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>;
fn clear(&mut self) -> Result<(), io::Error>;
fn size(&self) -> Result<Rect, io::Error>;
fn flush(&mut self) -> Result<(), io::Error>;
}

732
src/tui/buffer.rs Normal file
View file

@ -0,0 +1,732 @@
use crate::tui::{
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 tui::buffer::{Buffer, Cell};
/// use tui::layout::Rect;
/// use tui::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();
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: &[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 tui::buffer::Buffer;
/// # use tui::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 tui::buffer::Buffer;
/// # use tui::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 tui::buffer::Buffer;
/// # use tui::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 tui::buffer::Buffer;
/// # use tui::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 dimenstions 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, Cell::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();
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 width = self.area.width;
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
// Cells invalidated by drawing/replacing preceeding multi-width characters:
let mut invalidated: usize = 0;
// Cells from the current buffer to skip due to preceeding 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 = i as u16 % width;
let y = i as u16 / width;
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]
#[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]
#[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(&[" "]));
buffer.set_string(0, 0, "aaa", Style::default());
assert_eq!(buffer, Buffer::with_lines(&["aaa "]));
// Width limit:
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
assert_eq!(buffer, Buffer::with_lines(&["bbbb "]));
buffer.set_string(0, 0, "12345", Style::default());
assert_eq!(buffer, Buffer::with_lines(&["12345"]));
// Width truncation:
buffer.set_string(0, 0, "123456", Style::default());
assert_eq!(buffer, Buffer::with_lines(&["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(&["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(&["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(&["コン "]));
// Only 1 space left.
buffer.set_string(0, 0, "コンピ", Style::default());
assert_eq!(buffer, Buffer::with_lines(&["コン "]));
}
#[test]
fn buffer_with_lines() {
let buffer = Buffer::with_lines(&["┌────────┐", "│コンピュ│", "│ー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(&[
" ",
"┌Title─┐ ",
"│ │ ",
"│ │ ",
"└──────┘ ",
]);
let next = Buffer::with_lines(&[
" ",
"┌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(&[
"┌Title─┐ ",
"└──────┘ ",
]);
let next = Buffer::with_lines(&[
"┌称号──┐ ",
"└──────┘ ",
]);
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(&["┌称号──┐"]);
let next = Buffer::with_lines(&["┌─称号─┐"]);
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(&["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(&["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(&["222 ", "222 ", "2221", "2221"]);
merged.area = Rect {
x: 1,
y: 1,
width: 4,
height: 4,
};
assert_eq!(one, merged);
}
}

537
src/tui/layout.rs Normal file
View file

@ -0,0 +1,537 @@
use std::cell::RefCell;
use std::cmp::{max, min};
use std::collections::HashMap;
use cassowary::strength::{REQUIRED, WEAK};
use cassowary::WeightedRelation::{EQ, GE, LE};
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,
}
thread_local! {
static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = 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 tui::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,
/// vec![
/// 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,
/// vec![
/// Rect {
/// x: 0,
/// y: 0,
/// width: 3,
/// height: 2
/// },
/// Rect {
/// x: 3,
/// y: 0,
/// width: 6,
/// height: 2
/// }
/// ]
/// );
/// ```
pub fn split(&self, area: Rect) -> Vec<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()
})
}
}
#[allow(clippy::too_many_lines)]
fn split(area: Rect, layout: &Layout) -> Vec<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 results = layout
.constraints
.iter()
.map(|_| Rect::default())
.collect::<Vec<Rect>>();
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(WEAK) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
}
Constraint::Ratio(n, d) => {
elements[i].width
| EQ(WEAK)
| (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].width | LE(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(WEAK) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
}
Constraint::Ratio(n, d) => {
elements[i].height
| EQ(WEAK)
| (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].height | LE(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;
}
}
}
}
results
}
/// 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);
}
}

20
src/tui/mod.rs Normal file
View file

@ -0,0 +1,20 @@
//! Fork of `tui-rs`
#![allow(
clippy::module_name_repetitions,
clippy::bool_to_int_with_if,
clippy::similar_names,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
dead_code
)]
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};

278
src/tui/style.rs Normal file
View file

@ -0,0 +1,278 @@
//! `style` contains the primitives used to control how your user interface will look.
use bitflags::bitflags;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
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 tui::style::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
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 tui::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 tui::style::{Color, Modifier, Style};
/// # use tui::buffer::Buffer;
/// # use tui::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 tui::style::{Color, Modifier, Style};
/// # use tui::buffer::Buffer;
/// # use tui::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)]
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 tui::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 tui::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 tui::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 tui::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 tui::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)
);
}
}
}
}
}
}

233
src/tui/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,
}

321
src/tui/terminal.rs Normal file
View file

@ -0,0 +1,321 @@
use crate::tui::{
backend::Backend,
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
};
use std::io;
#[derive(Debug, Clone, PartialEq)]
/// UNSTABLE
enum ResizeBehavior {
Fixed,
Auto,
}
#[derive(Debug, Clone, PartialEq)]
/// UNSTABLE
pub struct Viewport {
area: Rect,
resize_behavior: ResizeBehavior,
}
impl Viewport {
/// UNSTABLE
pub fn fixed(area: Rect) -> Viewport {
Viewport {
area,
resize_behavior: ResizeBehavior::Fixed,
}
}
}
#[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,
}
/// Represents a consistent terminal interface for rendering.
pub struct Frame<'a, B: 'a + 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,
{
/// Terminal 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 tui::Terminal;
/// # use tui::backend::TestBackend;
/// # use tui::layout::Rect;
/// # use tui::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 tui::Terminal;
/// # use tui::backend::TestBackend;
/// # use tui::layout::Rect;
/// # use tui::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>> {
let size = backend.size()?;
Ok(Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport {
area: size,
resize_behavior: ResizeBehavior::Auto,
},
},
))
}
/// UNSTABLE
pub fn with_options(backend: B, options: TerminalOptions) -> Terminal<B> {
Terminal {
backend,
buffers: [
Buffer::empty(options.viewport.area),
Buffer::empty(options.viewport.area),
],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
}
}
/// 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);
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, area: Rect) -> io::Result<()> {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport.area = area;
self.clear()
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
if self.viewport.resize_behavior == ResizeBehavior::Auto {
let size = self.size()?;
if size != self.viewport.area {
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.viewport.area,
})
}
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)
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
self.backend.clear()?;
// 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()
}
}

428
src/tui/text.rs Normal file
View file

@ -0,0 +1,428 @@
//! 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. `tui` 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, `tui` 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 tui::widgets::Block;
//! # use tui::text::{Span, Spans};
//! # use tui::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::tui::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 tui::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 tui::text::Span;
/// # use tui::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 tui::text::{Span, StyledGrapheme};
/// # use tui::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 tui::text::{Span, Spans};
/// # use tui::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 tui::text::Text;
/// # use tui::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 tui::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>>,
{
Text {
lines: match content.into() {
Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
},
}
}
/// Create some text (potentially multiple lines) with a style.
///
/// # Examples
///
/// ```rust
/// # use tui::text::Text;
/// # use tui::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 tui::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 tui::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 tui::text::Text;
/// # use tui::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);
}
}

562
src/tui/widgets/block.rs Normal file
View file

@ -0,0 +1,562 @@
use crate::tui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::Style,
symbols::line,
text::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 tui::widgets::{Block, BorderType, Borders};
/// # use tui::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: Style::default(),
border_type: BorderType::Plain,
style: Style::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
}
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::tui::layout::Rect;
#[test]
#[allow(clippy::too_many_lines)]
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,
},
);
}
}

159
src/tui/widgets/mod.rs Normal file
View file

@ -0,0 +1,159 @@
//! `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`]
//! - [`Paragraph`]
mod block;
mod paragraph;
mod reflow;
pub use self::block::{Block, BorderType};
pub use self::paragraph::{Paragraph, Wrap};
use crate::tui::{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: u32 {
/// Show no border (default)
const NONE = 0b0000_0001;
/// Show the top border
const TOP = 0b0000_0010;
/// Show the right border
const RIGHT = 0b0000_0100;
/// Show the bottom border
const BOTTOM = 0b000_1000;
/// Show the left border
const LEFT = 0b0001_0000;
/// 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 tui::Terminal;
/// # use tui::backend::{Backend, TestBackend};
/// # use tui::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 tui.
/// 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,194 @@
use crate::tui::{
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 tui::text::{Text, Spans, Span};
/// # use tui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use tui::style::{Style, Color, Modifier};
/// # use tui::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 tui::widgets::{Paragraph, Wrap};
/// # use tui::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: Style::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 = self.block.take().map_or(area, |b| {
let inner_area = b.inner(area);
b.render(area, buf);
inner_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 self.alignment == Alignment::Left {
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 {
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 += symbol.width() as u16;
}
}
y += 1;
if y >= text_area.height + self.scroll.0 {
break;
}
}
}
}

537
src/tui/widgets/reflow.rs Normal file
View file

@ -0,0 +1,537 @@
use crate::tui::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 {
(self.current_line.len() - 1, self.max_line_width)
} else {
(symbols_to_last_word_end, width_to_last_word_end)
};
// 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 offet 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 crate::tui::style::Style;
use super::*;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone, Copy)]
enum Composer {
WordWrapper { trim: bool },
LineTruncator,
}
fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
let style = Style::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, folowed 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!"
]
);
}
}