aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock29
-rw-r--r--Cargo.toml1
-rw-r--r--helix-term/Cargo.toml2
-rw-r--r--helix-term/src/compositor.rs2
-rw-r--r--helix-term/src/main.rs1
-rw-r--r--helix-tui/.gitignore6
-rw-r--r--helix-tui/Cargo.toml19
-rw-r--r--helix-tui/README.md6
-rw-r--r--helix-tui/src/backend/crossterm.rs221
-rw-r--r--helix-tui/src/backend/mod.rs25
-rw-r--r--helix-tui/src/backend/test.rs151
-rw-r--r--helix-tui/src/buffer.rs745
-rw-r--r--helix-tui/src/layout.rs534
-rw-r--r--helix-tui/src/lib.rs134
-rw-r--r--helix-tui/src/style.rs281
-rw-r--r--helix-tui/src/symbols.rs233
-rw-r--r--helix-tui/src/terminal.rs (renamed from helix-term/src/terminal.rs)7
-rw-r--r--helix-tui/src/text.rs434
-rw-r--r--helix-tui/src/widgets/block.rs511
-rw-r--r--helix-tui/src/widgets/clear.rs36
-rw-r--r--helix-tui/src/widgets/list.rs249
-rw-r--r--helix-tui/src/widgets/mod.rs53
-rw-r--r--helix-tui/src/widgets/paragraph.rs197
-rw-r--r--helix-tui/src/widgets/reflow.rs534
-rw-r--r--helix-tui/src/widgets/table.rs538
-rw-r--r--helix-tui/tests/terminal.rs36
-rw-r--r--helix-tui/tests/widgets_block.rs213
-rw-r--r--helix-tui/tests/widgets_list.rs88
-rw-r--r--helix-tui/tests/widgets_paragraph.rs220
-rw-r--r--helix-tui/tests/widgets_table.rs717
-rw-r--r--helix-view/Cargo.toml2
31 files changed, 6200 insertions, 25 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 228f3873..d1bb9374 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -330,6 +330,7 @@ dependencies = [
"fuzzy-matcher",
"helix-core",
"helix-lsp",
+ "helix-tui",
"helix-view",
"ignore",
"log",
@@ -340,7 +341,18 @@ dependencies = [
"serde_json",
"tokio",
"toml",
- "tui",
+]
+
+[[package]]
+name = "helix-tui"
+version = "0.1.0"
+dependencies = [
+ "bitflags",
+ "cassowary",
+ "crossterm",
+ "serde",
+ "unicode-segmentation",
+ "unicode-width",
]
[[package]]
@@ -351,12 +363,12 @@ dependencies = [
"crossterm",
"helix-core",
"helix-lsp",
+ "helix-tui",
"once_cell",
"serde",
"slotmap",
"tokio",
"toml",
- "tui",
"url",
]
@@ -956,19 +968,6 @@ dependencies = [
]
[[package]]
-name = "tui"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "861d8f3ad314ede6219bcb2ab844054b1de279ee37a9bc38e3d606f9d3fb2a71"
-dependencies = [
- "bitflags",
- "cassowary",
- "crossterm",
- "unicode-segmentation",
- "unicode-width",
-]
-
-[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 7511688e..22d29260 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,7 @@ members = [
"helix-core",
"helix-view",
"helix-term",
+ "helix-tui",
"helix-syntax",
"helix-lsp",
]
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index a86549a7..972d5b73 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -21,7 +21,7 @@ once_cell = "1.4"
tokio = { version = "1", features = ["full"] }
num_cpus = "1"
-tui = { version = "0.15", default-features = false, features = ["crossterm"] }
+tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
crossterm = { version = "0.19", features = ["event-stream"] }
clap = { version = "3.0.0-beta.2 ", default-features = false, features = ["std", "cargo"] }
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 4570cab9..7b828358 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -67,7 +67,7 @@ pub trait Component: Any + AnyComponent {
use anyhow::Error;
use std::io::stdout;
use tui::backend::CrosstermBackend;
-type Terminal = crate::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
+type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
pub struct Compositor {
layers: Vec<Box<dyn Component>>,
diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs
index b0d244b7..386c2333 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -4,7 +4,6 @@ mod application;
mod commands;
mod compositor;
mod keymap;
-mod terminal;
mod ui;
use application::Application;
diff --git a/helix-tui/.gitignore b/helix-tui/.gitignore
new file mode 100644
index 00000000..dcb33fbb
--- /dev/null
+++ b/helix-tui/.gitignore
@@ -0,0 +1,6 @@
+target
+Cargo.lock
+*.log
+*.rs.rustfmt
+.gdb_history
+.idea/
diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml
new file mode 100644
index 00000000..badefe4c
--- /dev/null
+++ b/helix-tui/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "helix-tui"
+version = "0.1.0"
+authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
+description = """
+A library to build rich terminal user interfaces or dashboards
+"""
+edition = "2018"
+
+[features]
+default = ["crossterm"]
+
+[dependencies]
+bitflags = "1.0"
+cassowary = "0.3"
+unicode-segmentation = "1.2"
+unicode-width = "0.1"
+crossterm = { version = "0.19", optional = true }
+serde = { version = "1", "optional" = true, features = ["derive"]}
diff --git a/helix-tui/README.md b/helix-tui/README.md
new file mode 100644
index 00000000..97b3d1d9
--- /dev/null
+++ b/helix-tui/README.md
@@ -0,0 +1,6 @@
+# helix-tui
+
+This library is a fork of the great library
+[tui-rs](https://github.com/fdehau/tui-rs/). We've mainly relied on the double
+buffer implementation and render diffing, side-stepping it's widget and
+layouting.
diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs
new file mode 100644
index 00000000..f6703e14
--- /dev/null
+++ b/helix-tui/src/backend/crossterm.rs
@@ -0,0 +1,221 @@
+use crate::{
+ 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/helix-tui/src/backend/mod.rs b/helix-tui/src/backend/mod.rs
new file mode 100644
index 00000000..ed125200
--- /dev/null
+++ b/helix-tui/src/backend/mod.rs
@@ -0,0 +1,25 @@
+use std::io;
+
+use crate::buffer::Cell;
+use crate::layout::Rect;
+
+#[cfg(feature = "crossterm")]
+mod crossterm;
+#[cfg(feature = "crossterm")]
+pub use self::crossterm::CrosstermBackend;
+
+mod test;
+pub use self::test::TestBackend;
+
+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/helix-tui/src/backend/test.rs b/helix-tui/src/backend/test.rs
new file mode 100644
index 00000000..46b37261
--- /dev/null
+++ b/helix-tui/src/backend/test.rs
@@ -0,0 +1,151 @@
+use crate::{
+ backend::Backend,
+ buffer::{Buffer, Cell},
+ layout::Rect,
+};
+use std::{fmt::Write, io};
+use unicode_width::UnicodeWidthStr;
+
+/// A backend used for the integration tests.
+#[derive(Debug)]
+pub struct TestBackend {
+ width: u16,
+ buffer: Buffer,
+ height: u16,
+ cursor: bool,
+ pos: (u16, u16),
+}
+
+/// Returns a string representation of the given buffer for debugging purpose.
+fn buffer_view(buffer: &Buffer) -> String {
+ let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
+ for cells in buffer.content.chunks(buffer.area.width as usize) {
+ let mut overwritten = vec![];
+ let mut skip: usize = 0;
+ view.push('"');
+ for (x, c) in cells.iter().enumerate() {
+ if skip == 0 {
+ view.push_str(&c.symbol);
+ } else {
+ overwritten.push((x, &c.symbol))
+ }
+ skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
+ }
+ view.push('"');
+ if !overwritten.is_empty() {
+ write!(
+ &mut view,
+ " Hidden by multi-width symbols: {:?}",
+ overwritten
+ )
+ .unwrap();
+ }
+ view.push('\n');
+ }
+ view
+}
+
+impl TestBackend {
+ pub fn new(width: u16, height: u16) -> TestBackend {
+ TestBackend {
+ width,
+ height,
+ buffer: Buffer::empty(Rect::new(0, 0, width, height)),
+ cursor: false,
+ pos: (0, 0),
+ }
+ }
+
+ pub fn buffer(&self) -> &Buffer {
+ &self.buffer
+ }
+
+ pub fn resize(&mut self, width: u16, height: u16) {
+ self.buffer.resize(Rect::new(0, 0, width, height));
+ self.width = width;
+ self.height = height;
+ }
+
+ pub fn assert_buffer(&self, expected: &Buffer) {
+ assert_eq!(expected.area, self.buffer.area);
+ let diff = expected.diff(&self.buffer);
+ if diff.is_empty() {
+ return;
+ }
+
+ let mut debug_info = String::from("Buffers are not equal");
+ debug_info.push('\n');
+ debug_info.push_str("Expected:");
+ debug_info.push('\n');
+ let expected_view = buffer_view(expected);
+ debug_info.push_str(&expected_view);
+ debug_info.push('\n');
+ debug_info.push_str("Got:");
+ debug_info.push('\n');
+ let view = buffer_view(&self.buffer);
+ debug_info.push_str(&view);
+ debug_info.push('\n');
+
+ debug_info.push_str("Diff:");
+ debug_info.push('\n');
+ let nice_diff = diff
+ .iter()
+ .enumerate()
+ .map(|(i, (x, y, cell))| {
+ let expected_cell = expected.get(*x, *y);
+ format!(
+ "{}: at ({}, {}) expected {:?} got {:?}",
+ i, x, y, expected_cell, cell
+ )
+ })
+ .collect::<Vec<String>>()
+ .join("\n");
+ debug_info.push_str(&nice_diff);
+ panic!("{}", debug_info);
+ }
+}
+
+impl Backend for TestBackend {
+ fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
+ where
+ I: Iterator<Item = (u16, u16, &'a Cell)>,
+ {
+ for (x, y, c) in content {
+ let cell = self.buffer.get_mut(x, y);
+ *cell = c.clone();
+ }
+ Ok(())
+ }
+
+ fn hide_cursor(&mut self) -> Result<(), io::Error> {
+ self.cursor = false;
+ Ok(())
+ }
+
+ fn show_cursor(&mut self) -> Result<(), io::Error> {
+ self.cursor = true;
+ Ok(())
+ }
+
+ fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
+ Ok(self.pos)
+ }
+
+ fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
+ self.pos = (x, y);
+ Ok(())
+ }
+
+ fn clear(&mut self) -> Result<(), io::Error> {
+ self.buffer.reset();
+ Ok(())
+ }
+
+ fn size(&self) -> Result<Rect, io::Error> {
+ Ok(Rect::new(0, 0, self.width, self.height))
+ }
+
+ fn flush(&mut self) -> Result<(), io::Error> {
+ Ok(())
+ }
+}
diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs
new file mode 100644
index 00000000..8a227ce7
--- /dev/null
+++ b/helix-tui/src/buffer.rs
@@ -0,0 +1,745 @@
+use crate::{
+ 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)]
+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 helix_tui::buffer::{Buffer, Cell};
+/// use helix_tui::layout::Rect;
+/// use helix_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)]
+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 Default for Buffer {
+ fn default() -> Buffer {
+ Buffer {
+ area: Default::default(),
+ content: Vec::new(),
+ }
+ }
+}
+
+impl Buffer {
+ /// Returns a Buffer with all cells set to the default one
+ pub fn empty(area: Rect) -> Buffer {
+ let cell: Cell = Default::default();
+ Buffer::filled(area, &cell)
+ }
+
+ /// Returns a Buffer with all cells initialized with the attributes of the given Cell
+ pub fn filled(area: Rect, cell: &Cell) -> Buffer {
+ let size = area.area() as usize;
+ let mut content = Vec::with_capacity(size);
+ for _ in 0..size {
+ content.push(cell.clone());
+ }
+ Buffer { area, content }
+ }
+
+ /// Returns a Buffer containing the given lines
+ pub fn with_lines<S>(lines: Vec<S>) -> Buffer
+ where
+ S: AsRef<str>,
+ {
+ let height = lines.len() as u16;
+ let width = lines
+ .iter()
+ .map(|i| i.as_ref().width() as u16)
+ .max()
+ .unwrap_or_default();
+ let mut buffer = Buffer::empty(Rect {
+ x: 0,
+ y: 0,
+ width,
+ height,
+ });
+ for (y, line) in lines.iter().enumerate() {
+ buffer.set_string(0, y as u16, line, Style::default());
+ }
+ buffer
+ }
+
+ /// Returns the content of the buffer as a slice
+ pub fn content(&self) -> &[Cell] {
+ &self.content
+ }
+
+ /// Returns the area covered by this buffer
+ pub fn area(&self) -> &Rect {
+ &self.area
+ }
+
+ /// Returns a reference to Cell at the given coordinates
+ pub fn get(&self, x: u16, y: u16) -> &Cell {
+ let i = self.index_of(x, y);
+ &self.content[i]
+ }
+
+ /// Returns a mutable reference to Cell at the given coordinates
+ pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
+ let i = self.index_of(x, y);
+ &mut self.content[i]
+ }
+
+ /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
+ ///
+ /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use helix_tui::buffer::Buffer;
+ /// # use helix_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 helix_tui::buffer::Buffer;
+ /// # use helix_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 helix_tui::buffer::Buffer;
+ /// # use helix_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 helix_tui::buffer::Buffer;
+ /// # use helix_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<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, 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<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) {
+ self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
+ }
+
+ #[deprecated(
+ since = "0.10.0",
+ note = "You should use styling capabilities of `Buffer::set_style`"
+ )]
+ pub fn set_background(&mut self, area: Rect, color: Color) {
+ for y in area.top()..area.bottom() {
+ for x in area.left()..area.right() {
+ self.get_mut(x, y).set_bg(color);
+ }
+ }
+ }
+
+ pub fn set_style(&mut self, area: Rect, style: Style) {
+ for y in area.top()..area.bottom() {
+ for x in area.left()..area.right() {
+ self.get_mut(x, y).set_style(style);
+ }
+ }
+ }
+
+ /// Resize the buffer so that the mapped area matches the given area and that the buffer
+ /// length is equal to area.width * area.height
+ pub fn resize(&mut self, area: Rect) {
+ let length = area.area() as usize;
+ if self.content.len() > length {
+ self.content.truncate(length);
+ } else {
+ self.content.resize(length, Default::default());
+ }
+ self.area = area;
+ }
+
+ /// Reset all cells in the buffer
+ pub fn reset(&mut self) {
+ for c in &mut self.content {
+ c.reset();
+ }
+ }
+
+ /// Merge an other buffer into this one
+ pub fn merge(&mut self, other: &Buffer) {
+ let area = self.area.union(other.area);
+ let cell: Cell = Default::default();
+ self.content.resize(area.area() as usize, cell.clone());
+
+ // Move original content to the appropriate space
+ let size = self.area.area() as usize;
+ for i in (0..size).rev() {
+ let (x, y) = self.pos_of(i);
+ // New index in content
+ let k = ((y - area.y) * area.width + x - area.x) as usize;
+ if i != k {
+ self.content[k] = self.content[i].clone();
+ self.content[i] = cell.clone();
+ }
+ }
+
+ // Push content of the other buffer into this one (may erase previous
+ // data)
+ let size = other.area.area() as usize;
+ for i in 0..size {
+ let (x, y) = other.pos_of(i);
+ // New index in content
+ let k = ((y - area.y) * area.width + x - area.x) as usize;
+ self.content[k] = other.content[i].clone();
+ }
+ self.area = area;
+ }
+
+ /// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
+ /// self to other.
+ ///
+ /// We're assuming that buffers are well-formed, that is no double-width cell is followed by
+ /// a non-blank cell.
+ ///
+ /// # Multi-width characters handling:
+ ///
+ /// ```text
+ /// (Index:) `01`
+ /// Prev: `コ`
+ /// Next: `aa`
+ /// Updates: `0: a, 1: a'
+ /// ```
+ ///
+ /// ```text
+ /// (Index:) `01`
+ /// Prev: `a `
+ /// Next: `コ`
+ /// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
+ /// ```
+ ///
+ /// ```text
+ /// (Index:) `012`
+ /// Prev: `aaa`
+ /// Next: `aコ`
+ /// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
+ /// ```
+ pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
+ let previous_buffer = &self.content;
+ let next_buffer = &other.content;
+ let width = self.area.width;
+
+ let mut updates: Vec<(u16, u16, &Cell)> = vec![];
+ // Cells invalidated by drawing/replacing preceeding multi-width characters:
+ let mut invalidated: usize = 0;
+ // Cells from the current buffer to skip due to preceeding multi-width characters taking their
+ // place (the skipped cells should be blank anyway):
+ let mut to_skip: usize = 0;
+ for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
+ if (current != previous || invalidated > 0) && to_skip == 0 {
+ let x = i as u16 % width;
+ let y = i as u16 / width;
+ updates.push((x, y, &next_buffer[i]));
+ }
+
+ to_skip = current.symbol.width().saturating_sub(1);
+
+ let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
+ invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
+ }
+ updates
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn cell(s: &str) -> Cell {
+ let mut cell = Cell::default();
+ cell.set_symbol(s);
+ cell
+ }
+
+ #[test]
+ fn it_translates_to_and_from_coordinates() {
+ let rect = Rect::new(200, 100, 50, 80);
+ let buf = Buffer::empty(rect);
+
+ // First cell is at the upper left corner.
+ assert_eq!(buf.pos_of(0), (200, 100));
+ assert_eq!(buf.index_of(200, 100), 0);
+
+ // Last cell is in the lower right.
+ assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
+ assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
+ }
+
+ #[test]
+ #[should_panic(expected = "outside the buffer")]
+ fn pos_of_panics_on_out_of_bounds() {
+ let rect = Rect::new(0, 0, 10, 10);
+ let buf = Buffer::empty(rect);
+
+ // There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
+ buf.pos_of(100);
+ }
+
+ #[test]
+ #[should_panic(expected = "outside the buffer")]
+ fn index_of_panics_on_out_of_bounds() {
+ let rect = Rect::new(0, 0, 10, 10);
+ let buf = Buffer::empty(rect);
+
+ // width is 10; zero-indexed means that 10 would be the 11th cell.
+ buf.index_of(10, 0);
+ }
+
+ #[test]
+ fn buffer_set_string() {
+ let area = Rect::new(0, 0, 5, 1);
+ let mut buffer = Buffer::empty(area);
+
+ // Zero-width
+ buffer.set_stringn(0, 0, "aaa", 0, Style::default());
+ assert_eq!(buffer, Buffer::with_lines(vec![" "]));
+
+ buffer.set_string(0, 0, "aaa", Style::default());
+ assert_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
+
+ // Width limit:
+ buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
+ assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
+
+ buffer.set_string(0, 0, "12345", Style::default());
+ assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
+
+ // Width truncation:
+ buffer.set_string(0, 0, "123456", Style::default());
+ assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
+ }
+
+ #[test]
+ fn buffer_set_string_zero_width() {
+ let area = Rect::new(0, 0, 1, 1);
+ let mut buffer = Buffer::empty(area);
+
+ // Leading grapheme with zero width
+ let s = "\u{1}a";
+ buffer.set_stringn(0, 0, s, 1, Style::default());
+ assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
+
+ // Trailing grapheme with zero with
+ let s = "a\u{1}";
+ buffer.set_stringn(0, 0, s, 1, Style::default());
+ assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
+ }
+
+ #[test]
+ fn buffer_set_string_double_width() {
+ let area = Rect::new(0, 0, 5, 1);
+ let mut buffer = Buffer::empty(area);
+ buffer.set_string(0, 0, "コン", Style::default());
+ assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
+
+ // Only 1 space left.
+ buffer.set_string(0, 0, "コンピ", Style::default());
+ assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
+ }
+
+ #[test]
+ fn buffer_with_lines() {
+ let buffer =
+ Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
+ assert_eq!(buffer.area.x, 0);
+ assert_eq!(buffer.area.y, 0);
+ assert_eq!(buffer.area.width, 10);
+ assert_eq!(buffer.area.height, 4);
+ }
+
+ #[test]
+ fn buffer_diffing_empty_empty() {
+ let area = Rect::new(0, 0, 40, 40);
+ let prev = Buffer::empty(area);
+ let next = Buffer::empty(area);
+ let diff = prev.diff(&next);
+ assert_eq!(diff, vec![]);
+ }
+
+ #[test]
+ fn buffer_diffing_empty_filled() {
+ let area = Rect::new(0, 0, 40, 40);
+ let prev = Buffer::empty(area);
+ let next = Buffer::filled(area, Cell::default().set_symbol("a"));
+ let diff = prev.diff(&next);
+ assert_eq!(diff.len(), 40 * 40);
+ }
+
+ #[test]
+ fn buffer_diffing_filled_filled() {
+ let area = Rect::new(0, 0, 40, 40);
+ let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
+ let next = Buffer::filled(area, Cell::default().set_symbol("a"));
+ let diff = prev.diff(&next);
+ assert_eq!(diff, vec![]);
+ }
+
+ #[test]
+ fn buffer_diffing_single_width() {
+ let prev = Buffer::with_lines(vec![
+ " ",
+ "┌Title─┐ ",
+ "│ │ ",
+ "│ │ ",
+ "└──────┘ ",
+ ]);
+ let next = Buffer::with_lines(vec![
+ " ",
+ "┌TITLE─┐ ",
+ "│ │ ",
+ "│ │ ",
+ "└──────┘ ",
+ ]);
+ let diff = prev.diff(&next);
+ assert_eq!(
+ diff,
+ vec![
+ (2, 1, &cell("I")),
+ (3, 1, &cell("T")),
+ (4, 1, &cell("L")),
+ (5, 1, &cell("E")),
+ ]
+ );
+ }
+
+ #[test]
+ #[rustfmt::skip]
+ fn buffer_diffing_multi_width() {
+ let prev = Buffer::with_lines(vec![
+ "┌Title─┐ ",
+ "└──────┘ ",
+ ]);
+ let next = Buffer::with_lines(vec![
+ "┌称号──┐ ",
+ "└──────┘ ",
+ ]);
+ let diff = prev.diff(&next);
+ assert_eq!(
+ diff,
+ vec![
+ (1, 0, &cell("称")),
+ // Skipped "i"
+ (3, 0, &cell("号")),
+ // Skipped "l"
+ (5, 0, &cell("─")),
+ ]
+ );
+ }
+
+ #[test]
+ fn buffer_diffing_multi_width_offset() {
+ let prev = Buffer::with_lines(vec!["┌称号──┐"]);
+ let next = Buffer::with_lines(vec!["┌─称号─┐"]);
+
+ let diff = prev.diff(&next);
+ assert_eq!(
+ diff,
+ vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
+ );
+ }
+
+ #[test]
+ fn buffer_merge() {
+ let mut one = Buffer::filled(
+ Rect {
+ x: 0,
+ y: 0,
+ width: 2,
+ height: 2,
+ },
+ Cell::default().set_symbol("1"),
+ );
+ let two = Buffer::filled(
+ Rect {
+ x: 0,
+ y: 2,
+ width: 2,
+ height: 2,
+ },
+ Cell::default().set_symbol("2"),
+ );
+ one.merge(&two);
+ assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
+ }
+
+ #[test]
+ fn buffer_merge2() {
+ let mut one = Buffer::filled(
+ Rect {
+ x: 2,
+ y: 2,
+ width: 2,
+ height: 2,
+ },
+ Cell::default().set_symbol("1"),
+ );
+ let two = Buffer::filled(
+ Rect {
+ x: 0,
+ y: 0,
+ width: 2,
+ height: 2,
+ },
+ Cell::default().set_symbol("2"),
+ );
+ one.merge(&two);
+ assert_eq!(
+ one,
+ Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
+ );
+ }
+
+ #[test]
+ fn buffer_merge3() {
+ let mut one = Buffer::filled(
+ Rect {
+ x: 3,
+ y: 3,
+ width: 2,
+ height: 2,
+ },
+ Cell::default().set_symbol("1"),
+ );
+ let two = Buffer::filled(
+ Rect {
+ x: 1,
+ y: 1,
+ width: 3,
+ height: 4,
+ },
+ Cell::default().set_symbol("2"),
+ );
+ one.merge(&two);
+ let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
+ merged.area = Rect {
+ x: 1,
+ y: 1,
+ width: 4,
+ height: 4,
+ };
+ assert_eq!(one, merged);
+ }
+}
diff --git a/helix-tui/src/layout.rs b/helix-tui/src/layout.rs
new file mode 100644
index 00000000..5248f7bf
--- /dev/null
+++ b/helix-tui/src/layout.rs
@@ -0,0 +1,534 @@
+use std::cell::RefCell;
+use std::cmp::{max, min};
+use std::collections::HashMap;
+
+use cassowary::strength::{REQUIRED, WEAK};
+use cassowary::WeightedRelation::*;
+use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
+
+#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
+pub enum Corner {
+ TopLeft,
+ TopRight,
+ BottomRight,
+ BottomLeft,
+}
+
+#[derive(Debug, Hash, Clone, PartialEq, Eq)]
+pub enum Direction {
+ Horizontal,
+ Vertical,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Constraint {
+ // TODO: enforce range 0 - 100
+ Percentage(u16),
+ Ratio(u32, u32),
+ Length(u16),
+ Max(u16),
+ Min(u16),
+}
+
+impl Constraint {
+ pub fn apply(&self, length: u16) -> u16 {
+ match *self {
+ Constraint::Percentage(p) => length * p / 100,
+ Constraint::Ratio(num, den) => {
+ let r = num * u32::from(length) / den;
+ r as u16
+ }
+ Constraint::Length(l) => length.min(l),
+ Constraint::Max(m) => length.min(m),
+ Constraint::Min(m) => length.max(m),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Margin {
+ pub vertical: u16,
+ pub horizontal: u16,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Alignment {
+ Left,
+ Center,
+ Right,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Layout {
+ direction: Direction,
+ margin: Margin,
+ constraints: Vec<Constraint>,
+}
+
+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(),
+ }
+ }
+}
+
+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
+ }
+
+ /// 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 helix_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()
+ })
+ }
+}
+
+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 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;
+ }
+ _ => {}
+ }
+ }
+
+ // 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 an hint about the
+/// area they are supposed to render to.
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub struct Rect {
+ pub x: u16,
+ pub y: u16,
+ pub width: u16,
+ pub height: u16,
+}
+
+impl Default for Rect {
+ fn default() -> Rect {
+ Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ }
+ }
+}
+
+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/helix-tui/src/lib.rs b/helix-tui/src/lib.rs
new file mode 100644
index 00000000..5f59fd3d
--- /dev/null
+++ b/helix-tui/src/lib.rs
@@ -0,0 +1,134 @@
+//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
+//! terminal users interfaces and dashboards.
+//!
+//! ![](https://raw.githubusercontent.com/fdehau/tui-rs/master/assets/demo.gif)
+//!
+//! # Get started
+//!
+//! ## Adding `tui` as a dependency
+//!
+//! ```toml
+//! [dependencies]
+//! tui = "0.15"
+//! crossterm = "0.19"
+//! ```
+//!
+//! The same logic applies for all other available backends.
+//!
+//! ## Creating a `Terminal`
+//!
+//! Every application using `tui` should start by instantiating a `Terminal`. It is a light
+//! abstraction over available backends that provides basic functionalities such as clearing the
+//! screen, hiding the cursor, etc.
+//!
+//! ```rust,no_run
+//! use std::io;
+//! use helix_tui::Terminal;
+//! use helix_tui::backend::CrosstermBackend;
+//!
+//! fn main() -> Result<(), io::Error> {
+//! let stdout = io::stdout();
+//! let backend = CrosstermBackend::new(stdout);
+//! let mut terminal = Terminal::new(backend)?;
+//! Ok(())
+//! }
+//! ```
+//!
+//! You may also refer to the examples to find out how to create a `Terminal` for each available
+//! backend.
+//!
+//! ## Building a User Interface (UI)
+//!
+//! Every component of your interface will be implementing the `Widget` trait. The library comes
+//! with a predefined set of widgets that should meet most of your use cases. You are also free to
+//! implement your own.
+//!
+//! Each widget follows a builder pattern API providing a default configuration along with methods
+//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take
+//! your widget instance an area to draw to.
+//!
+//! The following example renders a block of the size of the terminal:
+//!
+//! ```rust,no_run
+//! use std::io;
+//! use crossterm::terminal;
+//! use helix_tui::Terminal;
+//! use helix_tui::backend::CrosstermBackend;
+//! use helix_tui::widgets::{Widget, Block, Borders};
+//! use helix_tui::layout::{Layout, Constraint, Direction};
+//!
+//! fn main() -> Result<(), io::Error> {
+//! terminal::enable_raw_mode()?;
+//! let stdout = io::stdout();
+//! let backend = CrosstermBackend::new(stdout);
+//! let mut terminal = Terminal::new(backend)?;
+//! terminal.draw(|f| {
+//! let size = f.size();
+//! let block = Block::default()
+//! .title("Block")
+//! .borders(Borders::ALL);
+//! f.render_widget(block, size);
+//! })?;
+//! Ok(())
+//! }
+//! ```
+//!
+//! ## Layout
+//!
+//! The library comes with a basic yet useful layout management object called `Layout`. As you may
+//! see below and in the examples, the library makes heavy use of the builder pattern to provide
+//! full customization. And `Layout` is no exception:
+//!
+//! ```rust,no_run
+//! use std::io;
+//! use crossterm::terminal;
+//! use helix_tui::Terminal;
+//! use helix_tui::backend::CrosstermBackend;
+//! use helix_tui::widgets::{Widget, Block, Borders};
+//! use helix_tui::layout::{Layout, Constraint, Direction};
+//!
+//! fn main() -> Result<(), io::Error> {
+//! terminal::enable_raw_mode()?;
+//! let stdout = io::stdout();
+//! let backend = CrosstermBackend::new(stdout);
+//! let mut terminal = Terminal::new(backend)?;
+//! terminal.draw(|f| {
+//! let chunks = Layout::default()
+//! .direction(Direction::Vertical)
+//! .margin(1)
+//! .constraints(
+//! [
+//! Constraint::Percentage(10),
+//! Constraint::Percentage(80),
+//! Constraint::Percentage(10)
+//! ].as_ref()
+//! )
+//! .split(f.size());
+//! let block = Block::default()
+//! .title("Block")
+//! .borders(Borders::ALL);
+//! f.render_widget(block, chunks[0]);
+//! let block = Block::default()
+//! .title("Block 2")
+//! .borders(Borders::ALL);
+//! f.render_widget(block, chunks[1]);
+//! })?;
+//! Ok(())
+//! }
+//! ```
+//!
+//! This let you describe responsive terminal UI by nesting layouts. You should note that by
+//! default the computed layout tries to fill the available space completely. So if for any reason
+//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
+//! corresponding area.
+
+pub mod backend;
+pub mod buffer;
+pub mod layout;
+pub mod style;
+pub mod symbols;
+pub mod terminal;
+pub mod text;
+pub mod widgets;
+
+pub use self::terminal::{Terminal, TerminalOptions, Viewport};
diff --git a/helix-tui/src/style.rs b/helix-tui/src/style.rs
new file mode 100644
index 00000000..f322d304
--- /dev/null
+++ b/helix-tui/src/style.rs
@@ -0,0 +1,281 @@
+//! `style` contains the primitives used to control how your user interface will look.
+
+use bitflags::bitflags;
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub enum Color {
+ Reset,
+ Black,
+ Red,
+ Green,
+ Yellow,
+ Blue,
+ Magenta,
+ Cyan,
+ Gray,
+ DarkGray,
+ LightRed,
+ LightGreen,
+ LightYellow,
+ LightBlue,
+ LightMagenta,
+ LightCyan,
+ White,
+ Rgb(u8, u8, u8),
+ Indexed(u8),
+}
+
+bitflags! {
+ /// Modifier changes the way a piece of text is displayed.
+ ///
+ /// They are bitflags so they can easily be composed.
+ ///
+ /// ## Examples
+ ///
+ /// ```rust
+ /// # use helix_tui::style::Modifier;
+ ///
+ /// let m = Modifier::BOLD | Modifier::ITALIC;
+ /// ```
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ pub struct Modifier: u16 {
+ const BOLD = 0b0000_0000_0001;
+ const DIM = 0b0000_0000_0010;
+ const ITALIC = 0b0000_0000_0100;
+ const UNDERLINED = 0b0000_0000_1000;
+ const SLOW_BLINK = 0b0000_0001_0000;
+ const RAPID_BLINK = 0b0000_0010_0000;
+ const REVERSED = 0b0000_0100_0000;
+ const HIDDEN = 0b0000_1000_0000;
+ const CROSSED_OUT = 0b0001_0000_0000;
+ }
+}
+
+/// Style let you control the main characteristics of the displayed elements.
+///
+/// ```rust
+/// # use helix_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 helix_tui::style::{Color, Modifier, Style};
+/// # use helix_tui::buffer::Buffer;
+/// # use helix_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 helix_tui::style::{Color, Modifier, Style};
+/// # use helix_tui::buffer::Buffer;
+/// # use helix_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)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct Style {
+ pub fg: Option<Color>,
+ pub bg: Option<Color>,
+ pub add_modifier: Modifier,
+ pub sub_modifier: Modifier,
+}
+
+impl Default for Style {
+ fn default() -> Style {
+ Style {
+ fg: None,
+ bg: None,
+ add_modifier: Modifier::empty(),
+ sub_modifier: Modifier::empty(),
+ }
+ }
+}
+
+impl Style {
+ /// Returns a `Style` resetting all properties.
+ pub fn reset() -> Style {
+ Style {
+ fg: Some(Color::Reset),
+ bg: Some(Color::Reset),
+ add_modifier: Modifier::empty(),
+ sub_modifier: Modifier::all(),
+ }
+ }
+
+ /// Changes the foreground color.
+ ///
+ /// ## Examples
+ ///
+ /// ```rust
+ /// # use helix_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 helix_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 helix_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 helix_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 helix_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/helix-tui/src/symbols.rs b/helix-tui/src/symbols.rs
new file mode 100644
index 00000000..040e77f6
--- /dev/null
+++ b/helix-tui/src/symbols.rs
@@ -0,0 +1,233 @@
+pub mod block {
+ pub const FULL: &str = "█";
+ pub const SEVEN_EIGHTHS: &str = "▉";
+ pub const THREE_QUARTERS: &str = "▊";
+ pub const FIVE_EIGHTHS: &str = "▋";
+ pub const HALF: &str = "▌";
+ pub const THREE_EIGHTHS: &str = "▍";
+ pub const ONE_QUARTER: &str = "▎";
+ pub const ONE_EIGHTH: &str = "▏";
+
+ #[derive(Debug, Clone)]
+ pub struct Set {
+ pub full: &'static str,
+ pub seven_eighths: &'static str,
+ pub three_quarters: &'static str,
+ pub five_eighths: &'static str,
+ pub half: &'static str,
+ pub three_eighths: &'static str,
+ pub one_quarter: &'static str,
+ pub one_eighth: &'static str,
+ pub empty: &'static str,
+ }
+
+ pub const THREE_LEVELS: Set = Set {
+ full: FULL,
+ seven_eighths: FULL,
+ three_quarters: HALF,
+ five_eighths: HALF,
+ half: HALF,
+ three_eighths: HALF,
+ one_quarter: HALF,
+ one_eighth: " ",
+ empty: " ",
+ };
+
+ pub const NINE_LEVELS: Set = Set {
+ full: FULL,
+ seven_eighths: SEVEN_EIGHTHS,
+ three_quarters: THREE_QUARTERS,
+ five_eighths: FIVE_EIGHTHS,
+ half: HALF,
+ three_eighths: THREE_EIGHTHS,
+ one_quarter: ONE_QUARTER,
+ one_eighth: ONE_EIGHTH,
+ empty: " ",
+ };
+}
+
+pub mod bar {
+ pub const FULL: &str = "█";
+ pub const SEVEN_EIGHTHS: &str = "▇";
+ pub const THREE_QUARTERS: &str = "▆";
+ pub const FIVE_EIGHTHS: &str = "▅";
+ pub const HALF: &str = "▄";
+ pub const THREE_EIGHTHS: &str = "▃";
+ pub const ONE_QUARTER: &str = "▂";
+ pub const ONE_EIGHTH: &str = "▁";
+
+ #[derive(Debug, Clone)]
+ pub struct Set {
+ pub full: &'static str,
+ pub seven_eighths: &'static str,
+ pub three_quarters: &'static str,
+ pub five_eighths: &'static str,
+ pub half: &'static str,
+ pub three_eighths: &'static str,
+ pub one_quarter: &'static str,
+ pub one_eighth: &'static str,
+ pub empty: &'static str,
+ }
+
+ pub const THREE_LEVELS: Set = Set {
+ full: FULL,
+ seven_eighths: FULL,
+ three_quarters: HALF,
+ five_eighths: HALF,
+ half: HALF,
+ three_eighths: HALF,
+ one_quarter: HALF,
+ one_eighth: " ",
+ empty: " ",
+ };
+
+ pub const NINE_LEVELS: Set = Set {
+ full: FULL,
+ seven_eighths: SEVEN_EIGHTHS,
+ three_quarters: THREE_QUARTERS,
+ five_eighths: FIVE_EIGHTHS,
+ half: HALF,
+ three_eighths: THREE_EIGHTHS,
+ one_quarter: ONE_QUARTER,
+ one_eighth: ONE_EIGHTH,
+ empty: " ",
+ };
+}
+
+pub mod line {
+ pub const VERTICAL: &str = "│";
+ pub const DOUBLE_VERTICAL: &str = "║";
+ pub const THICK_VERTICAL: &str = "┃";
+
+ pub const HORIZONTAL: &str = "─";
+ pub const DOUBLE_HORIZONTAL: &str = "═";
+ pub const THICK_HORIZONTAL: &str = "━";
+
+ pub const TOP_RIGHT: &str = "┐";
+ pub const ROUNDED_TOP_RIGHT: &str = "╮";
+ pub const DOUBLE_TOP_RIGHT: &str = "╗";
+ pub const THICK_TOP_RIGHT: &str = "┓";
+
+ pub const TOP_LEFT: &str = "┌";
+ pub const ROUNDED_TOP_LEFT: &str = "╭";
+ pub const DOUBLE_TOP_LEFT: &str = "╔";
+ pub const THICK_TOP_LEFT: &str = "┏";
+
+ pub const BOTTOM_RIGHT: &str = "┘";
+ pub const ROUNDED_BOTTOM_RIGHT: &str = "╯";
+ pub const DOUBLE_BOTTOM_RIGHT: &str = "╝";
+ pub const THICK_BOTTOM_RIGHT: &str = "┛";
+
+ pub const BOTTOM_LEFT: &str = "└";
+ pub const ROUNDED_BOTTOM_LEFT: &str = "╰";
+ pub const DOUBLE_BOTTOM_LEFT: &str = "╚";
+ pub const THICK_BOTTOM_LEFT: &str = "┗";
+
+ pub const VERTICAL_LEFT: &str = "┤";
+ pub const DOUBLE_VERTICAL_LEFT: &str = "╣";
+ pub const THICK_VERTICAL_LEFT: &str = "┫";
+
+ pub const VERTICAL_RIGHT: &str = "├";
+ pub const DOUBLE_VERTICAL_RIGHT: &str = "╠";
+ pub const THICK_VERTICAL_RIGHT: &str = "┣";
+
+ pub const HORIZONTAL_DOWN: &str = "┬";
+ pub const DOUBLE_HORIZONTAL_DOWN: &str = "╦";
+ pub const THICK_HORIZONTAL_DOWN: &str = "┳";
+
+ pub const HORIZONTAL_UP: &str = "┴";
+ pub const DOUBLE_HORIZONTAL_UP: &str = "╩";
+ pub const THICK_HORIZONTAL_UP: &str = "┻";
+
+ pub const CROSS: &str = "┼";
+ pub const DOUBLE_CROSS: &str = "╬";
+ pub const THICK_CROSS: &str = "╋";
+
+ #[derive(Debug, Clone)]
+ pub struct Set {
+ pub vertical: &'static str,
+ pub horizontal: &'static str,
+ pub top_right: &'static str,
+ pub top_left: &'static str,
+ pub bottom_right: &'static str,
+ pub bottom_left: &'static str,
+ pub vertical_left: &'static str,
+ pub vertical_right: &'static str,
+ pub horizontal_down: &'static str,
+ pub horizontal_up: &'static str,
+ pub cross: &'static str,
+ }
+
+ pub const NORMAL: Set = Set {
+ vertical: VERTICAL,
+ horizontal: HORIZONTAL,
+ top_right: TOP_RIGHT,
+ top_left: TOP_LEFT,
+ bottom_right: BOTTOM_RIGHT,
+ bottom_left: BOTTOM_LEFT,
+ vertical_left: VERTICAL_LEFT,
+ vertical_right: VERTICAL_RIGHT,
+ horizontal_down: HORIZONTAL_DOWN,
+ horizontal_up: HORIZONTAL_UP,
+ cross: CROSS,
+ };
+
+ pub const ROUNDED: Set = Set {
+ top_right: ROUNDED_TOP_RIGHT,
+ top_left: ROUNDED_TOP_LEFT,
+ bottom_right: ROUNDED_BOTTOM_RIGHT,
+ bottom_left: ROUNDED_BOTTOM_LEFT,
+ ..NORMAL
+ };
+
+ pub const DOUBLE: Set = Set {
+ vertical: DOUBLE_VERTICAL,
+ horizontal: DOUBLE_HORIZONTAL,
+ top_right: DOUBLE_TOP_RIGHT,
+ top_left: DOUBLE_TOP_LEFT,
+ bottom_right: DOUBLE_BOTTOM_RIGHT,
+ bottom_left: DOUBLE_BOTTOM_LEFT,
+ vertical_left: DOUBLE_VERTICAL_LEFT,
+ vertical_right: DOUBLE_VERTICAL_RIGHT,
+ horizontal_down: DOUBLE_HORIZONTAL_DOWN,
+ horizontal_up: DOUBLE_HORIZONTAL_UP,
+ cross: DOUBLE_CROSS,
+ };
+
+ pub const THICK: Set = Set {
+ vertical: THICK_VERTICAL,
+ horizontal: THICK_HORIZONTAL,
+ top_right: THICK_TOP_RIGHT,
+ top_left: THICK_TOP_LEFT,
+ bottom_right: THICK_BOTTOM_RIGHT,
+ bottom_left: THICK_BOTTOM_LEFT,
+ vertical_left: THICK_VERTICAL_LEFT,
+ vertical_right: THICK_VERTICAL_RIGHT,
+ horizontal_down: THICK_HORIZONTAL_DOWN,
+ horizontal_up: THICK_HORIZONTAL_UP,
+ cross: THICK_CROSS,
+ };
+}
+
+pub const DOT: &str = "•";
+
+pub mod braille {
+ pub const BLANK: u16 = 0x2800;
+ pub const DOTS: [[u16; 2]; 4] = [
+ [0x0001, 0x0008],
+ [0x0002, 0x0010],
+ [0x0004, 0x0020],
+ [0x0040, 0x0080],
+ ];
+}
+
+/// Marker to use when plotting data points
+#[derive(Debug, Clone, Copy)]
+pub enum Marker {
+ /// One point per cell in shape of dot
+ Dot,
+ /// One point per cell in shape of a block
+ Block,
+ /// Up to 8 points per cell
+ Braille,
+}
diff --git a/helix-term/src/terminal.rs b/helix-tui/src/terminal.rs
index fc9bc5ba..7346d66d 100644
--- a/helix-term/src/terminal.rs
+++ b/helix-tui/src/terminal.rs
@@ -1,10 +1,5 @@
+use crate::{backend::Backend, buffer::Buffer, layout::Rect};
use std::io;
-use tui::{
- backend::Backend,
- buffer::Buffer,
- layout::Rect,
- widgets::{StatefulWidget, Widget},
-};
#[derive(Debug, Clone, PartialEq)]
/// UNSTABLE
diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs
new file mode 100644
index 00000000..c671e918
--- /dev/null
+++ b/helix-tui/src/text.rs
@@ -0,0 +1,434 @@
+//! 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 helix_tui::widgets::Block;
+//! # use helix_tui::text::{Span, Spans};
+//! # use helix_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::style::Style;
+use std::borrow::Cow;
+use unicode_segmentation::UnicodeSegmentation;
+use unicode_width::UnicodeWidthStr;
+
+/// A grapheme associated to a style.
+#[derive(Debug, Clone, PartialEq)]
+pub struct StyledGrapheme<'a> {
+ pub symbol: &'a str,
+ pub style: Style,
+}
+
+/// A string where all graphemes have the same style.
+#[derive(Debug, Clone, PartialEq)]
+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 helix_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 helix_tui::text::Span;
+ /// # use helix_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 helix_tui::text::{Span, StyledGrapheme};
+ /// # use helix_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)]
+pub struct Spans<'a>(pub Vec<Span<'a>>);
+
+impl<'a> Default for Spans<'a> {
+ fn default() -> Spans<'a> {
+ Spans(Vec::new())
+ }
+}
+
+impl<'a> Spans<'a> {
+ /// Returns the width of the underlying string.
+ ///
+ /// ## Examples
+ ///
+ /// ```rust
+ /// # use helix_tui::text::{Span, Spans};
+ /// # use helix_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 helix_tui::text::Text;
+/// # use helix_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)]
+pub struct Text<'a> {
+ pub lines: Vec<Spans<'a>>,
+}
+
+impl<'a> Default for Text<'a> {
+ fn default() -> Text<'a> {
+ Text { lines: Vec::new() }
+ }
+}
+
+impl<'a> Text<'a> {
+ /// Create some text (potentially multiple lines) with no style.
+ ///
+ /// ## Examples
+ ///
+ /// ```rust
+ /// # use helix_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 helix_tui::text::Text;
+ /// # use helix_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 helix_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 helix_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 helix_tui::text::Text;
+ /// # use helix_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<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/helix-tui/src/widgets/block.rs b/helix-tui/src/widgets/block.rs
new file mode 100644
index 00000000..2569c17d
--- /dev/null
+++ b/helix-tui/src/widgets/block.rs
@@ -0,0 +1,511 @@
+use crate::{
+ buffer::Buffer,
+ layout::Rect,
+ style::Style,
+ symbols::line,
+ text::{Span, Spans},
+ widgets::{Borders, Widget},
+};
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+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 helix_tui::widgets::{Block, BorderType, Borders};
+/// # use helix_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)]
+pub struct Block<'a> {
+ /// Optional title place on the upper left of the block
+ title: Option<Spans<'a>>,
+ /// 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,
+ borders: Borders::NONE,
+ border_style: Default::default(),
+ border_type: BorderType::Plain,
+ style: Default::default(),
+ }
+ }
+}
+
+impl<'a> Block<'a> {
+ pub fn title<T>(mut self, title: T) -> Block<'a>
+ where
+ T: Into<Spans<'a>>,
+ {
+ self.title = Some(title.into());
+ self
+ }
+
+ #[deprecated(
+ since = "0.10.0",
+ note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
+ )]
+ pub fn title_style(mut self, style: Style) -> Block<'a> {
+ if let Some(t) = self.title {
+ let title = String::from(t);
+ self.title = Some(Spans::from(Span::styled(title, style)));
+ }
+ self
+ }
+
+ pub fn 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);
+ }
+
+ if let Some(title) = self.title {
+ let lx = if self.borders.intersects(Borders::LEFT) {
+ 1
+ } else {
+ 0
+ };
+ let rx = if self.borders.intersects(Borders::RIGHT) {
+ 1
+ } else {
+ 0
+ };
+ let width = area.width.saturating_sub(lx).saturating_sub(rx);
+ buf.set_spans(area.left() + lx, area.top(), &title, width);
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::layout::Rect;
+
+ #[test]
+ fn inner_takes_into_account_the_borders() {
+ // No borders
+ assert_eq!(
+ Block::default().inner(Rect::default()),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0
+ },
+ "no borders, width=0, height=0"
+ );
+ assert_eq!(
+ Block::default().inner(Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 1
+ }),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 1
+ },
+ "no borders, width=1, height=1"
+ );
+
+ // Left border
+ assert_eq!(
+ Block::default().borders(Borders::LEFT).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 1
+ }),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 1
+ },
+ "left, width=0"
+ );
+ assert_eq!(
+ Block::default().borders(Borders::LEFT).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 1
+ }),
+ Rect {
+ x: 1,
+ y: 0,
+ width: 0,
+ height: 1
+ },
+ "left, width=1"
+ );
+ assert_eq!(
+ Block::default().borders(Borders::LEFT).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 2,
+ height: 1
+ }),
+ Rect {
+ x: 1,
+ y: 0,
+ width: 1,
+ height: 1
+ },
+ "left, width=2"
+ );
+
+ // Top border
+ assert_eq!(
+ Block::default().borders(Borders::TOP).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 0
+ }),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 0
+ },
+ "top, height=0"
+ );
+ assert_eq!(
+ Block::default().borders(Borders::TOP).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 1
+ }),
+ Rect {
+ x: 0,
+ y: 1,
+ width: 1,
+ height: 0
+ },
+ "top, height=1"
+ );
+ assert_eq!(
+ Block::default().borders(Borders::TOP).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 2
+ }),
+ Rect {
+ x: 0,
+ y: 1,
+ width: 1,
+ height: 1
+ },
+ "top, height=2"
+ );
+
+ // Right border
+ assert_eq!(
+ Block::default().borders(Borders::RIGHT).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 1
+ }),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 1
+ },
+ "right, width=0"
+ );
+ assert_eq!(
+ Block::default().borders(Borders::RIGHT).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 1
+ }),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 1
+ },
+ "right, width=1"
+ );
+ assert_eq!(
+ Block::default().borders(Borders::RIGHT).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 2,
+ height: 1
+ }),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 1
+ },
+ "right, width=2"
+ );
+
+ // Bottom border
+ assert_eq!(
+ Block::default().borders(Borders::BOTTOM).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 0
+ }),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 0
+ },
+ "bottom, height=0"
+ );
+ assert_eq!(
+ Block::default().borders(Borders::BOTTOM).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 1
+ }),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 0
+ },
+ "bottom, height=1"
+ );
+ assert_eq!(
+ Block::default().borders(Borders::BOTTOM).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 2
+ }),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 1
+ },
+ "bottom, height=2"
+ );
+
+ // All borders
+ assert_eq!(
+ Block::default()
+ .borders(Borders::ALL)
+ .inner(Rect::default()),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0
+ },
+ "all borders, width=0, height=0"
+ );
+ assert_eq!(
+ Block::default().borders(Borders::ALL).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 1
+ }),
+ Rect {
+ x: 1,
+ y: 1,
+ width: 0,
+ height: 0,
+ },
+ "all borders, width=1, height=1"
+ );
+ assert_eq!(
+ Block::default().borders(Borders::ALL).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 2,
+ height: 2,
+ }),
+ Rect {
+ x: 1,
+ y: 1,
+ width: 0,
+ height: 0,
+ },
+ "all borders, width=2, height=2"
+ );
+ assert_eq!(
+ Block::default().borders(Borders::ALL).inner(Rect {
+ x: 0,
+ y: 0,
+ width: 3,
+ height: 3,
+ }),
+ Rect {
+ x: 1,
+ y: 1,
+ width: 1,
+ height: 1,
+ },
+ "all borders, width=3, height=3"
+ );
+ }
+
+ #[test]
+ fn inner_takes_into_account_the_title() {
+ assert_eq!(
+ Block::default().title("Test").inner(Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 1,
+ }),
+ Rect {
+ x: 0,
+ y: 1,
+ width: 0,
+ height: 0,
+ },
+ );
+ }
+}
diff --git a/helix-tui/src/widgets/clear.rs b/helix-tui/src/widgets/clear.rs
new file mode 100644
index 00000000..d1da40de
--- /dev/null
+++ b/helix-tui/src/widgets/clear.rs
@@ -0,0 +1,36 @@
+use crate::buffer::Buffer;
+use crate::layout::Rect;
+use crate::widgets::Widget;
+
+/// A widget to to clear/reset a certain area to allow overdrawing (e.g. for popups)
+///
+/// # Examples
+///
+/// ```
+/// # use helix_tui::widgets::{Clear, Block, Borders};
+/// # use helix_tui::layout::Rect;
+/// # use helix_tui::Frame;
+/// # use helix_tui::backend::Backend;
+/// fn draw_on_clear<B: Backend>(f: &mut Frame<B>, area: Rect) {
+/// let block = Block::default().title("Block").borders(Borders::ALL);
+/// f.render_widget(Clear, area); // <- this will clear/reset the area first
+/// f.render_widget(block, area); // now render the block widget
+/// }
+/// ```
+///
+/// # Popup Example
+///
+/// For a more complete example how to utilize `Clear` to realize popups see
+/// the example `examples/popup.rs`
+#[derive(Debug, Clone)]
+pub struct Clear;
+
+impl Widget for Clear {
+ fn render(self, area: Rect, buf: &mut Buffer) {
+ for x in area.left()..area.right() {
+ for y in area.top()..area.bottom() {
+ buf.get_mut(x, y).reset();
+ }
+ }
+ }
+}
diff --git a/helix-tui/src/widgets/list.rs b/helix-tui/src/widgets/list.rs
new file mode 100644
index 00000000..e913ce78
--- /dev/null
+++ b/helix-tui/src/widgets/list.rs
@@ -0,0 +1,249 @@
+use crate::{
+ buffer::Buffer,
+ layout::{Corner, Rect},
+ style::Style,
+ text::Text,
+ widgets::{Block, StatefulWidget, Widget},
+};
+use std::iter::{self, Iterator};
+use unicode_width::UnicodeWidthStr;
+
+#[derive(Debug, Clone)]
+pub struct ListState {
+ offset: usize,
+ selected: Option<usize>,
+}
+
+impl Default for ListState {
+ fn default() -> ListState {
+ ListState {
+ offset: 0,
+ selected: None,
+ }
+ }
+}
+
+impl ListState {
+ pub fn selected(&self) -> Option<usize> {
+ self.selected
+ }
+
+ pub fn select(&mut self, index: Option<usize>) {
+ self.selected = index;
+ if index.is_none() {
+ self.offset = 0;
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct ListItem<'a> {
+ content: Text<'a>,
+ style: Style,
+}
+
+impl<'a> ListItem<'a> {
+ pub fn new<T>(content: T) -> ListItem<'a>
+ where
+ T: Into<Text<'a>>,
+ {
+ ListItem {
+ content: content.into(),
+ style: Style::default(),
+ }
+ }
+
+ pub fn style(mut self, style: Style) -> ListItem<'a> {
+ self.style = style;
+ self
+ }
+
+ pub fn height(&self) -> usize {
+ self.content.height()
+ }
+}
+
+/// A widget to display several items among which one can be selected (optional)
+///
+/// # Examples
+///
+/// ```
+/// # use helix_tui::widgets::{Block, Borders, List, ListItem};
+/// # use helix_tui::style::{Style, Color, Modifier};
+/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")];
+/// List::new(items)
+/// .block(Block::default().title("List").borders(Borders::ALL))
+/// .style(Style::default().fg(Color::White))
+/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
+/// .highlight_symbol(">>");
+/// ```
+#[derive(Debug, Clone)]
+pub struct List<'a> {
+ block: Option<Block<'a>>,
+ items: Vec<ListItem<'a>>,
+ /// Style used as a base style for the widget
+ style: Style,
+ start_corner: Corner,
+ /// Style used to render selected item
+ highlight_style: Style,
+ /// Symbol in front of the selected item (Shift all items to the right)
+ highlight_symbol: Option<&'a str>,
+}
+
+impl<'a> List<'a> {
+ pub fn new<T>(items: T) -> List<'a>
+ where
+ T: Into<Vec<ListItem<'a>>>,
+ {
+ List {
+ block: None,
+ style: Style::default(),
+ items: items.into(),
+ start_corner: Corner::TopLeft,
+ highlight_style: Style::default(),
+ highlight_symbol: None,
+ }
+ }
+
+ pub fn block(mut self, block: Block<'a>) -> List<'a> {
+ self.block = Some(block);
+ self
+ }
+
+ pub fn style(mut self, style: Style) -> List<'a> {
+ self.style = style;
+ self
+ }
+
+ pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> List<'a> {
+ self.highlight_symbol = Some(highlight_symbol);
+ self
+ }
+
+ pub fn highlight_style(mut self, style: Style) -> List<'a> {
+ self.highlight_style = style;
+ self
+ }
+
+ pub fn start_corner(mut self, corner: Corner) -> List<'a> {
+ self.start_corner = corner;
+ self
+ }
+}
+
+impl<'a> StatefulWidget for List<'a> {
+ type State = ListState;
+
+ fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
+ buf.set_style(area, self.style);
+ let list_area = match self.block.take() {
+ Some(b) => {
+ let inner_area = b.inner(area);
+ b.render(area, buf);
+ inner_area
+ }
+ None => area,
+ };
+
+ if list_area.width < 1 || list_area.height < 1 {
+ return;
+ }
+
+ if self.items.is_empty() {
+ return;
+ }
+ let list_height = list_area.height as usize;
+
+ let mut start = state.offset;
+ let mut end = state.offset;
+ let mut height = 0;
+ for item in self.items.iter().skip(state.offset) {
+ if height + item.height() > list_height {
+ break;
+ }
+ height += item.height();
+ end += 1;
+ }
+
+ let selected = state.selected.unwrap_or(0).min(self.items.len() - 1);
+ while selected >= end {
+ height = height.saturating_add(self.items[end].height());
+ end += 1;
+ while height > list_height {
+ height = height.saturating_sub(self.items[start].height());
+ start += 1;
+ }
+ }
+ while selected < start {
+ start -= 1;
+ height = height.saturating_add(self.items[start].height());
+ while height > list_height {
+ end -= 1;
+ height = height.saturating_sub(self.items[end].height());
+ }
+ }
+ state.offset = start;
+
+ let highlight_symbol = self.highlight_symbol.unwrap_or("");
+ let blank_symbol = iter::repeat(" ")
+ .take(highlight_symbol.width())
+ .collect::<String>();
+
+ let mut current_height = 0;
+ let has_selection = state.selected.is_some();
+ for (i, item) in self
+ .items
+ .iter_mut()
+ .enumerate()
+ .skip(state.offset)
+ .take(end - start)
+ {
+ let (x, y) = match self.start_corner {
+ Corner::BottomLeft => {
+ current_height += item.height() as u16;
+ (list_area.left(), list_area.bottom() - current_height)
+ }
+ _ => {
+ let pos = (list_area.left(), list_area.top() + current_height);
+ current_height += item.height() as u16;
+ pos
+ }
+ };
+ let area = Rect {
+ x,
+ y,
+ width: list_area.width,
+ height: item.height() as u16,
+ };
+ let item_style = self.style.patch(item.style);
+ buf.set_style(area, item_style);
+
+ let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
+ let elem_x = if has_selection {
+ let symbol = if is_selected {
+ highlight_symbol
+ } else {
+ &blank_symbol
+ };
+ let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style);
+ x
+ } else {
+ x
+ };
+ let max_element_width = (list_area.width - (elem_x - x)) as usize;
+ for (j, line) in item.content.lines.iter().enumerate() {
+ buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
+ }
+ if is_selected {
+ buf.set_style(area, self.highlight_style);
+ }
+ }
+ }
+}
+
+impl<'a> Widget for List<'a> {
+ fn render(self, area: Rect, buf: &mut Buffer) {
+ let mut state = ListState::default();
+ StatefulWidget::render(self, area, buf, &mut state);
+ }
+}
diff --git a/helix-tui/src/widgets/mod.rs b/helix-tui/src/widgets/mod.rs
new file mode 100644
index 00000000..76eb73c1
--- /dev/null
+++ b/helix-tui/src/widgets/mod.rs
@@ -0,0 +1,53 @@
+//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
+//!
+//! All widgets are implemented using the builder pattern and are consumable objects. They are not
+//! meant to be stored but used as *commands* to draw common figures in the UI.
+//!
+//! The available widgets are:
+//! - [`Block`]
+//! - [`Tabs`]
+//! - [`List`]
+//! - [`Table`]
+//! - [`Paragraph`]
+//! - [`Clear`]
+
+mod block;
+mod clear;
+// mod list;
+mod paragraph;
+mod reflow;
+// mod table;
+
+pub use self::block::{Block, BorderType};
+pub use self::clear::Clear;
+// pub use self::list::{List, ListItem, ListState};
+pub use self::paragraph::{Paragraph, Wrap};
+// pub use self::table::{Cell, Row, Table, TableState};
+
+use crate::{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 the only method required to
+ /// implement a custom widget.
+ fn render(self, area: Rect, buf: &mut Buffer);
+}
diff --git a/helix-tui/src/widgets/paragraph.rs b/helix-tui/src/widgets/paragraph.rs
new file mode 100644
index 00000000..ecce8581
--- /dev/null
+++ b/helix-tui/src/widgets/paragraph.rs
@@ -0,0 +1,197 @@
+use crate::{
+ 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 helix_tui::text::{Text, Spans, Span};
+/// # use helix_tui::widgets::{Block, Borders, Paragraph, Wrap};
+/// # use helix_tui::style::{Style, Color, Modifier};
+/// # use helix_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 helix_tui::widgets::{Paragraph, Wrap};
+/// # use helix_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: Default::default(),
+ wrap: None,
+ text: text.into(),
+ scroll: (0, 0),
+ alignment: Alignment::Left,
+ }
+ }
+
+ pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> {
+ self.block = Some(block);
+ self
+ }
+
+ pub fn style(mut self, style: Style) -> Paragraph<'a> {
+ self.style = style;
+ self
+ }
+
+ pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
+ self.wrap = Some(wrap);
+ self
+ }
+
+ pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
+ self.scroll = offset;
+ self
+ }
+
+ pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> {
+ self.alignment = alignment;
+ self
+ }
+}
+
+impl<'a> Widget for Paragraph<'a> {
+ fn render(mut self, area: Rect, buf: &mut Buffer) {
+ buf.set_style(area, self.style);
+ let text_area = match self.block.take() {
+ Some(b) => {
+ let inner_area = b.inner(area);
+ b.render(area, buf);
+ inner_area
+ }
+ None => area,
+ };
+
+ if text_area.height < 1 {
+ return;
+ }
+
+ let style = self.style;
+ let mut styled = self.text.lines.iter().flat_map(|spans| {
+ spans
+ .0
+ .iter()
+ .flat_map(|span| span.styled_graphemes(style))
+ // Required given the way composers work but might be refactored out if we change
+ // composers to operate on lines instead of a stream of graphemes.
+ .chain(iter::once(StyledGrapheme {
+ symbol: "\n",
+ style: self.style,
+ }))
+ });
+
+ let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
+ Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
+ } else {
+ let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
+ if let Alignment::Left = self.alignment {
+ line_composer.set_horizontal_offset(self.scroll.1);
+ }
+ line_composer
+ };
+ let mut y = 0;
+ while let Some((current_line, current_line_width)) = line_composer.next_line() {
+ if y >= self.scroll.0 {
+ let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
+ for StyledGrapheme { symbol, style } in current_line {
+ 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/helix-tui/src/widgets/reflow.rs b/helix-tui/src/widgets/reflow.rs
new file mode 100644
index 00000000..94ff7330
--- /dev/null
+++ b/helix-tui/src/widgets/reflow.rs
@@ -0,0 +1,534 @@
+use crate::text::StyledGrapheme;
+use unicode_segmentation::UnicodeSegmentation;
+use unicode_width::UnicodeWidthStr;
+
+const NBSP: &str = "\u{00a0}";
+
+/// A state machine to pack styled symbols into lines.
+/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
+/// iterators for that).
+pub trait LineComposer<'a> {
+ fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>;
+}
+
+/// A state machine that wraps lines on word boundaries.
+pub struct WordWrapper<'a, 'b> {
+ symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
+ max_line_width: u16,
+ current_line: Vec<StyledGrapheme<'a>>,
+ next_line: Vec<StyledGrapheme<'a>>,
+ /// Removes the leading whitespace from lines
+ trim: bool,
+}
+
+impl<'a, 'b> WordWrapper<'a, 'b> {
+ pub fn new(
+ symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
+ max_line_width: u16,
+ trim: bool,
+ ) -> WordWrapper<'a, 'b> {
+ WordWrapper {
+ symbols,
+ max_line_width,
+ current_line: vec![],
+ next_line: vec![],
+ trim,
+ }
+ }
+}
+
+impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
+ fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
+ if self.max_line_width == 0 {
+ return None;
+ }
+ std::mem::swap(&mut self.current_line, &mut self.next_line);
+ self.next_line.truncate(0);
+
+ let mut current_line_width = self
+ .current_line
+ .iter()
+ .map(|StyledGrapheme { symbol, .. }| symbol.width() as u16)
+ .sum();
+
+ let mut symbols_to_last_word_end: usize = 0;
+ let mut width_to_last_word_end: u16 = 0;
+ let mut prev_whitespace = false;
+ let mut symbols_exhausted = true;
+ for StyledGrapheme { symbol, style } in &mut self.symbols {
+ symbols_exhausted = false;
+ let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
+
+ // Ignore characters wider that the total max width.
+ if symbol.width() as u16 > self.max_line_width
+ // Skip leading whitespace when trim is enabled.
+ || self.trim && symbol_whitespace && symbol != "\n" && current_line_width == 0
+ {
+ continue;
+ }
+
+ // Break on newline and discard it.
+ if symbol == "\n" {
+ if prev_whitespace {
+ current_line_width = width_to_last_word_end;
+ self.current_line.truncate(symbols_to_last_word_end);
+ }
+ break;
+ }
+
+ // Mark the previous symbol as word end.
+ if symbol_whitespace && !prev_whitespace {
+ symbols_to_last_word_end = self.current_line.len();
+ width_to_last_word_end = current_line_width;
+ }
+
+ self.current_line.push(StyledGrapheme { symbol, style });
+ current_line_width += symbol.width() as u16;
+
+ if current_line_width > self.max_line_width {
+ // If there was no word break in the text, wrap at the end of the line.
+ let (truncate_at, truncated_width) = if symbols_to_last_word_end != 0 {
+ (symbols_to_last_word_end, width_to_last_word_end)
+ } else {
+ (self.current_line.len() - 1, self.max_line_width)
+ };
+
+ // Push the remainder to the next line but strip leading whitespace:
+ {
+ let remainder = &self.current_line[truncate_at..];
+ if let Some(remainder_nonwhite) =
+ remainder.iter().position(|StyledGrapheme { symbol, .. }| {
+ !symbol.chars().all(&char::is_whitespace)
+ })
+ {
+ self.next_line
+ .extend_from_slice(&remainder[remainder_nonwhite..]);
+ }
+ }
+ self.current_line.truncate(truncate_at);
+ current_line_width = truncated_width;
+ break;
+ }
+
+ prev_whitespace = symbol_whitespace;
+ }
+
+ // Even if the iterator is exhausted, pass the previous remainder.
+ if symbols_exhausted && self.current_line.is_empty() {
+ None
+ } else {
+ Some((&self.current_line[..], current_line_width))
+ }
+ }
+}
+
+/// A state machine that truncates overhanging lines.
+pub struct LineTruncator<'a, 'b> {
+ symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
+ max_line_width: u16,
+ current_line: Vec<StyledGrapheme<'a>>,
+ /// Record the 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 super::*;
+ use unicode_segmentation::UnicodeSegmentation;
+
+ enum Composer {
+ WordWrapper { trim: bool },
+ LineTruncator,
+ }
+
+ fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
+ let style = Default::default();
+ let mut styled =
+ UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
+ let mut composer: Box<dyn LineComposer> = match which {
+ Composer::WordWrapper { trim } => {
+ Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
+ }
+ Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
+ };
+ let mut lines = vec![];
+ let mut widths = vec![];
+ while let Some((styled, width)) = composer.next_line() {
+ let line = styled
+ .iter()
+ .map(|StyledGrapheme { symbol, .. }| *symbol)
+ .collect::<String>();
+ assert!(width <= text_area_width);
+ lines.push(line);
+ widths.push(width);
+ }
+ (lines, widths)
+ }
+
+ #[test]
+ fn line_composer_one_line() {
+ let width = 40;
+ for i in 1..width {
+ let text = "a".repeat(i);
+ let (word_wrapper, _) =
+ run_composer(Composer::WordWrapper { trim: true }, &text, width as u16);
+ let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
+ let expected = vec![text];
+ assert_eq!(word_wrapper, expected);
+ assert_eq!(line_truncator, expected);
+ }
+ }
+
+ #[test]
+ fn line_composer_short_lines() {
+ let width = 20;
+ let text =
+ "abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
+
+ let wrapped: Vec<&str> = text.split('\n').collect();
+ assert_eq!(word_wrapper, wrapped);
+ assert_eq!(line_truncator, wrapped);
+ }
+
+ #[test]
+ fn line_composer_long_word() {
+ let width = 20;
+ let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
+ let (word_wrapper, _) =
+ run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
+
+ let wrapped = vec![
+ &text[..width],
+ &text[width..width * 2],
+ &text[width * 2..width * 3],
+ &text[width * 3..],
+ ];
+ assert_eq!(
+ word_wrapper, wrapped,
+ "WordWrapper should detect the line cannot be broken on word boundary and \
+ break it at line width limit."
+ );
+ assert_eq!(line_truncator, vec![&text[..width]]);
+ }
+
+ #[test]
+ fn line_composer_long_sentence() {
+ let width = 20;
+ let text =
+ "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
+ let text_multi_space =
+ "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
+ m n o";
+ let (word_wrapper_single_space, _) =
+ run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
+ let (word_wrapper_multi_space, _) = run_composer(
+ Composer::WordWrapper { trim: true },
+ text_multi_space,
+ width as u16,
+ );
+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
+
+ let word_wrapped = vec![
+ "abcd efghij",
+ "klmnopabcd efgh",
+ "ijklmnopabcdefg",
+ "hijkl mnopab c d e f",
+ "g h i j k l m n o",
+ ];
+ assert_eq!(word_wrapper_single_space, word_wrapped);
+ assert_eq!(word_wrapper_multi_space, word_wrapped);
+
+ assert_eq!(line_truncator, vec![&text[..width]]);
+ }
+
+ #[test]
+ fn line_composer_zero_width() {
+ let width = 0;
+ let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
+
+ let expected: Vec<&str> = Vec::new();
+ assert_eq!(word_wrapper, expected);
+ assert_eq!(line_truncator, expected);
+ }
+
+ #[test]
+ fn line_composer_max_line_width_of_1() {
+ let width = 1;
+ let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
+
+ let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
+ .filter(|g| g.chars().any(|c| !c.is_whitespace()))
+ .collect();
+ assert_eq!(word_wrapper, expected);
+ assert_eq!(line_truncator, vec!["a"]);
+ }
+
+ #[test]
+ fn line_composer_max_line_width_of_1_double_width_characters() {
+ let width = 1;
+ let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
+ 両端点では、";
+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
+ assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
+ assert_eq!(line_truncator, vec!["", "a"]);
+ }
+
+ /// Tests WordWrapper with words some of which exceed line length and some not.
+ #[test]
+ fn line_composer_word_wrapper_mixed_length() {
+ let width = 20;
+ let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
+ assert_eq!(
+ word_wrapper,
+ vec![
+ "abcd efghij",
+ "klmnopabcdefghijklmn",
+ "opabcdefghijkl",
+ "mnopab cdefghi j",
+ "klmno",
+ ]
+ )
+ }
+
+ #[test]
+ fn line_composer_double_width_chars() {
+ let width = 20;
+ let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
+ では、";
+ let (word_wrapper, word_wrapper_width) =
+ run_composer(Composer::WordWrapper { trim: true }, &text, width);
+ let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
+ assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
+ let wrapped = vec![
+ "コンピュータ上で文字",
+ "を扱う場合、典型的に",
+ "は文字による通信を行",
+ "う場合にその両端点で",
+ "は、",
+ ];
+ assert_eq!(word_wrapper, wrapped);
+ assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
+ }
+
+ #[test]
+ fn line_composer_leading_whitespace_removal() {
+ let width = 20;
+ let text = "AAAAAAAAAAAAAAAAAAAA AAA";
+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
+ assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
+ assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
+ }
+
+ /// Tests truncation of leading whitespace.
+ #[test]
+ fn line_composer_lots_of_spaces() {
+ let width = 20;
+ let text = " ";
+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
+ assert_eq!(word_wrapper, vec![""]);
+ assert_eq!(line_truncator, vec![" "]);
+ }
+
+ /// Tests an input starting with a letter, 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!"
+ ]
+ );
+ }
+}
diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs
new file mode 100644
index 00000000..31624a8f
--- /dev/null
+++ b/helix-tui/src/widgets/table.rs
@@ -0,0 +1,538 @@
+use crate::{
+ buffer::Buffer,
+ layout::{Constraint, Rect},
+ style::Style,
+ text::Text,
+ widgets::{Block, StatefulWidget, Widget},
+};
+use cassowary::{
+ strength::{MEDIUM, REQUIRED, WEAK},
+ WeightedRelation::*,
+ {Expression, Solver},
+};
+use std::{
+ collections::HashMap,
+ iter::{self, Iterator},
+};
+use unicode_width::UnicodeWidthStr;
+
+/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
+///
+/// It can be created from anything that can be converted to a [`Text`].
+/// ```rust
+/// # use helix_tui::widgets::Cell;
+/// # use helix_tui::style::{Style, Modifier};
+/// # use helix_tui::text::{Span, Spans, Text};
+/// Cell::from("simple string");
+///
+/// Cell::from(Span::from("span"));
+///
+/// Cell::from(Spans::from(vec![
+/// Span::raw("a vec of "),
+/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
+/// ]));
+///
+/// Cell::from(Text::from("a text"));
+/// ```
+///
+/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
+/// capabilities of [`Text`].
+#[derive(Debug, Clone, PartialEq, Default)]
+pub struct Cell<'a> {
+ content: Text<'a>,
+ style: Style,
+}
+
+impl<'a> Cell<'a> {
+ /// Set the `Style` of this cell.
+ pub fn style(mut self, style: Style) -> Self {
+ self.style = style;
+ self
+ }
+}
+
+impl<'a, T> From<T> for Cell<'a>
+where
+ T: Into<Text<'a>>,
+{
+ fn from(content: T) -> Cell<'a> {
+ Cell {
+ content: content.into(),
+ style: Style::default(),
+ }
+ }
+}
+
+/// Holds data to be displayed in a [`Table`] widget.
+///
+/// A [`Row`] is a collection of cells. It can be created from simple strings:
+/// ```rust
+/// # use helix_tui::widgets::Row;
+/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
+/// ```
+///
+/// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s:
+/// ```rust
+/// # use helix_tui::widgets::{Row, Cell};
+/// # use helix_tui::style::{Style, Color};
+/// Row::new(vec![
+/// Cell::from("Cell1"),
+/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
+/// ]);
+/// ```
+///
+/// By default, a row has a height of 1 but you can change this using [`Row::height`].
+#[derive(Debug, Clone, PartialEq, Default)]
+pub struct Row<'a> {
+ cells: Vec<Cell<'a>>,
+ height: u16,
+ style: Style,
+ bottom_margin: u16,
+}
+
+impl<'a> Row<'a> {
+ /// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
+ pub fn new<T>(cells: T) -> Self
+ where
+ T: IntoIterator,
+ T::Item: Into<Cell<'a>>,
+ {
+ Self {
+ height: 1,
+ cells: cells.into_iter().map(|c| c.into()).collect(),
+ style: Style::default(),
+ bottom_margin: 0,
+ }
+ }
+
+ /// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
+ /// height will see its content truncated.
+ pub fn height(mut self, height: u16) -> Self {
+ self.height = height;
+ self
+ }
+
+ /// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a
+ /// any individual [`Cell`] or event by their [`Text`] content.
+ pub fn style(mut self, style: Style) -> Self {
+ self.style = style;
+ self
+ }
+
+ /// Set the bottom margin. By default, the bottom margin is `0`.
+ pub fn bottom_margin(mut self, margin: u16) -> Self {
+ self.bottom_margin = margin;
+ self
+ }
+
+ /// Returns the total height of the row.
+ fn total_height(&self) -> u16 {
+ self.height.saturating_add(self.bottom_margin)
+ }
+}
+
+/// A widget to display data in formatted columns.
+///
+/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
+/// ```rust
+/// # use helix_tui::widgets::{Block, Borders, Table, Row, Cell};
+/// # use helix_tui::layout::Constraint;
+/// # use helix_tui::style::{Style, Color, Modifier};
+/// # use helix_tui::text::{Text, Spans, Span};
+/// Table::new(vec![
+/// // Row can be created from simple strings.
+/// Row::new(vec!["Row11", "Row12", "Row13"]),
+/// // You can style the entire row.
+/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
+/// // If you need more control over the styling you may need to create Cells directly
+/// Row::new(vec![
+/// Cell::from("Row31"),
+/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
+/// Cell::from(Spans::from(vec![
+/// Span::raw("Row"),
+/// Span::styled("33", Style::default().fg(Color::Green))
+/// ])),
+/// ]),
+/// // If a Row need to display some content over multiple lines, you just have to change
+/// // its height.
+/// Row::new(vec![
+/// Cell::from("Row\n41"),
+/// Cell::from("Row\n42"),
+/// Cell::from("Row\n43"),
+/// ]).height(2),
+/// ])
+/// // You can set the style of the entire Table.
+/// .style(Style::default().fg(Color::White))
+/// // It has an optional header, which is simply a Row always visible at the top.
+/// .header(
+/// Row::new(vec!["Col1", "Col2", "Col3"])
+/// .style(Style::default().fg(Color::Yellow))
+/// // If you want some space between the header and the rest of the rows, you can always
+/// // specify some margin at the bottom.
+/// .bottom_margin(1)
+/// )
+/// // As any other widget, a Table can be wrapped in a Block.
+/// .block(Block::default().title("Table"))
+/// // Columns widths are constrained in the same way as Layout...
+/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
+/// // ...and they can be separated by a fixed spacing.
+/// .column_spacing(1)
+/// // If you wish to highlight a row in any specific way when it is selected...
+/// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
+/// // ...and potentially show a symbol in front of the selection.
+/// .highlight_symbol(">>");
+/// ```
+#[derive(Debug, Clone, PartialEq)]
+pub struct Table<'a> {
+ /// A block to wrap the widget in
+ block: Option<Block<'a>>,
+ /// Base style for the widget
+ style: Style,
+ /// Width constraints for each column
+ widths: &'a [Constraint],
+ /// Space between each column
+ column_spacing: u16,
+ /// Style used to render the selected row
+ highlight_style: Style,
+ /// Symbol in front of the selected rom
+ highlight_symbol: Option<&'a str>,
+ /// Optional header
+ header: Option<Row<'a>>,
+ /// Data to display in each row
+ rows: Vec<Row<'a>>,
+}
+
+impl<'a> Table<'a> {
+ pub fn new<T>(rows: T) -> Self
+ where
+ T: IntoIterator<Item = Row<'a>>,
+ {
+ Self {
+ block: None,
+ style: Style::default(),
+ widths: &[],
+ column_spacing: 1,
+ highlight_style: Style::default(),
+ highlight_symbol: None,
+ header: None,
+ rows: rows.into_iter().collect(),
+ }
+ }
+
+ pub fn block(mut self, block: Block<'a>) -> Self {
+ self.block = Some(block);
+ self
+ }
+
+ pub fn header(mut self, header: Row<'a>) -> Self {
+ self.header = Some(header);
+ self
+ }
+
+ pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
+ let between_0_and_100 = |&w| match w {
+ Constraint::Percentage(p) => p <= 100,
+ _ => true,
+ };
+ assert!(
+ widths.iter().all(between_0_and_100),
+ "Percentages should be between 0 and 100 inclusively."
+ );
+ self.widths = widths;
+ self
+ }
+
+ pub fn style(mut self, style: Style) -> Self {
+ self.style = style;
+ self
+ }
+
+ pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
+ self.highlight_symbol = Some(highlight_symbol);
+ self
+ }
+
+ pub fn highlight_style(mut self, highlight_style: Style) -> Self {
+ self.highlight_style = highlight_style;
+ self
+ }
+
+ pub fn column_spacing(mut self, spacing: u16) -> Self {
+ self.column_spacing = spacing;
+ self
+ }
+
+ fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
+ let mut solver = Solver::new();
+ let mut var_indices = HashMap::new();
+ let mut ccs = Vec::new();
+ let mut variables = Vec::new();
+ for i in 0..self.widths.len() {
+ let var = cassowary::Variable::new();
+ variables.push(var);
+ var_indices.insert(var, i);
+ }
+ let spacing_width = (variables.len() as u16).saturating_sub(1) * self.column_spacing;
+ let mut available_width = max_width.saturating_sub(spacing_width);
+ if has_selection {
+ let highlight_symbol_width =
+ self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
+ available_width = available_width.saturating_sub(highlight_symbol_width);
+ }
+ for (i, constraint) in self.widths.iter().enumerate() {
+ ccs.push(variables[i] | GE(WEAK) | 0.);
+ ccs.push(match *constraint {
+ Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
+ Constraint::Percentage(v) => {
+ variables[i] | EQ(WEAK) | (f64::from(v * available_width) / 100.0)
+ }
+ Constraint::Ratio(n, d) => {
+ variables[i]
+ | EQ(WEAK)
+ | (f64::from(available_width) * f64::from(n) / f64::from(d))
+ }
+ Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
+ Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
+ })
+ }
+ solver
+ .add_constraint(
+ variables
+ .iter()
+ .fold(Expression::from_constant(0.), |acc, v| acc + *v)
+ | LE(REQUIRED)
+ | f64::from(available_width),
+ )
+ .unwrap();
+ solver.add_constraints(&ccs).unwrap();
+ let mut widths = vec![0; variables.len()];
+ for &(var, value) in solver.fetch_changes() {
+ let index = var_indices[&var];
+ let value = if value.is_sign_negative() {
+ 0
+ } else {
+ value.round() as u16
+ };
+ widths[index] = value;
+ }
+ // Cassowary could still return columns widths greater than the max width when there are
+ // fixed length constraints that cannot be satisfied. Therefore, we clamp the widths from
+ // left to right.
+ let mut available_width = max_width;
+ for w in &mut widths {
+ *w = available_width.min(*w);
+ available_width = available_width
+ .saturating_sub(*w)
+ .saturating_sub(self.column_spacing);
+ }
+ widths
+ }
+
+ fn get_row_bounds(
+ &self,
+ selected: Option<usize>,
+ offset: usize,
+ max_height: u16,
+ ) -> (usize, usize) {
+ let mut start = offset;
+ let mut end = offset;
+ let mut height = 0;
+ for item in self.rows.iter().skip(offset) {
+ if height + item.height > max_height {
+ break;
+ }
+ height += item.total_height();
+ end += 1;
+ }
+
+ let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
+ while selected >= end {
+ height = height.saturating_add(self.rows[end].total_height());
+ end += 1;
+ while height > max_height {
+ height = height.saturating_sub(self.rows[start].total_height());
+ start += 1;
+ }
+ }
+ while selected < start {
+ start -= 1;
+ height = height.saturating_add(self.rows[start].total_height());
+ while height > max_height {
+ end -= 1;
+ height = height.saturating_sub(self.rows[end].total_height());
+ }
+ }
+ (start, end)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct TableState {
+ offset: usize,
+ selected: Option<usize>,
+}
+
+impl Default for TableState {
+ fn default() -> TableState {
+ TableState {
+ offset: 0,
+ selected: None,
+ }
+ }
+}
+
+impl TableState {
+ pub fn selected(&self) -> Option<usize> {
+ self.selected
+ }
+
+ pub fn select(&mut self, index: Option<usize>) {
+ self.selected = index;
+ if index.is_none() {
+ self.offset = 0;
+ }
+ }
+}
+
+impl<'a> StatefulWidget for Table<'a> {
+ type State = TableState;
+
+ fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
+ if area.area() == 0 {
+ return;
+ }
+ buf.set_style(area, self.style);
+ let table_area = match self.block.take() {
+ Some(b) => {
+ let inner_area = b.inner(area);
+ b.render(area, buf);
+ inner_area
+ }
+ None => area,
+ };
+
+ let has_selection = state.selected.is_some();
+ let columns_widths = self.get_columns_widths(table_area.width, has_selection);
+ let highlight_symbol = self.highlight_symbol.unwrap_or("");
+ let blank_symbol = iter::repeat(" ")
+ .take(highlight_symbol.width())
+ .collect::<String>();
+ let mut current_height = 0;
+ let mut rows_height = table_area.height;
+
+ // Draw header
+ if let Some(ref header) = self.header {
+ let max_header_height = table_area.height.min(header.total_height());
+ buf.set_style(
+ Rect {
+ x: table_area.left(),
+ y: table_area.top(),
+ width: table_area.width,
+ height: table_area.height.min(header.height),
+ },
+ header.style,
+ );
+ let mut col = table_area.left();
+ if has_selection {
+ col += (highlight_symbol.width() as u16).min(table_area.width);
+ }
+ for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
+ render_cell(
+ buf,
+ cell,
+ Rect {
+ x: col,
+ y: table_area.top(),
+ width: *width,
+ height: max_header_height,
+ },
+ );
+ col += *width + self.column_spacing;
+ }
+ current_height += max_header_height;
+ rows_height = rows_height.saturating_sub(max_header_height);
+ }
+
+ // Draw rows
+ if self.rows.is_empty() {
+ return;
+ }
+ let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
+ state.offset = start;
+ for (i, table_row) in self
+ .rows
+ .iter_mut()
+ .enumerate()
+ .skip(state.offset)
+ .take(end - start)
+ {
+ let (row, col) = (table_area.top() + current_height, table_area.left());
+ current_height += table_row.total_height();
+ let table_row_area = Rect {
+ x: col,
+ y: row,
+ width: table_area.width,
+ height: table_row.height,
+ };
+ buf.set_style(table_row_area, table_row.style);
+ let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
+ let table_row_start_col = if has_selection {
+ let symbol = if is_selected {
+ highlight_symbol
+ } else {
+ &blank_symbol
+ };
+ let (col, _) =
+ buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
+ col
+ } else {
+ col
+ };
+ let mut col = table_row_start_col;
+ for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
+ render_cell(
+ buf,
+ cell,
+ Rect {
+ x: col,
+ y: row,
+ width: *width,
+ height: table_row.height,
+ },
+ );
+ col += *width + self.column_spacing;
+ }
+ if is_selected {
+ buf.set_style(table_row_area, self.highlight_style);
+ }
+ }
+ }
+}
+
+fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
+ buf.set_style(area, cell.style);
+ for (i, spans) in cell.content.lines.iter().enumerate() {
+ if i as u16 >= area.height {
+ break;
+ }
+ buf.set_spans(area.x, area.y + i as u16, spans, area.width);
+ }
+}
+
+impl<'a> Widget for Table<'a> {
+ fn render(self, area: Rect, buf: &mut Buffer) {
+ let mut state = TableState::default();
+ StatefulWidget::render(self, area, buf, &mut state);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ #[should_panic]
+ fn table_invalid_percentages() {
+ Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
+ }
+}
diff --git a/helix-tui/tests/terminal.rs b/helix-tui/tests/terminal.rs
new file mode 100644
index 00000000..4734dd9a
--- /dev/null
+++ b/helix-tui/tests/terminal.rs
@@ -0,0 +1,36 @@
+use helix_tui::{
+ backend::{Backend, TestBackend},
+ layout::Rect,
+ widgets::Paragraph,
+ Terminal,
+};
+use std::error::Error;
+
+#[test]
+fn terminal_buffer_size_should_be_limited() {
+ let backend = TestBackend::new(400, 400);
+ let terminal = Terminal::new(backend).unwrap();
+ let size = terminal.backend().size().unwrap();
+ assert_eq!(size.width, 255);
+ assert_eq!(size.height, 255);
+}
+
+// #[test]
+// fn terminal_draw_returns_the_completed_frame() -> Result<(), Box<dyn Error>> {
+// let backend = TestBackend::new(10, 10);
+// let mut terminal = Terminal::new(backend)?;
+// let frame = terminal.draw(|f| {
+// let paragrah = Paragraph::new("Test");
+// f.render_widget(paragrah, f.size());
+// })?;
+// assert_eq!(frame.buffer.get(0, 0).symbol, "T");
+// assert_eq!(frame.area, Rect::new(0, 0, 10, 10));
+// terminal.backend_mut().resize(8, 8);
+// let frame = terminal.draw(|f| {
+// let paragrah = Paragraph::new("test");
+// f.render_widget(paragrah, f.size());
+// })?;
+// assert_eq!(frame.buffer.get(0, 0).symbol, "t");
+// assert_eq!(frame.area, Rect::new(0, 0, 8, 8));
+// Ok(())
+// }
diff --git a/helix-tui/tests/widgets_block.rs b/helix-tui/tests/widgets_block.rs
new file mode 100644
index 00000000..8aaf905b
--- /dev/null
+++ b/helix-tui/tests/widgets_block.rs
@@ -0,0 +1,213 @@
+use helix_tui::{
+ backend::TestBackend,
+ buffer::Buffer,
+ layout::Rect,
+ style::{Color, Style},
+ text::Span,
+ widgets::{Block, Borders},
+ Terminal,
+};
+
+#[test]
+fn widgets_block_renders() {
+ let backend = TestBackend::new(10, 10);
+ let mut terminal = Terminal::new(backend).unwrap();
+ terminal
+ .draw(|f| {
+ let block = Block::default()
+ .title(Span::styled("Title", Style::default().fg(Color::LightBlue)))
+ .borders(Borders::ALL);
+ f.render_widget(
+ block,
+ Rect {
+ x: 0,
+ y: 0,
+ width: 8,
+ height: 8,
+ },
+ );
+ })
+ .unwrap();
+ let mut expected = Buffer::with_lines(vec![
+ "┌Title─┐ ",
+ "│ │ ",
+ "│ │ ",
+ "│ │ ",
+ "│ │ ",
+ "│ │ ",
+ "│ │ ",
+ "└──────┘ ",
+ " ",
+ " ",
+ ]);
+ for x in 1..=5 {
+ expected.get_mut(x, 0).set_fg(Color::LightBlue);
+ }
+ terminal.backend().assert_buffer(&expected);
+}
+
+#[test]
+fn widgets_block_renders_on_small_areas() {
+ let test_case = |block, area: Rect, expected| {
+ let backend = TestBackend::new(area.width, area.height);
+ let mut terminal = Terminal::new(backend).unwrap();
+ terminal
+ .draw(|f| {
+ f.render_widget(block, area);
+ })
+ .unwrap();
+ terminal.backend().assert_buffer(&expected);
+ };
+
+ let one_cell_test_cases = [
+ (Borders::NONE, "T"),
+ (Borders::LEFT, "│"),
+ (Borders::TOP, "T"),
+ (Borders::RIGHT, "│"),
+ (Borders::BOTTOM, "T"),
+ (Borders::ALL, "┌"),
+ ];
+ for (borders, symbol) in one_cell_test_cases.iter().cloned() {
+ test_case(
+ Block::default().title("Test").borders(borders),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ },
+ Buffer::empty(Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ }),
+ );
+ test_case(
+ Block::default().title("Test").borders(borders),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 0,
+ },
+ Buffer::empty(Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 0,
+ }),
+ );
+ test_case(
+ Block::default().title("Test").borders(borders),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 1,
+ },
+ Buffer::empty(Rect {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 1,
+ }),
+ );
+ test_case(
+ Block::default().title("Test").borders(borders),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 1,
+ },
+ Buffer::with_lines(vec![symbol]),
+ );
+ }
+ test_case(
+ Block::default().title("Test").borders(Borders::LEFT),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 4,
+ height: 1,
+ },
+ Buffer::with_lines(vec!["│Tes"]),
+ );
+ test_case(
+ Block::default().title("Test").borders(Borders::RIGHT),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 4,
+ height: 1,
+ },
+ Buffer::with_lines(vec!["Tes│"]),
+ );
+ test_case(
+ Block::default().title("Test").borders(Borders::RIGHT),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 4,
+ height: 1,
+ },
+ Buffer::with_lines(vec!["Tes│"]),
+ );
+ test_case(
+ Block::default()
+ .title("Test")
+ .borders(Borders::LEFT | Borders::RIGHT),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 4,
+ height: 1,
+ },
+ Buffer::with_lines(vec!["│Te│"]),
+ );
+ test_case(
+ Block::default().title("Test").borders(Borders::TOP),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 4,
+ height: 1,
+ },
+ Buffer::with_lines(vec!["Test"]),
+ );
+ test_case(
+ Block::default().title("Test").borders(Borders::TOP),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 5,
+ height: 1,
+ },
+ Buffer::with_lines(vec!["Test─"]),
+ );
+ test_case(
+ Block::default()
+ .title("Test")
+ .borders(Borders::LEFT | Borders::TOP),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 5,
+ height: 1,
+ },
+ Buffer::with_lines(vec!["┌Test"]),
+ );
+ test_case(
+ Block::default()
+ .title("Test")
+ .borders(Borders::LEFT | Borders::TOP),
+ Rect {
+ x: 0,
+ y: 0,
+ width: 6,
+ height: 1,
+ },
+ Buffer::with_lines(vec!["┌Test─"]),
+ );
+}
diff --git a/helix-tui/tests/widgets_list.rs b/helix-tui/tests/widgets_list.rs
new file mode 100644
index 00000000..e59accd8
--- /dev/null
+++ b/helix-tui/tests/widgets_list.rs
@@ -0,0 +1,88 @@
+use helix_tui::{
+ backend::TestBackend,
+ buffer::Buffer,
+ layout::Rect,
+ style::{Color, Style},
+ symbols,
+ widgets::{Block, Borders, List, ListItem, ListState},
+ Terminal,
+};
+
+#[test]
+fn widgets_list_should_highlight_the_selected_item() {
+ let backend = TestBackend::new(10, 3);
+ let mut terminal = Terminal::new(backend).unwrap();
+ let mut state = ListState::default();
+ state.select(Some(1));
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let items = vec![
+ ListItem::new("Item 1"),
+ ListItem::new("Item 2"),
+ ListItem::new("Item 3"),
+ ];
+ let list = List::new(items)
+ .highlight_style(Style::default().bg(Color::Yellow))
+ .highlight_symbol(">> ");
+ f.render_stateful_widget(list, size, &mut state);
+ })
+ .unwrap();
+ let mut expected = Buffer::with_lines(vec![" Item 1 ", ">> Item 2 ", " Item 3 "]);
+ for x in 0..10 {
+ expected.get_mut(x, 1).set_bg(Color::Yellow);
+ }
+ terminal.backend().assert_buffer(&expected);
+}
+
+#[test]
+fn widgets_list_should_truncate_items() {
+ let backend = TestBackend::new(10, 2);
+ let mut terminal = Terminal::new(backend).unwrap();
+
+ struct TruncateTestCase<'a> {
+ selected: Option<usize>,
+ items: Vec<ListItem<'a>>,
+ expected: Buffer,
+ }
+
+ let cases = vec![
+ // An item is selected
+ TruncateTestCase {
+ selected: Some(0),
+ items: vec![
+ ListItem::new("A very long line"),
+ ListItem::new("A very long line"),
+ ],
+ expected: Buffer::with_lines(vec![
+ format!(">> A ve{} ", symbols::line::VERTICAL),
+ format!(" A ve{} ", symbols::line::VERTICAL),
+ ]),
+ },
+ // No item is selected
+ TruncateTestCase {
+ selected: None,
+ items: vec![
+ ListItem::new("A very long line"),
+ ListItem::new("A very long line"),
+ ],
+ expected: Buffer::with_lines(vec![
+ format!("A very {} ", symbols::line::VERTICAL),
+ format!("A very {} ", symbols::line::VERTICAL),
+ ]),
+ },
+ ];
+ for case in cases {
+ let mut state = ListState::default();
+ state.select(case.selected);
+ terminal
+ .draw(|f| {
+ let list = List::new(case.items.clone())
+ .block(Block::default().borders(Borders::RIGHT))
+ .highlight_symbol(">> ");
+ f.render_stateful_widget(list, Rect::new(0, 0, 8, 2), &mut state);
+ })
+ .unwrap();
+ terminal.backend().assert_buffer(&case.expected);
+ }
+}
diff --git a/helix-tui/tests/widgets_paragraph.rs b/helix-tui/tests/widgets_paragraph.rs
new file mode 100644
index 00000000..33d693d8
--- /dev/null
+++ b/helix-tui/tests/widgets_paragraph.rs
@@ -0,0 +1,220 @@
+use helix_tui::{
+ backend::TestBackend,
+ buffer::Buffer,
+ layout::Alignment,
+ text::{Span, Spans, Text},
+ widgets::{Block, Borders, Paragraph, Wrap},
+ Terminal,
+};
+
+const SAMPLE_STRING: &str = "The library is based on the principle of immediate rendering with \
+ intermediate buffers. This means that at each new frame you should build all widgets that are \
+ supposed to be part of the UI. While providing a great flexibility for rich and \
+ interactive UI, this may introduce overhead for highly dynamic content.";
+
+#[test]
+fn widgets_paragraph_can_wrap_its_content() {
+ let test_case = |alignment, expected| {
+ let backend = TestBackend::new(20, 10);
+ let mut terminal = Terminal::new(backend).unwrap();
+
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let text = vec![Spans::from(SAMPLE_STRING)];
+ let paragraph = Paragraph::new(text)
+ .block(Block::default().borders(Borders::ALL))
+ .alignment(alignment)
+ .wrap(Wrap { trim: true });
+ f.render_widget(paragraph, size);
+ })
+ .unwrap();
+ terminal.backend().assert_buffer(&expected);
+ };
+
+ test_case(
+ Alignment::Left,
+ Buffer::with_lines(vec![
+ "┌──────────────────┐",
+ "│The library is │",
+ "│based on the │",
+ "│principle of │",
+ "│immediate │",
+ "│rendering with │",
+ "│intermediate │",
+ "│buffers. This │",
+ "│means that at each│",
+ "└──────────────────┘",
+ ]),
+ );
+ test_case(
+ Alignment::Right,
+ Buffer::with_lines(vec![
+ "┌──────────────────┐",
+ "│ The library is│",
+ "│ based on the│",
+ "│ principle of│",
+ "│ immediate│",
+ "│ rendering with│",
+ "│ intermediate│",
+ "│ buffers. This│",
+ "│means that at each│",
+ "└──────────────────┘",
+ ]),
+ );
+ test_case(
+ Alignment::Center,
+ Buffer::with_lines(vec![
+ "┌──────────────────┐",
+ "│ The library is │",
+ "│ based on the │",
+ "│ principle of │",
+ "│ immediate │",
+ "│ rendering with │",
+ "│ intermediate │",
+ "│ buffers. This │",
+ "│means that at each│",
+ "└──────────────────┘",
+ ]),
+ );
+}
+
+#[test]
+fn widgets_paragraph_renders_double_width_graphemes() {
+ let backend = TestBackend::new(10, 10);
+ let mut terminal = Terminal::new(backend).unwrap();
+
+ let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、";
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let text = vec![Spans::from(s)];
+ let paragraph = Paragraph::new(text)
+ .block(Block::default().borders(Borders::ALL))
+ .wrap(Wrap { trim: true });
+ f.render_widget(paragraph, size);
+ })
+ .unwrap();
+
+ let expected = Buffer::with_lines(vec![
+ "┌────────┐",
+ "│コンピュ│",
+ "│ータ上で│",
+ "│文字を扱│",
+ "│う場合、│",
+ "│典型的に│",
+ "│は文字に│",
+ "│よる通信│",
+ "│を行う場│",
+ "└────────┘",
+ ]);
+ terminal.backend().assert_buffer(&expected);
+}
+
+#[test]
+fn widgets_paragraph_renders_mixed_width_graphemes() {
+ let backend = TestBackend::new(10, 7);
+ let mut terminal = Terminal::new(backend).unwrap();
+
+ let s = "aコンピュータ上で文字を扱う場合、";
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let text = vec![Spans::from(s)];
+ let paragraph = Paragraph::new(text)
+ .block(Block::default().borders(Borders::ALL))
+ .wrap(Wrap { trim: true });
+ f.render_widget(paragraph, size);
+ })
+ .unwrap();
+
+ let expected = Buffer::with_lines(vec![
+ // The internal width is 8 so only 4 slots for double-width characters.
+ "┌────────┐",
+ "│aコンピ │", // Here we have 1 latin character so only 3 double-width ones can fit.
+ "│ュータ上│",
+ "│で文字を│",
+ "│扱う場合│",
+ "│、 │",
+ "└────────┘",
+ ]);
+ terminal.backend().assert_buffer(&expected);
+}
+
+#[test]
+fn widgets_paragraph_can_wrap_with_a_trailing_nbsp() {
+ let nbsp: &str = "\u{00a0}";
+ let line = Spans::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]);
+ let backend = TestBackend::new(20, 3);
+ let mut terminal = Terminal::new(backend).unwrap();
+ let expected = Buffer::with_lines(vec![
+ "┌──────────────────┐",
+ "│NBSP\u{00a0} │",
+ "└──────────────────┘",
+ ]);
+ terminal
+ .draw(|f| {
+ let size = f.size();
+
+ let paragraph = Paragraph::new(line).block(Block::default().borders(Borders::ALL));
+ f.render_widget(paragraph, size);
+ })
+ .unwrap();
+ terminal.backend().assert_buffer(&expected);
+}
+#[test]
+fn widgets_paragraph_can_scroll_horizontally() {
+ let test_case = |alignment, scroll, expected| {
+ let backend = TestBackend::new(20, 10);
+ let mut terminal = Terminal::new(backend).unwrap();
+
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let text = Text::from(
+ "段落现在可以水平滚动了!\nParagraph can scroll horizontally!\nShort line",
+ );
+ let paragraph = Paragraph::new(text)
+ .block(Block::default().borders(Borders::ALL))
+ .alignment(alignment)
+ .scroll(scroll);
+ f.render_widget(paragraph, size);
+ })
+ .unwrap();
+ terminal.backend().assert_buffer(&expected);
+ };
+
+ test_case(
+ Alignment::Left,
+ (0, 7),
+ Buffer::with_lines(vec![
+ "┌──────────────────┐",
+ "│在可以水平滚动了!│",
+ "│ph can scroll hori│",
+ "│ine │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "└──────────────────┘",
+ ]),
+ );
+ // only support Alignment::Left
+ test_case(
+ Alignment::Right,
+ (0, 7),
+ Buffer::with_lines(vec![
+ "┌──────────────────┐",
+ "│段落现在可以水平滚│",
+ "│Paragraph can scro│",
+ "│ Short line│",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "└──────────────────┘",
+ ]),
+ );
+}
diff --git a/helix-tui/tests/widgets_table.rs b/helix-tui/tests/widgets_table.rs
new file mode 100644
index 00000000..0e0b3003
--- /dev/null
+++ b/helix-tui/tests/widgets_table.rs
@@ -0,0 +1,717 @@
+use helix_tui::{
+ backend::TestBackend,
+ buffer::Buffer,
+ layout::Constraint,
+ style::{Color, Modifier, Style},
+ text::{Span, Spans},
+ widgets::{Block, Borders, Cell, Row, Table, TableState},
+ Terminal,
+};
+
+#[test]
+fn widgets_table_column_spacing_can_be_changed() {
+ let test_case = |column_spacing, expected| {
+ let backend = TestBackend::new(30, 10);
+ let mut terminal = Terminal::new(backend).unwrap();
+
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let table = Table::new(vec![
+ Row::new(vec!["Row11", "Row12", "Row13"]),
+ Row::new(vec!["Row21", "Row22", "Row23"]),
+ Row::new(vec!["Row31", "Row32", "Row33"]),
+ Row::new(vec!["Row41", "Row42", "Row43"]),
+ ])
+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
+ .block(Block::default().borders(Borders::ALL))
+ .widths(&[
+ Constraint::Length(5),
+ Constraint::Length(5),
+ Constraint::Length(5),
+ ])
+ .column_spacing(column_spacing);
+ f.render_widget(table, size);
+ })
+ .unwrap();
+ terminal.backend().assert_buffer(&expected);
+ };
+
+ // no space between columns
+ test_case(
+ 0,
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1Head2Head3 │",
+ "│ │",
+ "│Row11Row12Row13 │",
+ "│Row21Row22Row23 │",
+ "│Row31Row32Row33 │",
+ "│Row41Row42Row43 │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // one space between columns
+ test_case(
+ 1,
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1 Head2 Head3 │",
+ "│ │",
+ "│Row11 Row12 Row13 │",
+ "│Row21 Row22 Row23 │",
+ "│Row31 Row32 Row33 │",
+ "│Row41 Row42 Row43 │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // enough space to just not hide the third column
+ test_case(
+ 6,
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1 Head2 Head3 │",
+ "│ │",
+ "│Row11 Row12 Row13 │",
+ "│Row21 Row22 Row23 │",
+ "│Row31 Row32 Row33 │",
+ "│Row41 Row42 Row43 │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // enough space to hide part of the third column
+ test_case(
+ 7,
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1 Head2 Head│",
+ "│ │",
+ "│Row11 Row12 Row1│",
+ "│Row21 Row22 Row2│",
+ "│Row31 Row32 Row3│",
+ "│Row41 Row42 Row4│",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+}
+
+#[test]
+fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
+ let test_case = |widths, expected| {
+ let backend = TestBackend::new(30, 10);
+ let mut terminal = Terminal::new(backend).unwrap();
+
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let table = Table::new(vec![
+ Row::new(vec!["Row11", "Row12", "Row13"]),
+ Row::new(vec!["Row21", "Row22", "Row23"]),
+ Row::new(vec!["Row31", "Row32", "Row33"]),
+ Row::new(vec!["Row41", "Row42", "Row43"]),
+ ])
+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
+ .block(Block::default().borders(Borders::ALL))
+ .widths(widths);
+ f.render_widget(table, size);
+ })
+ .unwrap();
+ terminal.backend().assert_buffer(&expected);
+ };
+
+ // columns of zero width show nothing
+ test_case(
+ &[
+ Constraint::Length(0),
+ Constraint::Length(0),
+ Constraint::Length(0),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // columns of 1 width trim
+ test_case(
+ &[
+ Constraint::Length(1),
+ Constraint::Length(1),
+ Constraint::Length(1),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│H H H │",
+ "│ │",
+ "│R R R │",
+ "│R R R │",
+ "│R R R │",
+ "│R R R │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // columns of large width just before pushing a column off
+ test_case(
+ &[
+ Constraint::Length(8),
+ Constraint::Length(8),
+ Constraint::Length(8),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1 Head2 Head3 │",
+ "│ │",
+ "│Row11 Row12 Row13 │",
+ "│Row21 Row22 Row23 │",
+ "│Row31 Row32 Row33 │",
+ "│Row41 Row42 Row43 │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+}
+
+#[test]
+fn widgets_table_columns_widths_can_use_percentage_constraints() {
+ let test_case = |widths, expected| {
+ let backend = TestBackend::new(30, 10);
+ let mut terminal = Terminal::new(backend).unwrap();
+
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let table = Table::new(vec![
+ Row::new(vec!["Row11", "Row12", "Row13"]),
+ Row::new(vec!["Row21", "Row22", "Row23"]),
+ Row::new(vec!["Row31", "Row32", "Row33"]),
+ Row::new(vec!["Row41", "Row42", "Row43"]),
+ ])
+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
+ .block(Block::default().borders(Borders::ALL))
+ .widths(widths)
+ .column_spacing(0);
+ f.render_widget(table, size);
+ })
+ .unwrap();
+ terminal.backend().assert_buffer(&expected);
+ };
+
+ // columns of zero width show nothing
+ test_case(
+ &[
+ Constraint::Percentage(0),
+ Constraint::Percentage(0),
+ Constraint::Percentage(0),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // columns of not enough width trims the data
+ test_case(
+ &[
+ Constraint::Percentage(11),
+ Constraint::Percentage(11),
+ Constraint::Percentage(11),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│HeaHeaHea │",
+ "│ │",
+ "│RowRowRow │",
+ "│RowRowRow │",
+ "│RowRowRow │",
+ "│RowRowRow │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // columns of large width just before pushing a column off
+ test_case(
+ &[
+ Constraint::Percentage(33),
+ Constraint::Percentage(33),
+ Constraint::Percentage(33),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1 Head2 Head3 │",
+ "│ │",
+ "│Row11 Row12 Row13 │",
+ "│Row21 Row22 Row23 │",
+ "│Row31 Row32 Row33 │",
+ "│Row41 Row42 Row43 │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // percentages summing to 100 should give equal widths
+ test_case(
+ &[Constraint::Percentage(50), Constraint::Percentage(50)],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1 Head2 │",
+ "│ │",
+ "│Row11 Row12 │",
+ "│Row21 Row22 │",
+ "│Row31 Row32 │",
+ "│Row41 Row42 │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+}
+
+#[test]
+fn widgets_table_columns_widths_can_use_mixed_constraints() {
+ let test_case = |widths, expected| {
+ let backend = TestBackend::new(30, 10);
+ let mut terminal = Terminal::new(backend).unwrap();
+
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let table = Table::new(vec![
+ Row::new(vec!["Row11", "Row12", "Row13"]),
+ Row::new(vec!["Row21", "Row22", "Row23"]),
+ Row::new(vec!["Row31", "Row32", "Row33"]),
+ Row::new(vec!["Row41", "Row42", "Row43"]),
+ ])
+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
+ .block(Block::default().borders(Borders::ALL))
+ .widths(widths);
+ f.render_widget(table, size);
+ })
+ .unwrap();
+ terminal.backend().assert_buffer(&expected);
+ };
+
+ // columns of zero width show nothing
+ test_case(
+ &[
+ Constraint::Percentage(0),
+ Constraint::Length(0),
+ Constraint::Percentage(0),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // columns of not enough width trims the data
+ test_case(
+ &[
+ Constraint::Percentage(11),
+ Constraint::Length(20),
+ Constraint::Percentage(11),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Hea Head2 Hea│",
+ "│ │",
+ "│Row Row12 Row│",
+ "│Row Row22 Row│",
+ "│Row Row32 Row│",
+ "│Row Row42 Row│",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // columns of large width just before pushing a column off
+ test_case(
+ &[
+ Constraint::Percentage(33),
+ Constraint::Length(10),
+ Constraint::Percentage(33),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1 Head2 Head3 │",
+ "│ │",
+ "│Row11 Row12 Row13 │",
+ "│Row21 Row22 Row23 │",
+ "│Row31 Row32 Row33 │",
+ "│Row41 Row42 Row43 │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // columns of large size (>100% total) hide the last column
+ test_case(
+ &[
+ Constraint::Percentage(60),
+ Constraint::Length(10),
+ Constraint::Percentage(60),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1 Head2 │",
+ "│ │",
+ "│Row11 Row12 │",
+ "│Row21 Row22 │",
+ "│Row31 Row32 │",
+ "│Row41 Row42 │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+}
+
+#[test]
+fn widgets_table_columns_widths_can_use_ratio_constraints() {
+ let test_case = |widths, expected| {
+ let backend = TestBackend::new(30, 10);
+ let mut terminal = Terminal::new(backend).unwrap();
+
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let table = Table::new(vec![
+ Row::new(vec!["Row11", "Row12", "Row13"]),
+ Row::new(vec!["Row21", "Row22", "Row23"]),
+ Row::new(vec!["Row31", "Row32", "Row33"]),
+ Row::new(vec!["Row41", "Row42", "Row43"]),
+ ])
+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
+ .block(Block::default().borders(Borders::ALL))
+ .widths(widths)
+ .column_spacing(0);
+ f.render_widget(table, size);
+ })
+ .unwrap();
+ terminal.backend().assert_buffer(&expected);
+ };
+
+ // columns of zero width show nothing
+ test_case(
+ &[
+ Constraint::Ratio(0, 1),
+ Constraint::Ratio(0, 1),
+ Constraint::Ratio(0, 1),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // columns of not enough width trims the data
+ test_case(
+ &[
+ Constraint::Ratio(1, 9),
+ Constraint::Ratio(1, 9),
+ Constraint::Ratio(1, 9),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│HeaHeaHea │",
+ "│ │",
+ "│RowRowRow │",
+ "│RowRowRow │",
+ "│RowRowRow │",
+ "│RowRowRow │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // columns of large width just before pushing a column off
+ test_case(
+ &[
+ Constraint::Ratio(1, 3),
+ Constraint::Ratio(1, 3),
+ Constraint::Ratio(1, 3),
+ ],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1 Head2 Head3 │",
+ "│ │",
+ "│Row11 Row12 Row13 │",
+ "│Row21 Row22 Row23 │",
+ "│Row31 Row32 Row33 │",
+ "│Row41 Row42 Row43 │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // percentages summing to 100 should give equal widths
+ test_case(
+ &[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1 Head2 │",
+ "│ │",
+ "│Row11 Row12 │",
+ "│Row21 Row22 │",
+ "│Row31 Row32 │",
+ "│Row41 Row42 │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+}
+
+#[test]
+fn widgets_table_can_have_rows_with_multi_lines() {
+ let test_case = |state: &mut TableState, expected: Buffer| {
+ let backend = TestBackend::new(30, 8);
+ let mut terminal = Terminal::new(backend).unwrap();
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let table = Table::new(vec![
+ Row::new(vec!["Row11", "Row12", "Row13"]),
+ Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
+ Row::new(vec!["Row31", "Row32", "Row33"]),
+ Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
+ ])
+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
+ .block(Block::default().borders(Borders::ALL))
+ .highlight_symbol(">> ")
+ .widths(&[
+ Constraint::Length(5),
+ Constraint::Length(5),
+ Constraint::Length(5),
+ ])
+ .column_spacing(1);
+ f.render_stateful_widget(table, size, state);
+ })
+ .unwrap();
+ terminal.backend().assert_buffer(&expected);
+ };
+
+ let mut state = TableState::default();
+ // no selection
+ test_case(
+ &mut state,
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│Head1 Head2 Head3 │",
+ "│ │",
+ "│Row11 Row12 Row13 │",
+ "│Row21 Row22 Row23 │",
+ "│ │",
+ "│Row31 Row32 Row33 │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // select first
+ state.select(Some(0));
+ test_case(
+ &mut state,
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│ Head1 Head2 Head3 │",
+ "│ │",
+ "│>> Row11 Row12 Row13 │",
+ "│ Row21 Row22 Row23 │",
+ "│ │",
+ "│ Row31 Row32 Row33 │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // select second (we don't show partially the 4th row)
+ state.select(Some(1));
+ test_case(
+ &mut state,
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│ Head1 Head2 Head3 │",
+ "│ │",
+ "│ Row11 Row12 Row13 │",
+ "│>> Row21 Row22 Row23 │",
+ "│ │",
+ "│ Row31 Row32 Row33 │",
+ "└────────────────────────────┘",
+ ]),
+ );
+
+ // select 4th (we don't show partially the 1st row)
+ state.select(Some(3));
+ test_case(
+ &mut state,
+ Buffer::with_lines(vec![
+ "┌────────────────────────────┐",
+ "│ Head1 Head2 Head3 │",
+ "│ │",
+ "│ Row31 Row32 Row33 │",
+ "│>> Row41 Row42 Row43 │",
+ "│ │",
+ "│ │",
+ "└────────────────────────────┘",
+ ]),
+ );
+}
+
+#[test]
+fn widgets_table_can_have_elements_styled_individually() {
+ let backend = TestBackend::new(30, 4);
+ let mut terminal = Terminal::new(backend).unwrap();
+ let mut state = TableState::default();
+ state.select(Some(0));
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let table = Table::new(vec![
+ Row::new(vec!["Row11", "Row12", "Row13"]).style(Style::default().fg(Color::Green)),
+ Row::new(vec![
+ Cell::from("Row21"),
+ Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
+ Cell::from(Spans::from(vec![
+ Span::raw("Row"),
+ Span::styled("23", Style::default().fg(Color::Blue)),
+ ]))
+ .style(Style::default().fg(Color::Red)),
+ ])
+ .style(Style::default().fg(Color::LightGreen)),
+ ])
+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
+ .block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
+ .highlight_symbol(">> ")
+ .highlight_style(Style::default().add_modifier(Modifier::BOLD))
+ .widths(&[
+ Constraint::Length(6),
+ Constraint::Length(6),
+ Constraint::Length(6),
+ ])
+ .column_spacing(1);
+ f.render_stateful_widget(table, size, &mut state);
+ })
+ .unwrap();
+
+ let mut expected = Buffer::with_lines(vec![
+ "│ Head1 Head2 Head3 │",
+ "│ │",
+ "│>> Row11 Row12 Row13 │",
+ "│ Row21 Row22 Row23 │",
+ ]);
+ // First row = row color + highlight style
+ for col in 1..=28 {
+ expected.get_mut(col, 2).set_style(
+ Style::default()
+ .fg(Color::Green)
+ .add_modifier(Modifier::BOLD),
+ );
+ }
+ // Second row:
+ // 1. row color
+ for col in 1..=28 {
+ expected
+ .get_mut(col, 3)
+ .set_style(Style::default().fg(Color::LightGreen));
+ }
+ // 2. cell color
+ for col in 11..=16 {
+ expected
+ .get_mut(col, 3)
+ .set_style(Style::default().fg(Color::Yellow));
+ }
+ for col in 18..=23 {
+ expected
+ .get_mut(col, 3)
+ .set_style(Style::default().fg(Color::Red));
+ }
+ // 3. text color
+ for col in 21..=22 {
+ expected
+ .get_mut(col, 3)
+ .set_style(Style::default().fg(Color::Blue));
+ }
+ terminal.backend().assert_buffer(&expected);
+}
+
+#[test]
+fn widgets_table_should_render_even_if_empty() {
+ let backend = TestBackend::new(30, 4);
+ let mut terminal = Terminal::new(backend).unwrap();
+ terminal
+ .draw(|f| {
+ let size = f.size();
+ let table = Table::new(vec![])
+ .header(Row::new(vec!["Head1", "Head2", "Head3"]))
+ .block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
+ .widths(&[
+ Constraint::Length(6),
+ Constraint::Length(6),
+ Constraint::Length(6),
+ ])
+ .column_spacing(1);
+ f.render_widget(table, size);
+ })
+ .unwrap();
+
+ let expected = Buffer::with_lines(vec![
+ "│Head1 Head2 Head3 │",
+ "│ │",
+ "│ │",
+ "│ │",
+ ]);
+
+ terminal.backend().assert_buffer(&expected);
+}
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index b9d07d01..df87fafb 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -16,7 +16,7 @@ helix-core = { path = "../helix-core" }
helix-lsp = { path = "../helix-lsp"}
# Conversion traits
-tui = { version = "0.15", default-features = false, features = ["crossterm"], optional = true}
+tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"], optional = true }
crossterm = { version = "0.19", features = ["event-stream"], optional = true }
once_cell = "1.4"
url = "2"