aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorConrad Ludgate <conradludgate@gmail.com>2023-02-10 17:25:43 +0000
committerGitHub <noreply@github.com>2023-02-10 17:25:43 +0000
commitedda1b741a4a0816eb6e62eafd69fc9896603cf5 (patch)
treecc5cb45caecc4fbe6b34e08f2347fdfdf897d0b5 /src
parentBump debian from bullseye-20221205-slim to bullseye-20230208-slim (#701) (diff)
downloadatuin-edda1b741a4a0816eb6e62eafd69fc9896603cf5.zip
crossterm support (#331)
* crossterm v2 * patch crossterm * fix-version * no more tui dependency * lints
Diffstat (limited to 'src')
-rw-r--r--src/command/client/search.rs1
-rw-r--r--src/command/client/search/event.rs70
-rw-r--r--src/command/client/search/history_list.rs4
-rw-r--r--src/command/client/search/interactive.rs191
-rw-r--r--src/main.rs1
-rw-r--r--src/tui/LICENSE21
-rw-r--r--src/tui/README.md5
-rw-r--r--src/tui/backend/crossterm.rs221
-rw-r--r--src/tui/backend/mod.rs20
-rw-r--r--src/tui/buffer.rs732
-rw-r--r--src/tui/layout.rs537
-rw-r--r--src/tui/mod.rs20
-rw-r--r--src/tui/style.rs278
-rw-r--r--src/tui/symbols.rs233
-rw-r--r--src/tui/terminal.rs321
-rw-r--r--src/tui/text.rs428
-rw-r--r--src/tui/widgets/block.rs562
-rw-r--r--src/tui/widgets/mod.rs159
-rw-r--r--src/tui/widgets/paragraph.rs194
-rw-r--r--src/tui/widgets/reflow.rs537
20 files changed, 4399 insertions, 136 deletions
diff --git a/src/command/client/search.rs b/src/command/client/search.rs
index 53471ec1..9321f117 100644
--- a/src/command/client/search.rs
+++ b/src/command/client/search.rs
@@ -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};
diff --git a/src/command/client/search/event.rs b/src/command/client/search/event.rs
deleted file mode 100644
index 0e791c96..00000000
--- a/src/command/client/search/event.rs
+++ /dev/null
@@ -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()
- }
-}
diff --git a/src/command/client/search/history_list.rs b/src/command/client/search/history_list.rs
index d74221d8..f4725b02 100644
--- a/src/command/client/search/history_list.rs
+++ b/src/command/client/search/history_list.rs
@@ -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;
diff --git a/src/command/client/search/interactive.rs b/src/command/client/search/interactive.rs
index e0ceb091..c8ceab58 100644
--- a/src/command/client/search/interactive.rs
+++ b/src/command/client/search/interactive.rs
@@ -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);
+ }
+ KeyCode::Char('n' | 'j') if ctrl => {
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::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() {
diff --git a/src/main.rs b/src/main.rs
index 2f81f4fc..3004e0b1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,6 +6,7 @@ use eyre::Result;
use command::AtuinCmd;
mod command;
+mod tui;
const VERSION: &str = env!("CARGO_PKG_VERSION");
diff --git a/src/tui/LICENSE b/src/tui/LICENSE
new file mode 100644
index 00000000..7a0657cb
--- /dev/null
+++ b/src/tui/LICENSE
@@ -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.
diff --git a/src/tui/README.md b/src/tui/README.md
new file mode 100644
index 00000000..506bdf8f
--- /dev/null
+++ b/src/tui/README.md
@@ -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.
diff --git a/src/tui/backend/crossterm.rs b/src/tui/backend/crossterm.rs
new file mode 100644
index 00000000..2cbfd6e0
--- /dev/null
+++ b/src/tui/backend/crossterm.rs
@@ -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(())
+ }
+}
diff --git a/src/tui/backend/mod.rs b/src/tui/backend/mod.rs
new file mode 100644
index 00000000..1a197e79
--- /dev/null
+++ b/src/tui/backend/mod.rs
@@ -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>;
+}
diff --git a/src/tui/buffer.rs b/src/tui/buffer.rs
new file mode 100644
index 00000000..38307980
--- /dev/null
+++ b/src/tui/buffer.rs
@@ -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);
+ }
+}
diff --git a/src/tui/layout.rs b/src/tui/layout.rs
new file mode 100644
index 00000000..369e10f9
--- /dev/null
+++ b/src/tui/layout.rs
@@ -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);
+ }
+}
diff --git a/src/tui/mod.rs b/src/tui/mod.rs
new file mode 100644
index 00000000..29fe84cd
--- /dev/null
+++ b/src/tui/mod.rs
@@ -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};
diff --git a/src/tui/style.rs b/src/tui/style.rs
new file mode 100644
index 00000000..4aa19d4a
--- /dev/null
+++ b/src/tui/style.rs
@@ -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)
+ );
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/tui/symbols.rs b/src/tui/symbols.rs
new file mode 100644
index 00000000..040e77f6
--- /dev/null
+++ b/src/tui/symbols.rs
@@ -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,
+}
diff --git a/src/tui/terminal.rs b/src/tui/terminal.rs
new file mode 100644
index 00000000..e64690e9
--- /dev/null
+++ b/src/tui/terminal.rs
@@ -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()
+ }
+}
diff --git a/src/tui/text.rs b/src/tui/text.rs
new file mode 100644
index 00000000..e8a5aa73
--- /dev/null
+++ b/src/tui/text.rs
@@ -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);
+ }
+}
diff --git a/src/tui/widgets/block.rs b/src/tui/widgets/block.rs
new file mode 100644
index 00000000..eb59f9c3
--- /dev/null
+++ b/src/tui/widgets/block.rs
@@ -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,
+ },
+ );
+ }
+}
diff --git a/src/tui/widgets/mod.rs b/src/tui/widgets/mod.rs
new file mode 100644
index 00000000..635185e5
--- /dev/null
+++ b/src/tui/widgets/mod.rs
@@ -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);
+}
diff --git a/src/tui/widgets/paragraph.rs b/src/tui/widgets/paragraph.rs
new file mode 100644
index 00000000..a5036148
--- /dev/null
+++ b/src/tui/widgets/paragraph.rs
@@ -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;
+ }
+ }
+ }
+}
diff --git a/src/tui/widgets/reflow.rs b/src/tui/widgets/reflow.rs
new file mode 100644
index 00000000..d174932b
--- /dev/null
+++ b/src/tui/widgets/reflow.rs
@@ -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!"
+ ]
+ );
+ }
+}