aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock21
-rw-r--r--Cargo.toml6
-rw-r--r--src/command/client/search/history_list.rs4
-rw-r--r--src/command/client/search/interactive.rs18
-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.rs734
-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, 27 insertions, 4293 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2e75c2dc..c78a1d44 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -83,8 +83,6 @@ dependencies = [
"atuin-common",
"atuin-server",
"base64 0.20.0",
- "bitflags",
- "cassowary",
"chrono",
"clap",
"clap_complete",
@@ -99,6 +97,7 @@ dependencies = [
"interim",
"itertools",
"log",
+ "ratatui",
"rpassword",
"runtime-format",
"semver",
@@ -108,7 +107,6 @@ dependencies = [
"tiny-bip39",
"tokio",
"tracing-subscriber",
- "unicode-segmentation",
"unicode-width",
"whoami",
]
@@ -1764,6 +1762,19 @@ dependencies = [
]
[[package]]
+name = "ratatui"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcc0d032bccba900ee32151ec0265667535c230169f5a011154cdcd984e16829"
+dependencies = [
+ "bitflags",
+ "cassowary",
+ "crossterm",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
name = "rayon"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2790,9 +2801,9 @@ dependencies = [
[[package]]
name = "unicode-segmentation"
-version = "1.9.0"
+version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
+checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"
diff --git a/Cargo.toml b/Cargo.toml
index f2634851..286f51f9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -74,11 +74,7 @@ runtime-format = "0.1.2"
tiny-bip39 = "1"
futures-util = "0.3"
skim = { version = "0.10.2", default-features = false }
-
-# from tui
-bitflags = "1.3"
-cassowary = "0.3"
-unicode-segmentation = "1.2"
+ratatui = "0.20.1"
[dependencies.tracing-subscriber]
version = "0.3"
diff --git a/src/command/client/search/history_list.rs b/src/command/client/search/history_list.rs
index 27a8b294..60ec15a8 100644
--- a/src/command/client/search/history_list.rs
+++ b/src/command/client/search/history_list.rs
@@ -1,12 +1,12 @@
use std::{sync::Arc, time::Duration};
-use crate::tui::{
+use atuin_client::history::History;
+use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
widgets::{Block, StatefulWidget, Widget},
};
-use atuin_client::history::History;
use super::{format_duration, interactive::HistoryWrapper};
diff --git a/src/command/client/search/interactive.rs b/src/command/client/search/interactive.rs
index 6b26def8..4b5e0d7e 100644
--- a/src/command/client/search/interactive.rs
+++ b/src/command/client/search/interactive.rs
@@ -26,16 +26,14 @@ use super::{
cursor::Cursor,
history_list::{HistoryList, ListState, PREFIX_LENGTH},
};
-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,
- },
- VERSION,
+use crate::VERSION;
+use ratatui::{
+ backend::{Backend, CrosstermBackend},
+ layout::{Alignment, Constraint, Direction, Layout},
+ style::{Color, Modifier, Style},
+ text::{Span, Spans, Text},
+ widgets::{Block, BorderType, Borders, Paragraph},
+ Frame, Terminal,
};
const RETURN_ORIGINAL: usize = usize::MAX;
diff --git a/src/main.rs b/src/main.rs
index 3004e0b1..2f81f4fc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,7 +6,6 @@ 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
deleted file mode 100644
index 7a0657cb..00000000
--- a/src/tui/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2016 Florian Dehau
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/src/tui/README.md b/src/tui/README.md
deleted file mode 100644
index 506bdf8f..00000000
--- a/src/tui/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# 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
deleted file mode 100644
index 2cbfd6e0..00000000
--- a/src/tui/backend/crossterm.rs
+++ /dev/null
@@ -1,221 +0,0 @@
-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
deleted file mode 100644
index 1a197e79..00000000
--- a/src/tui/backend/mod.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-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
deleted file mode 100644
index 8bc49316..00000000
--- a/src/tui/buffer.rs
+++ /dev/null
@@ -1,734 +0,0 @@
-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]
- #[cfg(debug_assertions)]
- #[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]
- #[cfg(debug_assertions)]
- #[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
deleted file mode 100644
index 369e10f9..00000000
--- a/src/tui/layout.rs
+++ /dev/null
@@ -1,537 +0,0 @@
-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
deleted file mode 100644
index 29fe84cd..00000000
--- a/src/tui/mod.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-//! 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
deleted file mode 100644
index 4aa19d4a..00000000
--- a/src/tui/style.rs
+++ /dev/null
@@ -1,278 +0,0 @@
-//! `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
deleted file mode 100644
index 040e77f6..00000000
--- a/src/tui/symbols.rs
+++ /dev/null
@@ -1,233 +0,0 @@
-pub mod block {
- pub const FULL: &str = "█";
- pub const SEVEN_EIGHTHS: &str = "▉";
- pub const THREE_QUARTERS: &str = "▊";
- pub const FIVE_EIGHTHS: &str = "▋";
- pub const HALF: &str = "▌";
- pub const THREE_EIGHTHS: &str = "▍";
- pub const ONE_QUARTER: &str = "▎";
- pub const ONE_EIGHTH: &str = "▏";
-
- #[derive(Debug, Clone)]
- pub struct Set {
- pub full: &'static str,
- pub seven_eighths: &'static str,
- pub three_quarters: &'static str,
- pub five_eighths: &'static str,
- pub half: &'static str,
- pub three_eighths: &'static str,
- pub one_quarter: &'static str,
- pub one_eighth: &'static str,
- pub empty: &'static str,
- }
-
- pub const THREE_LEVELS: Set = Set {
- full: FULL,
- seven_eighths: FULL,
- three_quarters: HALF,
- five_eighths: HALF,
- half: HALF,
- three_eighths: HALF,
- one_quarter: HALF,
- one_eighth: " ",
- empty: " ",
- };
-
- pub const NINE_LEVELS: Set = Set {
- full: FULL,
- seven_eighths: SEVEN_EIGHTHS,
- three_quarters: THREE_QUARTERS,
- five_eighths: FIVE_EIGHTHS,
- half: HALF,
- three_eighths: THREE_EIGHTHS,
- one_quarter: ONE_QUARTER,
- one_eighth: ONE_EIGHTH,
- empty: " ",
- };
-}
-
-pub mod bar {
- pub const FULL: &str = "█";
- pub const SEVEN_EIGHTHS: &str = "▇";
- pub const THREE_QUARTERS: &str = "▆";
- pub const FIVE_EIGHTHS: &str = "▅";
- pub const HALF: &str = "▄";
- pub const THREE_EIGHTHS: &str = "▃";
- pub const ONE_QUARTER: &str = "▂";
- pub const ONE_EIGHTH: &str = "▁";
-
- #[derive(Debug, Clone)]
- pub struct Set {
- pub full: &'static str,
- pub seven_eighths: &'static str,
- pub three_quarters: &'static str,
- pub five_eighths: &'static str,
- pub half: &'static str,
- pub three_eighths: &'static str,
- pub one_quarter: &'static str,
- pub one_eighth: &'static str,
- pub empty: &'static str,
- }
-
- pub const THREE_LEVELS: Set = Set {
- full: FULL,
- seven_eighths: FULL,
- three_quarters: HALF,
- five_eighths: HALF,
- half: HALF,
- three_eighths: HALF,
- one_quarter: HALF,
- one_eighth: " ",
- empty: " ",
- };
-
- pub const NINE_LEVELS: Set = Set {
- full: FULL,
- seven_eighths: SEVEN_EIGHTHS,
- three_quarters: THREE_QUARTERS,
- five_eighths: FIVE_EIGHTHS,
- half: HALF,
- three_eighths: THREE_EIGHTHS,
- one_quarter: ONE_QUARTER,
- one_eighth: ONE_EIGHTH,
- empty: " ",
- };
-}
-
-pub mod line {
- pub const VERTICAL: &str = "│";
- pub const DOUBLE_VERTICAL: &str = "║";
- pub const THICK_VERTICAL: &str = "┃";
-
- pub const HORIZONTAL: &str = "─";
- pub const DOUBLE_HORIZONTAL: &str = "═";
- pub const THICK_HORIZONTAL: &str = "━";
-
- pub const TOP_RIGHT: &str = "┐";
- pub const ROUNDED_TOP_RIGHT: &str = "╮";
- pub const DOUBLE_TOP_RIGHT: &str = "╗";
- pub const THICK_TOP_RIGHT: &str = "┓";
-
- pub const TOP_LEFT: &str = "┌";
- pub const ROUNDED_TOP_LEFT: &str = "╭";
- pub const DOUBLE_TOP_LEFT: &str = "╔";
- pub const THICK_TOP_LEFT: &str = "┏";
-
- pub const BOTTOM_RIGHT: &str = "┘";
- pub const ROUNDED_BOTTOM_RIGHT: &str = "╯";
- pub const DOUBLE_BOTTOM_RIGHT: &str = "╝";
- pub const THICK_BOTTOM_RIGHT: &str = "┛";
-
- pub const BOTTOM_LEFT: &str = "└";
- pub const ROUNDED_BOTTOM_LEFT: &str = "╰";
- pub const DOUBLE_BOTTOM_LEFT: &str = "╚";
- pub const THICK_BOTTOM_LEFT: &str = "┗";
-
- pub const VERTICAL_LEFT: &str = "┤";
- pub const DOUBLE_VERTICAL_LEFT: &str = "╣";
- pub const THICK_VERTICAL_LEFT: &str = "┫";
-
- pub const VERTICAL_RIGHT: &str = "├";
- pub const DOUBLE_VERTICAL_RIGHT: &str = "╠";
- pub const THICK_VERTICAL_RIGHT: &str = "┣";
-
- pub const HORIZONTAL_DOWN: &str = "┬";
- pub const DOUBLE_HORIZONTAL_DOWN: &str = "╦";
- pub const THICK_HORIZONTAL_DOWN: &str = "┳";
-
- pub const HORIZONTAL_UP: &str = "┴";
- pub const DOUBLE_HORIZONTAL_UP: &str = "╩";
- pub const THICK_HORIZONTAL_UP: &str = "┻";
-
- pub const CROSS: &str = "┼";
- pub const DOUBLE_CROSS: &str = "╬";
- pub const THICK_CROSS: &str = "╋";
-
- #[derive(Debug, Clone)]
- pub struct Set {
- pub vertical: &'static str,
- pub horizontal: &'static str,
- pub top_right: &'static str,
- pub top_left: &'static str,
- pub bottom_right: &'static str,
- pub bottom_left: &'static str,
- pub vertical_left: &'static str,
- pub vertical_right: &'static str,
- pub horizontal_down: &'static str,
- pub horizontal_up: &'static str,
- pub cross: &'static str,
- }
-
- pub const NORMAL: Set = Set {
- vertical: VERTICAL,
- horizontal: HORIZONTAL,
- top_right: TOP_RIGHT,
- top_left: TOP_LEFT,
- bottom_right: BOTTOM_RIGHT,
- bottom_left: BOTTOM_LEFT,
- vertical_left: VERTICAL_LEFT,
- vertical_right: VERTICAL_RIGHT,
- horizontal_down: HORIZONTAL_DOWN,
- horizontal_up: HORIZONTAL_UP,
- cross: CROSS,
- };
-
- pub const ROUNDED: Set = Set {
- top_right: ROUNDED_TOP_RIGHT,
- top_left: ROUNDED_TOP_LEFT,
- bottom_right: ROUNDED_BOTTOM_RIGHT,
- bottom_left: ROUNDED_BOTTOM_LEFT,
- ..NORMAL
- };
-
- pub const DOUBLE: Set = Set {
- vertical: DOUBLE_VERTICAL,
- horizontal: DOUBLE_HORIZONTAL,
- top_right: DOUBLE_TOP_RIGHT,
- top_left: DOUBLE_TOP_LEFT,
- bottom_right: DOUBLE_BOTTOM_RIGHT,
- bottom_left: DOUBLE_BOTTOM_LEFT,
- vertical_left: DOUBLE_VERTICAL_LEFT,
- vertical_right: DOUBLE_VERTICAL_RIGHT,
- horizontal_down: DOUBLE_HORIZONTAL_DOWN,
- horizontal_up: DOUBLE_HORIZONTAL_UP,
- cross: DOUBLE_CROSS,
- };
-
- pub const THICK: Set = Set {
- vertical: THICK_VERTICAL,
- horizontal: THICK_HORIZONTAL,
- top_right: THICK_TOP_RIGHT,
- top_left: THICK_TOP_LEFT,
- bottom_right: THICK_BOTTOM_RIGHT,
- bottom_left: THICK_BOTTOM_LEFT,
- vertical_left: THICK_VERTICAL_LEFT,
- vertical_right: THICK_VERTICAL_RIGHT,
- horizontal_down: THICK_HORIZONTAL_DOWN,
- horizontal_up: THICK_HORIZONTAL_UP,
- cross: THICK_CROSS,
- };
-}
-
-pub const DOT: &str = "•";
-
-pub mod braille {
- pub const BLANK: u16 = 0x2800;
- pub const DOTS: [[u16; 2]; 4] = [
- [0x0001, 0x0008],
- [0x0002, 0x0010],
- [0x0004, 0x0020],
- [0x0040, 0x0080],
- ];
-}
-
-/// Marker to use when plotting data points
-#[derive(Debug, Clone, Copy)]
-pub enum Marker {
- /// One point per cell in shape of dot
- Dot,
- /// One point per cell in shape of a block
- Block,
- /// Up to 8 points per cell
- Braille,
-}
diff --git a/src/tui/terminal.rs b/src/tui/terminal.rs
deleted file mode 100644
index e64690e9..00000000
--- a/src/tui/terminal.rs
+++ /dev/null
@@ -1,321 +0,0 @@
-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
deleted file mode 100644
index e8a5aa73..00000000
--- a/src/tui/text.rs
+++ /dev/null
@@ -1,428 +0,0 @@
-//! Primitives for styled text.
-//!
-//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
-//! those strings may be associated to a set of styles. `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
deleted file mode 100644
index eb59f9c3..00000000
--- a/src/tui/widgets/block.rs
+++ /dev/null
@@ -1,562 +0,0 @@
-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
deleted file mode 100644
index 635185e5..00000000
--- a/src/tui/widgets/mod.rs
+++ /dev/null
@@ -1,159 +0,0 @@
-//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
-//!
-//! All widgets are implemented using the builder pattern and are consumable objects. They are not
-//! meant to be stored but used as *commands* to draw common figures in the UI.
-//!
-//! The available widgets are:
-//! - [`Block`]
-//! - [`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
deleted file mode 100644
index a5036148..00000000
--- a/src/tui/widgets/paragraph.rs
+++ /dev/null
@@ -1,194 +0,0 @@
-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
deleted file mode 100644
index d174932b..00000000
--- a/src/tui/widgets/reflow.rs
+++ /dev/null
@@ -1,537 +0,0 @@
-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!"
- ]
- );
- }
-}