on master help sign in
Inline tui as helix-tui fork.

We only rely on some of the rendering primitives and implement our Cursive-style compositor on top.

commit35606a3daa7ee273845a12f3e03728e0ae23928e authorBlaž Hrastnik <blaz@mxxn.io> dateSun 9 May 2021, 17:52 (5 years ago) parent6c705f0
31 files changed+6200−25
Cargo.lock+14−15 Cargo.toml+1 helix-term/Cargo.toml+1−1 helix-term/src/compositor.rs+1−1 helix-term/src/main.rs−1 helix-tui/.gitignore+6 helix-tui/Cargo.toml+19 helix-tui/README.md+6 helix-tui/src/backend/crossterm.rs+221 helix-tui/src/backend/mod.rs+25 helix-tui/src/backend/test.rs+151 helix-tui/src/buffer.rs+745 helix-tui/src/layout.rs+534 helix-tui/src/lib.rs+134 helix-tui/src/style.rs+281 helix-tui/src/symbols.rs+233 helix-term/src/terminal.rs → helix-tui/src/terminal.rs+1−6 helix-tui/src/text.rs+434 helix-tui/src/widgets/block.rs+511 helix-tui/src/widgets/clear.rs+36 helix-tui/src/widgets/list.rs+249 helix-tui/src/widgets/mod.rs+53 helix-tui/src/widgets/paragraph.rs+197 helix-tui/src/widgets/reflow.rs+534 helix-tui/src/widgets/table.rs+538 helix-tui/tests/terminal.rs+36 helix-tui/tests/widgets_block.rs+213 helix-tui/tests/widgets_list.rs+88 helix-tui/tests/widgets_paragraph.rs+220 helix-tui/tests/widgets_table.rs+717 helix-view/Cargo.toml+1−1
Cargo.lock+14−15
@@ -330,6 +330,7 @@ dependencies = [
330330 "fuzzy-matcher",
331331 "helix-core",
332332 "helix-lsp",
333+ "helix-tui",
333334 "helix-view",
334335 "ignore",
335336 "log",
@@ -340,7 +341,18 @@ dependencies = [
340341 "serde_json",
341342 "tokio",
342343 "toml",
343− "tui",
344+]
345+
346+[[package]]
347+name = "helix-tui"
348+version = "0.1.0"
349+dependencies = [
350+ "bitflags",
351+ "cassowary",
352+ "crossterm",
353+ "serde",
354+ "unicode-segmentation",
355+ "unicode-width",
344356]
345357
346358[[package]]
@@ -351,12 +363,12 @@ dependencies = [
351363 "crossterm",
352364 "helix-core",
353365 "helix-lsp",
366+ "helix-tui",
354367 "once_cell",
355368 "serde",
356369 "slotmap",
357370 "tokio",
358371 "toml",
359− "tui",
360372 "url",
361373]
362374
@@ -955,19 +967,6 @@ dependencies = [
955967 "regex",
956968]
957969
958−[[package]]
959−name = "tui"
960−version = "0.15.0"
961−source = "registry+https://github.com/rust-lang/crates.io-index"
962−checksum = "861d8f3ad314ede6219bcb2ab844054b1de279ee37a9bc38e3d606f9d3fb2a71"
963−dependencies = [
964− "bitflags",
965− "cassowary",
966− "crossterm",
967− "unicode-segmentation",
968− "unicode-width",
969−]
970−
971970[[package]]
972971name = "unicase"
973972version = "2.6.0"
Cargo.toml+1
@@ -3,6 +3,7 @@ members = [
33 "helix-core",
44 "helix-view",
55 "helix-term",
6+ "helix-tui",
67 "helix-syntax",
78 "helix-lsp",
89]
helix-term/Cargo.toml+1−1
@@ -21,7 +21,7 @@ once_cell = "1.4"
2121
2222tokio = { version = "1", features = ["full"] }
2323num_cpus = "1"
24−tui = { version = "0.15", default-features = false, features = ["crossterm"] }
24+tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
2525crossterm = { version = "0.19", features = ["event-stream"] }
2626clap = { version = "3.0.0-beta.2 ", default-features = false, features = ["std", "cargo"] }
2727
helix-term/src/compositor.rs+1−1
@@ -67,7 +67,7 @@ pub trait Component: Any + AnyComponent {
6767use anyhow::Error;
6868use std::io::stdout;
6969use tui::backend::CrosstermBackend;
70−type Terminal = crate::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
70+type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
7171
7272pub struct Compositor {
7373 layers: Vec<Box<dyn Component>>,
helix-term/src/main.rs−1
@@ -4,7 +4,6 @@ mod application;
44mod commands;
55mod compositor;
66mod keymap;
7−mod terminal;
87mod ui;
98
109use application::Application;
helix-tui/.gitignorenew file+6
@@ -0,0 +1,6 @@
1+target
2+Cargo.lock
3+*.log
4+*.rs.rustfmt
5+.gdb_history
6+.idea/
helix-tui/Cargo.tomlnew file+19
@@ -0,0 +1,19 @@
1+[package]
2+name = "helix-tui"
3+version = "0.1.0"
4+authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5+description = """
6+A library to build rich terminal user interfaces or dashboards
7+"""
8+edition = "2018"
9+
10+[features]
11+default = ["crossterm"]
12+
13+[dependencies]
14+bitflags = "1.0"
15+cassowary = "0.3"
16+unicode-segmentation = "1.2"
17+unicode-width = "0.1"
18+crossterm = { version = "0.19", optional = true }
19+serde = { version = "1", "optional" = true, features = ["derive"]}
helix-tui/README.mdnew file+6
@@ -0,0 +1,6 @@
1+# helix-tui
2+
3+This library is a fork of the great library
4+[tui-rs](https://github.com/fdehau/tui-rs/). We've mainly relied on the double
5+buffer implementation and render diffing, side-stepping it's widget and
6+layouting.
helix-tui/src/backend/crossterm.rsnew file+221
@@ -0,0 +1,221 @@
1+use crate::{
2+ backend::Backend,
3+ buffer::Cell,
4+ layout::Rect,
5+ style::{Color, Modifier},
6+};
7+use crossterm::{
8+ cursor::{Hide, MoveTo, Show},
9+ execute, queue,
10+ style::{
11+ Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
12+ SetForegroundColor,
13+ },
14+ terminal::{self, Clear, ClearType},
15+};
16+use std::io::{self, Write};
17+
18+pub struct CrosstermBackend<W: Write> {
19+ buffer: W,
20+}
21+
22+impl<W> CrosstermBackend<W>
23+where
24+ W: Write,
25+{
26+ pub fn new(buffer: W) -> CrosstermBackend<W> {
27+ CrosstermBackend { buffer }
28+ }
29+}
30+
31+impl<W> Write for CrosstermBackend<W>
32+where
33+ W: Write,
34+{
35+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
36+ self.buffer.write(buf)
37+ }
38+
39+ fn flush(&mut self) -> io::Result<()> {
40+ self.buffer.flush()
41+ }
42+}
43+
44+impl<W> Backend for CrosstermBackend<W>
45+where
46+ W: Write,
47+{
48+ fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
49+ where
50+ I: Iterator<Item = (u16, u16, &'a Cell)>,
51+ {
52+ let mut fg = Color::Reset;
53+ let mut bg = Color::Reset;
54+ let mut modifier = Modifier::empty();
55+ let mut last_pos: Option<(u16, u16)> = None;
56+ for (x, y, cell) in content {
57+ // Move the cursor if the previous location was not (x - 1, y)
58+ if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
59+ map_error(queue!(self.buffer, MoveTo(x, y)))?;
60+ }
61+ last_pos = Some((x, y));
62+ if cell.modifier != modifier {
63+ let diff = ModifierDiff {
64+ from: modifier,
65+ to: cell.modifier,
66+ };
67+ diff.queue(&mut self.buffer)?;
68+ modifier = cell.modifier;
69+ }
70+ if cell.fg != fg {
71+ let color = CColor::from(cell.fg);
72+ map_error(queue!(self.buffer, SetForegroundColor(color)))?;
73+ fg = cell.fg;
74+ }
75+ if cell.bg != bg {
76+ let color = CColor::from(cell.bg);
77+ map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
78+ bg = cell.bg;
79+ }
80+
81+ map_error(queue!(self.buffer, Print(&cell.symbol)))?;
82+ }
83+
84+ map_error(queue!(
85+ self.buffer,
86+ SetForegroundColor(CColor::Reset),
87+ SetBackgroundColor(CColor::Reset),
88+ SetAttribute(CAttribute::Reset)
89+ ))
90+ }
91+
92+ fn hide_cursor(&mut self) -> io::Result<()> {
93+ map_error(execute!(self.buffer, Hide))
94+ }
95+
96+ fn show_cursor(&mut self) -> io::Result<()> {
97+ map_error(execute!(self.buffer, Show))
98+ }
99+
100+ fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
101+ crossterm::cursor::position()
102+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
103+ }
104+
105+ fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
106+ map_error(execute!(self.buffer, MoveTo(x, y)))
107+ }
108+
109+ fn clear(&mut self) -> io::Result<()> {
110+ map_error(execute!(self.buffer, Clear(ClearType::All)))
111+ }
112+
113+ fn size(&self) -> io::Result<Rect> {
114+ let (width, height) =
115+ terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
116+
117+ Ok(Rect::new(0, 0, width, height))
118+ }
119+
120+ fn flush(&mut self) -> io::Result<()> {
121+ self.buffer.flush()
122+ }
123+}
124+
125+fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
126+ error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
127+}
128+
129+impl From<Color> for CColor {
130+ fn from(color: Color) -> Self {
131+ match color {
132+ Color::Reset => CColor::Reset,
133+ Color::Black => CColor::Black,
134+ Color::Red => CColor::DarkRed,
135+ Color::Green => CColor::DarkGreen,
136+ Color::Yellow => CColor::DarkYellow,
137+ Color::Blue => CColor::DarkBlue,
138+ Color::Magenta => CColor::DarkMagenta,
139+ Color::Cyan => CColor::DarkCyan,
140+ Color::Gray => CColor::Grey,
141+ Color::DarkGray => CColor::DarkGrey,
142+ Color::LightRed => CColor::Red,
143+ Color::LightGreen => CColor::Green,
144+ Color::LightBlue => CColor::Blue,
145+ Color::LightYellow => CColor::Yellow,
146+ Color::LightMagenta => CColor::Magenta,
147+ Color::LightCyan => CColor::Cyan,
148+ Color::White => CColor::White,
149+ Color::Indexed(i) => CColor::AnsiValue(i),
150+ Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
151+ }
152+ }
153+}
154+
155+#[derive(Debug)]
156+struct ModifierDiff {
157+ pub from: Modifier,
158+ pub to: Modifier,
159+}
160+
161+impl ModifierDiff {
162+ fn queue<W>(&self, mut w: W) -> io::Result<()>
163+ where
164+ W: io::Write,
165+ {
166+ //use crossterm::Attribute;
167+ let removed = self.from - self.to;
168+ if removed.contains(Modifier::REVERSED) {
169+ map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
170+ }
171+ if removed.contains(Modifier::BOLD) {
172+ map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
173+ if self.to.contains(Modifier::DIM) {
174+ map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
175+ }
176+ }
177+ if removed.contains(Modifier::ITALIC) {
178+ map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
179+ }
180+ if removed.contains(Modifier::UNDERLINED) {
181+ map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
182+ }
183+ if removed.contains(Modifier::DIM) {
184+ map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
185+ }
186+ if removed.contains(Modifier::CROSSED_OUT) {
187+ map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
188+ }
189+ if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
190+ map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
191+ }
192+
193+ let added = self.to - self.from;
194+ if added.contains(Modifier::REVERSED) {
195+ map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
196+ }
197+ if added.contains(Modifier::BOLD) {
198+ map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
199+ }
200+ if added.contains(Modifier::ITALIC) {
201+ map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
202+ }
203+ if added.contains(Modifier::UNDERLINED) {
204+ map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
205+ }
206+ if added.contains(Modifier::DIM) {
207+ map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
208+ }
209+ if added.contains(Modifier::CROSSED_OUT) {
210+ map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
211+ }
212+ if added.contains(Modifier::SLOW_BLINK) {
213+ map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
214+ }
215+ if added.contains(Modifier::RAPID_BLINK) {
216+ map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
217+ }
218+
219+ Ok(())
220+ }
221+}
helix-tui/src/backend/mod.rsnew file+25
@@ -0,0 +1,25 @@
1+use std::io;
2+
3+use crate::buffer::Cell;
4+use crate::layout::Rect;
5+
6+#[cfg(feature = "crossterm")]
7+mod crossterm;
8+#[cfg(feature = "crossterm")]
9+pub use self::crossterm::CrosstermBackend;
10+
11+mod test;
12+pub use self::test::TestBackend;
13+
14+pub trait Backend {
15+ fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
16+ where
17+ I: Iterator<Item = (u16, u16, &'a Cell)>;
18+ fn hide_cursor(&mut self) -> Result<(), io::Error>;
19+ fn show_cursor(&mut self) -> Result<(), io::Error>;
20+ fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
21+ fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
22+ fn clear(&mut self) -> Result<(), io::Error>;
23+ fn size(&self) -> Result<Rect, io::Error>;
24+ fn flush(&mut self) -> Result<(), io::Error>;
25+}
helix-tui/src/backend/test.rsnew file+151
@@ -0,0 +1,151 @@
1+use crate::{
2+ backend::Backend,
3+ buffer::{Buffer, Cell},
4+ layout::Rect,
5+};
6+use std::{fmt::Write, io};
7+use unicode_width::UnicodeWidthStr;
8+
9+/// A backend used for the integration tests.
10+#[derive(Debug)]
11+pub struct TestBackend {
12+ width: u16,
13+ buffer: Buffer,
14+ height: u16,
15+ cursor: bool,
16+ pos: (u16, u16),
17+}
18+
19+/// Returns a string representation of the given buffer for debugging purpose.
20+fn buffer_view(buffer: &Buffer) -> String {
21+ let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
22+ for cells in buffer.content.chunks(buffer.area.width as usize) {
23+ let mut overwritten = vec![];
24+ let mut skip: usize = 0;
25+ view.push('"');
26+ for (x, c) in cells.iter().enumerate() {
27+ if skip == 0 {
28+ view.push_str(&c.symbol);
29+ } else {
30+ overwritten.push((x, &c.symbol))
31+ }
32+ skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
33+ }
34+ view.push('"');
35+ if !overwritten.is_empty() {
36+ write!(
37+ &mut view,
38+ " Hidden by multi-width symbols: {:?}",
39+ overwritten
40+ )
41+ .unwrap();
42+ }
43+ view.push('\n');
44+ }
45+ view
46+}
47+
48+impl TestBackend {
49+ pub fn new(width: u16, height: u16) -> TestBackend {
50+ TestBackend {
51+ width,
52+ height,
53+ buffer: Buffer::empty(Rect::new(0, 0, width, height)),
54+ cursor: false,
55+ pos: (0, 0),
56+ }
57+ }
58+
59+ pub fn buffer(&self) -> &Buffer {
60+ &self.buffer
61+ }
62+
63+ pub fn resize(&mut self, width: u16, height: u16) {
64+ self.buffer.resize(Rect::new(0, 0, width, height));
65+ self.width = width;
66+ self.height = height;
67+ }
68+
69+ pub fn assert_buffer(&self, expected: &Buffer) {
70+ assert_eq!(expected.area, self.buffer.area);
71+ let diff = expected.diff(&self.buffer);
72+ if diff.is_empty() {
73+ return;
74+ }
75+
76+ let mut debug_info = String::from("Buffers are not equal");
77+ debug_info.push('\n');
78+ debug_info.push_str("Expected:");
79+ debug_info.push('\n');
80+ let expected_view = buffer_view(expected);
81+ debug_info.push_str(&expected_view);
82+ debug_info.push('\n');
83+ debug_info.push_str("Got:");
84+ debug_info.push('\n');
85+ let view = buffer_view(&self.buffer);
86+ debug_info.push_str(&view);
87+ debug_info.push('\n');
88+
89+ debug_info.push_str("Diff:");
90+ debug_info.push('\n');
91+ let nice_diff = diff
92+ .iter()
93+ .enumerate()
94+ .map(|(i, (x, y, cell))| {
95+ let expected_cell = expected.get(*x, *y);
96+ format!(
97+ "{}: at ({}, {}) expected {:?} got {:?}",
98+ i, x, y, expected_cell, cell
99+ )
100+ })
101+ .collect::<Vec<String>>()
102+ .join("\n");
103+ debug_info.push_str(&nice_diff);
104+ panic!("{}", debug_info);
105+ }
106+}
107+
108+impl Backend for TestBackend {
109+ fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
110+ where
111+ I: Iterator<Item = (u16, u16, &'a Cell)>,
112+ {
113+ for (x, y, c) in content {
114+ let cell = self.buffer.get_mut(x, y);
115+ *cell = c.clone();
116+ }
117+ Ok(())
118+ }
119+
120+ fn hide_cursor(&mut self) -> Result<(), io::Error> {
121+ self.cursor = false;
122+ Ok(())
123+ }
124+
125+ fn show_cursor(&mut self) -> Result<(), io::Error> {
126+ self.cursor = true;
127+ Ok(())
128+ }
129+
130+ fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
131+ Ok(self.pos)
132+ }
133+
134+ fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
135+ self.pos = (x, y);
136+ Ok(())
137+ }
138+
139+ fn clear(&mut self) -> Result<(), io::Error> {
140+ self.buffer.reset();
141+ Ok(())
142+ }
143+
144+ fn size(&self) -> Result<Rect, io::Error> {
145+ Ok(Rect::new(0, 0, self.width, self.height))
146+ }
147+
148+ fn flush(&mut self) -> Result<(), io::Error> {
149+ Ok(())
150+ }
151+}
helix-tui/src/buffer.rsnew file+745
@@ -0,0 +1,745 @@
1+use crate::{
2+ layout::Rect,
3+ style::{Color, Modifier, Style},
4+ text::{Span, Spans},
5+};
6+use std::cmp::min;
7+use unicode_segmentation::UnicodeSegmentation;
8+use unicode_width::UnicodeWidthStr;
9+
10+/// A buffer cell
11+#[derive(Debug, Clone, PartialEq)]
12+pub struct Cell {
13+ pub symbol: String,
14+ pub fg: Color,
15+ pub bg: Color,
16+ pub modifier: Modifier,
17+}
18+
19+impl Cell {
20+ pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
21+ self.symbol.clear();
22+ self.symbol.push_str(symbol);
23+ self
24+ }
25+
26+ pub fn set_char(&mut self, ch: char) -> &mut Cell {
27+ self.symbol.clear();
28+ self.symbol.push(ch);
29+ self
30+ }
31+
32+ pub fn set_fg(&mut self, color: Color) -> &mut Cell {
33+ self.fg = color;
34+ self
35+ }
36+
37+ pub fn set_bg(&mut self, color: Color) -> &mut Cell {
38+ self.bg = color;
39+ self
40+ }
41+
42+ pub fn set_style(&mut self, style: Style) -> &mut Cell {
43+ if let Some(c) = style.fg {
44+ self.fg = c;
45+ }
46+ if let Some(c) = style.bg {
47+ self.bg = c;
48+ }
49+ self.modifier.insert(style.add_modifier);
50+ self.modifier.remove(style.sub_modifier);
51+ self
52+ }
53+
54+ pub fn style(&self) -> Style {
55+ Style::default()
56+ .fg(self.fg)
57+ .bg(self.bg)
58+ .add_modifier(self.modifier)
59+ }
60+
61+ pub fn reset(&mut self) {
62+ self.symbol.clear();
63+ self.symbol.push(' ');
64+ self.fg = Color::Reset;
65+ self.bg = Color::Reset;
66+ self.modifier = Modifier::empty();
67+ }
68+}
69+
70+impl Default for Cell {
71+ fn default() -> Cell {
72+ Cell {
73+ symbol: " ".into(),
74+ fg: Color::Reset,
75+ bg: Color::Reset,
76+ modifier: Modifier::empty(),
77+ }
78+ }
79+}
80+
81+/// A buffer that maps to the desired content of the terminal after the draw call
82+///
83+/// No widget in the library interacts directly with the terminal. Instead each of them is required
84+/// to draw their state to an intermediate buffer. It is basically a grid where each cell contains
85+/// a grapheme, a foreground color and a background color. This grid will then be used to output
86+/// the appropriate escape sequences and characters to draw the UI as the user has defined it.
87+///
88+/// # Examples:
89+///
90+/// ```
91+/// use helix_tui::buffer::{Buffer, Cell};
92+/// use helix_tui::layout::Rect;
93+/// use helix_tui::style::{Color, Style, Modifier};
94+///
95+/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
96+/// buf.get_mut(0, 2).set_symbol("x");
97+/// assert_eq!(buf.get(0, 2).symbol, "x");
98+/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
99+/// assert_eq!(buf.get(5, 0), &Cell{
100+/// symbol: String::from("r"),
101+/// fg: Color::Red,
102+/// bg: Color::White,
103+/// modifier: Modifier::empty()
104+/// });
105+/// buf.get_mut(5, 0).set_char('x');
106+/// assert_eq!(buf.get(5, 0).symbol, "x");
107+/// ```
108+#[derive(Debug, Clone, PartialEq)]
109+pub struct Buffer {
110+ /// The area represented by this buffer
111+ pub area: Rect,
112+ /// The content of the buffer. The length of this Vec should always be equal to area.width *
113+ /// area.height
114+ pub content: Vec<Cell>,
115+}
116+
117+impl Default for Buffer {
118+ fn default() -> Buffer {
119+ Buffer {
120+ area: Default::default(),
121+ content: Vec::new(),
122+ }
123+ }
124+}
125+
126+impl Buffer {
127+ /// Returns a Buffer with all cells set to the default one
128+ pub fn empty(area: Rect) -> Buffer {
129+ let cell: Cell = Default::default();
130+ Buffer::filled(area, &cell)
131+ }
132+
133+ /// Returns a Buffer with all cells initialized with the attributes of the given Cell
134+ pub fn filled(area: Rect, cell: &Cell) -> Buffer {
135+ let size = area.area() as usize;
136+ let mut content = Vec::with_capacity(size);
137+ for _ in 0..size {
138+ content.push(cell.clone());
139+ }
140+ Buffer { area, content }
141+ }
142+
143+ /// Returns a Buffer containing the given lines
144+ pub fn with_lines<S>(lines: Vec<S>) -> Buffer
145+ where
146+ S: AsRef<str>,
147+ {
148+ let height = lines.len() as u16;
149+ let width = lines
150+ .iter()
151+ .map(|i| i.as_ref().width() as u16)
152+ .max()
153+ .unwrap_or_default();
154+ let mut buffer = Buffer::empty(Rect {
155+ x: 0,
156+ y: 0,
157+ width,
158+ height,
159+ });
160+ for (y, line) in lines.iter().enumerate() {
161+ buffer.set_string(0, y as u16, line, Style::default());
162+ }
163+ buffer
164+ }
165+
166+ /// Returns the content of the buffer as a slice
167+ pub fn content(&self) -> &[Cell] {
168+ &self.content
169+ }
170+
171+ /// Returns the area covered by this buffer
172+ pub fn area(&self) -> &Rect {
173+ &self.area
174+ }
175+
176+ /// Returns a reference to Cell at the given coordinates
177+ pub fn get(&self, x: u16, y: u16) -> &Cell {
178+ let i = self.index_of(x, y);
179+ &self.content[i]
180+ }
181+
182+ /// Returns a mutable reference to Cell at the given coordinates
183+ pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
184+ let i = self.index_of(x, y);
185+ &mut self.content[i]
186+ }
187+
188+ /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
189+ ///
190+ /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
191+ ///
192+ /// # Examples
193+ ///
194+ /// ```
195+ /// # use helix_tui::buffer::Buffer;
196+ /// # use helix_tui::layout::Rect;
197+ /// let rect = Rect::new(200, 100, 10, 10);
198+ /// let buffer = Buffer::empty(rect);
199+ /// // Global coordinates to the top corner of this buffer's area
200+ /// assert_eq!(buffer.index_of(200, 100), 0);
201+ /// ```
202+ ///
203+ /// # Panics
204+ ///
205+ /// Panics when given an coordinate that is outside of this Buffer's area.
206+ ///
207+ /// ```should_panic
208+ /// # use helix_tui::buffer::Buffer;
209+ /// # use helix_tui::layout::Rect;
210+ /// let rect = Rect::new(200, 100, 10, 10);
211+ /// let buffer = Buffer::empty(rect);
212+ /// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
213+ /// // starts at (200, 100).
214+ /// buffer.index_of(0, 0); // Panics
215+ /// ```
216+ pub fn index_of(&self, x: u16, y: u16) -> usize {
217+ debug_assert!(
218+ x >= self.area.left()
219+ && x < self.area.right()
220+ && y >= self.area.top()
221+ && y < self.area.bottom(),
222+ "Trying to access position outside the buffer: x={}, y={}, area={:?}",
223+ x,
224+ y,
225+ self.area
226+ );
227+ ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
228+ }
229+
230+ /// Returns the (global) coordinates of a cell given its index
231+ ///
232+ /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
233+ ///
234+ /// # Examples
235+ ///
236+ /// ```
237+ /// # use helix_tui::buffer::Buffer;
238+ /// # use helix_tui::layout::Rect;
239+ /// let rect = Rect::new(200, 100, 10, 10);
240+ /// let buffer = Buffer::empty(rect);
241+ /// assert_eq!(buffer.pos_of(0), (200, 100));
242+ /// assert_eq!(buffer.pos_of(14), (204, 101));
243+ /// ```
244+ ///
245+ /// # Panics
246+ ///
247+ /// Panics when given an index that is outside the Buffer's content.
248+ ///
249+ /// ```should_panic
250+ /// # use helix_tui::buffer::Buffer;
251+ /// # use helix_tui::layout::Rect;
252+ /// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
253+ /// let buffer = Buffer::empty(rect);
254+ /// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
255+ /// buffer.pos_of(100); // Panics
256+ /// ```
257+ pub fn pos_of(&self, i: usize) -> (u16, u16) {
258+ debug_assert!(
259+ i < self.content.len(),
260+ "Trying to get the coords of a cell outside the buffer: i={} len={}",
261+ i,
262+ self.content.len()
263+ );
264+ (
265+ self.area.x + i as u16 % self.area.width,
266+ self.area.y + i as u16 / self.area.width,
267+ )
268+ }
269+
270+ /// Print a string, starting at the position (x, y)
271+ pub fn set_string<S>(&mut self, x: u16, y: u16, string: S, style: Style)
272+ where
273+ S: AsRef<str>,
274+ {
275+ self.set_stringn(x, y, string, usize::MAX, style);
276+ }
277+
278+ /// Print at most the first n characters of a string if enough space is available
279+ /// until the end of the line
280+ pub fn set_stringn<S>(
281+ &mut self,
282+ x: u16,
283+ y: u16,
284+ string: S,
285+ width: usize,
286+ style: Style,
287+ ) -> (u16, u16)
288+ where
289+ S: AsRef<str>,
290+ {
291+ let mut index = self.index_of(x, y);
292+ let mut x_offset = x as usize;
293+ let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
294+ let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
295+ for s in graphemes {
296+ let width = s.width();
297+ if width == 0 {
298+ continue;
299+ }
300+ // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
301+ // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
302+ if width > max_offset.saturating_sub(x_offset) {
303+ break;
304+ }
305+
306+ self.content[index].set_symbol(s);
307+ self.content[index].set_style(style);
308+ // Reset following cells if multi-width (they would be hidden by the grapheme),
309+ for i in index + 1..index + width {
310+ self.content[i].reset();
311+ }
312+ index += width;
313+ x_offset += width;
314+ }
315+ (x_offset as u16, y)
316+ }
317+
318+ pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) {
319+ let mut remaining_width = width;
320+ let mut x = x;
321+ for span in &spans.0 {
322+ if remaining_width == 0 {
323+ break;
324+ }
325+ let pos = self.set_stringn(
326+ x,
327+ y,
328+ span.content.as_ref(),
329+ remaining_width as usize,
330+ span.style,
331+ );
332+ let w = pos.0.saturating_sub(x);
333+ x = pos.0;
334+ remaining_width = remaining_width.saturating_sub(w);
335+ }
336+ (x, y)
337+ }
338+
339+ pub fn set_span<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) {
340+ self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
341+ }
342+
343+ #[deprecated(
344+ since = "0.10.0",
345+ note = "You should use styling capabilities of `Buffer::set_style`"
346+ )]
347+ pub fn set_background(&mut self, area: Rect, color: Color) {
348+ for y in area.top()..area.bottom() {
349+ for x in area.left()..area.right() {
350+ self.get_mut(x, y).set_bg(color);
351+ }
352+ }
353+ }
354+
355+ pub fn set_style(&mut self, area: Rect, style: Style) {
356+ for y in area.top()..area.bottom() {
357+ for x in area.left()..area.right() {
358+ self.get_mut(x, y).set_style(style);
359+ }
360+ }
361+ }
362+
363+ /// Resize the buffer so that the mapped area matches the given area and that the buffer
364+ /// length is equal to area.width * area.height
365+ pub fn resize(&mut self, area: Rect) {
366+ let length = area.area() as usize;
367+ if self.content.len() > length {
368+ self.content.truncate(length);
369+ } else {
370+ self.content.resize(length, Default::default());
371+ }
372+ self.area = area;
373+ }
374+
375+ /// Reset all cells in the buffer
376+ pub fn reset(&mut self) {
377+ for c in &mut self.content {
378+ c.reset();
379+ }
380+ }
381+
382+ /// Merge an other buffer into this one
383+ pub fn merge(&mut self, other: &Buffer) {
384+ let area = self.area.union(other.area);
385+ let cell: Cell = Default::default();
386+ self.content.resize(area.area() as usize, cell.clone());
387+
388+ // Move original content to the appropriate space
389+ let size = self.area.area() as usize;
390+ for i in (0..size).rev() {
391+ let (x, y) = self.pos_of(i);
392+ // New index in content
393+ let k = ((y - area.y) * area.width + x - area.x) as usize;
394+ if i != k {
395+ self.content[k] = self.content[i].clone();
396+ self.content[i] = cell.clone();
397+ }
398+ }
399+
400+ // Push content of the other buffer into this one (may erase previous
401+ // data)
402+ let size = other.area.area() as usize;
403+ for i in 0..size {
404+ let (x, y) = other.pos_of(i);
405+ // New index in content
406+ let k = ((y - area.y) * area.width + x - area.x) as usize;
407+ self.content[k] = other.content[i].clone();
408+ }
409+ self.area = area;
410+ }
411+
412+ /// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
413+ /// self to other.
414+ ///
415+ /// We're assuming that buffers are well-formed, that is no double-width cell is followed by
416+ /// a non-blank cell.
417+ ///
418+ /// # Multi-width characters handling:
419+ ///
420+ /// ```text
421+ /// (Index:) `01`
422+ /// Prev: `コ`
423+ /// Next: `aa`
424+ /// Updates: `0: a, 1: a'
425+ /// ```
426+ ///
427+ /// ```text
428+ /// (Index:) `01`
429+ /// Prev: `a `
430+ /// Next: `コ`
431+ /// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
432+ /// ```
433+ ///
434+ /// ```text
435+ /// (Index:) `012`
436+ /// Prev: `aaa`
437+ /// Next: `aコ`
438+ /// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
439+ /// ```
440+ pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
441+ let previous_buffer = &self.content;
442+ let next_buffer = &other.content;
443+ let width = self.area.width;
444+
445+ let mut updates: Vec<(u16, u16, &Cell)> = vec![];
446+ // Cells invalidated by drawing/replacing preceeding multi-width characters:
447+ let mut invalidated: usize = 0;
448+ // Cells from the current buffer to skip due to preceeding multi-width characters taking their
449+ // place (the skipped cells should be blank anyway):
450+ let mut to_skip: usize = 0;
451+ for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
452+ if (current != previous || invalidated > 0) && to_skip == 0 {
453+ let x = i as u16 % width;
454+ let y = i as u16 / width;
455+ updates.push((x, y, &next_buffer[i]));
456+ }
457+
458+ to_skip = current.symbol.width().saturating_sub(1);
459+
460+ let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
461+ invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
462+ }
463+ updates
464+ }
465+}
466+
467+#[cfg(test)]
468+mod tests {
469+ use super::*;
470+
471+ fn cell(s: &str) -> Cell {
472+ let mut cell = Cell::default();
473+ cell.set_symbol(s);
474+ cell
475+ }
476+
477+ #[test]
478+ fn it_translates_to_and_from_coordinates() {
479+ let rect = Rect::new(200, 100, 50, 80);
480+ let buf = Buffer::empty(rect);
481+
482+ // First cell is at the upper left corner.
483+ assert_eq!(buf.pos_of(0), (200, 100));
484+ assert_eq!(buf.index_of(200, 100), 0);
485+
486+ // Last cell is in the lower right.
487+ assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
488+ assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
489+ }
490+
491+ #[test]
492+ #[should_panic(expected = "outside the buffer")]
493+ fn pos_of_panics_on_out_of_bounds() {
494+ let rect = Rect::new(0, 0, 10, 10);
495+ let buf = Buffer::empty(rect);
496+
497+ // There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
498+ buf.pos_of(100);
499+ }
500+
501+ #[test]
502+ #[should_panic(expected = "outside the buffer")]
503+ fn index_of_panics_on_out_of_bounds() {
504+ let rect = Rect::new(0, 0, 10, 10);
505+ let buf = Buffer::empty(rect);
506+
507+ // width is 10; zero-indexed means that 10 would be the 11th cell.
508+ buf.index_of(10, 0);
509+ }
510+
511+ #[test]
512+ fn buffer_set_string() {
513+ let area = Rect::new(0, 0, 5, 1);
514+ let mut buffer = Buffer::empty(area);
515+
516+ // Zero-width
517+ buffer.set_stringn(0, 0, "aaa", 0, Style::default());
518+ assert_eq!(buffer, Buffer::with_lines(vec![" "]));
519+
520+ buffer.set_string(0, 0, "aaa", Style::default());
521+ assert_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
522+
523+ // Width limit:
524+ buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
525+ assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
526+
527+ buffer.set_string(0, 0, "12345", Style::default());
528+ assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
529+
530+ // Width truncation:
531+ buffer.set_string(0, 0, "123456", Style::default());
532+ assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
533+ }
534+
535+ #[test]
536+ fn buffer_set_string_zero_width() {
537+ let area = Rect::new(0, 0, 1, 1);
538+ let mut buffer = Buffer::empty(area);
539+
540+ // Leading grapheme with zero width
541+ let s = "\u{1}a";
542+ buffer.set_stringn(0, 0, s, 1, Style::default());
543+ assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
544+
545+ // Trailing grapheme with zero with
546+ let s = "a\u{1}";
547+ buffer.set_stringn(0, 0, s, 1, Style::default());
548+ assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
549+ }
550+
551+ #[test]
552+ fn buffer_set_string_double_width() {
553+ let area = Rect::new(0, 0, 5, 1);
554+ let mut buffer = Buffer::empty(area);
555+ buffer.set_string(0, 0, "コン", Style::default());
556+ assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
557+
558+ // Only 1 space left.
559+ buffer.set_string(0, 0, "コンピ", Style::default());
560+ assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
561+ }
562+
563+ #[test]
564+ fn buffer_with_lines() {
565+ let buffer =
566+ Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
567+ assert_eq!(buffer.area.x, 0);
568+ assert_eq!(buffer.area.y, 0);
569+ assert_eq!(buffer.area.width, 10);
570+ assert_eq!(buffer.area.height, 4);
571+ }
572+
573+ #[test]
574+ fn buffer_diffing_empty_empty() {
575+ let area = Rect::new(0, 0, 40, 40);
576+ let prev = Buffer::empty(area);
577+ let next = Buffer::empty(area);
578+ let diff = prev.diff(&next);
579+ assert_eq!(diff, vec![]);
580+ }
581+
582+ #[test]
583+ fn buffer_diffing_empty_filled() {
584+ let area = Rect::new(0, 0, 40, 40);
585+ let prev = Buffer::empty(area);
586+ let next = Buffer::filled(area, Cell::default().set_symbol("a"));
587+ let diff = prev.diff(&next);
588+ assert_eq!(diff.len(), 40 * 40);
589+ }
590+
591+ #[test]
592+ fn buffer_diffing_filled_filled() {
593+ let area = Rect::new(0, 0, 40, 40);
594+ let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
595+ let next = Buffer::filled(area, Cell::default().set_symbol("a"));
596+ let diff = prev.diff(&next);
597+ assert_eq!(diff, vec![]);
598+ }
599+
600+ #[test]
601+ fn buffer_diffing_single_width() {
602+ let prev = Buffer::with_lines(vec![
603+ " ",
604+ "┌Title─┐ ",
605+ "│ │ ",
606+ "│ │ ",
607+ "└──────┘ ",
608+ ]);
609+ let next = Buffer::with_lines(vec![
610+ " ",
611+ "┌TITLE─┐ ",
612+ "│ │ ",
613+ "│ │ ",
614+ "└──────┘ ",
615+ ]);
616+ let diff = prev.diff(&next);
617+ assert_eq!(
618+ diff,
619+ vec![
620+ (2, 1, &cell("I")),
621+ (3, 1, &cell("T")),
622+ (4, 1, &cell("L")),
623+ (5, 1, &cell("E")),
624+ ]
625+ );
626+ }
627+
628+ #[test]
629+ #[rustfmt::skip]
630+ fn buffer_diffing_multi_width() {
631+ let prev = Buffer::with_lines(vec![
632+ "┌Title─┐ ",
633+ "└──────┘ ",
634+ ]);
635+ let next = Buffer::with_lines(vec![
636+ "┌称号──┐ ",
637+ "└──────┘ ",
638+ ]);
639+ let diff = prev.diff(&next);
640+ assert_eq!(
641+ diff,
642+ vec![
643+ (1, 0, &cell("称")),
644+ // Skipped "i"
645+ (3, 0, &cell("号")),
646+ // Skipped "l"
647+ (5, 0, &cell("─")),
648+ ]
649+ );
650+ }
651+
652+ #[test]
653+ fn buffer_diffing_multi_width_offset() {
654+ let prev = Buffer::with_lines(vec!["┌称号──┐"]);
655+ let next = Buffer::with_lines(vec!["┌─称号─┐"]);
656+
657+ let diff = prev.diff(&next);
658+ assert_eq!(
659+ diff,
660+ vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
661+ );
662+ }
663+
664+ #[test]
665+ fn buffer_merge() {
666+ let mut one = Buffer::filled(
667+ Rect {
668+ x: 0,
669+ y: 0,
670+ width: 2,
671+ height: 2,
672+ },
673+ Cell::default().set_symbol("1"),
674+ );
675+ let two = Buffer::filled(
676+ Rect {
677+ x: 0,
678+ y: 2,
679+ width: 2,
680+ height: 2,
681+ },
682+ Cell::default().set_symbol("2"),
683+ );
684+ one.merge(&two);
685+ assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
686+ }
687+
688+ #[test]
689+ fn buffer_merge2() {
690+ let mut one = Buffer::filled(
691+ Rect {
692+ x: 2,
693+ y: 2,
694+ width: 2,
695+ height: 2,
696+ },
697+ Cell::default().set_symbol("1"),
698+ );
699+ let two = Buffer::filled(
700+ Rect {
701+ x: 0,
702+ y: 0,
703+ width: 2,
704+ height: 2,
705+ },
706+ Cell::default().set_symbol("2"),
707+ );
708+ one.merge(&two);
709+ assert_eq!(
710+ one,
711+ Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
712+ );
713+ }
714+
715+ #[test]
716+ fn buffer_merge3() {
717+ let mut one = Buffer::filled(
718+ Rect {
719+ x: 3,
720+ y: 3,
721+ width: 2,
722+ height: 2,
723+ },
724+ Cell::default().set_symbol("1"),
725+ );
726+ let two = Buffer::filled(
727+ Rect {
728+ x: 1,
729+ y: 1,
730+ width: 3,
731+ height: 4,
732+ },
733+ Cell::default().set_symbol("2"),
734+ );
735+ one.merge(&two);
736+ let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
737+ merged.area = Rect {
738+ x: 1,
739+ y: 1,
740+ width: 4,
741+ height: 4,
742+ };
743+ assert_eq!(one, merged);
744+ }
745+}
helix-tui/src/layout.rsnew file+534
@@ -0,0 +1,534 @@
1+use std::cell::RefCell;
2+use std::cmp::{max, min};
3+use std::collections::HashMap;
4+
5+use cassowary::strength::{REQUIRED, WEAK};
6+use cassowary::WeightedRelation::*;
7+use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
8+
9+#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
10+pub enum Corner {
11+ TopLeft,
12+ TopRight,
13+ BottomRight,
14+ BottomLeft,
15+}
16+
17+#[derive(Debug, Hash, Clone, PartialEq, Eq)]
18+pub enum Direction {
19+ Horizontal,
20+ Vertical,
21+}
22+
23+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24+pub enum Constraint {
25+ // TODO: enforce range 0 - 100
26+ Percentage(u16),
27+ Ratio(u32, u32),
28+ Length(u16),
29+ Max(u16),
30+ Min(u16),
31+}
32+
33+impl Constraint {
34+ pub fn apply(&self, length: u16) -> u16 {
35+ match *self {
36+ Constraint::Percentage(p) => length * p / 100,
37+ Constraint::Ratio(num, den) => {
38+ let r = num * u32::from(length) / den;
39+ r as u16
40+ }
41+ Constraint::Length(l) => length.min(l),
42+ Constraint::Max(m) => length.min(m),
43+ Constraint::Min(m) => length.max(m),
44+ }
45+ }
46+}
47+
48+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
49+pub struct Margin {
50+ pub vertical: u16,
51+ pub horizontal: u16,
52+}
53+
54+#[derive(Debug, Clone, Copy, PartialEq)]
55+pub enum Alignment {
56+ Left,
57+ Center,
58+ Right,
59+}
60+
61+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62+pub struct Layout {
63+ direction: Direction,
64+ margin: Margin,
65+ constraints: Vec<Constraint>,
66+}
67+
68+thread_local! {
69+ static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
70+}
71+
72+impl Default for Layout {
73+ fn default() -> Layout {
74+ Layout {
75+ direction: Direction::Vertical,
76+ margin: Margin {
77+ horizontal: 0,
78+ vertical: 0,
79+ },
80+ constraints: Vec::new(),
81+ }
82+ }
83+}
84+
85+impl Layout {
86+ pub fn constraints<C>(mut self, constraints: C) -> Layout
87+ where
88+ C: Into<Vec<Constraint>>,
89+ {
90+ self.constraints = constraints.into();
91+ self
92+ }
93+
94+ pub fn margin(mut self, margin: u16) -> Layout {
95+ self.margin = Margin {
96+ horizontal: margin,
97+ vertical: margin,
98+ };
99+ self
100+ }
101+
102+ pub fn horizontal_margin(mut self, horizontal: u16) -> Layout {
103+ self.margin.horizontal = horizontal;
104+ self
105+ }
106+
107+ pub fn vertical_margin(mut self, vertical: u16) -> Layout {
108+ self.margin.vertical = vertical;
109+ self
110+ }
111+
112+ pub fn direction(mut self, direction: Direction) -> Layout {
113+ self.direction = direction;
114+ self
115+ }
116+
117+ /// Wrapper function around the cassowary-rs solver to be able to split a given
118+ /// area into smaller ones based on the preferred widths or heights and the direction.
119+ ///
120+ /// # Examples
121+ /// ```
122+ /// # use helix_tui::layout::{Rect, Constraint, Direction, Layout};
123+ /// let chunks = Layout::default()
124+ /// .direction(Direction::Vertical)
125+ /// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
126+ /// .split(Rect {
127+ /// x: 2,
128+ /// y: 2,
129+ /// width: 10,
130+ /// height: 10,
131+ /// });
132+ /// assert_eq!(
133+ /// chunks,
134+ /// vec![
135+ /// Rect {
136+ /// x: 2,
137+ /// y: 2,
138+ /// width: 10,
139+ /// height: 5
140+ /// },
141+ /// Rect {
142+ /// x: 2,
143+ /// y: 7,
144+ /// width: 10,
145+ /// height: 5
146+ /// }
147+ /// ]
148+ /// );
149+ ///
150+ /// let chunks = Layout::default()
151+ /// .direction(Direction::Horizontal)
152+ /// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
153+ /// .split(Rect {
154+ /// x: 0,
155+ /// y: 0,
156+ /// width: 9,
157+ /// height: 2,
158+ /// });
159+ /// assert_eq!(
160+ /// chunks,
161+ /// vec![
162+ /// Rect {
163+ /// x: 0,
164+ /// y: 0,
165+ /// width: 3,
166+ /// height: 2
167+ /// },
168+ /// Rect {
169+ /// x: 3,
170+ /// y: 0,
171+ /// width: 6,
172+ /// height: 2
173+ /// }
174+ /// ]
175+ /// );
176+ /// ```
177+ pub fn split(&self, area: Rect) -> Vec<Rect> {
178+ // TODO: Maybe use a fixed size cache ?
179+ LAYOUT_CACHE.with(|c| {
180+ c.borrow_mut()
181+ .entry((area, self.clone()))
182+ .or_insert_with(|| split(area, self))
183+ .clone()
184+ })
185+ }
186+}
187+
188+fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
189+ let mut solver = Solver::new();
190+ let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
191+ let elements = layout
192+ .constraints
193+ .iter()
194+ .map(|_| Element::new())
195+ .collect::<Vec<Element>>();
196+ let mut results = layout
197+ .constraints
198+ .iter()
199+ .map(|_| Rect::default())
200+ .collect::<Vec<Rect>>();
201+
202+ let dest_area = area.inner(&layout.margin);
203+ for (i, e) in elements.iter().enumerate() {
204+ vars.insert(e.x, (i, 0));
205+ vars.insert(e.y, (i, 1));
206+ vars.insert(e.width, (i, 2));
207+ vars.insert(e.height, (i, 3));
208+ }
209+ let mut ccs: Vec<CassowaryConstraint> =
210+ Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6);
211+ for elt in &elements {
212+ ccs.push(elt.width | GE(REQUIRED) | 0f64);
213+ ccs.push(elt.height | GE(REQUIRED) | 0f64);
214+ ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
215+ ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
216+ ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
217+ ccs.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom()));
218+ }
219+ if let Some(first) = elements.first() {
220+ ccs.push(match layout.direction {
221+ Direction::Horizontal => first.left() | EQ(REQUIRED) | f64::from(dest_area.left()),
222+ Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
223+ });
224+ }
225+ if let Some(last) = elements.last() {
226+ ccs.push(match layout.direction {
227+ Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
228+ Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
229+ });
230+ }
231+ match layout.direction {
232+ Direction::Horizontal => {
233+ for pair in elements.windows(2) {
234+ ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
235+ }
236+ for (i, size) in layout.constraints.iter().enumerate() {
237+ ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
238+ ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
239+ ccs.push(match *size {
240+ Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
241+ Constraint::Percentage(v) => {
242+ elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
243+ }
244+ Constraint::Ratio(n, d) => {
245+ elements[i].width
246+ | EQ(WEAK)
247+ | (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
248+ }
249+ Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
250+ Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
251+ });
252+ }
253+ }
254+ Direction::Vertical => {
255+ for pair in elements.windows(2) {
256+ ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
257+ }
258+ for (i, size) in layout.constraints.iter().enumerate() {
259+ ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
260+ ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
261+ ccs.push(match *size {
262+ Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
263+ Constraint::Percentage(v) => {
264+ elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
265+ }
266+ Constraint::Ratio(n, d) => {
267+ elements[i].height
268+ | EQ(WEAK)
269+ | (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
270+ }
271+ Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
272+ Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
273+ });
274+ }
275+ }
276+ }
277+ solver.add_constraints(&ccs).unwrap();
278+ for &(var, value) in solver.fetch_changes() {
279+ let (index, attr) = vars[&var];
280+ let value = if value.is_sign_negative() {
281+ 0
282+ } else {
283+ value as u16
284+ };
285+ match attr {
286+ 0 => {
287+ results[index].x = value;
288+ }
289+ 1 => {
290+ results[index].y = value;
291+ }
292+ 2 => {
293+ results[index].width = value;
294+ }
295+ 3 => {
296+ results[index].height = value;
297+ }
298+ _ => {}
299+ }
300+ }
301+
302+ // Fix imprecision by extending the last item a bit if necessary
303+ if let Some(last) = results.last_mut() {
304+ match layout.direction {
305+ Direction::Vertical => {
306+ last.height = dest_area.bottom() - last.y;
307+ }
308+ Direction::Horizontal => {
309+ last.width = dest_area.right() - last.x;
310+ }
311+ }
312+ }
313+ results
314+}
315+
316+/// A container used by the solver inside split
317+struct Element {
318+ x: Variable,
319+ y: Variable,
320+ width: Variable,
321+ height: Variable,
322+}
323+
324+impl Element {
325+ fn new() -> Element {
326+ Element {
327+ x: Variable::new(),
328+ y: Variable::new(),
329+ width: Variable::new(),
330+ height: Variable::new(),
331+ }
332+ }
333+
334+ fn left(&self) -> Variable {
335+ self.x
336+ }
337+
338+ fn top(&self) -> Variable {
339+ self.y
340+ }
341+
342+ fn right(&self) -> Expression {
343+ self.x + self.width
344+ }
345+
346+ fn bottom(&self) -> Expression {
347+ self.y + self.height
348+ }
349+}
350+
351+/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
352+/// area they are supposed to render to.
353+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
354+pub struct Rect {
355+ pub x: u16,
356+ pub y: u16,
357+ pub width: u16,
358+ pub height: u16,
359+}
360+
361+impl Default for Rect {
362+ fn default() -> Rect {
363+ Rect {
364+ x: 0,
365+ y: 0,
366+ width: 0,
367+ height: 0,
368+ }
369+ }
370+}
371+
372+impl Rect {
373+ /// Creates a new rect, with width and height limited to keep the area under max u16.
374+ /// If clipped, aspect ratio will be preserved.
375+ pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
376+ let max_area = u16::max_value();
377+ let (clipped_width, clipped_height) =
378+ if u32::from(width) * u32::from(height) > u32::from(max_area) {
379+ let aspect_ratio = f64::from(width) / f64::from(height);
380+ let max_area_f = f64::from(max_area);
381+ let height_f = (max_area_f / aspect_ratio).sqrt();
382+ let width_f = height_f * aspect_ratio;
383+ (width_f as u16, height_f as u16)
384+ } else {
385+ (width, height)
386+ };
387+ Rect {
388+ x,
389+ y,
390+ width: clipped_width,
391+ height: clipped_height,
392+ }
393+ }
394+
395+ pub fn area(self) -> u16 {
396+ self.width * self.height
397+ }
398+
399+ pub fn left(self) -> u16 {
400+ self.x
401+ }
402+
403+ pub fn right(self) -> u16 {
404+ self.x.saturating_add(self.width)
405+ }
406+
407+ pub fn top(self) -> u16 {
408+ self.y
409+ }
410+
411+ pub fn bottom(self) -> u16 {
412+ self.y.saturating_add(self.height)
413+ }
414+
415+ pub fn inner(self, margin: &Margin) -> Rect {
416+ if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
417+ Rect::default()
418+ } else {
419+ Rect {
420+ x: self.x + margin.horizontal,
421+ y: self.y + margin.vertical,
422+ width: self.width - 2 * margin.horizontal,
423+ height: self.height - 2 * margin.vertical,
424+ }
425+ }
426+ }
427+
428+ pub fn union(self, other: Rect) -> Rect {
429+ let x1 = min(self.x, other.x);
430+ let y1 = min(self.y, other.y);
431+ let x2 = max(self.x + self.width, other.x + other.width);
432+ let y2 = max(self.y + self.height, other.y + other.height);
433+ Rect {
434+ x: x1,
435+ y: y1,
436+ width: x2 - x1,
437+ height: y2 - y1,
438+ }
439+ }
440+
441+ pub fn intersection(self, other: Rect) -> Rect {
442+ let x1 = max(self.x, other.x);
443+ let y1 = max(self.y, other.y);
444+ let x2 = min(self.x + self.width, other.x + other.width);
445+ let y2 = min(self.y + self.height, other.y + other.height);
446+ Rect {
447+ x: x1,
448+ y: y1,
449+ width: x2 - x1,
450+ height: y2 - y1,
451+ }
452+ }
453+
454+ pub fn intersects(self, other: Rect) -> bool {
455+ self.x < other.x + other.width
456+ && self.x + self.width > other.x
457+ && self.y < other.y + other.height
458+ && self.y + self.height > other.y
459+ }
460+}
461+
462+#[cfg(test)]
463+mod tests {
464+ use super::*;
465+
466+ #[test]
467+ fn test_vertical_split_by_height() {
468+ let target = Rect {
469+ x: 2,
470+ y: 2,
471+ width: 10,
472+ height: 10,
473+ };
474+
475+ let chunks = Layout::default()
476+ .direction(Direction::Vertical)
477+ .constraints(
478+ [
479+ Constraint::Percentage(10),
480+ Constraint::Max(5),
481+ Constraint::Min(1),
482+ ]
483+ .as_ref(),
484+ )
485+ .split(target);
486+
487+ assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
488+ chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
489+ }
490+
491+ #[test]
492+ fn test_rect_size_truncation() {
493+ for width in 256u16..300u16 {
494+ for height in 256u16..300u16 {
495+ let rect = Rect::new(0, 0, width, height);
496+ rect.area(); // Should not panic.
497+ assert!(rect.width < width || rect.height < height);
498+ // The target dimensions are rounded down so the math will not be too precise
499+ // but let's make sure the ratios don't diverge crazily.
500+ assert!(
501+ (f64::from(rect.width) / f64::from(rect.height)
502+ - f64::from(width) / f64::from(height))
503+ .abs()
504+ < 1.0
505+ )
506+ }
507+ }
508+
509+ // One dimension below 255, one above. Area above max u16.
510+ let width = 900;
511+ let height = 100;
512+ let rect = Rect::new(0, 0, width, height);
513+ assert_ne!(rect.width, 900);
514+ assert_ne!(rect.height, 100);
515+ assert!(rect.width < width || rect.height < height);
516+ }
517+
518+ #[test]
519+ fn test_rect_size_preservation() {
520+ for width in 0..256u16 {
521+ for height in 0..256u16 {
522+ let rect = Rect::new(0, 0, width, height);
523+ rect.area(); // Should not panic.
524+ assert_eq!(rect.width, width);
525+ assert_eq!(rect.height, height);
526+ }
527+ }
528+
529+ // One dimension below 255, one above. Area below max u16.
530+ let rect = Rect::new(0, 0, 300, 100);
531+ assert_eq!(rect.width, 300);
532+ assert_eq!(rect.height, 100);
533+ }
534+}
helix-tui/src/lib.rsnew file+134
@@ -0,0 +1,134 @@
1+//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
2+//! terminal users interfaces and dashboards.
3+//!
4+//! ![](https://raw.githubusercontent.com/fdehau/tui-rs/master/assets/demo.gif)
5+//!
6+//! # Get started
7+//!
8+//! ## Adding `tui` as a dependency
9+//!
10+//! ```toml
11+//! [dependencies]
12+//! tui = "0.15"
13+//! crossterm = "0.19"
14+//! ```
15+//!
16+//! The same logic applies for all other available backends.
17+//!
18+//! ## Creating a `Terminal`
19+//!
20+//! Every application using `tui` should start by instantiating a `Terminal`. It is a light
21+//! abstraction over available backends that provides basic functionalities such as clearing the
22+//! screen, hiding the cursor, etc.
23+//!
24+//! ```rust,no_run
25+//! use std::io;
26+//! use helix_tui::Terminal;
27+//! use helix_tui::backend::CrosstermBackend;
28+//!
29+//! fn main() -> Result<(), io::Error> {
30+//! let stdout = io::stdout();
31+//! let backend = CrosstermBackend::new(stdout);
32+//! let mut terminal = Terminal::new(backend)?;
33+//! Ok(())
34+//! }
35+//! ```
36+//!
37+//! You may also refer to the examples to find out how to create a `Terminal` for each available
38+//! backend.
39+//!
40+//! ## Building a User Interface (UI)
41+//!
42+//! Every component of your interface will be implementing the `Widget` trait. The library comes
43+//! with a predefined set of widgets that should meet most of your use cases. You are also free to
44+//! implement your own.
45+//!
46+//! Each widget follows a builder pattern API providing a default configuration along with methods
47+//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take
48+//! your widget instance an area to draw to.
49+//!
50+//! The following example renders a block of the size of the terminal:
51+//!
52+//! ```rust,no_run
53+//! use std::io;
54+//! use crossterm::terminal;
55+//! use helix_tui::Terminal;
56+//! use helix_tui::backend::CrosstermBackend;
57+//! use helix_tui::widgets::{Widget, Block, Borders};
58+//! use helix_tui::layout::{Layout, Constraint, Direction};
59+//!
60+//! fn main() -> Result<(), io::Error> {
61+//! terminal::enable_raw_mode()?;
62+//! let stdout = io::stdout();
63+//! let backend = CrosstermBackend::new(stdout);
64+//! let mut terminal = Terminal::new(backend)?;
65+//! terminal.draw(|f| {
66+//! let size = f.size();
67+//! let block = Block::default()
68+//! .title("Block")
69+//! .borders(Borders::ALL);
70+//! f.render_widget(block, size);
71+//! })?;
72+//! Ok(())
73+//! }
74+//! ```
75+//!
76+//! ## Layout
77+//!
78+//! The library comes with a basic yet useful layout management object called `Layout`. As you may
79+//! see below and in the examples, the library makes heavy use of the builder pattern to provide
80+//! full customization. And `Layout` is no exception:
81+//!
82+//! ```rust,no_run
83+//! use std::io;
84+//! use crossterm::terminal;
85+//! use helix_tui::Terminal;
86+//! use helix_tui::backend::CrosstermBackend;
87+//! use helix_tui::widgets::{Widget, Block, Borders};
88+//! use helix_tui::layout::{Layout, Constraint, Direction};
89+//!
90+//! fn main() -> Result<(), io::Error> {
91+//! terminal::enable_raw_mode()?;
92+//! let stdout = io::stdout();
93+//! let backend = CrosstermBackend::new(stdout);
94+//! let mut terminal = Terminal::new(backend)?;
95+//! terminal.draw(|f| {
96+//! let chunks = Layout::default()
97+//! .direction(Direction::Vertical)
98+//! .margin(1)
99+//! .constraints(
100+//! [
101+//! Constraint::Percentage(10),
102+//! Constraint::Percentage(80),
103+//! Constraint::Percentage(10)
104+//! ].as_ref()
105+//! )
106+//! .split(f.size());
107+//! let block = Block::default()
108+//! .title("Block")
109+//! .borders(Borders::ALL);
110+//! f.render_widget(block, chunks[0]);
111+//! let block = Block::default()
112+//! .title("Block 2")
113+//! .borders(Borders::ALL);
114+//! f.render_widget(block, chunks[1]);
115+//! })?;
116+//! Ok(())
117+//! }
118+//! ```
119+//!
120+//! This let you describe responsive terminal UI by nesting layouts. You should note that by
121+//! default the computed layout tries to fill the available space completely. So if for any reason
122+//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
123+//! corresponding area.
124+
125+pub mod backend;
126+pub mod buffer;
127+pub mod layout;
128+pub mod style;
129+pub mod symbols;
130+pub mod terminal;
131+pub mod text;
132+pub mod widgets;
133+
134+pub use self::terminal::{Terminal, TerminalOptions, Viewport};
helix-tui/src/style.rsnew file+281
@@ -0,0 +1,281 @@
1+//! `style` contains the primitives used to control how your user interface will look.
2+
3+use bitflags::bitflags;
4+
5+#[derive(Debug, Clone, Copy, PartialEq)]
6+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7+pub enum Color {
8+ Reset,
9+ Black,
10+ Red,
11+ Green,
12+ Yellow,
13+ Blue,
14+ Magenta,
15+ Cyan,
16+ Gray,
17+ DarkGray,
18+ LightRed,
19+ LightGreen,
20+ LightYellow,
21+ LightBlue,
22+ LightMagenta,
23+ LightCyan,
24+ White,
25+ Rgb(u8, u8, u8),
26+ Indexed(u8),
27+}
28+
29+bitflags! {
30+ /// Modifier changes the way a piece of text is displayed.
31+ ///
32+ /// They are bitflags so they can easily be composed.
33+ ///
34+ /// ## Examples
35+ ///
36+ /// ```rust
37+ /// # use helix_tui::style::Modifier;
38+ ///
39+ /// let m = Modifier::BOLD | Modifier::ITALIC;
40+ /// ```
41+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42+ pub struct Modifier: u16 {
43+ const BOLD = 0b0000_0000_0001;
44+ const DIM = 0b0000_0000_0010;
45+ const ITALIC = 0b0000_0000_0100;
46+ const UNDERLINED = 0b0000_0000_1000;
47+ const SLOW_BLINK = 0b0000_0001_0000;
48+ const RAPID_BLINK = 0b0000_0010_0000;
49+ const REVERSED = 0b0000_0100_0000;
50+ const HIDDEN = 0b0000_1000_0000;
51+ const CROSSED_OUT = 0b0001_0000_0000;
52+ }
53+}
54+
55+/// Style let you control the main characteristics of the displayed elements.
56+///
57+/// ```rust
58+/// # use helix_tui::style::{Color, Modifier, Style};
59+/// Style::default()
60+/// .fg(Color::Black)
61+/// .bg(Color::Green)
62+/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
63+/// ```
64+///
65+/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
66+/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
67+/// just S3.
68+///
69+/// ```rust
70+/// # use helix_tui::style::{Color, Modifier, Style};
71+/// # use helix_tui::buffer::Buffer;
72+/// # use helix_tui::layout::Rect;
73+/// let styles = [
74+/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
75+/// Style::default().bg(Color::Red),
76+/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
77+/// ];
78+/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
79+/// for style in &styles {
80+/// buffer.get_mut(0, 0).set_style(*style);
81+/// }
82+/// assert_eq!(
83+/// Style {
84+/// fg: Some(Color::Yellow),
85+/// bg: Some(Color::Red),
86+/// add_modifier: Modifier::BOLD,
87+/// sub_modifier: Modifier::empty(),
88+/// },
89+/// buffer.get(0, 0).style(),
90+/// );
91+/// ```
92+///
93+/// The default implementation returns a `Style` that does not modify anything. If you wish to
94+/// reset all properties until that point use [`Style::reset`].
95+///
96+/// ```
97+/// # use helix_tui::style::{Color, Modifier, Style};
98+/// # use helix_tui::buffer::Buffer;
99+/// # use helix_tui::layout::Rect;
100+/// let styles = [
101+/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
102+/// Style::reset().fg(Color::Yellow),
103+/// ];
104+/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
105+/// for style in &styles {
106+/// buffer.get_mut(0, 0).set_style(*style);
107+/// }
108+/// assert_eq!(
109+/// Style {
110+/// fg: Some(Color::Yellow),
111+/// bg: Some(Color::Reset),
112+/// add_modifier: Modifier::empty(),
113+/// sub_modifier: Modifier::empty(),
114+/// },
115+/// buffer.get(0, 0).style(),
116+/// );
117+/// ```
118+#[derive(Debug, Clone, Copy, PartialEq)]
119+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120+pub struct Style {
121+ pub fg: Option<Color>,
122+ pub bg: Option<Color>,
123+ pub add_modifier: Modifier,
124+ pub sub_modifier: Modifier,
125+}
126+
127+impl Default for Style {
128+ fn default() -> Style {
129+ Style {
130+ fg: None,
131+ bg: None,
132+ add_modifier: Modifier::empty(),
133+ sub_modifier: Modifier::empty(),
134+ }
135+ }
136+}
137+
138+impl Style {
139+ /// Returns a `Style` resetting all properties.
140+ pub fn reset() -> Style {
141+ Style {
142+ fg: Some(Color::Reset),
143+ bg: Some(Color::Reset),
144+ add_modifier: Modifier::empty(),
145+ sub_modifier: Modifier::all(),
146+ }
147+ }
148+
149+ /// Changes the foreground color.
150+ ///
151+ /// ## Examples
152+ ///
153+ /// ```rust
154+ /// # use helix_tui::style::{Color, Style};
155+ /// let style = Style::default().fg(Color::Blue);
156+ /// let diff = Style::default().fg(Color::Red);
157+ /// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
158+ /// ```
159+ pub fn fg(mut self, color: Color) -> Style {
160+ self.fg = Some(color);
161+ self
162+ }
163+
164+ /// Changes the background color.
165+ ///
166+ /// ## Examples
167+ ///
168+ /// ```rust
169+ /// # use helix_tui::style::{Color, Style};
170+ /// let style = Style::default().bg(Color::Blue);
171+ /// let diff = Style::default().bg(Color::Red);
172+ /// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
173+ /// ```
174+ pub fn bg(mut self, color: Color) -> Style {
175+ self.bg = Some(color);
176+ self
177+ }
178+
179+ /// Changes the text emphasis.
180+ ///
181+ /// When applied, it adds the given modifier to the `Style` modifiers.
182+ ///
183+ /// ## Examples
184+ ///
185+ /// ```rust
186+ /// # use helix_tui::style::{Color, Modifier, Style};
187+ /// let style = Style::default().add_modifier(Modifier::BOLD);
188+ /// let diff = Style::default().add_modifier(Modifier::ITALIC);
189+ /// let patched = style.patch(diff);
190+ /// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
191+ /// assert_eq!(patched.sub_modifier, Modifier::empty());
192+ /// ```
193+ pub fn add_modifier(mut self, modifier: Modifier) -> Style {
194+ self.sub_modifier.remove(modifier);
195+ self.add_modifier.insert(modifier);
196+ self
197+ }
198+
199+ /// Changes the text emphasis.
200+ ///
201+ /// When applied, it removes the given modifier from the `Style` modifiers.
202+ ///
203+ /// ## Examples
204+ ///
205+ /// ```rust
206+ /// # use helix_tui::style::{Color, Modifier, Style};
207+ /// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
208+ /// let diff = Style::default().remove_modifier(Modifier::ITALIC);
209+ /// let patched = style.patch(diff);
210+ /// assert_eq!(patched.add_modifier, Modifier::BOLD);
211+ /// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
212+ /// ```
213+ pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
214+ self.add_modifier.remove(modifier);
215+ self.sub_modifier.insert(modifier);
216+ self
217+ }
218+
219+ /// Results in a combined style that is equivalent to applying the two individual styles to
220+ /// a style one after the other.
221+ ///
222+ /// ## Examples
223+ /// ```
224+ /// # use helix_tui::style::{Color, Modifier, Style};
225+ /// let style_1 = Style::default().fg(Color::Yellow);
226+ /// let style_2 = Style::default().bg(Color::Red);
227+ /// let combined = style_1.patch(style_2);
228+ /// assert_eq!(
229+ /// Style::default().patch(style_1).patch(style_2),
230+ /// Style::default().patch(combined));
231+ /// ```
232+ pub fn patch(mut self, other: Style) -> Style {
233+ self.fg = other.fg.or(self.fg);
234+ self.bg = other.bg.or(self.bg);
235+
236+ self.add_modifier.remove(other.sub_modifier);
237+ self.add_modifier.insert(other.add_modifier);
238+ self.sub_modifier.remove(other.add_modifier);
239+ self.sub_modifier.insert(other.sub_modifier);
240+
241+ self
242+ }
243+}
244+
245+#[cfg(test)]
246+mod tests {
247+ use super::*;
248+
249+ fn styles() -> Vec<Style> {
250+ vec![
251+ Style::default(),
252+ Style::default().fg(Color::Yellow),
253+ Style::default().bg(Color::Yellow),
254+ Style::default().add_modifier(Modifier::BOLD),
255+ Style::default().remove_modifier(Modifier::BOLD),
256+ Style::default().add_modifier(Modifier::ITALIC),
257+ Style::default().remove_modifier(Modifier::ITALIC),
258+ Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
259+ Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
260+ ]
261+ }
262+
263+ #[test]
264+ fn combined_patch_gives_same_result_as_individual_patch() {
265+ let styles = styles();
266+ for &a in &styles {
267+ for &b in &styles {
268+ for &c in &styles {
269+ for &d in &styles {
270+ let combined = a.patch(b.patch(c.patch(d)));
271+
272+ assert_eq!(
273+ Style::default().patch(a).patch(b).patch(c).patch(d),
274+ Style::default().patch(combined)
275+ );
276+ }
277+ }
278+ }
279+ }
280+ }
281+}
helix-tui/src/symbols.rsnew file+233
@@ -0,0 +1,233 @@
1+pub mod block {
2+ pub const FULL: &str = "█";
3+ pub const SEVEN_EIGHTHS: &str = "▉";
4+ pub const THREE_QUARTERS: &str = "▊";
5+ pub const FIVE_EIGHTHS: &str = "▋";
6+ pub const HALF: &str = "▌";
7+ pub const THREE_EIGHTHS: &str = "▍";
8+ pub const ONE_QUARTER: &str = "▎";
9+ pub const ONE_EIGHTH: &str = "▏";
10+
11+ #[derive(Debug, Clone)]
12+ pub struct Set {
13+ pub full: &'static str,
14+ pub seven_eighths: &'static str,
15+ pub three_quarters: &'static str,
16+ pub five_eighths: &'static str,
17+ pub half: &'static str,
18+ pub three_eighths: &'static str,
19+ pub one_quarter: &'static str,
20+ pub one_eighth: &'static str,
21+ pub empty: &'static str,
22+ }
23+
24+ pub const THREE_LEVELS: Set = Set {
25+ full: FULL,
26+ seven_eighths: FULL,
27+ three_quarters: HALF,
28+ five_eighths: HALF,
29+ half: HALF,
30+ three_eighths: HALF,
31+ one_quarter: HALF,
32+ one_eighth: " ",
33+ empty: " ",
34+ };
35+
36+ pub const NINE_LEVELS: Set = Set {
37+ full: FULL,
38+ seven_eighths: SEVEN_EIGHTHS,
39+ three_quarters: THREE_QUARTERS,
40+ five_eighths: FIVE_EIGHTHS,
41+ half: HALF,
42+ three_eighths: THREE_EIGHTHS,
43+ one_quarter: ONE_QUARTER,
44+ one_eighth: ONE_EIGHTH,
45+ empty: " ",
46+ };
47+}
48+
49+pub mod bar {
50+ pub const FULL: &str = "█";
51+ pub const SEVEN_EIGHTHS: &str = "▇";
52+ pub const THREE_QUARTERS: &str = "▆";
53+ pub const FIVE_EIGHTHS: &str = "▅";
54+ pub const HALF: &str = "▄";
55+ pub const THREE_EIGHTHS: &str = "▃";
56+ pub const ONE_QUARTER: &str = "▂";
57+ pub const ONE_EIGHTH: &str = "▁";
58+
59+ #[derive(Debug, Clone)]
60+ pub struct Set {
61+ pub full: &'static str,
62+ pub seven_eighths: &'static str,
63+ pub three_quarters: &'static str,
64+ pub five_eighths: &'static str,
65+ pub half: &'static str,
66+ pub three_eighths: &'static str,
67+ pub one_quarter: &'static str,
68+ pub one_eighth: &'static str,
69+ pub empty: &'static str,
70+ }
71+
72+ pub const THREE_LEVELS: Set = Set {
73+ full: FULL,
74+ seven_eighths: FULL,
75+ three_quarters: HALF,
76+ five_eighths: HALF,
77+ half: HALF,
78+ three_eighths: HALF,
79+ one_quarter: HALF,
80+ one_eighth: " ",
81+ empty: " ",
82+ };
83+
84+ pub const NINE_LEVELS: Set = Set {
85+ full: FULL,
86+ seven_eighths: SEVEN_EIGHTHS,
87+ three_quarters: THREE_QUARTERS,
88+ five_eighths: FIVE_EIGHTHS,
89+ half: HALF,
90+ three_eighths: THREE_EIGHTHS,
91+ one_quarter: ONE_QUARTER,
92+ one_eighth: ONE_EIGHTH,
93+ empty: " ",
94+ };
95+}
96+
97+pub mod line {
98+ pub const VERTICAL: &str = "│";
99+ pub const DOUBLE_VERTICAL: &str = "║";
100+ pub const THICK_VERTICAL: &str = "┃";
101+
102+ pub const HORIZONTAL: &str = "─";
103+ pub const DOUBLE_HORIZONTAL: &str = "═";
104+ pub const THICK_HORIZONTAL: &str = "━";
105+
106+ pub const TOP_RIGHT: &str = "┐";
107+ pub const ROUNDED_TOP_RIGHT: &str = "╮";
108+ pub const DOUBLE_TOP_RIGHT: &str = "╗";
109+ pub const THICK_TOP_RIGHT: &str = "┓";
110+
111+ pub const TOP_LEFT: &str = "┌";
112+ pub const ROUNDED_TOP_LEFT: &str = "╭";
113+ pub const DOUBLE_TOP_LEFT: &str = "╔";
114+ pub const THICK_TOP_LEFT: &str = "┏";
115+
116+ pub const BOTTOM_RIGHT: &str = "┘";
117+ pub const ROUNDED_BOTTOM_RIGHT: &str = "╯";
118+ pub const DOUBLE_BOTTOM_RIGHT: &str = "╝";
119+ pub const THICK_BOTTOM_RIGHT: &str = "┛";
120+
121+ pub const BOTTOM_LEFT: &str = "└";
122+ pub const ROUNDED_BOTTOM_LEFT: &str = "╰";
123+ pub const DOUBLE_BOTTOM_LEFT: &str = "╚";
124+ pub const THICK_BOTTOM_LEFT: &str = "┗";
125+
126+ pub const VERTICAL_LEFT: &str = "┤";
127+ pub const DOUBLE_VERTICAL_LEFT: &str = "╣";
128+ pub const THICK_VERTICAL_LEFT: &str = "┫";
129+
130+ pub const VERTICAL_RIGHT: &str = "├";
131+ pub const DOUBLE_VERTICAL_RIGHT: &str = "╠";
132+ pub const THICK_VERTICAL_RIGHT: &str = "┣";
133+
134+ pub const HORIZONTAL_DOWN: &str = "┬";
135+ pub const DOUBLE_HORIZONTAL_DOWN: &str = "╦";
136+ pub const THICK_HORIZONTAL_DOWN: &str = "┳";
137+
138+ pub const HORIZONTAL_UP: &str = "┴";
139+ pub const DOUBLE_HORIZONTAL_UP: &str = "╩";
140+ pub const THICK_HORIZONTAL_UP: &str = "┻";
141+
142+ pub const CROSS: &str = "┼";
143+ pub const DOUBLE_CROSS: &str = "╬";
144+ pub const THICK_CROSS: &str = "╋";
145+
146+ #[derive(Debug, Clone)]
147+ pub struct Set {
148+ pub vertical: &'static str,
149+ pub horizontal: &'static str,
150+ pub top_right: &'static str,
151+ pub top_left: &'static str,
152+ pub bottom_right: &'static str,
153+ pub bottom_left: &'static str,
154+ pub vertical_left: &'static str,
155+ pub vertical_right: &'static str,
156+ pub horizontal_down: &'static str,
157+ pub horizontal_up: &'static str,
158+ pub cross: &'static str,
159+ }
160+
161+ pub const NORMAL: Set = Set {
162+ vertical: VERTICAL,
163+ horizontal: HORIZONTAL,
164+ top_right: TOP_RIGHT,
165+ top_left: TOP_LEFT,
166+ bottom_right: BOTTOM_RIGHT,
167+ bottom_left: BOTTOM_LEFT,
168+ vertical_left: VERTICAL_LEFT,
169+ vertical_right: VERTICAL_RIGHT,
170+ horizontal_down: HORIZONTAL_DOWN,
171+ horizontal_up: HORIZONTAL_UP,
172+ cross: CROSS,
173+ };
174+
175+ pub const ROUNDED: Set = Set {
176+ top_right: ROUNDED_TOP_RIGHT,
177+ top_left: ROUNDED_TOP_LEFT,
178+ bottom_right: ROUNDED_BOTTOM_RIGHT,
179+ bottom_left: ROUNDED_BOTTOM_LEFT,
180+ ..NORMAL
181+ };
182+
183+ pub const DOUBLE: Set = Set {
184+ vertical: DOUBLE_VERTICAL,
185+ horizontal: DOUBLE_HORIZONTAL,
186+ top_right: DOUBLE_TOP_RIGHT,
187+ top_left: DOUBLE_TOP_LEFT,
188+ bottom_right: DOUBLE_BOTTOM_RIGHT,
189+ bottom_left: DOUBLE_BOTTOM_LEFT,
190+ vertical_left: DOUBLE_VERTICAL_LEFT,
191+ vertical_right: DOUBLE_VERTICAL_RIGHT,
192+ horizontal_down: DOUBLE_HORIZONTAL_DOWN,
193+ horizontal_up: DOUBLE_HORIZONTAL_UP,
194+ cross: DOUBLE_CROSS,
195+ };
196+
197+ pub const THICK: Set = Set {
198+ vertical: THICK_VERTICAL,
199+ horizontal: THICK_HORIZONTAL,
200+ top_right: THICK_TOP_RIGHT,
201+ top_left: THICK_TOP_LEFT,
202+ bottom_right: THICK_BOTTOM_RIGHT,
203+ bottom_left: THICK_BOTTOM_LEFT,
204+ vertical_left: THICK_VERTICAL_LEFT,
205+ vertical_right: THICK_VERTICAL_RIGHT,
206+ horizontal_down: THICK_HORIZONTAL_DOWN,
207+ horizontal_up: THICK_HORIZONTAL_UP,
208+ cross: THICK_CROSS,
209+ };
210+}
211+
212+pub const DOT: &str = "•";
213+
214+pub mod braille {
215+ pub const BLANK: u16 = 0x2800;
216+ pub const DOTS: [[u16; 2]; 4] = [
217+ [0x0001, 0x0008],
218+ [0x0002, 0x0010],
219+ [0x0004, 0x0020],
220+ [0x0040, 0x0080],
221+ ];
222+}
223+
224+/// Marker to use when plotting data points
225+#[derive(Debug, Clone, Copy)]
226+pub enum Marker {
227+ /// One point per cell in shape of dot
228+ Dot,
229+ /// One point per cell in shape of a block
230+ Block,
231+ /// Up to 8 points per cell
232+ Braille,
233+}
helix-term/src/terminal.rs → helix-tui/src/terminal.rsrenamed+1−6
@@ -1,10 +1,5 @@
1+use crate::{backend::Backend, buffer::Buffer, layout::Rect};
12use std::io;
2−use tui::{
3− backend::Backend,
4− buffer::Buffer,
5− layout::Rect,
6− widgets::{StatefulWidget, Widget},
7−};
83
94#[derive(Debug, Clone, PartialEq)]
105/// UNSTABLE
helix-tui/src/text.rsnew file+434
@@ -0,0 +1,434 @@
1+//! Primitives for styled text.
2+//!
3+//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
4+//! those strings may be associated to a set of styles. `tui` has three ways to represent them:
5+//! - A single line string where all graphemes have the same style is represented by a [`Span`].
6+//! - A single line string where each grapheme may have its own style is represented by [`Spans`].
7+//! - A multiple line string where each grapheme may have its own style is represented by a
8+//! [`Text`].
9+//!
10+//! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`]
11+//! is a [`Spans`].
12+//!
13+//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
14+//! supported for their properties. Moreover, `tui` provides convenient `From` implementations so
15+//! that you can start by using simple `String` or `&str` and then promote them to the previous
16+//! primitives when you need additional styling capabilities.
17+//!
18+//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
19+//! its `title` property (which is a [`Spans`] under the hood):
20+//!
21+//! ```rust
22+//! # use helix_tui::widgets::Block;
23+//! # use helix_tui::text::{Span, Spans};
24+//! # use helix_tui::style::{Color, Style};
25+//! // A simple string with no styling.
26+//! // Converted to Spans(vec![
27+//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
28+//! // ])
29+//! let block = Block::default().title("My title");
30+//!
31+//! // A simple string with a unique style.
32+//! // Converted to Spans(vec![
33+//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
34+//! // ])
35+//! let block = Block::default().title(
36+//! Span::styled("My title", Style::default().fg(Color::Yellow))
37+//! );
38+//!
39+//! // A string with multiple styles.
40+//! // Converted to Spans(vec![
41+//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
42+//! // Span { content: Cow::Borrowed(" title"), .. }
43+//! // ])
44+//! let block = Block::default().title(vec![
45+//! Span::styled("My", Style::default().fg(Color::Yellow)),
46+//! Span::raw(" title"),
47+//! ]);
48+//! ```
49+use crate::style::Style;
50+use std::borrow::Cow;
51+use unicode_segmentation::UnicodeSegmentation;
52+use unicode_width::UnicodeWidthStr;
53+
54+/// A grapheme associated to a style.
55+#[derive(Debug, Clone, PartialEq)]
56+pub struct StyledGrapheme<'a> {
57+ pub symbol: &'a str,
58+ pub style: Style,
59+}
60+
61+/// A string where all graphemes have the same style.
62+#[derive(Debug, Clone, PartialEq)]
63+pub struct Span<'a> {
64+ pub content: Cow<'a, str>,
65+ pub style: Style,
66+}
67+
68+impl<'a> Span<'a> {
69+ /// Create a span with no style.
70+ ///
71+ /// ## Examples
72+ ///
73+ /// ```rust
74+ /// # use helix_tui::text::Span;
75+ /// Span::raw("My text");
76+ /// Span::raw(String::from("My text"));
77+ /// ```
78+ pub fn raw<T>(content: T) -> Span<'a>
79+ where
80+ T: Into<Cow<'a, str>>,
81+ {
82+ Span {
83+ content: content.into(),
84+ style: Style::default(),
85+ }
86+ }
87+
88+ /// Create a span with a style.
89+ ///
90+ /// # Examples
91+ ///
92+ /// ```rust
93+ /// # use helix_tui::text::Span;
94+ /// # use helix_tui::style::{Color, Modifier, Style};
95+ /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
96+ /// Span::styled("My text", style);
97+ /// Span::styled(String::from("My text"), style);
98+ /// ```
99+ pub fn styled<T>(content: T, style: Style) -> Span<'a>
100+ where
101+ T: Into<Cow<'a, str>>,
102+ {
103+ Span {
104+ content: content.into(),
105+ style,
106+ }
107+ }
108+
109+ /// Returns the width of the content held by this span.
110+ pub fn width(&self) -> usize {
111+ self.content.width()
112+ }
113+
114+ /// Returns an iterator over the graphemes held by this span.
115+ ///
116+ /// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
117+ /// the resulting [`Style`].
118+ ///
119+ /// ## Examples
120+ ///
121+ /// ```rust
122+ /// # use helix_tui::text::{Span, StyledGrapheme};
123+ /// # use helix_tui::style::{Color, Modifier, Style};
124+ /// # use std::iter::Iterator;
125+ /// let style = Style::default().fg(Color::Yellow);
126+ /// let span = Span::styled("Text", style);
127+ /// let style = Style::default().fg(Color::Green).bg(Color::Black);
128+ /// let styled_graphemes = span.styled_graphemes(style);
129+ /// assert_eq!(
130+ /// vec![
131+ /// StyledGrapheme {
132+ /// symbol: "T",
133+ /// style: Style {
134+ /// fg: Some(Color::Yellow),
135+ /// bg: Some(Color::Black),
136+ /// add_modifier: Modifier::empty(),
137+ /// sub_modifier: Modifier::empty(),
138+ /// },
139+ /// },
140+ /// StyledGrapheme {
141+ /// symbol: "e",
142+ /// style: Style {
143+ /// fg: Some(Color::Yellow),
144+ /// bg: Some(Color::Black),
145+ /// add_modifier: Modifier::empty(),
146+ /// sub_modifier: Modifier::empty(),
147+ /// },
148+ /// },
149+ /// StyledGrapheme {
150+ /// symbol: "x",
151+ /// style: Style {
152+ /// fg: Some(Color::Yellow),
153+ /// bg: Some(Color::Black),
154+ /// add_modifier: Modifier::empty(),
155+ /// sub_modifier: Modifier::empty(),
156+ /// },
157+ /// },
158+ /// StyledGrapheme {
159+ /// symbol: "t",
160+ /// style: Style {
161+ /// fg: Some(Color::Yellow),
162+ /// bg: Some(Color::Black),
163+ /// add_modifier: Modifier::empty(),
164+ /// sub_modifier: Modifier::empty(),
165+ /// },
166+ /// },
167+ /// ],
168+ /// styled_graphemes.collect::<Vec<StyledGrapheme>>()
169+ /// );
170+ /// ```
171+ pub fn styled_graphemes(
172+ &'a self,
173+ base_style: Style,
174+ ) -> impl Iterator<Item = StyledGrapheme<'a>> {
175+ UnicodeSegmentation::graphemes(self.content.as_ref(), true)
176+ .map(move |g| StyledGrapheme {
177+ symbol: g,
178+ style: base_style.patch(self.style),
179+ })
180+ .filter(|s| s.symbol != "\n")
181+ }
182+}
183+
184+impl<'a> From<String> for Span<'a> {
185+ fn from(s: String) -> Span<'a> {
186+ Span::raw(s)
187+ }
188+}
189+
190+impl<'a> From<&'a str> for Span<'a> {
191+ fn from(s: &'a str) -> Span<'a> {
192+ Span::raw(s)
193+ }
194+}
195+
196+/// A string composed of clusters of graphemes, each with their own style.
197+#[derive(Debug, Clone, PartialEq)]
198+pub struct Spans<'a>(pub Vec<Span<'a>>);
199+
200+impl<'a> Default for Spans<'a> {
201+ fn default() -> Spans<'a> {
202+ Spans(Vec::new())
203+ }
204+}
205+
206+impl<'a> Spans<'a> {
207+ /// Returns the width of the underlying string.
208+ ///
209+ /// ## Examples
210+ ///
211+ /// ```rust
212+ /// # use helix_tui::text::{Span, Spans};
213+ /// # use helix_tui::style::{Color, Style};
214+ /// let spans = Spans::from(vec![
215+ /// Span::styled("My", Style::default().fg(Color::Yellow)),
216+ /// Span::raw(" text"),
217+ /// ]);
218+ /// assert_eq!(7, spans.width());
219+ /// ```
220+ pub fn width(&self) -> usize {
221+ self.0.iter().map(Span::width).sum()
222+ }
223+}
224+
225+impl<'a> From<String> for Spans<'a> {
226+ fn from(s: String) -> Spans<'a> {
227+ Spans(vec![Span::from(s)])
228+ }
229+}
230+
231+impl<'a> From<&'a str> for Spans<'a> {
232+ fn from(s: &'a str) -> Spans<'a> {
233+ Spans(vec![Span::from(s)])
234+ }
235+}
236+
237+impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
238+ fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
239+ Spans(spans)
240+ }
241+}
242+
243+impl<'a> From<Span<'a>> for Spans<'a> {
244+ fn from(span: Span<'a>) -> Spans<'a> {
245+ Spans(vec![span])
246+ }
247+}
248+
249+impl<'a> From<Spans<'a>> for String {
250+ fn from(line: Spans<'a>) -> String {
251+ line.0.iter().fold(String::new(), |mut acc, s| {
252+ acc.push_str(s.content.as_ref());
253+ acc
254+ })
255+ }
256+}
257+
258+/// A string split over multiple lines where each line is composed of several clusters, each with
259+/// their own style.
260+///
261+/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
262+/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
263+/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
264+///
265+/// ```rust
266+/// # use helix_tui::text::Text;
267+/// # use helix_tui::style::{Color, Modifier, Style};
268+/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
269+///
270+/// // An initial two lines of `Text` built from a `&str`
271+/// let mut text = Text::from("The first line\nThe second line");
272+/// assert_eq!(2, text.height());
273+///
274+/// // Adding two more unstyled lines
275+/// text.extend(Text::raw("These are two\nmore lines!"));
276+/// assert_eq!(4, text.height());
277+///
278+/// // Adding a final two styled lines
279+/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
280+/// assert_eq!(6, text.height());
281+/// ```
282+#[derive(Debug, Clone, PartialEq)]
283+pub struct Text<'a> {
284+ pub lines: Vec<Spans<'a>>,
285+}
286+
287+impl<'a> Default for Text<'a> {
288+ fn default() -> Text<'a> {
289+ Text { lines: Vec::new() }
290+ }
291+}
292+
293+impl<'a> Text<'a> {
294+ /// Create some text (potentially multiple lines) with no style.
295+ ///
296+ /// ## Examples
297+ ///
298+ /// ```rust
299+ /// # use helix_tui::text::Text;
300+ /// Text::raw("The first line\nThe second line");
301+ /// Text::raw(String::from("The first line\nThe second line"));
302+ /// ```
303+ pub fn raw<T>(content: T) -> Text<'a>
304+ where
305+ T: Into<Cow<'a, str>>,
306+ {
307+ Text {
308+ lines: match content.into() {
309+ Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
310+ Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
311+ },
312+ }
313+ }
314+
315+ /// Create some text (potentially multiple lines) with a style.
316+ ///
317+ /// # Examples
318+ ///
319+ /// ```rust
320+ /// # use helix_tui::text::Text;
321+ /// # use helix_tui::style::{Color, Modifier, Style};
322+ /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
323+ /// Text::styled("The first line\nThe second line", style);
324+ /// Text::styled(String::from("The first line\nThe second line"), style);
325+ /// ```
326+ pub fn styled<T>(content: T, style: Style) -> Text<'a>
327+ where
328+ T: Into<Cow<'a, str>>,
329+ {
330+ let mut text = Text::raw(content);
331+ text.patch_style(style);
332+ text
333+ }
334+
335+ /// Returns the max width of all the lines.
336+ ///
337+ /// ## Examples
338+ ///
339+ /// ```rust
340+ /// use helix_tui::text::Text;
341+ /// let text = Text::from("The first line\nThe second line");
342+ /// assert_eq!(15, text.width());
343+ /// ```
344+ pub fn width(&self) -> usize {
345+ self.lines
346+ .iter()
347+ .map(Spans::width)
348+ .max()
349+ .unwrap_or_default()
350+ }
351+
352+ /// Returns the height.
353+ ///
354+ /// ## Examples
355+ ///
356+ /// ```rust
357+ /// use helix_tui::text::Text;
358+ /// let text = Text::from("The first line\nThe second line");
359+ /// assert_eq!(2, text.height());
360+ /// ```
361+ pub fn height(&self) -> usize {
362+ self.lines.len()
363+ }
364+
365+ /// Apply a new style to existing text.
366+ ///
367+ /// # Examples
368+ ///
369+ /// ```rust
370+ /// # use helix_tui::text::Text;
371+ /// # use helix_tui::style::{Color, Modifier, Style};
372+ /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
373+ /// let mut raw_text = Text::raw("The first line\nThe second line");
374+ /// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
375+ /// assert_ne!(raw_text, styled_text);
376+ ///
377+ /// raw_text.patch_style(style);
378+ /// assert_eq!(raw_text, styled_text);
379+ /// ```
380+ pub fn patch_style(&mut self, style: Style) {
381+ for line in &mut self.lines {
382+ for span in &mut line.0 {
383+ span.style = span.style.patch(style);
384+ }
385+ }
386+ }
387+}
388+
389+impl<'a> From<String> for Text<'a> {
390+ fn from(s: String) -> Text<'a> {
391+ Text::raw(s)
392+ }
393+}
394+
395+impl<'a> From<&'a str> for Text<'a> {
396+ fn from(s: &'a str) -> Text<'a> {
397+ Text::raw(s)
398+ }
399+}
400+
401+impl<'a> From<Span<'a>> for Text<'a> {
402+ fn from(span: Span<'a>) -> Text<'a> {
403+ Text {
404+ lines: vec![Spans::from(span)],
405+ }
406+ }
407+}
408+
409+impl<'a> From<Spans<'a>> for Text<'a> {
410+ fn from(spans: Spans<'a>) -> Text<'a> {
411+ Text { lines: vec![spans] }
412+ }
413+}
414+
415+impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
416+ fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
417+ Text { lines }
418+ }
419+}
420+
421+impl<'a> IntoIterator for Text<'a> {
422+ type Item = Spans<'a>;
423+ type IntoIter = std::vec::IntoIter<Self::Item>;
424+
425+ fn into_iter(self) -> Self::IntoIter {
426+ self.lines.into_iter()
427+ }
428+}
429+
430+impl<'a> Extend<Spans<'a>> for Text<'a> {
431+ fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
432+ self.lines.extend(iter);
433+ }
434+}
helix-tui/src/widgets/block.rsnew file+511
@@ -0,0 +1,511 @@
1+use crate::{
2+ buffer::Buffer,
3+ layout::Rect,
4+ style::Style,
5+ symbols::line,
6+ text::{Span, Spans},
7+ widgets::{Borders, Widget},
8+};
9+
10+#[derive(Debug, Clone, Copy, PartialEq)]
11+pub enum BorderType {
12+ Plain,
13+ Rounded,
14+ Double,
15+ Thick,
16+}
17+
18+impl BorderType {
19+ pub fn line_symbols(border_type: BorderType) -> line::Set {
20+ match border_type {
21+ BorderType::Plain => line::NORMAL,
22+ BorderType::Rounded => line::ROUNDED,
23+ BorderType::Double => line::DOUBLE,
24+ BorderType::Thick => line::THICK,
25+ }
26+ }
27+}
28+
29+/// Base widget to be used with all upper level ones. It may be used to display a box border around
30+/// the widget and/or add a title.
31+///
32+/// # Examples
33+///
34+/// ```
35+/// # use helix_tui::widgets::{Block, BorderType, Borders};
36+/// # use helix_tui::style::{Style, Color};
37+/// Block::default()
38+/// .title("Block")
39+/// .borders(Borders::LEFT | Borders::RIGHT)
40+/// .border_style(Style::default().fg(Color::White))
41+/// .border_type(BorderType::Rounded)
42+/// .style(Style::default().bg(Color::Black));
43+/// ```
44+#[derive(Debug, Clone, PartialEq)]
45+pub struct Block<'a> {
46+ /// Optional title place on the upper left of the block
47+ title: Option<Spans<'a>>,
48+ /// Visible borders
49+ borders: Borders,
50+ /// Border style
51+ border_style: Style,
52+ /// Type of the border. The default is plain lines but one can choose to have rounded corners
53+ /// or doubled lines instead.
54+ border_type: BorderType,
55+ /// Widget style
56+ style: Style,
57+}
58+
59+impl<'a> Default for Block<'a> {
60+ fn default() -> Block<'a> {
61+ Block {
62+ title: None,
63+ borders: Borders::NONE,
64+ border_style: Default::default(),
65+ border_type: BorderType::Plain,
66+ style: Default::default(),
67+ }
68+ }
69+}
70+
71+impl<'a> Block<'a> {
72+ pub fn title<T>(mut self, title: T) -> Block<'a>
73+ where
74+ T: Into<Spans<'a>>,
75+ {
76+ self.title = Some(title.into());
77+ self
78+ }
79+
80+ #[deprecated(
81+ since = "0.10.0",
82+ note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
83+ )]
84+ pub fn title_style(mut self, style: Style) -> Block<'a> {
85+ if let Some(t) = self.title {
86+ let title = String::from(t);
87+ self.title = Some(Spans::from(Span::styled(title, style)));
88+ }
89+ self
90+ }
91+
92+ pub fn border_style(mut self, style: Style) -> Block<'a> {
93+ self.border_style = style;
94+ self
95+ }
96+
97+ pub fn style(mut self, style: Style) -> Block<'a> {
98+ self.style = style;
99+ self
100+ }
101+
102+ pub fn borders(mut self, flag: Borders) -> Block<'a> {
103+ self.borders = flag;
104+ self
105+ }
106+
107+ pub fn border_type(mut self, border_type: BorderType) -> Block<'a> {
108+ self.border_type = border_type;
109+ self
110+ }
111+
112+ /// Compute the inner area of a block based on its border visibility rules.
113+ pub fn inner(&self, area: Rect) -> Rect {
114+ let mut inner = area;
115+ if self.borders.intersects(Borders::LEFT) {
116+ inner.x = inner.x.saturating_add(1).min(inner.right());
117+ inner.width = inner.width.saturating_sub(1);
118+ }
119+ if self.borders.intersects(Borders::TOP) || self.title.is_some() {
120+ inner.y = inner.y.saturating_add(1).min(inner.bottom());
121+ inner.height = inner.height.saturating_sub(1);
122+ }
123+ if self.borders.intersects(Borders::RIGHT) {
124+ inner.width = inner.width.saturating_sub(1);
125+ }
126+ if self.borders.intersects(Borders::BOTTOM) {
127+ inner.height = inner.height.saturating_sub(1);
128+ }
129+ inner
130+ }
131+}
132+
133+impl<'a> Widget for Block<'a> {
134+ fn render(self, area: Rect, buf: &mut Buffer) {
135+ if area.area() == 0 {
136+ return;
137+ }
138+ buf.set_style(area, self.style);
139+ let symbols = BorderType::line_symbols(self.border_type);
140+
141+ // Sides
142+ if self.borders.intersects(Borders::LEFT) {
143+ for y in area.top()..area.bottom() {
144+ buf.get_mut(area.left(), y)
145+ .set_symbol(symbols.vertical)
146+ .set_style(self.border_style);
147+ }
148+ }
149+ if self.borders.intersects(Borders::TOP) {
150+ for x in area.left()..area.right() {
151+ buf.get_mut(x, area.top())
152+ .set_symbol(symbols.horizontal)
153+ .set_style(self.border_style);
154+ }
155+ }
156+ if self.borders.intersects(Borders::RIGHT) {
157+ let x = area.right() - 1;
158+ for y in area.top()..area.bottom() {
159+ buf.get_mut(x, y)
160+ .set_symbol(symbols.vertical)
161+ .set_style(self.border_style);
162+ }
163+ }
164+ if self.borders.intersects(Borders::BOTTOM) {
165+ let y = area.bottom() - 1;
166+ for x in area.left()..area.right() {
167+ buf.get_mut(x, y)
168+ .set_symbol(symbols.horizontal)
169+ .set_style(self.border_style);
170+ }
171+ }
172+
173+ // Corners
174+ if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
175+ buf.get_mut(area.right() - 1, area.bottom() - 1)
176+ .set_symbol(symbols.bottom_right)
177+ .set_style(self.border_style);
178+ }
179+ if self.borders.contains(Borders::RIGHT | Borders::TOP) {
180+ buf.get_mut(area.right() - 1, area.top())
181+ .set_symbol(symbols.top_right)
182+ .set_style(self.border_style);
183+ }
184+ if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
185+ buf.get_mut(area.left(), area.bottom() - 1)
186+ .set_symbol(symbols.bottom_left)
187+ .set_style(self.border_style);
188+ }
189+ if self.borders.contains(Borders::LEFT | Borders::TOP) {
190+ buf.get_mut(area.left(), area.top())
191+ .set_symbol(symbols.top_left)
192+ .set_style(self.border_style);
193+ }
194+
195+ if let Some(title) = self.title {
196+ let lx = if self.borders.intersects(Borders::LEFT) {
197+ 1
198+ } else {
199+ 0
200+ };
201+ let rx = if self.borders.intersects(Borders::RIGHT) {
202+ 1
203+ } else {
204+ 0
205+ };
206+ let width = area.width.saturating_sub(lx).saturating_sub(rx);
207+ buf.set_spans(area.left() + lx, area.top(), &title, width);
208+ }
209+ }
210+}
211+
212+#[cfg(test)]
213+mod tests {
214+ use super::*;
215+ use crate::layout::Rect;
216+
217+ #[test]
218+ fn inner_takes_into_account_the_borders() {
219+ // No borders
220+ assert_eq!(
221+ Block::default().inner(Rect::default()),
222+ Rect {
223+ x: 0,
224+ y: 0,
225+ width: 0,
226+ height: 0
227+ },
228+ "no borders, width=0, height=0"
229+ );
230+ assert_eq!(
231+ Block::default().inner(Rect {
232+ x: 0,
233+ y: 0,
234+ width: 1,
235+ height: 1
236+ }),
237+ Rect {
238+ x: 0,
239+ y: 0,
240+ width: 1,
241+ height: 1
242+ },
243+ "no borders, width=1, height=1"
244+ );
245+
246+ // Left border
247+ assert_eq!(
248+ Block::default().borders(Borders::LEFT).inner(Rect {
249+ x: 0,
250+ y: 0,
251+ width: 0,
252+ height: 1
253+ }),
254+ Rect {
255+ x: 0,
256+ y: 0,
257+ width: 0,
258+ height: 1
259+ },
260+ "left, width=0"
261+ );
262+ assert_eq!(
263+ Block::default().borders(Borders::LEFT).inner(Rect {
264+ x: 0,
265+ y: 0,
266+ width: 1,
267+ height: 1
268+ }),
269+ Rect {
270+ x: 1,
271+ y: 0,
272+ width: 0,
273+ height: 1
274+ },
275+ "left, width=1"
276+ );
277+ assert_eq!(
278+ Block::default().borders(Borders::LEFT).inner(Rect {
279+ x: 0,
280+ y: 0,
281+ width: 2,
282+ height: 1
283+ }),
284+ Rect {
285+ x: 1,
286+ y: 0,
287+ width: 1,
288+ height: 1
289+ },
290+ "left, width=2"
291+ );
292+
293+ // Top border
294+ assert_eq!(
295+ Block::default().borders(Borders::TOP).inner(Rect {
296+ x: 0,
297+ y: 0,
298+ width: 1,
299+ height: 0
300+ }),
301+ Rect {
302+ x: 0,
303+ y: 0,
304+ width: 1,
305+ height: 0
306+ },
307+ "top, height=0"
308+ );
309+ assert_eq!(
310+ Block::default().borders(Borders::TOP).inner(Rect {
311+ x: 0,
312+ y: 0,
313+ width: 1,
314+ height: 1
315+ }),
316+ Rect {
317+ x: 0,
318+ y: 1,
319+ width: 1,
320+ height: 0
321+ },
322+ "top, height=1"
323+ );
324+ assert_eq!(
325+ Block::default().borders(Borders::TOP).inner(Rect {
326+ x: 0,
327+ y: 0,
328+ width: 1,
329+ height: 2
330+ }),
331+ Rect {
332+ x: 0,
333+ y: 1,
334+ width: 1,
335+ height: 1
336+ },
337+ "top, height=2"
338+ );
339+
340+ // Right border
341+ assert_eq!(
342+ Block::default().borders(Borders::RIGHT).inner(Rect {
343+ x: 0,
344+ y: 0,
345+ width: 0,
346+ height: 1
347+ }),
348+ Rect {
349+ x: 0,
350+ y: 0,
351+ width: 0,
352+ height: 1
353+ },
354+ "right, width=0"
355+ );
356+ assert_eq!(
357+ Block::default().borders(Borders::RIGHT).inner(Rect {
358+ x: 0,
359+ y: 0,
360+ width: 1,
361+ height: 1
362+ }),
363+ Rect {
364+ x: 0,
365+ y: 0,
366+ width: 0,
367+ height: 1
368+ },
369+ "right, width=1"
370+ );
371+ assert_eq!(
372+ Block::default().borders(Borders::RIGHT).inner(Rect {
373+ x: 0,
374+ y: 0,
375+ width: 2,
376+ height: 1
377+ }),
378+ Rect {
379+ x: 0,
380+ y: 0,
381+ width: 1,
382+ height: 1
383+ },
384+ "right, width=2"
385+ );
386+
387+ // Bottom border
388+ assert_eq!(
389+ Block::default().borders(Borders::BOTTOM).inner(Rect {
390+ x: 0,
391+ y: 0,
392+ width: 1,
393+ height: 0
394+ }),
395+ Rect {
396+ x: 0,
397+ y: 0,
398+ width: 1,
399+ height: 0
400+ },
401+ "bottom, height=0"
402+ );
403+ assert_eq!(
404+ Block::default().borders(Borders::BOTTOM).inner(Rect {
405+ x: 0,
406+ y: 0,
407+ width: 1,
408+ height: 1
409+ }),
410+ Rect {
411+ x: 0,
412+ y: 0,
413+ width: 1,
414+ height: 0
415+ },
416+ "bottom, height=1"
417+ );
418+ assert_eq!(
419+ Block::default().borders(Borders::BOTTOM).inner(Rect {
420+ x: 0,
421+ y: 0,
422+ width: 1,
423+ height: 2
424+ }),
425+ Rect {
426+ x: 0,
427+ y: 0,
428+ width: 1,
429+ height: 1
430+ },
431+ "bottom, height=2"
432+ );
433+
434+ // All borders
435+ assert_eq!(
436+ Block::default()
437+ .borders(Borders::ALL)
438+ .inner(Rect::default()),
439+ Rect {
440+ x: 0,
441+ y: 0,
442+ width: 0,
443+ height: 0
444+ },
445+ "all borders, width=0, height=0"
446+ );
447+ assert_eq!(
448+ Block::default().borders(Borders::ALL).inner(Rect {
449+ x: 0,
450+ y: 0,
451+ width: 1,
452+ height: 1
453+ }),
454+ Rect {
455+ x: 1,
456+ y: 1,
457+ width: 0,
458+ height: 0,
459+ },
460+ "all borders, width=1, height=1"
461+ );
462+ assert_eq!(
463+ Block::default().borders(Borders::ALL).inner(Rect {
464+ x: 0,
465+ y: 0,
466+ width: 2,
467+ height: 2,
468+ }),
469+ Rect {
470+ x: 1,
471+ y: 1,
472+ width: 0,
473+ height: 0,
474+ },
475+ "all borders, width=2, height=2"
476+ );
477+ assert_eq!(
478+ Block::default().borders(Borders::ALL).inner(Rect {
479+ x: 0,
480+ y: 0,
481+ width: 3,
482+ height: 3,
483+ }),
484+ Rect {
485+ x: 1,
486+ y: 1,
487+ width: 1,
488+ height: 1,
489+ },
490+ "all borders, width=3, height=3"
491+ );
492+ }
493+
494+ #[test]
495+ fn inner_takes_into_account_the_title() {
496+ assert_eq!(
497+ Block::default().title("Test").inner(Rect {
498+ x: 0,
499+ y: 0,
500+ width: 0,
501+ height: 1,
502+ }),
503+ Rect {
504+ x: 0,
505+ y: 1,
506+ width: 0,
507+ height: 0,
508+ },
509+ );
510+ }
511+}
helix-tui/src/widgets/clear.rsnew file+36
@@ -0,0 +1,36 @@
1+use crate::buffer::Buffer;
2+use crate::layout::Rect;
3+use crate::widgets::Widget;
4+
5+/// A widget to to clear/reset a certain area to allow overdrawing (e.g. for popups)
6+///
7+/// # Examples
8+///
9+/// ```
10+/// # use helix_tui::widgets::{Clear, Block, Borders};
11+/// # use helix_tui::layout::Rect;
12+/// # use helix_tui::Frame;
13+/// # use helix_tui::backend::Backend;
14+/// fn draw_on_clear<B: Backend>(f: &mut Frame<B>, area: Rect) {
15+/// let block = Block::default().title("Block").borders(Borders::ALL);
16+/// f.render_widget(Clear, area); // <- this will clear/reset the area first
17+/// f.render_widget(block, area); // now render the block widget
18+/// }
19+/// ```
20+///
21+/// # Popup Example
22+///
23+/// For a more complete example how to utilize `Clear` to realize popups see
24+/// the example `examples/popup.rs`
25+#[derive(Debug, Clone)]
26+pub struct Clear;
27+
28+impl Widget for Clear {
29+ fn render(self, area: Rect, buf: &mut Buffer) {
30+ for x in area.left()..area.right() {
31+ for y in area.top()..area.bottom() {
32+ buf.get_mut(x, y).reset();
33+ }
34+ }
35+ }
36+}
helix-tui/src/widgets/list.rsnew file+249
@@ -0,0 +1,249 @@
1+use crate::{
2+ buffer::Buffer,
3+ layout::{Corner, Rect},
4+ style::Style,
5+ text::Text,
6+ widgets::{Block, StatefulWidget, Widget},
7+};
8+use std::iter::{self, Iterator};
9+use unicode_width::UnicodeWidthStr;
10+
11+#[derive(Debug, Clone)]
12+pub struct ListState {
13+ offset: usize,
14+ selected: Option<usize>,
15+}
16+
17+impl Default for ListState {
18+ fn default() -> ListState {
19+ ListState {
20+ offset: 0,
21+ selected: None,
22+ }
23+ }
24+}
25+
26+impl ListState {
27+ pub fn selected(&self) -> Option<usize> {
28+ self.selected
29+ }
30+
31+ pub fn select(&mut self, index: Option<usize>) {
32+ self.selected = index;
33+ if index.is_none() {
34+ self.offset = 0;
35+ }
36+ }
37+}
38+
39+#[derive(Debug, Clone, PartialEq)]
40+pub struct ListItem<'a> {
41+ content: Text<'a>,
42+ style: Style,
43+}
44+
45+impl<'a> ListItem<'a> {
46+ pub fn new<T>(content: T) -> ListItem<'a>
47+ where
48+ T: Into<Text<'a>>,
49+ {
50+ ListItem {
51+ content: content.into(),
52+ style: Style::default(),
53+ }
54+ }
55+
56+ pub fn style(mut self, style: Style) -> ListItem<'a> {
57+ self.style = style;
58+ self
59+ }
60+
61+ pub fn height(&self) -> usize {
62+ self.content.height()
63+ }
64+}
65+
66+/// A widget to display several items among which one can be selected (optional)
67+///
68+/// # Examples
69+///
70+/// ```
71+/// # use helix_tui::widgets::{Block, Borders, List, ListItem};
72+/// # use helix_tui::style::{Style, Color, Modifier};
73+/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")];
74+/// List::new(items)
75+/// .block(Block::default().title("List").borders(Borders::ALL))
76+/// .style(Style::default().fg(Color::White))
77+/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
78+/// .highlight_symbol(">>");
79+/// ```
80+#[derive(Debug, Clone)]
81+pub struct List<'a> {
82+ block: Option<Block<'a>>,
83+ items: Vec<ListItem<'a>>,
84+ /// Style used as a base style for the widget
85+ style: Style,
86+ start_corner: Corner,
87+ /// Style used to render selected item
88+ highlight_style: Style,
89+ /// Symbol in front of the selected item (Shift all items to the right)
90+ highlight_symbol: Option<&'a str>,
91+}
92+
93+impl<'a> List<'a> {
94+ pub fn new<T>(items: T) -> List<'a>
95+ where
96+ T: Into<Vec<ListItem<'a>>>,
97+ {
98+ List {
99+ block: None,
100+ style: Style::default(),
101+ items: items.into(),
102+ start_corner: Corner::TopLeft,
103+ highlight_style: Style::default(),
104+ highlight_symbol: None,
105+ }
106+ }
107+
108+ pub fn block(mut self, block: Block<'a>) -> List<'a> {
109+ self.block = Some(block);
110+ self
111+ }
112+
113+ pub fn style(mut self, style: Style) -> List<'a> {
114+ self.style = style;
115+ self
116+ }
117+
118+ pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> List<'a> {
119+ self.highlight_symbol = Some(highlight_symbol);
120+ self
121+ }
122+
123+ pub fn highlight_style(mut self, style: Style) -> List<'a> {
124+ self.highlight_style = style;
125+ self
126+ }
127+
128+ pub fn start_corner(mut self, corner: Corner) -> List<'a> {
129+ self.start_corner = corner;
130+ self
131+ }
132+}
133+
134+impl<'a> StatefulWidget for List<'a> {
135+ type State = ListState;
136+
137+ fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
138+ buf.set_style(area, self.style);
139+ let list_area = match self.block.take() {
140+ Some(b) => {
141+ let inner_area = b.inner(area);
142+ b.render(area, buf);
143+ inner_area
144+ }
145+ None => area,
146+ };
147+
148+ if list_area.width < 1 || list_area.height < 1 {
149+ return;
150+ }
151+
152+ if self.items.is_empty() {
153+ return;
154+ }
155+ let list_height = list_area.height as usize;
156+
157+ let mut start = state.offset;
158+ let mut end = state.offset;
159+ let mut height = 0;
160+ for item in self.items.iter().skip(state.offset) {
161+ if height + item.height() > list_height {
162+ break;
163+ }
164+ height += item.height();
165+ end += 1;
166+ }
167+
168+ let selected = state.selected.unwrap_or(0).min(self.items.len() - 1);
169+ while selected >= end {
170+ height = height.saturating_add(self.items[end].height());
171+ end += 1;
172+ while height > list_height {
173+ height = height.saturating_sub(self.items[start].height());
174+ start += 1;
175+ }
176+ }
177+ while selected < start {
178+ start -= 1;
179+ height = height.saturating_add(self.items[start].height());
180+ while height > list_height {
181+ end -= 1;
182+ height = height.saturating_sub(self.items[end].height());
183+ }
184+ }
185+ state.offset = start;
186+
187+ let highlight_symbol = self.highlight_symbol.unwrap_or("");
188+ let blank_symbol = iter::repeat(" ")
189+ .take(highlight_symbol.width())
190+ .collect::<String>();
191+
192+ let mut current_height = 0;
193+ let has_selection = state.selected.is_some();
194+ for (i, item) in self
195+ .items
196+ .iter_mut()
197+ .enumerate()
198+ .skip(state.offset)
199+ .take(end - start)
200+ {
201+ let (x, y) = match self.start_corner {
202+ Corner::BottomLeft => {
203+ current_height += item.height() as u16;
204+ (list_area.left(), list_area.bottom() - current_height)
205+ }
206+ _ => {
207+ let pos = (list_area.left(), list_area.top() + current_height);
208+ current_height += item.height() as u16;
209+ pos
210+ }
211+ };
212+ let area = Rect {
213+ x,
214+ y,
215+ width: list_area.width,
216+ height: item.height() as u16,
217+ };
218+ let item_style = self.style.patch(item.style);
219+ buf.set_style(area, item_style);
220+
221+ let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
222+ let elem_x = if has_selection {
223+ let symbol = if is_selected {
224+ highlight_symbol
225+ } else {
226+ &blank_symbol
227+ };
228+ let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style);
229+ x
230+ } else {
231+ x
232+ };
233+ let max_element_width = (list_area.width - (elem_x - x)) as usize;
234+ for (j, line) in item.content.lines.iter().enumerate() {
235+ buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
236+ }
237+ if is_selected {
238+ buf.set_style(area, self.highlight_style);
239+ }
240+ }
241+ }
242+}
243+
244+impl<'a> Widget for List<'a> {
245+ fn render(self, area: Rect, buf: &mut Buffer) {
246+ let mut state = ListState::default();
247+ StatefulWidget::render(self, area, buf, &mut state);
248+ }
249+}
helix-tui/src/widgets/mod.rsnew file+53
@@ -0,0 +1,53 @@
1+//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
2+//!
3+//! All widgets are implemented using the builder pattern and are consumable objects. They are not
4+//! meant to be stored but used as *commands* to draw common figures in the UI.
5+//!
6+//! The available widgets are:
7+//! - [`Block`]
8+//! - [`Tabs`]
9+//! - [`List`]
10+//! - [`Table`]
11+//! - [`Paragraph`]
12+//! - [`Clear`]
13+
14+mod block;
15+mod clear;
16+// mod list;
17+mod paragraph;
18+mod reflow;
19+// mod table;
20+
21+pub use self::block::{Block, BorderType};
22+pub use self::clear::Clear;
23+// pub use self::list::{List, ListItem, ListState};
24+pub use self::paragraph::{Paragraph, Wrap};
25+// pub use self::table::{Cell, Row, Table, TableState};
26+
27+use crate::{buffer::Buffer, layout::Rect};
28+use bitflags::bitflags;
29+
30+bitflags! {
31+ /// Bitflags that can be composed to set the visible borders essentially on the block widget.
32+ pub struct Borders: u32 {
33+ /// Show no border (default)
34+ const NONE = 0b0000_0001;
35+ /// Show the top border
36+ const TOP = 0b0000_0010;
37+ /// Show the right border
38+ const RIGHT = 0b0000_0100;
39+ /// Show the bottom border
40+ const BOTTOM = 0b000_1000;
41+ /// Show the left border
42+ const LEFT = 0b0001_0000;
43+ /// Show all borders
44+ const ALL = Self::TOP.bits | Self::RIGHT.bits | Self::BOTTOM.bits | Self::LEFT.bits;
45+ }
46+}
47+
48+/// Base requirements for a Widget
49+pub trait Widget {
50+ /// Draws the current state of the widget in the given buffer. That the only method required to
51+ /// implement a custom widget.
52+ fn render(self, area: Rect, buf: &mut Buffer);
53+}
helix-tui/src/widgets/paragraph.rsnew file+197
@@ -0,0 +1,197 @@
1+use crate::{
2+ buffer::Buffer,
3+ layout::{Alignment, Rect},
4+ style::Style,
5+ text::{StyledGrapheme, Text},
6+ widgets::{
7+ reflow::{LineComposer, LineTruncator, WordWrapper},
8+ Block, Widget,
9+ },
10+};
11+use std::iter;
12+use unicode_width::UnicodeWidthStr;
13+
14+fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
15+ match alignment {
16+ Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
17+ Alignment::Right => text_area_width.saturating_sub(line_width),
18+ Alignment::Left => 0,
19+ }
20+}
21+
22+/// A widget to display some text.
23+///
24+/// # Examples
25+///
26+/// ```
27+/// # use helix_tui::text::{Text, Spans, Span};
28+/// # use helix_tui::widgets::{Block, Borders, Paragraph, Wrap};
29+/// # use helix_tui::style::{Style, Color, Modifier};
30+/// # use helix_tui::layout::{Alignment};
31+/// let text = vec![
32+/// Spans::from(vec![
33+/// Span::raw("First"),
34+/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)),
35+/// Span::raw("."),
36+/// ]),
37+/// Spans::from(Span::styled("Second line", Style::default().fg(Color::Red))),
38+/// ];
39+/// Paragraph::new(text)
40+/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
41+/// .style(Style::default().fg(Color::White).bg(Color::Black))
42+/// .alignment(Alignment::Center)
43+/// .wrap(Wrap { trim: true });
44+/// ```
45+#[derive(Debug, Clone)]
46+pub struct Paragraph<'a> {
47+ /// A block to wrap the widget in
48+ block: Option<Block<'a>>,
49+ /// Widget style
50+ style: Style,
51+ /// How to wrap the text
52+ wrap: Option<Wrap>,
53+ /// The text to display
54+ text: Text<'a>,
55+ /// Scroll
56+ scroll: (u16, u16),
57+ /// Alignment of the text
58+ alignment: Alignment,
59+}
60+
61+/// Describes how to wrap text across lines.
62+///
63+/// ## Examples
64+///
65+/// ```
66+/// # use helix_tui::widgets::{Paragraph, Wrap};
67+/// # use helix_tui::text::Text;
68+/// let bullet_points = Text::from(r#"Some indented points:
69+/// - First thing goes here and is long so that it wraps
70+/// - Here is another point that is long enough to wrap"#);
71+///
72+/// // With leading spaces trimmed (window width of 30 chars):
73+/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
74+/// // Some indented points:
75+/// // - First thing goes here and is
76+/// // long so that it wraps
77+/// // - Here is another point that
78+/// // is long enough to wrap
79+///
80+/// // But without trimming, indentation is preserved:
81+/// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
82+/// // Some indented points:
83+/// // - First thing goes here
84+/// // and is long so that it wraps
85+/// // - Here is another point
86+/// // that is long enough to wrap
87+/// ```
88+#[derive(Debug, Clone, Copy)]
89+pub struct Wrap {
90+ /// Should leading whitespace be trimmed
91+ pub trim: bool,
92+}
93+
94+impl<'a> Paragraph<'a> {
95+ pub fn new<T>(text: T) -> Paragraph<'a>
96+ where
97+ T: Into<Text<'a>>,
98+ {
99+ Paragraph {
100+ block: None,
101+ style: Default::default(),
102+ wrap: None,
103+ text: text.into(),
104+ scroll: (0, 0),
105+ alignment: Alignment::Left,
106+ }
107+ }
108+
109+ pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> {
110+ self.block = Some(block);
111+ self
112+ }
113+
114+ pub fn style(mut self, style: Style) -> Paragraph<'a> {
115+ self.style = style;
116+ self
117+ }
118+
119+ pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
120+ self.wrap = Some(wrap);
121+ self
122+ }
123+
124+ pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
125+ self.scroll = offset;
126+ self
127+ }
128+
129+ pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> {
130+ self.alignment = alignment;
131+ self
132+ }
133+}
134+
135+impl<'a> Widget for Paragraph<'a> {
136+ fn render(mut self, area: Rect, buf: &mut Buffer) {
137+ buf.set_style(area, self.style);
138+ let text_area = match self.block.take() {
139+ Some(b) => {
140+ let inner_area = b.inner(area);
141+ b.render(area, buf);
142+ inner_area
143+ }
144+ None => area,
145+ };
146+
147+ if text_area.height < 1 {
148+ return;
149+ }
150+
151+ let style = self.style;
152+ let mut styled = self.text.lines.iter().flat_map(|spans| {
153+ spans
154+ .0
155+ .iter()
156+ .flat_map(|span| span.styled_graphemes(style))
157+ // Required given the way composers work but might be refactored out if we change
158+ // composers to operate on lines instead of a stream of graphemes.
159+ .chain(iter::once(StyledGrapheme {
160+ symbol: "\n",
161+ style: self.style,
162+ }))
163+ });
164+
165+ let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
166+ Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
167+ } else {
168+ let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
169+ if let Alignment::Left = self.alignment {
170+ line_composer.set_horizontal_offset(self.scroll.1);
171+ }
172+ line_composer
173+ };
174+ let mut y = 0;
175+ while let Some((current_line, current_line_width)) = line_composer.next_line() {
176+ if y >= self.scroll.0 {
177+ let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
178+ for StyledGrapheme { symbol, style } in current_line {
179+ buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
180+ .set_symbol(if symbol.is_empty() {
181+ // If the symbol is empty, the last char which rendered last time will
182+ // leave on the line. It's a quick fix.
183+ " "
184+ } else {
185+ symbol
186+ })
187+ .set_style(*style);
188+ x += symbol.width() as u16;
189+ }
190+ }
191+ y += 1;
192+ if y >= text_area.height + self.scroll.0 {
193+ break;
194+ }
195+ }
196+ }
197+}
helix-tui/src/widgets/reflow.rsnew file+534
@@ -0,0 +1,534 @@
1+use crate::text::StyledGrapheme;
2+use unicode_segmentation::UnicodeSegmentation;
3+use unicode_width::UnicodeWidthStr;
4+
5+const NBSP: &str = "\u{00a0}";
6+
7+/// A state machine to pack styled symbols into lines.
8+/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
9+/// iterators for that).
10+pub trait LineComposer<'a> {
11+ fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>;
12+}
13+
14+/// A state machine that wraps lines on word boundaries.
15+pub struct WordWrapper<'a, 'b> {
16+ symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
17+ max_line_width: u16,
18+ current_line: Vec<StyledGrapheme<'a>>,
19+ next_line: Vec<StyledGrapheme<'a>>,
20+ /// Removes the leading whitespace from lines
21+ trim: bool,
22+}
23+
24+impl<'a, 'b> WordWrapper<'a, 'b> {
25+ pub fn new(
26+ symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
27+ max_line_width: u16,
28+ trim: bool,
29+ ) -> WordWrapper<'a, 'b> {
30+ WordWrapper {
31+ symbols,
32+ max_line_width,
33+ current_line: vec![],
34+ next_line: vec![],
35+ trim,
36+ }
37+ }
38+}
39+
40+impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
41+ fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
42+ if self.max_line_width == 0 {
43+ return None;
44+ }
45+ std::mem::swap(&mut self.current_line, &mut self.next_line);
46+ self.next_line.truncate(0);
47+
48+ let mut current_line_width = self
49+ .current_line
50+ .iter()
51+ .map(|StyledGrapheme { symbol, .. }| symbol.width() as u16)
52+ .sum();
53+
54+ let mut symbols_to_last_word_end: usize = 0;
55+ let mut width_to_last_word_end: u16 = 0;
56+ let mut prev_whitespace = false;
57+ let mut symbols_exhausted = true;
58+ for StyledGrapheme { symbol, style } in &mut self.symbols {
59+ symbols_exhausted = false;
60+ let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
61+
62+ // Ignore characters wider that the total max width.
63+ if symbol.width() as u16 > self.max_line_width
64+ // Skip leading whitespace when trim is enabled.
65+ || self.trim && symbol_whitespace && symbol != "\n" && current_line_width == 0
66+ {
67+ continue;
68+ }
69+
70+ // Break on newline and discard it.
71+ if symbol == "\n" {
72+ if prev_whitespace {
73+ current_line_width = width_to_last_word_end;
74+ self.current_line.truncate(symbols_to_last_word_end);
75+ }
76+ break;
77+ }
78+
79+ // Mark the previous symbol as word end.
80+ if symbol_whitespace && !prev_whitespace {
81+ symbols_to_last_word_end = self.current_line.len();
82+ width_to_last_word_end = current_line_width;
83+ }
84+
85+ self.current_line.push(StyledGrapheme { symbol, style });
86+ current_line_width += symbol.width() as u16;
87+
88+ if current_line_width > self.max_line_width {
89+ // If there was no word break in the text, wrap at the end of the line.
90+ let (truncate_at, truncated_width) = if symbols_to_last_word_end != 0 {
91+ (symbols_to_last_word_end, width_to_last_word_end)
92+ } else {
93+ (self.current_line.len() - 1, self.max_line_width)
94+ };
95+
96+ // Push the remainder to the next line but strip leading whitespace:
97+ {
98+ let remainder = &self.current_line[truncate_at..];
99+ if let Some(remainder_nonwhite) =
100+ remainder.iter().position(|StyledGrapheme { symbol, .. }| {
101+ !symbol.chars().all(&char::is_whitespace)
102+ })
103+ {
104+ self.next_line
105+ .extend_from_slice(&remainder[remainder_nonwhite..]);
106+ }
107+ }
108+ self.current_line.truncate(truncate_at);
109+ current_line_width = truncated_width;
110+ break;
111+ }
112+
113+ prev_whitespace = symbol_whitespace;
114+ }
115+
116+ // Even if the iterator is exhausted, pass the previous remainder.
117+ if symbols_exhausted && self.current_line.is_empty() {
118+ None
119+ } else {
120+ Some((&self.current_line[..], current_line_width))
121+ }
122+ }
123+}
124+
125+/// A state machine that truncates overhanging lines.
126+pub struct LineTruncator<'a, 'b> {
127+ symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
128+ max_line_width: u16,
129+ current_line: Vec<StyledGrapheme<'a>>,
130+ /// Record the offet to skip render
131+ horizontal_offset: u16,
132+}
133+
134+impl<'a, 'b> LineTruncator<'a, 'b> {
135+ pub fn new(
136+ symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
137+ max_line_width: u16,
138+ ) -> LineTruncator<'a, 'b> {
139+ LineTruncator {
140+ symbols,
141+ max_line_width,
142+ horizontal_offset: 0,
143+ current_line: vec![],
144+ }
145+ }
146+
147+ pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) {
148+ self.horizontal_offset = horizontal_offset;
149+ }
150+}
151+
152+impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
153+ fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
154+ if self.max_line_width == 0 {
155+ return None;
156+ }
157+
158+ self.current_line.truncate(0);
159+ let mut current_line_width = 0;
160+
161+ let mut skip_rest = false;
162+ let mut symbols_exhausted = true;
163+ let mut horizontal_offset = self.horizontal_offset as usize;
164+ for StyledGrapheme { symbol, style } in &mut self.symbols {
165+ symbols_exhausted = false;
166+
167+ // Ignore characters wider that the total max width.
168+ if symbol.width() as u16 > self.max_line_width {
169+ continue;
170+ }
171+
172+ // Break on newline and discard it.
173+ if symbol == "\n" {
174+ break;
175+ }
176+
177+ if current_line_width + symbol.width() as u16 > self.max_line_width {
178+ // Exhaust the remainder of the line.
179+ skip_rest = true;
180+ break;
181+ }
182+
183+ let symbol = if horizontal_offset == 0 {
184+ symbol
185+ } else {
186+ let w = symbol.width();
187+ if w > horizontal_offset {
188+ let t = trim_offset(symbol, horizontal_offset);
189+ horizontal_offset = 0;
190+ t
191+ } else {
192+ horizontal_offset -= w;
193+ ""
194+ }
195+ };
196+ current_line_width += symbol.width() as u16;
197+ self.current_line.push(StyledGrapheme { symbol, style });
198+ }
199+
200+ if skip_rest {
201+ for StyledGrapheme { symbol, .. } in &mut self.symbols {
202+ if symbol == "\n" {
203+ break;
204+ }
205+ }
206+ }
207+
208+ if symbols_exhausted && self.current_line.is_empty() {
209+ None
210+ } else {
211+ Some((&self.current_line[..], current_line_width))
212+ }
213+ }
214+}
215+
216+/// This function will return a str slice which start at specified offset.
217+/// As src is a unicode str, start offset has to be calculated with each character.
218+fn trim_offset(src: &str, mut offset: usize) -> &str {
219+ let mut start = 0;
220+ for c in UnicodeSegmentation::graphemes(src, true) {
221+ let w = c.width();
222+ if w <= offset {
223+ offset -= w;
224+ start += c.len();
225+ } else {
226+ break;
227+ }
228+ }
229+ &src[start..]
230+}
231+
232+#[cfg(test)]
233+mod test {
234+ use super::*;
235+ use unicode_segmentation::UnicodeSegmentation;
236+
237+ enum Composer {
238+ WordWrapper { trim: bool },
239+ LineTruncator,
240+ }
241+
242+ fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
243+ let style = Default::default();
244+ let mut styled =
245+ UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
246+ let mut composer: Box<dyn LineComposer> = match which {
247+ Composer::WordWrapper { trim } => {
248+ Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
249+ }
250+ Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
251+ };
252+ let mut lines = vec![];
253+ let mut widths = vec![];
254+ while let Some((styled, width)) = composer.next_line() {
255+ let line = styled
256+ .iter()
257+ .map(|StyledGrapheme { symbol, .. }| *symbol)
258+ .collect::<String>();
259+ assert!(width <= text_area_width);
260+ lines.push(line);
261+ widths.push(width);
262+ }
263+ (lines, widths)
264+ }
265+
266+ #[test]
267+ fn line_composer_one_line() {
268+ let width = 40;
269+ for i in 1..width {
270+ let text = "a".repeat(i);
271+ let (word_wrapper, _) =
272+ run_composer(Composer::WordWrapper { trim: true }, &text, width as u16);
273+ let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
274+ let expected = vec![text];
275+ assert_eq!(word_wrapper, expected);
276+ assert_eq!(line_truncator, expected);
277+ }
278+ }
279+
280+ #[test]
281+ fn line_composer_short_lines() {
282+ let width = 20;
283+ let text =
284+ "abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
285+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
286+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
287+
288+ let wrapped: Vec<&str> = text.split('\n').collect();
289+ assert_eq!(word_wrapper, wrapped);
290+ assert_eq!(line_truncator, wrapped);
291+ }
292+
293+ #[test]
294+ fn line_composer_long_word() {
295+ let width = 20;
296+ let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
297+ let (word_wrapper, _) =
298+ run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
299+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
300+
301+ let wrapped = vec![
302+ &text[..width],
303+ &text[width..width * 2],
304+ &text[width * 2..width * 3],
305+ &text[width * 3..],
306+ ];
307+ assert_eq!(
308+ word_wrapper, wrapped,
309+ "WordWrapper should detect the line cannot be broken on word boundary and \
310+ break it at line width limit."
311+ );
312+ assert_eq!(line_truncator, vec![&text[..width]]);
313+ }
314+
315+ #[test]
316+ fn line_composer_long_sentence() {
317+ let width = 20;
318+ let text =
319+ "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
320+ let text_multi_space =
321+ "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
322+ m n o";
323+ let (word_wrapper_single_space, _) =
324+ run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
325+ let (word_wrapper_multi_space, _) = run_composer(
326+ Composer::WordWrapper { trim: true },
327+ text_multi_space,
328+ width as u16,
329+ );
330+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
331+
332+ let word_wrapped = vec![
333+ "abcd efghij",
334+ "klmnopabcd efgh",
335+ "ijklmnopabcdefg",
336+ "hijkl mnopab c d e f",
337+ "g h i j k l m n o",
338+ ];
339+ assert_eq!(word_wrapper_single_space, word_wrapped);
340+ assert_eq!(word_wrapper_multi_space, word_wrapped);
341+
342+ assert_eq!(line_truncator, vec![&text[..width]]);
343+ }
344+
345+ #[test]
346+ fn line_composer_zero_width() {
347+ let width = 0;
348+ let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
349+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
350+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
351+
352+ let expected: Vec<&str> = Vec::new();
353+ assert_eq!(word_wrapper, expected);
354+ assert_eq!(line_truncator, expected);
355+ }
356+
357+ #[test]
358+ fn line_composer_max_line_width_of_1() {
359+ let width = 1;
360+ let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
361+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
362+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
363+
364+ let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
365+ .filter(|g| g.chars().any(|c| !c.is_whitespace()))
366+ .collect();
367+ assert_eq!(word_wrapper, expected);
368+ assert_eq!(line_truncator, vec!["a"]);
369+ }
370+
371+ #[test]
372+ fn line_composer_max_line_width_of_1_double_width_characters() {
373+ let width = 1;
374+ let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
375+ 両端点では、";
376+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
377+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
378+ assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
379+ assert_eq!(line_truncator, vec!["", "a"]);
380+ }
381+
382+ /// Tests WordWrapper with words some of which exceed line length and some not.
383+ #[test]
384+ fn line_composer_word_wrapper_mixed_length() {
385+ let width = 20;
386+ let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
387+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
388+ assert_eq!(
389+ word_wrapper,
390+ vec![
391+ "abcd efghij",
392+ "klmnopabcdefghijklmn",
393+ "opabcdefghijkl",
394+ "mnopab cdefghi j",
395+ "klmno",
396+ ]
397+ )
398+ }
399+
400+ #[test]
401+ fn line_composer_double_width_chars() {
402+ let width = 20;
403+ let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
404+ では、";
405+ let (word_wrapper, word_wrapper_width) =
406+ run_composer(Composer::WordWrapper { trim: true }, &text, width);
407+ let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
408+ assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
409+ let wrapped = vec![
410+ "コンピュータ上で文字",
411+ "を扱う場合、典型的に",
412+ "は文字による通信を行",
413+ "う場合にその両端点で",
414+ "は、",
415+ ];
416+ assert_eq!(word_wrapper, wrapped);
417+ assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
418+ }
419+
420+ #[test]
421+ fn line_composer_leading_whitespace_removal() {
422+ let width = 20;
423+ let text = "AAAAAAAAAAAAAAAAAAAA AAA";
424+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
425+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
426+ assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
427+ assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
428+ }
429+
430+ /// Tests truncation of leading whitespace.
431+ #[test]
432+ fn line_composer_lots_of_spaces() {
433+ let width = 20;
434+ let text = " ";
435+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
436+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
437+ assert_eq!(word_wrapper, vec![""]);
438+ assert_eq!(line_truncator, vec![" "]);
439+ }
440+
441+ /// Tests an input starting with a letter, folowed by spaces - some of the behaviour is
442+ /// incidental.
443+ #[test]
444+ fn line_composer_char_plus_lots_of_spaces() {
445+ let width = 20;
446+ let text = "a ";
447+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
448+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
449+ // What's happening below is: the first line gets consumed, trailing spaces discarded,
450+ // after 20 of which a word break occurs (probably shouldn't). The second line break
451+ // discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
452+ // that much.
453+ assert_eq!(word_wrapper, vec!["a", ""]);
454+ assert_eq!(line_truncator, vec!["a "]);
455+ }
456+
457+ #[test]
458+ fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
459+ let width = 20;
460+ // Japanese seems not to use spaces but we should break on spaces anyway... We're using it
461+ // to test double-width chars.
462+ // You are more than welcome to add word boundary detection based of alterations of
463+ // hiragana and katakana...
464+ // This happens to also be a test case for mixed width because regular spaces are single width.
465+ let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
466+ let (word_wrapper, word_wrapper_width) =
467+ run_composer(Composer::WordWrapper { trim: true }, text, width);
468+ assert_eq!(
469+ word_wrapper,
470+ vec![
471+ "コンピュ",
472+ "ータ上で文字を扱う場",
473+ "合、 典型的には文",
474+ "字による 通信を行",
475+ "う場合にその両端点で",
476+ "は、",
477+ ]
478+ );
479+ // Odd-sized lines have a space in them.
480+ assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]);
481+ }
482+
483+ /// Ensure words separated by nbsp are wrapped as if they were a single one.
484+ #[test]
485+ fn line_composer_word_wrapper_nbsp() {
486+ let width = 20;
487+ let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
488+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
489+ assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
490+
491+ // Ensure that if the character was a regular space, it would be wrapped differently.
492+ let text_space = text.replace("\u{00a0}", " ");
493+ let (word_wrapper_space, _) =
494+ run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
495+ assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
496+ }
497+
498+ #[test]
499+ fn line_composer_word_wrapper_preserve_indentation() {
500+ let width = 20;
501+ let text = "AAAAAAAAAAAAAAAAAAAA AAA";
502+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
503+ assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
504+ }
505+
506+ #[test]
507+ fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
508+ let width = 10;
509+ let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
510+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
511+ assert_eq!(
512+ word_wrapper,
513+ vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"]
514+ );
515+ }
516+
517+ #[test]
518+ fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
519+ let width = 10;
520+ let text = " 4 Indent\n must wrap!";
521+ let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
522+ assert_eq!(
523+ word_wrapper,
524+ vec![
525+ " ",
526+ " 4",
527+ "Indent",
528+ " ",
529+ " must",
530+ "wrap!"
531+ ]
532+ );
533+ }
534+}
helix-tui/src/widgets/table.rsnew file+538
@@ -0,0 +1,538 @@
1+use crate::{
2+ buffer::Buffer,
3+ layout::{Constraint, Rect},
4+ style::Style,
5+ text::Text,
6+ widgets::{Block, StatefulWidget, Widget},
7+};
8+use cassowary::{
9+ strength::{MEDIUM, REQUIRED, WEAK},
10+ WeightedRelation::*,
11+ {Expression, Solver},
12+};
13+use std::{
14+ collections::HashMap,
15+ iter::{self, Iterator},
16+};
17+use unicode_width::UnicodeWidthStr;
18+
19+/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
20+///
21+/// It can be created from anything that can be converted to a [`Text`].
22+/// ```rust
23+/// # use helix_tui::widgets::Cell;
24+/// # use helix_tui::style::{Style, Modifier};
25+/// # use helix_tui::text::{Span, Spans, Text};
26+/// Cell::from("simple string");
27+///
28+/// Cell::from(Span::from("span"));
29+///
30+/// Cell::from(Spans::from(vec![
31+/// Span::raw("a vec of "),
32+/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
33+/// ]));
34+///
35+/// Cell::from(Text::from("a text"));
36+/// ```
37+///
38+/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
39+/// capabilities of [`Text`].
40+#[derive(Debug, Clone, PartialEq, Default)]
41+pub struct Cell<'a> {
42+ content: Text<'a>,
43+ style: Style,
44+}
45+
46+impl<'a> Cell<'a> {
47+ /// Set the `Style` of this cell.
48+ pub fn style(mut self, style: Style) -> Self {
49+ self.style = style;
50+ self
51+ }
52+}
53+
54+impl<'a, T> From<T> for Cell<'a>
55+where
56+ T: Into<Text<'a>>,
57+{
58+ fn from(content: T) -> Cell<'a> {
59+ Cell {
60+ content: content.into(),
61+ style: Style::default(),
62+ }
63+ }
64+}
65+
66+/// Holds data to be displayed in a [`Table`] widget.
67+///
68+/// A [`Row`] is a collection of cells. It can be created from simple strings:
69+/// ```rust
70+/// # use helix_tui::widgets::Row;
71+/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
72+/// ```
73+///
74+/// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s:
75+/// ```rust
76+/// # use helix_tui::widgets::{Row, Cell};
77+/// # use helix_tui::style::{Style, Color};
78+/// Row::new(vec![
79+/// Cell::from("Cell1"),
80+/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
81+/// ]);
82+/// ```
83+///
84+/// By default, a row has a height of 1 but you can change this using [`Row::height`].
85+#[derive(Debug, Clone, PartialEq, Default)]
86+pub struct Row<'a> {
87+ cells: Vec<Cell<'a>>,
88+ height: u16,
89+ style: Style,
90+ bottom_margin: u16,
91+}
92+
93+impl<'a> Row<'a> {
94+ /// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
95+ pub fn new<T>(cells: T) -> Self
96+ where
97+ T: IntoIterator,
98+ T::Item: Into<Cell<'a>>,
99+ {
100+ Self {
101+ height: 1,
102+ cells: cells.into_iter().map(|c| c.into()).collect(),
103+ style: Style::default(),
104+ bottom_margin: 0,
105+ }
106+ }
107+
108+ /// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
109+ /// height will see its content truncated.
110+ pub fn height(mut self, height: u16) -> Self {
111+ self.height = height;
112+ self
113+ }
114+
115+ /// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a
116+ /// any individual [`Cell`] or event by their [`Text`] content.
117+ pub fn style(mut self, style: Style) -> Self {
118+ self.style = style;
119+ self
120+ }
121+
122+ /// Set the bottom margin. By default, the bottom margin is `0`.
123+ pub fn bottom_margin(mut self, margin: u16) -> Self {
124+ self.bottom_margin = margin;
125+ self
126+ }
127+
128+ /// Returns the total height of the row.
129+ fn total_height(&self) -> u16 {
130+ self.height.saturating_add(self.bottom_margin)
131+ }
132+}
133+
134+/// A widget to display data in formatted columns.
135+///
136+/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
137+/// ```rust
138+/// # use helix_tui::widgets::{Block, Borders, Table, Row, Cell};
139+/// # use helix_tui::layout::Constraint;
140+/// # use helix_tui::style::{Style, Color, Modifier};
141+/// # use helix_tui::text::{Text, Spans, Span};
142+/// Table::new(vec![
143+/// // Row can be created from simple strings.
144+/// Row::new(vec!["Row11", "Row12", "Row13"]),
145+/// // You can style the entire row.
146+/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
147+/// // If you need more control over the styling you may need to create Cells directly
148+/// Row::new(vec![
149+/// Cell::from("Row31"),
150+/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
151+/// Cell::from(Spans::from(vec![
152+/// Span::raw("Row"),
153+/// Span::styled("33", Style::default().fg(Color::Green))
154+/// ])),
155+/// ]),
156+/// // If a Row need to display some content over multiple lines, you just have to change
157+/// // its height.
158+/// Row::new(vec![
159+/// Cell::from("Row\n41"),
160+/// Cell::from("Row\n42"),
161+/// Cell::from("Row\n43"),
162+/// ]).height(2),
163+/// ])
164+/// // You can set the style of the entire Table.
165+/// .style(Style::default().fg(Color::White))
166+/// // It has an optional header, which is simply a Row always visible at the top.
167+/// .header(
168+/// Row::new(vec!["Col1", "Col2", "Col3"])
169+/// .style(Style::default().fg(Color::Yellow))
170+/// // If you want some space between the header and the rest of the rows, you can always
171+/// // specify some margin at the bottom.
172+/// .bottom_margin(1)
173+/// )
174+/// // As any other widget, a Table can be wrapped in a Block.
175+/// .block(Block::default().title("Table"))
176+/// // Columns widths are constrained in the same way as Layout...
177+/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
178+/// // ...and they can be separated by a fixed spacing.
179+/// .column_spacing(1)
180+/// // If you wish to highlight a row in any specific way when it is selected...
181+/// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
182+/// // ...and potentially show a symbol in front of the selection.
183+/// .highlight_symbol(">>");
184+/// ```
185+#[derive(Debug, Clone, PartialEq)]
186+pub struct Table<'a> {
187+ /// A block to wrap the widget in
188+ block: Option<Block<'a>>,
189+ /// Base style for the widget
190+ style: Style,
191+ /// Width constraints for each column
192+ widths: &'a [Constraint],
193+ /// Space between each column
194+ column_spacing: u16,
195+ /// Style used to render the selected row
196+ highlight_style: Style,
197+ /// Symbol in front of the selected rom
198+ highlight_symbol: Option<&'a str>,
199+ /// Optional header
200+ header: Option<Row<'a>>,
201+ /// Data to display in each row
202+ rows: Vec<Row<'a>>,
203+}
204+
205+impl<'a> Table<'a> {
206+ pub fn new<T>(rows: T) -> Self
207+ where
208+ T: IntoIterator<Item = Row<'a>>,
209+ {
210+ Self {
211+ block: None,
212+ style: Style::default(),
213+ widths: &[],
214+ column_spacing: 1,
215+ highlight_style: Style::default(),
216+ highlight_symbol: None,
217+ header: None,
218+ rows: rows.into_iter().collect(),
219+ }
220+ }
221+
222+ pub fn block(mut self, block: Block<'a>) -> Self {
223+ self.block = Some(block);
224+ self
225+ }
226+
227+ pub fn header(mut self, header: Row<'a>) -> Self {
228+ self.header = Some(header);
229+ self
230+ }
231+
232+ pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
233+ let between_0_and_100 = |&w| match w {
234+ Constraint::Percentage(p) => p <= 100,
235+ _ => true,
236+ };
237+ assert!(
238+ widths.iter().all(between_0_and_100),
239+ "Percentages should be between 0 and 100 inclusively."
240+ );
241+ self.widths = widths;
242+ self
243+ }
244+
245+ pub fn style(mut self, style: Style) -> Self {
246+ self.style = style;
247+ self
248+ }
249+
250+ pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
251+ self.highlight_symbol = Some(highlight_symbol);
252+ self
253+ }
254+
255+ pub fn highlight_style(mut self, highlight_style: Style) -> Self {
256+ self.highlight_style = highlight_style;
257+ self
258+ }
259+
260+ pub fn column_spacing(mut self, spacing: u16) -> Self {
261+ self.column_spacing = spacing;
262+ self
263+ }
264+
265+ fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
266+ let mut solver = Solver::new();
267+ let mut var_indices = HashMap::new();
268+ let mut ccs = Vec::new();
269+ let mut variables = Vec::new();
270+ for i in 0..self.widths.len() {
271+ let var = cassowary::Variable::new();
272+ variables.push(var);
273+ var_indices.insert(var, i);
274+ }
275+ let spacing_width = (variables.len() as u16).saturating_sub(1) * self.column_spacing;
276+ let mut available_width = max_width.saturating_sub(spacing_width);
277+ if has_selection {
278+ let highlight_symbol_width =
279+ self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
280+ available_width = available_width.saturating_sub(highlight_symbol_width);
281+ }
282+ for (i, constraint) in self.widths.iter().enumerate() {
283+ ccs.push(variables[i] | GE(WEAK) | 0.);
284+ ccs.push(match *constraint {
285+ Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
286+ Constraint::Percentage(v) => {
287+ variables[i] | EQ(WEAK) | (f64::from(v * available_width) / 100.0)
288+ }
289+ Constraint::Ratio(n, d) => {
290+ variables[i]
291+ | EQ(WEAK)
292+ | (f64::from(available_width) * f64::from(n) / f64::from(d))
293+ }
294+ Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
295+ Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
296+ })
297+ }
298+ solver
299+ .add_constraint(
300+ variables
301+ .iter()
302+ .fold(Expression::from_constant(0.), |acc, v| acc + *v)
303+ | LE(REQUIRED)
304+ | f64::from(available_width),
305+ )
306+ .unwrap();
307+ solver.add_constraints(&ccs).unwrap();
308+ let mut widths = vec![0; variables.len()];
309+ for &(var, value) in solver.fetch_changes() {
310+ let index = var_indices[&var];
311+ let value = if value.is_sign_negative() {
312+ 0
313+ } else {
314+ value.round() as u16
315+ };
316+ widths[index] = value;
317+ }
318+ // Cassowary could still return columns widths greater than the max width when there are
319+ // fixed length constraints that cannot be satisfied. Therefore, we clamp the widths from
320+ // left to right.
321+ let mut available_width = max_width;
322+ for w in &mut widths {
323+ *w = available_width.min(*w);
324+ available_width = available_width
325+ .saturating_sub(*w)
326+ .saturating_sub(self.column_spacing);
327+ }
328+ widths
329+ }
330+
331+ fn get_row_bounds(
332+ &self,
333+ selected: Option<usize>,
334+ offset: usize,
335+ max_height: u16,
336+ ) -> (usize, usize) {
337+ let mut start = offset;
338+ let mut end = offset;
339+ let mut height = 0;
340+ for item in self.rows.iter().skip(offset) {
341+ if height + item.height > max_height {
342+ break;
343+ }
344+ height += item.total_height();
345+ end += 1;
346+ }
347+
348+ let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
349+ while selected >= end {
350+ height = height.saturating_add(self.rows[end].total_height());
351+ end += 1;
352+ while height > max_height {
353+ height = height.saturating_sub(self.rows[start].total_height());
354+ start += 1;
355+ }
356+ }
357+ while selected < start {
358+ start -= 1;
359+ height = height.saturating_add(self.rows[start].total_height());
360+ while height > max_height {
361+ end -= 1;
362+ height = height.saturating_sub(self.rows[end].total_height());
363+ }
364+ }
365+ (start, end)
366+ }
367+}
368+
369+#[derive(Debug, Clone)]
370+pub struct TableState {
371+ offset: usize,
372+ selected: Option<usize>,
373+}
374+
375+impl Default for TableState {
376+ fn default() -> TableState {
377+ TableState {
378+ offset: 0,
379+ selected: None,
380+ }
381+ }
382+}
383+
384+impl TableState {
385+ pub fn selected(&self) -> Option<usize> {
386+ self.selected
387+ }
388+
389+ pub fn select(&mut self, index: Option<usize>) {
390+ self.selected = index;
391+ if index.is_none() {
392+ self.offset = 0;
393+ }
394+ }
395+}
396+
397+impl<'a> StatefulWidget for Table<'a> {
398+ type State = TableState;
399+
400+ fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
401+ if area.area() == 0 {
402+ return;
403+ }
404+ buf.set_style(area, self.style);
405+ let table_area = match self.block.take() {
406+ Some(b) => {
407+ let inner_area = b.inner(area);
408+ b.render(area, buf);
409+ inner_area
410+ }
411+ None => area,
412+ };
413+
414+ let has_selection = state.selected.is_some();
415+ let columns_widths = self.get_columns_widths(table_area.width, has_selection);
416+ let highlight_symbol = self.highlight_symbol.unwrap_or("");
417+ let blank_symbol = iter::repeat(" ")
418+ .take(highlight_symbol.width())
419+ .collect::<String>();
420+ let mut current_height = 0;
421+ let mut rows_height = table_area.height;
422+
423+ // Draw header
424+ if let Some(ref header) = self.header {
425+ let max_header_height = table_area.height.min(header.total_height());
426+ buf.set_style(
427+ Rect {
428+ x: table_area.left(),
429+ y: table_area.top(),
430+ width: table_area.width,
431+ height: table_area.height.min(header.height),
432+ },
433+ header.style,
434+ );
435+ let mut col = table_area.left();
436+ if has_selection {
437+ col += (highlight_symbol.width() as u16).min(table_area.width);
438+ }
439+ for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
440+ render_cell(
441+ buf,
442+ cell,
443+ Rect {
444+ x: col,
445+ y: table_area.top(),
446+ width: *width,
447+ height: max_header_height,
448+ },
449+ );
450+ col += *width + self.column_spacing;
451+ }
452+ current_height += max_header_height;
453+ rows_height = rows_height.saturating_sub(max_header_height);
454+ }
455+
456+ // Draw rows
457+ if self.rows.is_empty() {
458+ return;
459+ }
460+ let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
461+ state.offset = start;
462+ for (i, table_row) in self
463+ .rows
464+ .iter_mut()
465+ .enumerate()
466+ .skip(state.offset)
467+ .take(end - start)
468+ {
469+ let (row, col) = (table_area.top() + current_height, table_area.left());
470+ current_height += table_row.total_height();
471+ let table_row_area = Rect {
472+ x: col,
473+ y: row,
474+ width: table_area.width,
475+ height: table_row.height,
476+ };
477+ buf.set_style(table_row_area, table_row.style);
478+ let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
479+ let table_row_start_col = if has_selection {
480+ let symbol = if is_selected {
481+ highlight_symbol
482+ } else {
483+ &blank_symbol
484+ };
485+ let (col, _) =
486+ buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
487+ col
488+ } else {
489+ col
490+ };
491+ let mut col = table_row_start_col;
492+ for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
493+ render_cell(
494+ buf,
495+ cell,
496+ Rect {
497+ x: col,
498+ y: row,
499+ width: *width,
500+ height: table_row.height,
501+ },
502+ );
503+ col += *width + self.column_spacing;
504+ }
505+ if is_selected {
506+ buf.set_style(table_row_area, self.highlight_style);
507+ }
508+ }
509+ }
510+}
511+
512+fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
513+ buf.set_style(area, cell.style);
514+ for (i, spans) in cell.content.lines.iter().enumerate() {
515+ if i as u16 >= area.height {
516+ break;
517+ }
518+ buf.set_spans(area.x, area.y + i as u16, spans, area.width);
519+ }
520+}
521+
522+impl<'a> Widget for Table<'a> {
523+ fn render(self, area: Rect, buf: &mut Buffer) {
524+ let mut state = TableState::default();
525+ StatefulWidget::render(self, area, buf, &mut state);
526+ }
527+}
528+
529+#[cfg(test)]
530+mod tests {
531+ use super::*;
532+
533+ #[test]
534+ #[should_panic]
535+ fn table_invalid_percentages() {
536+ Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
537+ }
538+}
helix-tui/tests/terminal.rsnew file+36
@@ -0,0 +1,36 @@
1+use helix_tui::{
2+ backend::{Backend, TestBackend},
3+ layout::Rect,
4+ widgets::Paragraph,
5+ Terminal,
6+};
7+use std::error::Error;
8+
9+#[test]
10+fn terminal_buffer_size_should_be_limited() {
11+ let backend = TestBackend::new(400, 400);
12+ let terminal = Terminal::new(backend).unwrap();
13+ let size = terminal.backend().size().unwrap();
14+ assert_eq!(size.width, 255);
15+ assert_eq!(size.height, 255);
16+}
17+
18+// #[test]
19+// fn terminal_draw_returns_the_completed_frame() -> Result<(), Box<dyn Error>> {
20+// let backend = TestBackend::new(10, 10);
21+// let mut terminal = Terminal::new(backend)?;
22+// let frame = terminal.draw(|f| {
23+// let paragrah = Paragraph::new("Test");
24+// f.render_widget(paragrah, f.size());
25+// })?;
26+// assert_eq!(frame.buffer.get(0, 0).symbol, "T");
27+// assert_eq!(frame.area, Rect::new(0, 0, 10, 10));
28+// terminal.backend_mut().resize(8, 8);
29+// let frame = terminal.draw(|f| {
30+// let paragrah = Paragraph::new("test");
31+// f.render_widget(paragrah, f.size());
32+// })?;
33+// assert_eq!(frame.buffer.get(0, 0).symbol, "t");
34+// assert_eq!(frame.area, Rect::new(0, 0, 8, 8));
35+// Ok(())
36+// }
helix-tui/tests/widgets_block.rsnew file+213
@@ -0,0 +1,213 @@
1+use helix_tui::{
2+ backend::TestBackend,
3+ buffer::Buffer,
4+ layout::Rect,
5+ style::{Color, Style},
6+ text::Span,
7+ widgets::{Block, Borders},
8+ Terminal,
9+};
10+
11+#[test]
12+fn widgets_block_renders() {
13+ let backend = TestBackend::new(10, 10);
14+ let mut terminal = Terminal::new(backend).unwrap();
15+ terminal
16+ .draw(|f| {
17+ let block = Block::default()
18+ .title(Span::styled("Title", Style::default().fg(Color::LightBlue)))
19+ .borders(Borders::ALL);
20+ f.render_widget(
21+ block,
22+ Rect {
23+ x: 0,
24+ y: 0,
25+ width: 8,
26+ height: 8,
27+ },
28+ );
29+ })
30+ .unwrap();
31+ let mut expected = Buffer::with_lines(vec![
32+ "┌Title─┐ ",
33+ "│ │ ",
34+ "│ │ ",
35+ "│ │ ",
36+ "│ │ ",
37+ "│ │ ",
38+ "│ │ ",
39+ "└──────┘ ",
40+ " ",
41+ " ",
42+ ]);
43+ for x in 1..=5 {
44+ expected.get_mut(x, 0).set_fg(Color::LightBlue);
45+ }
46+ terminal.backend().assert_buffer(&expected);
47+}
48+
49+#[test]
50+fn widgets_block_renders_on_small_areas() {
51+ let test_case = |block, area: Rect, expected| {
52+ let backend = TestBackend::new(area.width, area.height);
53+ let mut terminal = Terminal::new(backend).unwrap();
54+ terminal
55+ .draw(|f| {
56+ f.render_widget(block, area);
57+ })
58+ .unwrap();
59+ terminal.backend().assert_buffer(&expected);
60+ };
61+
62+ let one_cell_test_cases = [
63+ (Borders::NONE, "T"),
64+ (Borders::LEFT, "│"),
65+ (Borders::TOP, "T"),
66+ (Borders::RIGHT, "│"),
67+ (Borders::BOTTOM, "T"),
68+ (Borders::ALL, "┌"),
69+ ];
70+ for (borders, symbol) in one_cell_test_cases.iter().cloned() {
71+ test_case(
72+ Block::default().title("Test").borders(borders),
73+ Rect {
74+ x: 0,
75+ y: 0,
76+ width: 0,
77+ height: 0,
78+ },
79+ Buffer::empty(Rect {
80+ x: 0,
81+ y: 0,
82+ width: 0,
83+ height: 0,
84+ }),
85+ );
86+ test_case(
87+ Block::default().title("Test").borders(borders),
88+ Rect {
89+ x: 0,
90+ y: 0,
91+ width: 1,
92+ height: 0,
93+ },
94+ Buffer::empty(Rect {
95+ x: 0,
96+ y: 0,
97+ width: 1,
98+ height: 0,
99+ }),
100+ );
101+ test_case(
102+ Block::default().title("Test").borders(borders),
103+ Rect {
104+ x: 0,
105+ y: 0,
106+ width: 0,
107+ height: 1,
108+ },
109+ Buffer::empty(Rect {
110+ x: 0,
111+ y: 0,
112+ width: 0,
113+ height: 1,
114+ }),
115+ );
116+ test_case(
117+ Block::default().title("Test").borders(borders),
118+ Rect {
119+ x: 0,
120+ y: 0,
121+ width: 1,
122+ height: 1,
123+ },
124+ Buffer::with_lines(vec![symbol]),
125+ );
126+ }
127+ test_case(
128+ Block::default().title("Test").borders(Borders::LEFT),
129+ Rect {
130+ x: 0,
131+ y: 0,
132+ width: 4,
133+ height: 1,
134+ },
135+ Buffer::with_lines(vec!["│Tes"]),
136+ );
137+ test_case(
138+ Block::default().title("Test").borders(Borders::RIGHT),
139+ Rect {
140+ x: 0,
141+ y: 0,
142+ width: 4,
143+ height: 1,
144+ },
145+ Buffer::with_lines(vec!["Tes│"]),
146+ );
147+ test_case(
148+ Block::default().title("Test").borders(Borders::RIGHT),
149+ Rect {
150+ x: 0,
151+ y: 0,
152+ width: 4,
153+ height: 1,
154+ },
155+ Buffer::with_lines(vec!["Tes│"]),
156+ );
157+ test_case(
158+ Block::default()
159+ .title("Test")
160+ .borders(Borders::LEFT | Borders::RIGHT),
161+ Rect {
162+ x: 0,
163+ y: 0,
164+ width: 4,
165+ height: 1,
166+ },
167+ Buffer::with_lines(vec!["│Te│"]),
168+ );
169+ test_case(
170+ Block::default().title("Test").borders(Borders::TOP),
171+ Rect {
172+ x: 0,
173+ y: 0,
174+ width: 4,
175+ height: 1,
176+ },
177+ Buffer::with_lines(vec!["Test"]),
178+ );
179+ test_case(
180+ Block::default().title("Test").borders(Borders::TOP),
181+ Rect {
182+ x: 0,
183+ y: 0,
184+ width: 5,
185+ height: 1,
186+ },
187+ Buffer::with_lines(vec!["Test─"]),
188+ );
189+ test_case(
190+ Block::default()
191+ .title("Test")
192+ .borders(Borders::LEFT | Borders::TOP),
193+ Rect {
194+ x: 0,
195+ y: 0,
196+ width: 5,
197+ height: 1,
198+ },
199+ Buffer::with_lines(vec!["┌Test"]),
200+ );
201+ test_case(
202+ Block::default()
203+ .title("Test")
204+ .borders(Borders::LEFT | Borders::TOP),
205+ Rect {
206+ x: 0,
207+ y: 0,
208+ width: 6,
209+ height: 1,
210+ },
211+ Buffer::with_lines(vec!["┌Test─"]),
212+ );
213+}
helix-tui/tests/widgets_list.rsnew file+88
@@ -0,0 +1,88 @@
1+use helix_tui::{
2+ backend::TestBackend,
3+ buffer::Buffer,
4+ layout::Rect,
5+ style::{Color, Style},
6+ symbols,
7+ widgets::{Block, Borders, List, ListItem, ListState},
8+ Terminal,
9+};
10+
11+#[test]
12+fn widgets_list_should_highlight_the_selected_item() {
13+ let backend = TestBackend::new(10, 3);
14+ let mut terminal = Terminal::new(backend).unwrap();
15+ let mut state = ListState::default();
16+ state.select(Some(1));
17+ terminal
18+ .draw(|f| {
19+ let size = f.size();
20+ let items = vec![
21+ ListItem::new("Item 1"),
22+ ListItem::new("Item 2"),
23+ ListItem::new("Item 3"),
24+ ];
25+ let list = List::new(items)
26+ .highlight_style(Style::default().bg(Color::Yellow))
27+ .highlight_symbol(">> ");
28+ f.render_stateful_widget(list, size, &mut state);
29+ })
30+ .unwrap();
31+ let mut expected = Buffer::with_lines(vec![" Item 1 ", ">> Item 2 ", " Item 3 "]);
32+ for x in 0..10 {
33+ expected.get_mut(x, 1).set_bg(Color::Yellow);
34+ }
35+ terminal.backend().assert_buffer(&expected);
36+}
37+
38+#[test]
39+fn widgets_list_should_truncate_items() {
40+ let backend = TestBackend::new(10, 2);
41+ let mut terminal = Terminal::new(backend).unwrap();
42+
43+ struct TruncateTestCase<'a> {
44+ selected: Option<usize>,
45+ items: Vec<ListItem<'a>>,
46+ expected: Buffer,
47+ }
48+
49+ let cases = vec![
50+ // An item is selected
51+ TruncateTestCase {
52+ selected: Some(0),
53+ items: vec![
54+ ListItem::new("A very long line"),
55+ ListItem::new("A very long line"),
56+ ],
57+ expected: Buffer::with_lines(vec![
58+ format!(">> A ve{} ", symbols::line::VERTICAL),
59+ format!(" A ve{} ", symbols::line::VERTICAL),
60+ ]),
61+ },
62+ // No item is selected
63+ TruncateTestCase {
64+ selected: None,
65+ items: vec![
66+ ListItem::new("A very long line"),
67+ ListItem::new("A very long line"),
68+ ],
69+ expected: Buffer::with_lines(vec![
70+ format!("A very {} ", symbols::line::VERTICAL),
71+ format!("A very {} ", symbols::line::VERTICAL),
72+ ]),
73+ },
74+ ];
75+ for case in cases {
76+ let mut state = ListState::default();
77+ state.select(case.selected);
78+ terminal
79+ .draw(|f| {
80+ let list = List::new(case.items.clone())
81+ .block(Block::default().borders(Borders::RIGHT))
82+ .highlight_symbol(">> ");
83+ f.render_stateful_widget(list, Rect::new(0, 0, 8, 2), &mut state);
84+ })
85+ .unwrap();
86+ terminal.backend().assert_buffer(&case.expected);
87+ }
88+}
helix-tui/tests/widgets_paragraph.rsnew file+220
@@ -0,0 +1,220 @@
1+use helix_tui::{
2+ backend::TestBackend,
3+ buffer::Buffer,
4+ layout::Alignment,
5+ text::{Span, Spans, Text},
6+ widgets::{Block, Borders, Paragraph, Wrap},
7+ Terminal,
8+};
9+
10+const SAMPLE_STRING: &str = "The library is based on the principle of immediate rendering with \
11+ intermediate buffers. This means that at each new frame you should build all widgets that are \
12+ supposed to be part of the UI. While providing a great flexibility for rich and \
13+ interactive UI, this may introduce overhead for highly dynamic content.";
14+
15+#[test]
16+fn widgets_paragraph_can_wrap_its_content() {
17+ let test_case = |alignment, expected| {
18+ let backend = TestBackend::new(20, 10);
19+ let mut terminal = Terminal::new(backend).unwrap();
20+
21+ terminal
22+ .draw(|f| {
23+ let size = f.size();
24+ let text = vec![Spans::from(SAMPLE_STRING)];
25+ let paragraph = Paragraph::new(text)
26+ .block(Block::default().borders(Borders::ALL))
27+ .alignment(alignment)
28+ .wrap(Wrap { trim: true });
29+ f.render_widget(paragraph, size);
30+ })
31+ .unwrap();
32+ terminal.backend().assert_buffer(&expected);
33+ };
34+
35+ test_case(
36+ Alignment::Left,
37+ Buffer::with_lines(vec![
38+ "┌──────────────────┐",
39+ "│The library is │",
40+ "│based on the │",
41+ "│principle of │",
42+ "│immediate │",
43+ "│rendering with │",
44+ "│intermediate │",
45+ "│buffers. This │",
46+ "│means that at each│",
47+ "└──────────────────┘",
48+ ]),
49+ );
50+ test_case(
51+ Alignment::Right,
52+ Buffer::with_lines(vec![
53+ "┌──────────────────┐",
54+ "│ The library is│",
55+ "│ based on the│",
56+ "│ principle of│",
57+ "│ immediate│",
58+ "│ rendering with│",
59+ "│ intermediate│",
60+ "│ buffers. This│",
61+ "│means that at each│",
62+ "└──────────────────┘",
63+ ]),
64+ );
65+ test_case(
66+ Alignment::Center,
67+ Buffer::with_lines(vec![
68+ "┌──────────────────┐",
69+ "│ The library is │",
70+ "│ based on the │",
71+ "│ principle of │",
72+ "│ immediate │",
73+ "│ rendering with │",
74+ "│ intermediate │",
75+ "│ buffers. This │",
76+ "│means that at each│",
77+ "└──────────────────┘",
78+ ]),
79+ );
80+}
81+
82+#[test]
83+fn widgets_paragraph_renders_double_width_graphemes() {
84+ let backend = TestBackend::new(10, 10);
85+ let mut terminal = Terminal::new(backend).unwrap();
86+
87+ let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、";
88+ terminal
89+ .draw(|f| {
90+ let size = f.size();
91+ let text = vec![Spans::from(s)];
92+ let paragraph = Paragraph::new(text)
93+ .block(Block::default().borders(Borders::ALL))
94+ .wrap(Wrap { trim: true });
95+ f.render_widget(paragraph, size);
96+ })
97+ .unwrap();
98+
99+ let expected = Buffer::with_lines(vec![
100+ "┌────────┐",
101+ "│コンピュ│",
102+ "│ータ上で│",
103+ "│文字を扱│",
104+ "│う場合、│",
105+ "│典型的に│",
106+ "│は文字に│",
107+ "│よる通信│",
108+ "│を行う場│",
109+ "└────────┘",
110+ ]);
111+ terminal.backend().assert_buffer(&expected);
112+}
113+
114+#[test]
115+fn widgets_paragraph_renders_mixed_width_graphemes() {
116+ let backend = TestBackend::new(10, 7);
117+ let mut terminal = Terminal::new(backend).unwrap();
118+
119+ let s = "aコンピュータ上で文字を扱う場合、";
120+ terminal
121+ .draw(|f| {
122+ let size = f.size();
123+ let text = vec![Spans::from(s)];
124+ let paragraph = Paragraph::new(text)
125+ .block(Block::default().borders(Borders::ALL))
126+ .wrap(Wrap { trim: true });
127+ f.render_widget(paragraph, size);
128+ })
129+ .unwrap();
130+
131+ let expected = Buffer::with_lines(vec![
132+ // The internal width is 8 so only 4 slots for double-width characters.
133+ "┌────────┐",
134+ "│aコンピ │", // Here we have 1 latin character so only 3 double-width ones can fit.
135+ "│ュータ上│",
136+ "│で文字を│",
137+ "│扱う場合│",
138+ "│、 │",
139+ "└────────┘",
140+ ]);
141+ terminal.backend().assert_buffer(&expected);
142+}
143+
144+#[test]
145+fn widgets_paragraph_can_wrap_with_a_trailing_nbsp() {
146+ let nbsp: &str = "\u{00a0}";
147+ let line = Spans::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]);
148+ let backend = TestBackend::new(20, 3);
149+ let mut terminal = Terminal::new(backend).unwrap();
150+ let expected = Buffer::with_lines(vec![
151+ "┌──────────────────┐",
152+ "│NBSP\u{00a0} │",
153+ "└──────────────────┘",
154+ ]);
155+ terminal
156+ .draw(|f| {
157+ let size = f.size();
158+
159+ let paragraph = Paragraph::new(line).block(Block::default().borders(Borders::ALL));
160+ f.render_widget(paragraph, size);
161+ })
162+ .unwrap();
163+ terminal.backend().assert_buffer(&expected);
164+}
165+#[test]
166+fn widgets_paragraph_can_scroll_horizontally() {
167+ let test_case = |alignment, scroll, expected| {
168+ let backend = TestBackend::new(20, 10);
169+ let mut terminal = Terminal::new(backend).unwrap();
170+
171+ terminal
172+ .draw(|f| {
173+ let size = f.size();
174+ let text = Text::from(
175+ "段落现在可以水平滚动了!\nParagraph can scroll horizontally!\nShort line",
176+ );
177+ let paragraph = Paragraph::new(text)
178+ .block(Block::default().borders(Borders::ALL))
179+ .alignment(alignment)
180+ .scroll(scroll);
181+ f.render_widget(paragraph, size);
182+ })
183+ .unwrap();
184+ terminal.backend().assert_buffer(&expected);
185+ };
186+
187+ test_case(
188+ Alignment::Left,
189+ (0, 7),
190+ Buffer::with_lines(vec![
191+ "┌──────────────────┐",
192+ "│在可以水平滚动了!│",
193+ "│ph can scroll hori│",
194+ "│ine │",
195+ "│ │",
196+ "│ │",
197+ "│ │",
198+ "│ │",
199+ "│ │",
200+ "└──────────────────┘",
201+ ]),
202+ );
203+ // only support Alignment::Left
204+ test_case(
205+ Alignment::Right,
206+ (0, 7),
207+ Buffer::with_lines(vec![
208+ "┌──────────────────┐",
209+ "│段落现在可以水平滚│",
210+ "│Paragraph can scro│",
211+ "│ Short line│",
212+ "│ │",
213+ "│ │",
214+ "│ │",
215+ "│ │",
216+ "│ │",
217+ "└──────────────────┘",
218+ ]),
219+ );
220+}
helix-tui/tests/widgets_table.rsnew file+717
@@ -0,0 +1,717 @@
1+use helix_tui::{
2+ backend::TestBackend,
3+ buffer::Buffer,
4+ layout::Constraint,
5+ style::{Color, Modifier, Style},
6+ text::{Span, Spans},
7+ widgets::{Block, Borders, Cell, Row, Table, TableState},
8+ Terminal,
9+};
10+
11+#[test]
12+fn widgets_table_column_spacing_can_be_changed() {
13+ let test_case = |column_spacing, expected| {
14+ let backend = TestBackend::new(30, 10);
15+ let mut terminal = Terminal::new(backend).unwrap();
16+
17+ terminal
18+ .draw(|f| {
19+ let size = f.size();
20+ let table = Table::new(vec![
21+ Row::new(vec!["Row11", "Row12", "Row13"]),
22+ Row::new(vec!["Row21", "Row22", "Row23"]),
23+ Row::new(vec!["Row31", "Row32", "Row33"]),
24+ Row::new(vec!["Row41", "Row42", "Row43"]),
25+ ])
26+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
27+ .block(Block::default().borders(Borders::ALL))
28+ .widths(&[
29+ Constraint::Length(5),
30+ Constraint::Length(5),
31+ Constraint::Length(5),
32+ ])
33+ .column_spacing(column_spacing);
34+ f.render_widget(table, size);
35+ })
36+ .unwrap();
37+ terminal.backend().assert_buffer(&expected);
38+ };
39+
40+ // no space between columns
41+ test_case(
42+ 0,
43+ Buffer::with_lines(vec![
44+ "┌────────────────────────────┐",
45+ "│Head1Head2Head3 │",
46+ "│ │",
47+ "│Row11Row12Row13 │",
48+ "│Row21Row22Row23 │",
49+ "│Row31Row32Row33 │",
50+ "│Row41Row42Row43 │",
51+ "│ │",
52+ "│ │",
53+ "└────────────────────────────┘",
54+ ]),
55+ );
56+
57+ // one space between columns
58+ test_case(
59+ 1,
60+ Buffer::with_lines(vec![
61+ "┌────────────────────────────┐",
62+ "│Head1 Head2 Head3 │",
63+ "│ │",
64+ "│Row11 Row12 Row13 │",
65+ "│Row21 Row22 Row23 │",
66+ "│Row31 Row32 Row33 │",
67+ "│Row41 Row42 Row43 │",
68+ "│ │",
69+ "│ │",
70+ "└────────────────────────────┘",
71+ ]),
72+ );
73+
74+ // enough space to just not hide the third column
75+ test_case(
76+ 6,
77+ Buffer::with_lines(vec![
78+ "┌────────────────────────────┐",
79+ "│Head1 Head2 Head3 │",
80+ "│ │",
81+ "│Row11 Row12 Row13 │",
82+ "│Row21 Row22 Row23 │",
83+ "│Row31 Row32 Row33 │",
84+ "│Row41 Row42 Row43 │",
85+ "│ │",
86+ "│ │",
87+ "└────────────────────────────┘",
88+ ]),
89+ );
90+
91+ // enough space to hide part of the third column
92+ test_case(
93+ 7,
94+ Buffer::with_lines(vec![
95+ "┌────────────────────────────┐",
96+ "│Head1 Head2 Head│",
97+ "│ │",
98+ "│Row11 Row12 Row1│",
99+ "│Row21 Row22 Row2│",
100+ "│Row31 Row32 Row3│",
101+ "│Row41 Row42 Row4│",
102+ "│ │",
103+ "│ │",
104+ "└────────────────────────────┘",
105+ ]),
106+ );
107+}
108+
109+#[test]
110+fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
111+ let test_case = |widths, expected| {
112+ let backend = TestBackend::new(30, 10);
113+ let mut terminal = Terminal::new(backend).unwrap();
114+
115+ terminal
116+ .draw(|f| {
117+ let size = f.size();
118+ let table = Table::new(vec![
119+ Row::new(vec!["Row11", "Row12", "Row13"]),
120+ Row::new(vec!["Row21", "Row22", "Row23"]),
121+ Row::new(vec!["Row31", "Row32", "Row33"]),
122+ Row::new(vec!["Row41", "Row42", "Row43"]),
123+ ])
124+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
125+ .block(Block::default().borders(Borders::ALL))
126+ .widths(widths);
127+ f.render_widget(table, size);
128+ })
129+ .unwrap();
130+ terminal.backend().assert_buffer(&expected);
131+ };
132+
133+ // columns of zero width show nothing
134+ test_case(
135+ &[
136+ Constraint::Length(0),
137+ Constraint::Length(0),
138+ Constraint::Length(0),
139+ ],
140+ Buffer::with_lines(vec![
141+ "┌────────────────────────────┐",
142+ "│ │",
143+ "│ │",
144+ "│ │",
145+ "│ │",
146+ "│ │",
147+ "│ │",
148+ "│ │",
149+ "│ │",
150+ "└────────────────────────────┘",
151+ ]),
152+ );
153+
154+ // columns of 1 width trim
155+ test_case(
156+ &[
157+ Constraint::Length(1),
158+ Constraint::Length(1),
159+ Constraint::Length(1),
160+ ],
161+ Buffer::with_lines(vec![
162+ "┌────────────────────────────┐",
163+ "│H H H │",
164+ "│ │",
165+ "│R R R │",
166+ "│R R R │",
167+ "│R R R │",
168+ "│R R R │",
169+ "│ │",
170+ "│ │",
171+ "└────────────────────────────┘",
172+ ]),
173+ );
174+
175+ // columns of large width just before pushing a column off
176+ test_case(
177+ &[
178+ Constraint::Length(8),
179+ Constraint::Length(8),
180+ Constraint::Length(8),
181+ ],
182+ Buffer::with_lines(vec![
183+ "┌────────────────────────────┐",
184+ "│Head1 Head2 Head3 │",
185+ "│ │",
186+ "│Row11 Row12 Row13 │",
187+ "│Row21 Row22 Row23 │",
188+ "│Row31 Row32 Row33 │",
189+ "│Row41 Row42 Row43 │",
190+ "│ │",
191+ "│ │",
192+ "└────────────────────────────┘",
193+ ]),
194+ );
195+}
196+
197+#[test]
198+fn widgets_table_columns_widths_can_use_percentage_constraints() {
199+ let test_case = |widths, expected| {
200+ let backend = TestBackend::new(30, 10);
201+ let mut terminal = Terminal::new(backend).unwrap();
202+
203+ terminal
204+ .draw(|f| {
205+ let size = f.size();
206+ let table = Table::new(vec![
207+ Row::new(vec!["Row11", "Row12", "Row13"]),
208+ Row::new(vec!["Row21", "Row22", "Row23"]),
209+ Row::new(vec!["Row31", "Row32", "Row33"]),
210+ Row::new(vec!["Row41", "Row42", "Row43"]),
211+ ])
212+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
213+ .block(Block::default().borders(Borders::ALL))
214+ .widths(widths)
215+ .column_spacing(0);
216+ f.render_widget(table, size);
217+ })
218+ .unwrap();
219+ terminal.backend().assert_buffer(&expected);
220+ };
221+
222+ // columns of zero width show nothing
223+ test_case(
224+ &[
225+ Constraint::Percentage(0),
226+ Constraint::Percentage(0),
227+ Constraint::Percentage(0),
228+ ],
229+ Buffer::with_lines(vec![
230+ "┌────────────────────────────┐",
231+ "│ │",
232+ "│ │",
233+ "│ │",
234+ "│ │",
235+ "│ │",
236+ "│ │",
237+ "│ │",
238+ "│ │",
239+ "└────────────────────────────┘",
240+ ]),
241+ );
242+
243+ // columns of not enough width trims the data
244+ test_case(
245+ &[
246+ Constraint::Percentage(11),
247+ Constraint::Percentage(11),
248+ Constraint::Percentage(11),
249+ ],
250+ Buffer::with_lines(vec![
251+ "┌────────────────────────────┐",
252+ "│HeaHeaHea │",
253+ "│ │",
254+ "│RowRowRow │",
255+ "│RowRowRow │",
256+ "│RowRowRow │",
257+ "│RowRowRow │",
258+ "│ │",
259+ "│ │",
260+ "└────────────────────────────┘",
261+ ]),
262+ );
263+
264+ // columns of large width just before pushing a column off
265+ test_case(
266+ &[
267+ Constraint::Percentage(33),
268+ Constraint::Percentage(33),
269+ Constraint::Percentage(33),
270+ ],
271+ Buffer::with_lines(vec![
272+ "┌────────────────────────────┐",
273+ "│Head1 Head2 Head3 │",
274+ "│ │",
275+ "│Row11 Row12 Row13 │",
276+ "│Row21 Row22 Row23 │",
277+ "│Row31 Row32 Row33 │",
278+ "│Row41 Row42 Row43 │",
279+ "│ │",
280+ "│ │",
281+ "└────────────────────────────┘",
282+ ]),
283+ );
284+
285+ // percentages summing to 100 should give equal widths
286+ test_case(
287+ &[Constraint::Percentage(50), Constraint::Percentage(50)],
288+ Buffer::with_lines(vec![
289+ "┌────────────────────────────┐",
290+ "│Head1 Head2 │",
291+ "│ │",
292+ "│Row11 Row12 │",
293+ "│Row21 Row22 │",
294+ "│Row31 Row32 │",
295+ "│Row41 Row42 │",
296+ "│ │",
297+ "│ │",
298+ "└────────────────────────────┘",
299+ ]),
300+ );
301+}
302+
303+#[test]
304+fn widgets_table_columns_widths_can_use_mixed_constraints() {
305+ let test_case = |widths, expected| {
306+ let backend = TestBackend::new(30, 10);
307+ let mut terminal = Terminal::new(backend).unwrap();
308+
309+ terminal
310+ .draw(|f| {
311+ let size = f.size();
312+ let table = Table::new(vec![
313+ Row::new(vec!["Row11", "Row12", "Row13"]),
314+ Row::new(vec!["Row21", "Row22", "Row23"]),
315+ Row::new(vec!["Row31", "Row32", "Row33"]),
316+ Row::new(vec!["Row41", "Row42", "Row43"]),
317+ ])
318+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
319+ .block(Block::default().borders(Borders::ALL))
320+ .widths(widths);
321+ f.render_widget(table, size);
322+ })
323+ .unwrap();
324+ terminal.backend().assert_buffer(&expected);
325+ };
326+
327+ // columns of zero width show nothing
328+ test_case(
329+ &[
330+ Constraint::Percentage(0),
331+ Constraint::Length(0),
332+ Constraint::Percentage(0),
333+ ],
334+ Buffer::with_lines(vec![
335+ "┌────────────────────────────┐",
336+ "│ │",
337+ "│ │",
338+ "│ │",
339+ "│ │",
340+ "│ │",
341+ "│ │",
342+ "│ │",
343+ "│ │",
344+ "└────────────────────────────┘",
345+ ]),
346+ );
347+
348+ // columns of not enough width trims the data
349+ test_case(
350+ &[
351+ Constraint::Percentage(11),
352+ Constraint::Length(20),
353+ Constraint::Percentage(11),
354+ ],
355+ Buffer::with_lines(vec![
356+ "┌────────────────────────────┐",
357+ "│Hea Head2 Hea│",
358+ "│ │",
359+ "│Row Row12 Row│",
360+ "│Row Row22 Row│",
361+ "│Row Row32 Row│",
362+ "│Row Row42 Row│",
363+ "│ │",
364+ "│ │",
365+ "└────────────────────────────┘",
366+ ]),
367+ );
368+
369+ // columns of large width just before pushing a column off
370+ test_case(
371+ &[
372+ Constraint::Percentage(33),
373+ Constraint::Length(10),
374+ Constraint::Percentage(33),
375+ ],
376+ Buffer::with_lines(vec![
377+ "┌────────────────────────────┐",
378+ "│Head1 Head2 Head3 │",
379+ "│ │",
380+ "│Row11 Row12 Row13 │",
381+ "│Row21 Row22 Row23 │",
382+ "│Row31 Row32 Row33 │",
383+ "│Row41 Row42 Row43 │",
384+ "│ │",
385+ "│ │",
386+ "└────────────────────────────┘",
387+ ]),
388+ );
389+
390+ // columns of large size (>100% total) hide the last column
391+ test_case(
392+ &[
393+ Constraint::Percentage(60),
394+ Constraint::Length(10),
395+ Constraint::Percentage(60),
396+ ],
397+ Buffer::with_lines(vec![
398+ "┌────────────────────────────┐",
399+ "│Head1 Head2 │",
400+ "│ │",
401+ "│Row11 Row12 │",
402+ "│Row21 Row22 │",
403+ "│Row31 Row32 │",
404+ "│Row41 Row42 │",
405+ "│ │",
406+ "│ │",
407+ "└────────────────────────────┘",
408+ ]),
409+ );
410+}
411+
412+#[test]
413+fn widgets_table_columns_widths_can_use_ratio_constraints() {
414+ let test_case = |widths, expected| {
415+ let backend = TestBackend::new(30, 10);
416+ let mut terminal = Terminal::new(backend).unwrap();
417+
418+ terminal
419+ .draw(|f| {
420+ let size = f.size();
421+ let table = Table::new(vec![
422+ Row::new(vec!["Row11", "Row12", "Row13"]),
423+ Row::new(vec!["Row21", "Row22", "Row23"]),
424+ Row::new(vec!["Row31", "Row32", "Row33"]),
425+ Row::new(vec!["Row41", "Row42", "Row43"]),
426+ ])
427+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
428+ .block(Block::default().borders(Borders::ALL))
429+ .widths(widths)
430+ .column_spacing(0);
431+ f.render_widget(table, size);
432+ })
433+ .unwrap();
434+ terminal.backend().assert_buffer(&expected);
435+ };
436+
437+ // columns of zero width show nothing
438+ test_case(
439+ &[
440+ Constraint::Ratio(0, 1),
441+ Constraint::Ratio(0, 1),
442+ Constraint::Ratio(0, 1),
443+ ],
444+ Buffer::with_lines(vec![
445+ "┌────────────────────────────┐",
446+ "│ │",
447+ "│ │",
448+ "│ │",
449+ "│ │",
450+ "│ │",
451+ "│ │",
452+ "│ │",
453+ "│ │",
454+ "└────────────────────────────┘",
455+ ]),
456+ );
457+
458+ // columns of not enough width trims the data
459+ test_case(
460+ &[
461+ Constraint::Ratio(1, 9),
462+ Constraint::Ratio(1, 9),
463+ Constraint::Ratio(1, 9),
464+ ],
465+ Buffer::with_lines(vec![
466+ "┌────────────────────────────┐",
467+ "│HeaHeaHea │",
468+ "│ │",
469+ "│RowRowRow │",
470+ "│RowRowRow │",
471+ "│RowRowRow │",
472+ "│RowRowRow │",
473+ "│ │",
474+ "│ │",
475+ "└────────────────────────────┘",
476+ ]),
477+ );
478+
479+ // columns of large width just before pushing a column off
480+ test_case(
481+ &[
482+ Constraint::Ratio(1, 3),
483+ Constraint::Ratio(1, 3),
484+ Constraint::Ratio(1, 3),
485+ ],
486+ Buffer::with_lines(vec![
487+ "┌────────────────────────────┐",
488+ "│Head1 Head2 Head3 │",
489+ "│ │",
490+ "│Row11 Row12 Row13 │",
491+ "│Row21 Row22 Row23 │",
492+ "│Row31 Row32 Row33 │",
493+ "│Row41 Row42 Row43 │",
494+ "│ │",
495+ "│ │",
496+ "└────────────────────────────┘",
497+ ]),
498+ );
499+
500+ // percentages summing to 100 should give equal widths
501+ test_case(
502+ &[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
503+ Buffer::with_lines(vec![
504+ "┌────────────────────────────┐",
505+ "│Head1 Head2 │",
506+ "│ │",
507+ "│Row11 Row12 │",
508+ "│Row21 Row22 │",
509+ "│Row31 Row32 │",
510+ "│Row41 Row42 │",
511+ "│ │",
512+ "│ │",
513+ "└────────────────────────────┘",
514+ ]),
515+ );
516+}
517+
518+#[test]
519+fn widgets_table_can_have_rows_with_multi_lines() {
520+ let test_case = |state: &mut TableState, expected: Buffer| {
521+ let backend = TestBackend::new(30, 8);
522+ let mut terminal = Terminal::new(backend).unwrap();
523+ terminal
524+ .draw(|f| {
525+ let size = f.size();
526+ let table = Table::new(vec![
527+ Row::new(vec!["Row11", "Row12", "Row13"]),
528+ Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
529+ Row::new(vec!["Row31", "Row32", "Row33"]),
530+ Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
531+ ])
532+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
533+ .block(Block::default().borders(Borders::ALL))
534+ .highlight_symbol(">> ")
535+ .widths(&[
536+ Constraint::Length(5),
537+ Constraint::Length(5),
538+ Constraint::Length(5),
539+ ])
540+ .column_spacing(1);
541+ f.render_stateful_widget(table, size, state);
542+ })
543+ .unwrap();
544+ terminal.backend().assert_buffer(&expected);
545+ };
546+
547+ let mut state = TableState::default();
548+ // no selection
549+ test_case(
550+ &mut state,
551+ Buffer::with_lines(vec![
552+ "┌────────────────────────────┐",
553+ "│Head1 Head2 Head3 │",
554+ "│ │",
555+ "│Row11 Row12 Row13 │",
556+ "│Row21 Row22 Row23 │",
557+ "│ │",
558+ "│Row31 Row32 Row33 │",
559+ "└────────────────────────────┘",
560+ ]),
561+ );
562+
563+ // select first
564+ state.select(Some(0));
565+ test_case(
566+ &mut state,
567+ Buffer::with_lines(vec![
568+ "┌────────────────────────────┐",
569+ "│ Head1 Head2 Head3 │",
570+ "│ │",
571+ "│>> Row11 Row12 Row13 │",
572+ "│ Row21 Row22 Row23 │",
573+ "│ │",
574+ "│ Row31 Row32 Row33 │",
575+ "└────────────────────────────┘",
576+ ]),
577+ );
578+
579+ // select second (we don't show partially the 4th row)
580+ state.select(Some(1));
581+ test_case(
582+ &mut state,
583+ Buffer::with_lines(vec![
584+ "┌────────────────────────────┐",
585+ "│ Head1 Head2 Head3 │",
586+ "│ │",
587+ "│ Row11 Row12 Row13 │",
588+ "│>> Row21 Row22 Row23 │",
589+ "│ │",
590+ "│ Row31 Row32 Row33 │",
591+ "└────────────────────────────┘",
592+ ]),
593+ );
594+
595+ // select 4th (we don't show partially the 1st row)
596+ state.select(Some(3));
597+ test_case(
598+ &mut state,
599+ Buffer::with_lines(vec![
600+ "┌────────────────────────────┐",
601+ "│ Head1 Head2 Head3 │",
602+ "│ │",
603+ "│ Row31 Row32 Row33 │",
604+ "│>> Row41 Row42 Row43 │",
605+ "│ │",
606+ "│ │",
607+ "└────────────────────────────┘",
608+ ]),
609+ );
610+}
611+
612+#[test]
613+fn widgets_table_can_have_elements_styled_individually() {
614+ let backend = TestBackend::new(30, 4);
615+ let mut terminal = Terminal::new(backend).unwrap();
616+ let mut state = TableState::default();
617+ state.select(Some(0));
618+ terminal
619+ .draw(|f| {
620+ let size = f.size();
621+ let table = Table::new(vec![
622+ Row::new(vec!["Row11", "Row12", "Row13"]).style(Style::default().fg(Color::Green)),
623+ Row::new(vec![
624+ Cell::from("Row21"),
625+ Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
626+ Cell::from(Spans::from(vec![
627+ Span::raw("Row"),
628+ Span::styled("23", Style::default().fg(Color::Blue)),
629+ ]))
630+ .style(Style::default().fg(Color::Red)),
631+ ])
632+ .style(Style::default().fg(Color::LightGreen)),
633+ ])
634+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
635+ .block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
636+ .highlight_symbol(">> ")
637+ .highlight_style(Style::default().add_modifier(Modifier::BOLD))
638+ .widths(&[
639+ Constraint::Length(6),
640+ Constraint::Length(6),
641+ Constraint::Length(6),
642+ ])
643+ .column_spacing(1);
644+ f.render_stateful_widget(table, size, &mut state);
645+ })
646+ .unwrap();
647+
648+ let mut expected = Buffer::with_lines(vec![
649+ "│ Head1 Head2 Head3 │",
650+ "│ │",
651+ "│>> Row11 Row12 Row13 │",
652+ "│ Row21 Row22 Row23 │",
653+ ]);
654+ // First row = row color + highlight style
655+ for col in 1..=28 {
656+ expected.get_mut(col, 2).set_style(
657+ Style::default()
658+ .fg(Color::Green)
659+ .add_modifier(Modifier::BOLD),
660+ );
661+ }
662+ // Second row:
663+ // 1. row color
664+ for col in 1..=28 {
665+ expected
666+ .get_mut(col, 3)
667+ .set_style(Style::default().fg(Color::LightGreen));
668+ }
669+ // 2. cell color
670+ for col in 11..=16 {
671+ expected
672+ .get_mut(col, 3)
673+ .set_style(Style::default().fg(Color::Yellow));
674+ }
675+ for col in 18..=23 {
676+ expected
677+ .get_mut(col, 3)
678+ .set_style(Style::default().fg(Color::Red));
679+ }
680+ // 3. text color
681+ for col in 21..=22 {
682+ expected
683+ .get_mut(col, 3)
684+ .set_style(Style::default().fg(Color::Blue));
685+ }
686+ terminal.backend().assert_buffer(&expected);
687+}
688+
689+#[test]
690+fn widgets_table_should_render_even_if_empty() {
691+ let backend = TestBackend::new(30, 4);
692+ let mut terminal = Terminal::new(backend).unwrap();
693+ terminal
694+ .draw(|f| {
695+ let size = f.size();
696+ let table = Table::new(vec![])
697+ .header(Row::new(vec!["Head1", "Head2", "Head3"]))
698+ .block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
699+ .widths(&[
700+ Constraint::Length(6),
701+ Constraint::Length(6),
702+ Constraint::Length(6),
703+ ])
704+ .column_spacing(1);
705+ f.render_widget(table, size);
706+ })
707+ .unwrap();
708+
709+ let expected = Buffer::with_lines(vec![
710+ "│Head1 Head2 Head3 │",
711+ "│ │",
712+ "│ │",
713+ "│ │",
714+ ]);
715+
716+ terminal.backend().assert_buffer(&expected);
717+}
helix-view/Cargo.toml+1−1
@@ -16,7 +16,7 @@ helix-core = { path = "../helix-core" }
1616helix-lsp = { path = "../helix-lsp"}
1717
1818# Conversion traits
19−tui = { version = "0.15", default-features = false, features = ["crossterm"], optional = true}
19+tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"], optional = true }
2020crossterm = { version = "0.19", features = ["event-stream"], optional = true }
2121once_cell = "1.4"
2222url = "2"