diff options
Diffstat (limited to 'src/tui')
| -rw-r--r-- | src/tui/LICENSE | 21 | ||||
| -rw-r--r-- | src/tui/README.md | 5 | ||||
| -rw-r--r-- | src/tui/backend/crossterm.rs | 221 | ||||
| -rw-r--r-- | src/tui/backend/mod.rs | 20 | ||||
| -rw-r--r-- | src/tui/buffer.rs | 734 | ||||
| -rw-r--r-- | src/tui/layout.rs | 537 | ||||
| -rw-r--r-- | src/tui/mod.rs | 20 | ||||
| -rw-r--r-- | src/tui/style.rs | 278 | ||||
| -rw-r--r-- | src/tui/symbols.rs | 233 | ||||
| -rw-r--r-- | src/tui/terminal.rs | 321 | ||||
| -rw-r--r-- | src/tui/text.rs | 428 | ||||
| -rw-r--r-- | src/tui/widgets/block.rs | 562 | ||||
| -rw-r--r-- | src/tui/widgets/mod.rs | 159 | ||||
| -rw-r--r-- | src/tui/widgets/paragraph.rs | 194 | ||||
| -rw-r--r-- | src/tui/widgets/reflow.rs | 537 |
15 files changed, 0 insertions, 4270 deletions
diff --git a/src/tui/LICENSE b/src/tui/LICENSE deleted file mode 100644 index 7a0657cb..00000000 --- a/src/tui/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Florian Dehau - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/tui/README.md b/src/tui/README.md deleted file mode 100644 index 506bdf8f..00000000 --- a/src/tui/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# tui-rs - -A fork of https://crates.io/crates/tui/0.19.0 since it is now unmaintained. - -Some parts have been removed or modified for simplicity, but it is currently mostly equivalent. diff --git a/src/tui/backend/crossterm.rs b/src/tui/backend/crossterm.rs deleted file mode 100644 index 2cbfd6e0..00000000 --- a/src/tui/backend/crossterm.rs +++ /dev/null @@ -1,221 +0,0 @@ -use crate::tui::{ - backend::Backend, - buffer::Cell, - layout::Rect, - style::{Color, Modifier}, -}; -use crossterm::{ - cursor::{Hide, MoveTo, Show}, - execute, queue, - style::{ - Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, - SetForegroundColor, - }, - terminal::{self, Clear, ClearType}, -}; -use std::io::{self, Write}; - -pub struct CrosstermBackend<W: Write> { - buffer: W, -} - -impl<W> CrosstermBackend<W> -where - W: Write, -{ - pub fn new(buffer: W) -> CrosstermBackend<W> { - CrosstermBackend { buffer } - } -} - -impl<W> Write for CrosstermBackend<W> -where - W: Write, -{ - fn write(&mut self, buf: &[u8]) -> io::Result<usize> { - self.buffer.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.buffer.flush() - } -} - -impl<W> Backend for CrosstermBackend<W> -where - W: Write, -{ - fn draw<'a, I>(&mut self, content: I) -> io::Result<()> - where - I: Iterator<Item = (u16, u16, &'a Cell)>, - { - let mut fg = Color::Reset; - let mut bg = Color::Reset; - let mut modifier = Modifier::empty(); - let mut last_pos: Option<(u16, u16)> = None; - for (x, y, cell) in content { - // Move the cursor if the previous location was not (x - 1, y) - if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) { - map_error(queue!(self.buffer, MoveTo(x, y)))?; - } - last_pos = Some((x, y)); - if cell.modifier != modifier { - let diff = ModifierDiff { - from: modifier, - to: cell.modifier, - }; - diff.queue(&mut self.buffer)?; - modifier = cell.modifier; - } - if cell.fg != fg { - let color = CColor::from(cell.fg); - map_error(queue!(self.buffer, SetForegroundColor(color)))?; - fg = cell.fg; - } - if cell.bg != bg { - let color = CColor::from(cell.bg); - map_error(queue!(self.buffer, SetBackgroundColor(color)))?; - bg = cell.bg; - } - - map_error(queue!(self.buffer, Print(&cell.symbol)))?; - } - - map_error(queue!( - self.buffer, - SetForegroundColor(CColor::Reset), - SetBackgroundColor(CColor::Reset), - SetAttribute(CAttribute::Reset) - )) - } - - fn hide_cursor(&mut self) -> io::Result<()> { - map_error(execute!(self.buffer, Hide)) - } - - fn show_cursor(&mut self) -> io::Result<()> { - map_error(execute!(self.buffer, Show)) - } - - fn get_cursor(&mut self) -> io::Result<(u16, u16)> { - crossterm::cursor::position() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string())) - } - - fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { - map_error(execute!(self.buffer, MoveTo(x, y))) - } - - fn clear(&mut self) -> io::Result<()> { - map_error(execute!(self.buffer, Clear(ClearType::All))) - } - - fn size(&self) -> io::Result<Rect> { - let (width, height) = - terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - - Ok(Rect::new(0, 0, width, height)) - } - - fn flush(&mut self) -> io::Result<()> { - self.buffer.flush() - } -} - -fn map_error(error: crossterm::Result<()>) -> io::Result<()> { - error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string())) -} - -impl From<Color> for CColor { - fn from(color: Color) -> Self { - match color { - Color::Reset => CColor::Reset, - Color::Black => CColor::Black, - Color::Red => CColor::DarkRed, - Color::Green => CColor::DarkGreen, - Color::Yellow => CColor::DarkYellow, - Color::Blue => CColor::DarkBlue, - Color::Magenta => CColor::DarkMagenta, - Color::Cyan => CColor::DarkCyan, - Color::Gray => CColor::Grey, - Color::DarkGray => CColor::DarkGrey, - Color::LightRed => CColor::Red, - Color::LightGreen => CColor::Green, - Color::LightBlue => CColor::Blue, - Color::LightYellow => CColor::Yellow, - Color::LightMagenta => CColor::Magenta, - Color::LightCyan => CColor::Cyan, - Color::White => CColor::White, - Color::Indexed(i) => CColor::AnsiValue(i), - Color::Rgb(r, g, b) => CColor::Rgb { r, g, b }, - } - } -} - -#[derive(Debug)] -struct ModifierDiff { - pub from: Modifier, - pub to: Modifier, -} - -impl ModifierDiff { - fn queue<W>(&self, mut w: W) -> io::Result<()> - where - W: io::Write, - { - //use crossterm::Attribute; - let removed = self.from - self.to; - if removed.contains(Modifier::REVERSED) { - map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?; - } - if removed.contains(Modifier::BOLD) { - map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; - if self.to.contains(Modifier::DIM) { - map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; - } - } - if removed.contains(Modifier::ITALIC) { - map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?; - } - if removed.contains(Modifier::UNDERLINED) { - map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?; - } - if removed.contains(Modifier::DIM) { - map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; - } - if removed.contains(Modifier::CROSSED_OUT) { - map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?; - } - if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { - map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?; - } - - let added = self.to - self.from; - if added.contains(Modifier::REVERSED) { - map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?; - } - if added.contains(Modifier::BOLD) { - map_error(queue!(w, SetAttribute(CAttribute::Bold)))?; - } - if added.contains(Modifier::ITALIC) { - map_error(queue!(w, SetAttribute(CAttribute::Italic)))?; - } - if added.contains(Modifier::UNDERLINED) { - map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?; - } - if added.contains(Modifier::DIM) { - map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; - } - if added.contains(Modifier::CROSSED_OUT) { - map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?; - } - if added.contains(Modifier::SLOW_BLINK) { - map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?; - } - if added.contains(Modifier::RAPID_BLINK) { - map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?; - } - - Ok(()) - } -} diff --git a/src/tui/backend/mod.rs b/src/tui/backend/mod.rs deleted file mode 100644 index 1a197e79..00000000 --- a/src/tui/backend/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::io; - -use crate::tui::buffer::Cell; -use crate::tui::layout::Rect; - -mod crossterm; -pub use self::crossterm::CrosstermBackend; - -pub trait Backend { - fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error> - where - I: Iterator<Item = (u16, u16, &'a Cell)>; - fn hide_cursor(&mut self) -> Result<(), io::Error>; - fn show_cursor(&mut self) -> Result<(), io::Error>; - fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>; - fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>; - fn clear(&mut self) -> Result<(), io::Error>; - fn size(&self) -> Result<Rect, io::Error>; - fn flush(&mut self) -> Result<(), io::Error>; -} diff --git a/src/tui/buffer.rs b/src/tui/buffer.rs deleted file mode 100644 index 8bc49316..00000000 --- a/src/tui/buffer.rs +++ /dev/null @@ -1,734 +0,0 @@ -use crate::tui::{ - layout::Rect, - style::{Color, Modifier, Style}, - text::{Span, Spans}, -}; -use std::cmp::min; -use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; - -/// A buffer cell -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Cell { - pub symbol: String, - pub fg: Color, - pub bg: Color, - pub modifier: Modifier, -} - -impl Cell { - pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell { - self.symbol.clear(); - self.symbol.push_str(symbol); - self - } - - pub fn set_char(&mut self, ch: char) -> &mut Cell { - self.symbol.clear(); - self.symbol.push(ch); - self - } - - pub fn set_fg(&mut self, color: Color) -> &mut Cell { - self.fg = color; - self - } - - pub fn set_bg(&mut self, color: Color) -> &mut Cell { - self.bg = color; - self - } - - pub fn set_style(&mut self, style: Style) -> &mut Cell { - if let Some(c) = style.fg { - self.fg = c; - } - if let Some(c) = style.bg { - self.bg = c; - } - self.modifier.insert(style.add_modifier); - self.modifier.remove(style.sub_modifier); - self - } - - pub fn style(&self) -> Style { - Style::default() - .fg(self.fg) - .bg(self.bg) - .add_modifier(self.modifier) - } - - pub fn reset(&mut self) { - self.symbol.clear(); - self.symbol.push(' '); - self.fg = Color::Reset; - self.bg = Color::Reset; - self.modifier = Modifier::empty(); - } -} - -impl Default for Cell { - fn default() -> Cell { - Cell { - symbol: " ".into(), - fg: Color::Reset, - bg: Color::Reset, - modifier: Modifier::empty(), - } - } -} - -/// A buffer that maps to the desired content of the terminal after the draw call -/// -/// No widget in the library interacts directly with the terminal. Instead each of them is required -/// to draw their state to an intermediate buffer. It is basically a grid where each cell contains -/// a grapheme, a foreground color and a background color. This grid will then be used to output -/// the appropriate escape sequences and characters to draw the UI as the user has defined it. -/// -/// # Examples: -/// -/// ``` -/// use tui::buffer::{Buffer, Cell}; -/// use tui::layout::Rect; -/// use tui::style::{Color, Style, Modifier}; -/// -/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5}); -/// buf.get_mut(0, 2).set_symbol("x"); -/// assert_eq!(buf.get(0, 2).symbol, "x"); -/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White)); -/// assert_eq!(buf.get(5, 0), &Cell{ -/// symbol: String::from("r"), -/// fg: Color::Red, -/// bg: Color::White, -/// modifier: Modifier::empty() -/// }); -/// buf.get_mut(5, 0).set_char('x'); -/// assert_eq!(buf.get(5, 0).symbol, "x"); -/// ``` -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct Buffer { - /// The area represented by this buffer - pub area: Rect, - /// The content of the buffer. The length of this Vec should always be equal to area.width * - /// area.height - pub content: Vec<Cell>, -} - -impl Buffer { - /// Returns a Buffer with all cells set to the default one - pub fn empty(area: Rect) -> Buffer { - let cell = Cell::default(); - Buffer::filled(area, &cell) - } - - /// Returns a Buffer with all cells initialized with the attributes of the given Cell - pub fn filled(area: Rect, cell: &Cell) -> Buffer { - let size = area.area() as usize; - let mut content = Vec::with_capacity(size); - for _ in 0..size { - content.push(cell.clone()); - } - Buffer { area, content } - } - - /// Returns a Buffer containing the given lines - pub fn with_lines<S>(lines: &[S]) -> Buffer - where - S: AsRef<str>, - { - let height = lines.len() as u16; - let width = lines - .iter() - .map(|i| i.as_ref().width() as u16) - .max() - .unwrap_or_default(); - let mut buffer = Buffer::empty(Rect { - x: 0, - y: 0, - width, - height, - }); - for (y, line) in lines.iter().enumerate() { - buffer.set_string(0, y as u16, line, Style::default()); - } - buffer - } - - /// Returns the content of the buffer as a slice - pub fn content(&self) -> &[Cell] { - &self.content - } - - /// Returns the area covered by this buffer - pub fn area(&self) -> &Rect { - &self.area - } - - /// Returns a reference to Cell at the given coordinates - pub fn get(&self, x: u16, y: u16) -> &Cell { - let i = self.index_of(x, y); - &self.content[i] - } - - /// Returns a mutable reference to Cell at the given coordinates - pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell { - let i = self.index_of(x, y); - &mut self.content[i] - } - - /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates. - /// - /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). - /// - /// # Examples - /// - /// ``` - /// # use tui::buffer::Buffer; - /// # use tui::layout::Rect; - /// let rect = Rect::new(200, 100, 10, 10); - /// let buffer = Buffer::empty(rect); - /// // Global coordinates to the top corner of this buffer's area - /// assert_eq!(buffer.index_of(200, 100), 0); - /// ``` - /// - /// # Panics - /// - /// Panics when given an coordinate that is outside of this Buffer's area. - /// - /// ```should_panic - /// # use tui::buffer::Buffer; - /// # use tui::layout::Rect; - /// let rect = Rect::new(200, 100, 10, 10); - /// let buffer = Buffer::empty(rect); - /// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area - /// // starts at (200, 100). - /// buffer.index_of(0, 0); // Panics - /// ``` - pub fn index_of(&self, x: u16, y: u16) -> usize { - debug_assert!( - x >= self.area.left() - && x < self.area.right() - && y >= self.area.top() - && y < self.area.bottom(), - "Trying to access position outside the buffer: x={}, y={}, area={:?}", - x, - y, - self.area - ); - ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize - } - - /// Returns the (global) coordinates of a cell given its index - /// - /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). - /// - /// # Examples - /// - /// ``` - /// # use tui::buffer::Buffer; - /// # use tui::layout::Rect; - /// let rect = Rect::new(200, 100, 10, 10); - /// let buffer = Buffer::empty(rect); - /// assert_eq!(buffer.pos_of(0), (200, 100)); - /// assert_eq!(buffer.pos_of(14), (204, 101)); - /// ``` - /// - /// # Panics - /// - /// Panics when given an index that is outside the Buffer's content. - /// - /// ```should_panic - /// # use tui::buffer::Buffer; - /// # use tui::layout::Rect; - /// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total - /// let buffer = Buffer::empty(rect); - /// // Index 100 is the 101th cell, which lies outside of the area of this Buffer. - /// buffer.pos_of(100); // Panics - /// ``` - pub fn pos_of(&self, i: usize) -> (u16, u16) { - debug_assert!( - i < self.content.len(), - "Trying to get the coords of a cell outside the buffer: i={} len={}", - i, - self.content.len() - ); - ( - self.area.x + i as u16 % self.area.width, - self.area.y + i as u16 / self.area.width, - ) - } - - /// Print a string, starting at the position (x, y) - pub fn set_string<S>(&mut self, x: u16, y: u16, string: S, style: Style) - where - S: AsRef<str>, - { - self.set_stringn(x, y, string, usize::MAX, style); - } - - /// Print at most the first n characters of a string if enough space is available - /// until the end of the line - pub fn set_stringn<S>( - &mut self, - x: u16, - y: u16, - string: S, - width: usize, - style: Style, - ) -> (u16, u16) - where - S: AsRef<str>, - { - let mut index = self.index_of(x, y); - let mut x_offset = x as usize; - let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true); - let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize)); - for s in graphemes { - let width = s.width(); - if width == 0 { - continue; - } - // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we - // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32. - if width > max_offset.saturating_sub(x_offset) { - break; - } - - self.content[index].set_symbol(s); - self.content[index].set_style(style); - // Reset following cells if multi-width (they would be hidden by the grapheme), - for i in index + 1..index + width { - self.content[i].reset(); - } - index += width; - x_offset += width; - } - (x_offset as u16, y) - } - - pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) { - let mut remaining_width = width; - let mut x = x; - for span in &spans.0 { - if remaining_width == 0 { - break; - } - let pos = self.set_stringn( - x, - y, - span.content.as_ref(), - remaining_width as usize, - span.style, - ); - let w = pos.0.saturating_sub(x); - x = pos.0; - remaining_width = remaining_width.saturating_sub(w); - } - (x, y) - } - - pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) { - self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style) - } - - #[deprecated( - since = "0.10.0", - note = "You should use styling capabilities of `Buffer::set_style`" - )] - pub fn set_background(&mut self, area: Rect, color: Color) { - for y in area.top()..area.bottom() { - for x in area.left()..area.right() { - self.get_mut(x, y).set_bg(color); - } - } - } - - pub fn set_style(&mut self, area: Rect, style: Style) { - for y in area.top()..area.bottom() { - for x in area.left()..area.right() { - self.get_mut(x, y).set_style(style); - } - } - } - - /// Resize the buffer so that the mapped area matches the given area and that the buffer - /// length is equal to area.width * area.height - pub fn resize(&mut self, area: Rect) { - let length = area.area() as usize; - if self.content.len() > length { - self.content.truncate(length); - } else { - self.content.resize(length, Cell::default()); - } - self.area = area; - } - - /// Reset all cells in the buffer - pub fn reset(&mut self) { - for c in &mut self.content { - c.reset(); - } - } - - /// Merge an other buffer into this one - pub fn merge(&mut self, other: &Buffer) { - let area = self.area.union(other.area); - let cell = Cell::default(); - self.content.resize(area.area() as usize, cell.clone()); - - // Move original content to the appropriate space - let size = self.area.area() as usize; - for i in (0..size).rev() { - let (x, y) = self.pos_of(i); - // New index in content - let k = ((y - area.y) * area.width + x - area.x) as usize; - if i != k { - self.content[k] = self.content[i].clone(); - self.content[i] = cell.clone(); - } - } - - // Push content of the other buffer into this one (may erase previous - // data) - let size = other.area.area() as usize; - for i in 0..size { - let (x, y) = other.pos_of(i); - // New index in content - let k = ((y - area.y) * area.width + x - area.x) as usize; - self.content[k] = other.content[i].clone(); - } - self.area = area; - } - - /// Builds a minimal sequence of coordinates and Cells necessary to update the UI from - /// self to other. - /// - /// We're assuming that buffers are well-formed, that is no double-width cell is followed by - /// a non-blank cell. - /// - /// # Multi-width characters handling: - /// - /// ```text - /// (Index:) `01` - /// Prev: `コ` - /// Next: `aa` - /// Updates: `0: a, 1: a' - /// ``` - /// - /// ```text - /// (Index:) `01` - /// Prev: `a ` - /// Next: `コ` - /// Updates: `0: コ` (double width symbol at index 0 - skip index 1) - /// ``` - /// - /// ```text - /// (Index:) `012` - /// Prev: `aaa` - /// Next: `aコ` - /// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2) - /// ``` - pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> { - let previous_buffer = &self.content; - let next_buffer = &other.content; - let width = self.area.width; - - let mut updates: Vec<(u16, u16, &Cell)> = vec![]; - // Cells invalidated by drawing/replacing preceeding multi-width characters: - let mut invalidated: usize = 0; - // Cells from the current buffer to skip due to preceeding multi-width characters taking their - // place (the skipped cells should be blank anyway): - let mut to_skip: usize = 0; - for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() { - if (current != previous || invalidated > 0) && to_skip == 0 { - let x = i as u16 % width; - let y = i as u16 / width; - updates.push((x, y, &next_buffer[i])); - } - - to_skip = current.symbol.width().saturating_sub(1); - - let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width()); - invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1); - } - updates - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn cell(s: &str) -> Cell { - let mut cell = Cell::default(); - cell.set_symbol(s); - cell - } - - #[test] - fn it_translates_to_and_from_coordinates() { - let rect = Rect::new(200, 100, 50, 80); - let buf = Buffer::empty(rect); - - // First cell is at the upper left corner. - assert_eq!(buf.pos_of(0), (200, 100)); - assert_eq!(buf.index_of(200, 100), 0); - - // Last cell is in the lower right. - assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179)); - assert_eq!(buf.index_of(249, 179), buf.content.len() - 1); - } - - #[test] - #[cfg(debug_assertions)] - #[should_panic(expected = "outside the buffer")] - fn pos_of_panics_on_out_of_bounds() { - let rect = Rect::new(0, 0, 10, 10); - let buf = Buffer::empty(rect); - - // There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell. - buf.pos_of(100); - } - - #[test] - #[cfg(debug_assertions)] - #[should_panic(expected = "outside the buffer")] - fn index_of_panics_on_out_of_bounds() { - let rect = Rect::new(0, 0, 10, 10); - let buf = Buffer::empty(rect); - - // width is 10; zero-indexed means that 10 would be the 11th cell. - buf.index_of(10, 0); - } - - #[test] - fn buffer_set_string() { - let area = Rect::new(0, 0, 5, 1); - let mut buffer = Buffer::empty(area); - - // Zero-width - buffer.set_stringn(0, 0, "aaa", 0, Style::default()); - assert_eq!(buffer, Buffer::with_lines(&[" "])); - - buffer.set_string(0, 0, "aaa", Style::default()); - assert_eq!(buffer, Buffer::with_lines(&["aaa "])); - - // Width limit: - buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default()); - assert_eq!(buffer, Buffer::with_lines(&["bbbb "])); - - buffer.set_string(0, 0, "12345", Style::default()); - assert_eq!(buffer, Buffer::with_lines(&["12345"])); - - // Width truncation: - buffer.set_string(0, 0, "123456", Style::default()); - assert_eq!(buffer, Buffer::with_lines(&["12345"])); - } - - #[test] - fn buffer_set_string_zero_width() { - let area = Rect::new(0, 0, 1, 1); - let mut buffer = Buffer::empty(area); - - // Leading grapheme with zero width - let s = "\u{1}a"; - buffer.set_stringn(0, 0, s, 1, Style::default()); - assert_eq!(buffer, Buffer::with_lines(&["a"])); - - // Trailing grapheme with zero with - let s = "a\u{1}"; - buffer.set_stringn(0, 0, s, 1, Style::default()); - assert_eq!(buffer, Buffer::with_lines(&["a"])); - } - - #[test] - fn buffer_set_string_double_width() { - let area = Rect::new(0, 0, 5, 1); - let mut buffer = Buffer::empty(area); - buffer.set_string(0, 0, "コン", Style::default()); - assert_eq!(buffer, Buffer::with_lines(&["コン "])); - - // Only 1 space left. - buffer.set_string(0, 0, "コンピ", Style::default()); - assert_eq!(buffer, Buffer::with_lines(&["コン "])); - } - - #[test] - fn buffer_with_lines() { - let buffer = Buffer::with_lines(&["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]); - assert_eq!(buffer.area.x, 0); - assert_eq!(buffer.area.y, 0); - assert_eq!(buffer.area.width, 10); - assert_eq!(buffer.area.height, 4); - } - - #[test] - fn buffer_diffing_empty_empty() { - let area = Rect::new(0, 0, 40, 40); - let prev = Buffer::empty(area); - let next = Buffer::empty(area); - let diff = prev.diff(&next); - assert_eq!(diff, vec![]); - } - - #[test] - fn buffer_diffing_empty_filled() { - let area = Rect::new(0, 0, 40, 40); - let prev = Buffer::empty(area); - let next = Buffer::filled(area, Cell::default().set_symbol("a")); - let diff = prev.diff(&next); - assert_eq!(diff.len(), 40 * 40); - } - - #[test] - fn buffer_diffing_filled_filled() { - let area = Rect::new(0, 0, 40, 40); - let prev = Buffer::filled(area, Cell::default().set_symbol("a")); - let next = Buffer::filled(area, Cell::default().set_symbol("a")); - let diff = prev.diff(&next); - assert_eq!(diff, vec![]); - } - - #[test] - fn buffer_diffing_single_width() { - let prev = Buffer::with_lines(&[ - " ", - "┌Title─┐ ", - "│ │ ", - "│ │ ", - "└──────┘ ", - ]); - let next = Buffer::with_lines(&[ - " ", - "┌TITLE─┐ ", - "│ │ ", - "│ │ ", - "└──────┘ ", - ]); - let diff = prev.diff(&next); - assert_eq!( - diff, - vec![ - (2, 1, &cell("I")), - (3, 1, &cell("T")), - (4, 1, &cell("L")), - (5, 1, &cell("E")), - ] - ); - } - - #[test] - #[rustfmt::skip] - fn buffer_diffing_multi_width() { - let prev = Buffer::with_lines(&[ - "┌Title─┐ ", - "└──────┘ ", - ]); - let next = Buffer::with_lines(&[ - "┌称号──┐ ", - "└──────┘ ", - ]); - let diff = prev.diff(&next); - assert_eq!( - diff, - vec![ - (1, 0, &cell("称")), - // Skipped "i" - (3, 0, &cell("号")), - // Skipped "l" - (5, 0, &cell("─")), - ] - ); - } - - #[test] - fn buffer_diffing_multi_width_offset() { - let prev = Buffer::with_lines(&["┌称号──┐"]); - let next = Buffer::with_lines(&["┌─称号─┐"]); - - let diff = prev.diff(&next); - assert_eq!( - diff, - vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),] - ); - } - - #[test] - fn buffer_merge() { - let mut one = Buffer::filled( - Rect { - x: 0, - y: 0, - width: 2, - height: 2, - }, - Cell::default().set_symbol("1"), - ); - let two = Buffer::filled( - Rect { - x: 0, - y: 2, - width: 2, - height: 2, - }, - Cell::default().set_symbol("2"), - ); - one.merge(&two); - assert_eq!(one, Buffer::with_lines(&["11", "11", "22", "22"])); - } - - #[test] - fn buffer_merge2() { - let mut one = Buffer::filled( - Rect { - x: 2, - y: 2, - width: 2, - height: 2, - }, - Cell::default().set_symbol("1"), - ); - let two = Buffer::filled( - Rect { - x: 0, - y: 0, - width: 2, - height: 2, - }, - Cell::default().set_symbol("2"), - ); - one.merge(&two); - assert_eq!(one, Buffer::with_lines(&["22 ", "22 ", " 11", " 11"])); - } - - #[test] - fn buffer_merge3() { - let mut one = Buffer::filled( - Rect { - x: 3, - y: 3, - width: 2, - height: 2, - }, - Cell::default().set_symbol("1"), - ); - let two = Buffer::filled( - Rect { - x: 1, - y: 1, - width: 3, - height: 4, - }, - Cell::default().set_symbol("2"), - ); - one.merge(&two); - let mut merged = Buffer::with_lines(&["222 ", "222 ", "2221", "2221"]); - merged.area = Rect { - x: 1, - y: 1, - width: 4, - height: 4, - }; - assert_eq!(one, merged); - } -} diff --git a/src/tui/layout.rs b/src/tui/layout.rs deleted file mode 100644 index 369e10f9..00000000 --- a/src/tui/layout.rs +++ /dev/null @@ -1,537 +0,0 @@ -use std::cell::RefCell; -use std::cmp::{max, min}; -use std::collections::HashMap; - -use cassowary::strength::{REQUIRED, WEAK}; -use cassowary::WeightedRelation::{EQ, GE, LE}; -use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable}; - -#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] -pub enum Corner { - TopLeft, - TopRight, - BottomRight, - BottomLeft, -} - -#[derive(Debug, Hash, Clone, PartialEq, Eq)] -pub enum Direction { - Horizontal, - Vertical, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Constraint { - // TODO: enforce range 0 - 100 - Percentage(u16), - Ratio(u32, u32), - Length(u16), - Max(u16), - Min(u16), -} - -impl Constraint { - pub fn apply(&self, length: u16) -> u16 { - match *self { - Constraint::Percentage(p) => length * p / 100, - Constraint::Ratio(num, den) => { - let r = num * u32::from(length) / den; - r as u16 - } - Constraint::Length(l) => length.min(l), - Constraint::Max(m) => length.min(m), - Constraint::Min(m) => length.max(m), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Margin { - pub vertical: u16, - pub horizontal: u16, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Alignment { - Left, - Center, - Right, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Layout { - direction: Direction, - margin: Margin, - constraints: Vec<Constraint>, - /// Whether the last chunk of the computed layout should be expanded to fill the available - /// space. - expand_to_fill: bool, -} - -thread_local! { - static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new()); -} - -impl Default for Layout { - fn default() -> Layout { - Layout { - direction: Direction::Vertical, - margin: Margin { - horizontal: 0, - vertical: 0, - }, - constraints: Vec::new(), - expand_to_fill: true, - } - } -} - -impl Layout { - pub fn constraints<C>(mut self, constraints: C) -> Layout - where - C: Into<Vec<Constraint>>, - { - self.constraints = constraints.into(); - self - } - - pub fn margin(mut self, margin: u16) -> Layout { - self.margin = Margin { - horizontal: margin, - vertical: margin, - }; - self - } - - pub fn horizontal_margin(mut self, horizontal: u16) -> Layout { - self.margin.horizontal = horizontal; - self - } - - pub fn vertical_margin(mut self, vertical: u16) -> Layout { - self.margin.vertical = vertical; - self - } - - pub fn direction(mut self, direction: Direction) -> Layout { - self.direction = direction; - self - } - - pub(crate) fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout { - self.expand_to_fill = expand_to_fill; - self - } - - /// Wrapper function around the cassowary-rs solver to be able to split a given - /// area into smaller ones based on the preferred widths or heights and the direction. - /// - /// # Examples - /// ``` - /// # use tui::layout::{Rect, Constraint, Direction, Layout}; - /// let chunks = Layout::default() - /// .direction(Direction::Vertical) - /// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref()) - /// .split(Rect { - /// x: 2, - /// y: 2, - /// width: 10, - /// height: 10, - /// }); - /// assert_eq!( - /// chunks, - /// vec![ - /// Rect { - /// x: 2, - /// y: 2, - /// width: 10, - /// height: 5 - /// }, - /// Rect { - /// x: 2, - /// y: 7, - /// width: 10, - /// height: 5 - /// } - /// ] - /// ); - /// - /// let chunks = Layout::default() - /// .direction(Direction::Horizontal) - /// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref()) - /// .split(Rect { - /// x: 0, - /// y: 0, - /// width: 9, - /// height: 2, - /// }); - /// assert_eq!( - /// chunks, - /// vec![ - /// Rect { - /// x: 0, - /// y: 0, - /// width: 3, - /// height: 2 - /// }, - /// Rect { - /// x: 3, - /// y: 0, - /// width: 6, - /// height: 2 - /// } - /// ] - /// ); - /// ``` - pub fn split(&self, area: Rect) -> Vec<Rect> { - // TODO: Maybe use a fixed size cache ? - LAYOUT_CACHE.with(|c| { - c.borrow_mut() - .entry((area, self.clone())) - .or_insert_with(|| split(area, self)) - .clone() - }) - } -} - -#[allow(clippy::too_many_lines)] -fn split(area: Rect, layout: &Layout) -> Vec<Rect> { - let mut solver = Solver::new(); - let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new(); - let elements = layout - .constraints - .iter() - .map(|_| Element::new()) - .collect::<Vec<Element>>(); - let mut results = layout - .constraints - .iter() - .map(|_| Rect::default()) - .collect::<Vec<Rect>>(); - - let dest_area = area.inner(&layout.margin); - for (i, e) in elements.iter().enumerate() { - vars.insert(e.x, (i, 0)); - vars.insert(e.y, (i, 1)); - vars.insert(e.width, (i, 2)); - vars.insert(e.height, (i, 3)); - } - let mut ccs: Vec<CassowaryConstraint> = - Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6); - for elt in &elements { - ccs.push(elt.width | GE(REQUIRED) | 0f64); - ccs.push(elt.height | GE(REQUIRED) | 0f64); - ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left())); - ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top())); - ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right())); - ccs.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom())); - } - if let Some(first) = elements.first() { - ccs.push(match layout.direction { - Direction::Horizontal => first.left() | EQ(REQUIRED) | f64::from(dest_area.left()), - Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()), - }); - } - if layout.expand_to_fill { - if let Some(last) = elements.last() { - ccs.push(match layout.direction { - Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()), - Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()), - }); - } - } - match layout.direction { - Direction::Horizontal => { - for pair in elements.windows(2) { - ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x); - } - for (i, size) in layout.constraints.iter().enumerate() { - ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y)); - ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height)); - ccs.push(match *size { - Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v), - Constraint::Percentage(v) => { - elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0) - } - Constraint::Ratio(n, d) => { - elements[i].width - | EQ(WEAK) - | (f64::from(dest_area.width) * f64::from(n) / f64::from(d)) - } - Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v), - Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v), - }); - } - } - Direction::Vertical => { - for pair in elements.windows(2) { - ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y); - } - for (i, size) in layout.constraints.iter().enumerate() { - ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x)); - ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width)); - ccs.push(match *size { - Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v), - Constraint::Percentage(v) => { - elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0) - } - Constraint::Ratio(n, d) => { - elements[i].height - | EQ(WEAK) - | (f64::from(dest_area.height) * f64::from(n) / f64::from(d)) - } - Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v), - Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v), - }); - } - } - } - solver.add_constraints(&ccs).unwrap(); - for &(var, value) in solver.fetch_changes() { - let (index, attr) = vars[&var]; - let value = if value.is_sign_negative() { - 0 - } else { - value as u16 - }; - match attr { - 0 => { - results[index].x = value; - } - 1 => { - results[index].y = value; - } - 2 => { - results[index].width = value; - } - 3 => { - results[index].height = value; - } - _ => {} - } - } - - if layout.expand_to_fill { - // Fix imprecision by extending the last item a bit if necessary - if let Some(last) = results.last_mut() { - match layout.direction { - Direction::Vertical => { - last.height = dest_area.bottom() - last.y; - } - Direction::Horizontal => { - last.width = dest_area.right() - last.x; - } - } - } - } - results -} - -/// A container used by the solver inside split -struct Element { - x: Variable, - y: Variable, - width: Variable, - height: Variable, -} - -impl Element { - fn new() -> Element { - Element { - x: Variable::new(), - y: Variable::new(), - width: Variable::new(), - height: Variable::new(), - } - } - - fn left(&self) -> Variable { - self.x - } - - fn top(&self) -> Variable { - self.y - } - - fn right(&self) -> Expression { - self.x + self.width - } - - fn bottom(&self) -> Expression { - self.y + self.height - } -} - -/// A simple rectangle used in the computation of the layout and to give widgets a hint about the -/// area they are supposed to render to. -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)] -pub struct Rect { - pub x: u16, - pub y: u16, - pub width: u16, - pub height: u16, -} - -impl Rect { - /// Creates a new rect, with width and height limited to keep the area under max u16. - /// If clipped, aspect ratio will be preserved. - pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect { - let max_area = u16::max_value(); - let (clipped_width, clipped_height) = - if u32::from(width) * u32::from(height) > u32::from(max_area) { - let aspect_ratio = f64::from(width) / f64::from(height); - let max_area_f = f64::from(max_area); - let height_f = (max_area_f / aspect_ratio).sqrt(); - let width_f = height_f * aspect_ratio; - (width_f as u16, height_f as u16) - } else { - (width, height) - }; - Rect { - x, - y, - width: clipped_width, - height: clipped_height, - } - } - - pub fn area(self) -> u16 { - self.width * self.height - } - - pub fn left(self) -> u16 { - self.x - } - - pub fn right(self) -> u16 { - self.x.saturating_add(self.width) - } - - pub fn top(self) -> u16 { - self.y - } - - pub fn bottom(self) -> u16 { - self.y.saturating_add(self.height) - } - - pub fn inner(self, margin: &Margin) -> Rect { - if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical { - Rect::default() - } else { - Rect { - x: self.x + margin.horizontal, - y: self.y + margin.vertical, - width: self.width - 2 * margin.horizontal, - height: self.height - 2 * margin.vertical, - } - } - } - - pub fn union(self, other: Rect) -> Rect { - let x1 = min(self.x, other.x); - let y1 = min(self.y, other.y); - let x2 = max(self.x + self.width, other.x + other.width); - let y2 = max(self.y + self.height, other.y + other.height); - Rect { - x: x1, - y: y1, - width: x2 - x1, - height: y2 - y1, - } - } - - pub fn intersection(self, other: Rect) -> Rect { - let x1 = max(self.x, other.x); - let y1 = max(self.y, other.y); - let x2 = min(self.x + self.width, other.x + other.width); - let y2 = min(self.y + self.height, other.y + other.height); - Rect { - x: x1, - y: y1, - width: x2 - x1, - height: y2 - y1, - } - } - - pub fn intersects(self, other: Rect) -> bool { - self.x < other.x + other.width - && self.x + self.width > other.x - && self.y < other.y + other.height - && self.y + self.height > other.y - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_vertical_split_by_height() { - let target = Rect { - x: 2, - y: 2, - width: 10, - height: 10, - }; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(10), - Constraint::Max(5), - Constraint::Min(1), - ] - .as_ref(), - ) - .split(target); - - assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>()); - chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y)); - } - - #[test] - fn test_rect_size_truncation() { - for width in 256u16..300u16 { - for height in 256u16..300u16 { - let rect = Rect::new(0, 0, width, height); - rect.area(); // Should not panic. - assert!(rect.width < width || rect.height < height); - // The target dimensions are rounded down so the math will not be too precise - // but let's make sure the ratios don't diverge crazily. - assert!( - (f64::from(rect.width) / f64::from(rect.height) - - f64::from(width) / f64::from(height)) - .abs() - < 1.0 - ); - } - } - - // One dimension below 255, one above. Area above max u16. - let width = 900; - let height = 100; - let rect = Rect::new(0, 0, width, height); - assert_ne!(rect.width, 900); - assert_ne!(rect.height, 100); - assert!(rect.width < width || rect.height < height); - } - - #[test] - fn test_rect_size_preservation() { - for width in 0..256u16 { - for height in 0..256u16 { - let rect = Rect::new(0, 0, width, height); - rect.area(); // Should not panic. - assert_eq!(rect.width, width); - assert_eq!(rect.height, height); - } - } - - // One dimension below 255, one above. Area below max u16. - let rect = Rect::new(0, 0, 300, 100); - assert_eq!(rect.width, 300); - assert_eq!(rect.height, 100); - } -} diff --git a/src/tui/mod.rs b/src/tui/mod.rs deleted file mode 100644 index 29fe84cd..00000000 --- a/src/tui/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Fork of `tui-rs` -#![allow( - clippy::module_name_repetitions, - clippy::bool_to_int_with_if, - clippy::similar_names, - clippy::cast_possible_truncation, - clippy::cast_sign_loss, - dead_code -)] - -pub mod backend; -pub mod buffer; -pub mod layout; -pub mod style; -pub mod symbols; -pub mod terminal; -pub mod text; -pub mod widgets; - -pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport}; diff --git a/src/tui/style.rs b/src/tui/style.rs deleted file mode 100644 index 4aa19d4a..00000000 --- a/src/tui/style.rs +++ /dev/null @@ -1,278 +0,0 @@ -//! `style` contains the primitives used to control how your user interface will look. - -use bitflags::bitflags; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Color { - Reset, - Black, - Red, - Green, - Yellow, - Blue, - Magenta, - Cyan, - Gray, - DarkGray, - LightRed, - LightGreen, - LightYellow, - LightBlue, - LightMagenta, - LightCyan, - White, - Rgb(u8, u8, u8), - Indexed(u8), -} - -bitflags! { - /// Modifier changes the way a piece of text is displayed. - /// - /// They are bitflags so they can easily be composed. - /// - /// ## Examples - /// - /// ```rust - /// # use tui::style::Modifier; - /// - /// let m = Modifier::BOLD | Modifier::ITALIC; - /// ``` - pub struct Modifier: u16 { - const BOLD = 0b0000_0000_0001; - const DIM = 0b0000_0000_0010; - const ITALIC = 0b0000_0000_0100; - const UNDERLINED = 0b0000_0000_1000; - const SLOW_BLINK = 0b0000_0001_0000; - const RAPID_BLINK = 0b0000_0010_0000; - const REVERSED = 0b0000_0100_0000; - const HIDDEN = 0b0000_1000_0000; - const CROSSED_OUT = 0b0001_0000_0000; - } -} - -/// Style let you control the main characteristics of the displayed elements. -/// -/// ```rust -/// # use tui::style::{Color, Modifier, Style}; -/// Style::default() -/// .fg(Color::Black) -/// .bg(Color::Green) -/// .add_modifier(Modifier::ITALIC | Modifier::BOLD); -/// ``` -/// -/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the -/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not -/// just S3. -/// -/// ```rust -/// # use tui::style::{Color, Modifier, Style}; -/// # use tui::buffer::Buffer; -/// # use tui::layout::Rect; -/// let styles = [ -/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC), -/// Style::default().bg(Color::Red), -/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC), -/// ]; -/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1)); -/// for style in &styles { -/// buffer.get_mut(0, 0).set_style(*style); -/// } -/// assert_eq!( -/// Style { -/// fg: Some(Color::Yellow), -/// bg: Some(Color::Red), -/// add_modifier: Modifier::BOLD, -/// sub_modifier: Modifier::empty(), -/// }, -/// buffer.get(0, 0).style(), -/// ); -/// ``` -/// -/// The default implementation returns a `Style` that does not modify anything. If you wish to -/// reset all properties until that point use [`Style::reset`]. -/// -/// ``` -/// # use tui::style::{Color, Modifier, Style}; -/// # use tui::buffer::Buffer; -/// # use tui::layout::Rect; -/// let styles = [ -/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC), -/// Style::reset().fg(Color::Yellow), -/// ]; -/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1)); -/// for style in &styles { -/// buffer.get_mut(0, 0).set_style(*style); -/// } -/// assert_eq!( -/// Style { -/// fg: Some(Color::Yellow), -/// bg: Some(Color::Reset), -/// add_modifier: Modifier::empty(), -/// sub_modifier: Modifier::empty(), -/// }, -/// buffer.get(0, 0).style(), -/// ); -/// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Style { - pub fg: Option<Color>, - pub bg: Option<Color>, - pub add_modifier: Modifier, - pub sub_modifier: Modifier, -} - -impl Default for Style { - fn default() -> Style { - Style { - fg: None, - bg: None, - add_modifier: Modifier::empty(), - sub_modifier: Modifier::empty(), - } - } -} - -impl Style { - /// Returns a `Style` resetting all properties. - pub fn reset() -> Style { - Style { - fg: Some(Color::Reset), - bg: Some(Color::Reset), - add_modifier: Modifier::empty(), - sub_modifier: Modifier::all(), - } - } - - /// Changes the foreground color. - /// - /// ## Examples - /// - /// ```rust - /// # use tui::style::{Color, Style}; - /// let style = Style::default().fg(Color::Blue); - /// let diff = Style::default().fg(Color::Red); - /// assert_eq!(style.patch(diff), Style::default().fg(Color::Red)); - /// ``` - pub fn fg(mut self, color: Color) -> Style { - self.fg = Some(color); - self - } - - /// Changes the background color. - /// - /// ## Examples - /// - /// ```rust - /// # use tui::style::{Color, Style}; - /// let style = Style::default().bg(Color::Blue); - /// let diff = Style::default().bg(Color::Red); - /// assert_eq!(style.patch(diff), Style::default().bg(Color::Red)); - /// ``` - pub fn bg(mut self, color: Color) -> Style { - self.bg = Some(color); - self - } - - /// Changes the text emphasis. - /// - /// When applied, it adds the given modifier to the `Style` modifiers. - /// - /// ## Examples - /// - /// ```rust - /// # use tui::style::{Color, Modifier, Style}; - /// let style = Style::default().add_modifier(Modifier::BOLD); - /// let diff = Style::default().add_modifier(Modifier::ITALIC); - /// let patched = style.patch(diff); - /// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC); - /// assert_eq!(patched.sub_modifier, Modifier::empty()); - /// ``` - pub fn add_modifier(mut self, modifier: Modifier) -> Style { - self.sub_modifier.remove(modifier); - self.add_modifier.insert(modifier); - self - } - - /// Changes the text emphasis. - /// - /// When applied, it removes the given modifier from the `Style` modifiers. - /// - /// ## Examples - /// - /// ```rust - /// # use tui::style::{Color, Modifier, Style}; - /// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC); - /// let diff = Style::default().remove_modifier(Modifier::ITALIC); - /// let patched = style.patch(diff); - /// assert_eq!(patched.add_modifier, Modifier::BOLD); - /// assert_eq!(patched.sub_modifier, Modifier::ITALIC); - /// ``` - pub fn remove_modifier(mut self, modifier: Modifier) -> Style { - self.add_modifier.remove(modifier); - self.sub_modifier.insert(modifier); - self - } - - /// Results in a combined style that is equivalent to applying the two individual styles to - /// a style one after the other. - /// - /// ## Examples - /// ``` - /// # use tui::style::{Color, Modifier, Style}; - /// let style_1 = Style::default().fg(Color::Yellow); - /// let style_2 = Style::default().bg(Color::Red); - /// let combined = style_1.patch(style_2); - /// assert_eq!( - /// Style::default().patch(style_1).patch(style_2), - /// Style::default().patch(combined)); - /// ``` - pub fn patch(mut self, other: Style) -> Style { - self.fg = other.fg.or(self.fg); - self.bg = other.bg.or(self.bg); - - self.add_modifier.remove(other.sub_modifier); - self.add_modifier.insert(other.add_modifier); - self.sub_modifier.remove(other.add_modifier); - self.sub_modifier.insert(other.sub_modifier); - - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn styles() -> Vec<Style> { - vec![ - Style::default(), - Style::default().fg(Color::Yellow), - Style::default().bg(Color::Yellow), - Style::default().add_modifier(Modifier::BOLD), - Style::default().remove_modifier(Modifier::BOLD), - Style::default().add_modifier(Modifier::ITALIC), - Style::default().remove_modifier(Modifier::ITALIC), - Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD), - Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD), - ] - } - - #[test] - fn combined_patch_gives_same_result_as_individual_patch() { - let styles = styles(); - for &a in &styles { - for &b in &styles { - for &c in &styles { - for &d in &styles { - let combined = a.patch(b.patch(c.patch(d))); - - assert_eq!( - Style::default().patch(a).patch(b).patch(c).patch(d), - Style::default().patch(combined) - ); - } - } - } - } - } -} diff --git a/src/tui/symbols.rs b/src/tui/symbols.rs deleted file mode 100644 index 040e77f6..00000000 --- a/src/tui/symbols.rs +++ /dev/null @@ -1,233 +0,0 @@ -pub mod block { - pub const FULL: &str = "█"; - pub const SEVEN_EIGHTHS: &str = "▉"; - pub const THREE_QUARTERS: &str = "▊"; - pub const FIVE_EIGHTHS: &str = "▋"; - pub const HALF: &str = "▌"; - pub const THREE_EIGHTHS: &str = "▍"; - pub const ONE_QUARTER: &str = "▎"; - pub const ONE_EIGHTH: &str = "▏"; - - #[derive(Debug, Clone)] - pub struct Set { - pub full: &'static str, - pub seven_eighths: &'static str, - pub three_quarters: &'static str, - pub five_eighths: &'static str, - pub half: &'static str, - pub three_eighths: &'static str, - pub one_quarter: &'static str, - pub one_eighth: &'static str, - pub empty: &'static str, - } - - pub const THREE_LEVELS: Set = Set { - full: FULL, - seven_eighths: FULL, - three_quarters: HALF, - five_eighths: HALF, - half: HALF, - three_eighths: HALF, - one_quarter: HALF, - one_eighth: " ", - empty: " ", - }; - - pub const NINE_LEVELS: Set = Set { - full: FULL, - seven_eighths: SEVEN_EIGHTHS, - three_quarters: THREE_QUARTERS, - five_eighths: FIVE_EIGHTHS, - half: HALF, - three_eighths: THREE_EIGHTHS, - one_quarter: ONE_QUARTER, - one_eighth: ONE_EIGHTH, - empty: " ", - }; -} - -pub mod bar { - pub const FULL: &str = "█"; - pub const SEVEN_EIGHTHS: &str = "▇"; - pub const THREE_QUARTERS: &str = "▆"; - pub const FIVE_EIGHTHS: &str = "▅"; - pub const HALF: &str = "▄"; - pub const THREE_EIGHTHS: &str = "▃"; - pub const ONE_QUARTER: &str = "▂"; - pub const ONE_EIGHTH: &str = "▁"; - - #[derive(Debug, Clone)] - pub struct Set { - pub full: &'static str, - pub seven_eighths: &'static str, - pub three_quarters: &'static str, - pub five_eighths: &'static str, - pub half: &'static str, - pub three_eighths: &'static str, - pub one_quarter: &'static str, - pub one_eighth: &'static str, - pub empty: &'static str, - } - - pub const THREE_LEVELS: Set = Set { - full: FULL, - seven_eighths: FULL, - three_quarters: HALF, - five_eighths: HALF, - half: HALF, - three_eighths: HALF, - one_quarter: HALF, - one_eighth: " ", - empty: " ", - }; - - pub const NINE_LEVELS: Set = Set { - full: FULL, - seven_eighths: SEVEN_EIGHTHS, - three_quarters: THREE_QUARTERS, - five_eighths: FIVE_EIGHTHS, - half: HALF, - three_eighths: THREE_EIGHTHS, - one_quarter: ONE_QUARTER, - one_eighth: ONE_EIGHTH, - empty: " ", - }; -} - -pub mod line { - pub const VERTICAL: &str = "│"; - pub const DOUBLE_VERTICAL: &str = "║"; - pub const THICK_VERTICAL: &str = "┃"; - - pub const HORIZONTAL: &str = "─"; - pub const DOUBLE_HORIZONTAL: &str = "═"; - pub const THICK_HORIZONTAL: &str = "━"; - - pub const TOP_RIGHT: &str = "┐"; - pub const ROUNDED_TOP_RIGHT: &str = "╮"; - pub const DOUBLE_TOP_RIGHT: &str = "╗"; - pub const THICK_TOP_RIGHT: &str = "┓"; - - pub const TOP_LEFT: &str = "┌"; - pub const ROUNDED_TOP_LEFT: &str = "╭"; - pub const DOUBLE_TOP_LEFT: &str = "╔"; - pub const THICK_TOP_LEFT: &str = "┏"; - - pub const BOTTOM_RIGHT: &str = "┘"; - pub const ROUNDED_BOTTOM_RIGHT: &str = "╯"; - pub const DOUBLE_BOTTOM_RIGHT: &str = "╝"; - pub const THICK_BOTTOM_RIGHT: &str = "┛"; - - pub const BOTTOM_LEFT: &str = "└"; - pub const ROUNDED_BOTTOM_LEFT: &str = "╰"; - pub const DOUBLE_BOTTOM_LEFT: &str = "╚"; - pub const THICK_BOTTOM_LEFT: &str = "┗"; - - pub const VERTICAL_LEFT: &str = "┤"; - pub const DOUBLE_VERTICAL_LEFT: &str = "╣"; - pub const THICK_VERTICAL_LEFT: &str = "┫"; - - pub const VERTICAL_RIGHT: &str = "├"; - pub const DOUBLE_VERTICAL_RIGHT: &str = "╠"; - pub const THICK_VERTICAL_RIGHT: &str = "┣"; - - pub const HORIZONTAL_DOWN: &str = "┬"; - pub const DOUBLE_HORIZONTAL_DOWN: &str = "╦"; - pub const THICK_HORIZONTAL_DOWN: &str = "┳"; - - pub const HORIZONTAL_UP: &str = "┴"; - pub const DOUBLE_HORIZONTAL_UP: &str = "╩"; - pub const THICK_HORIZONTAL_UP: &str = "┻"; - - pub const CROSS: &str = "┼"; - pub const DOUBLE_CROSS: &str = "╬"; - pub const THICK_CROSS: &str = "╋"; - - #[derive(Debug, Clone)] - pub struct Set { - pub vertical: &'static str, - pub horizontal: &'static str, - pub top_right: &'static str, - pub top_left: &'static str, - pub bottom_right: &'static str, - pub bottom_left: &'static str, - pub vertical_left: &'static str, - pub vertical_right: &'static str, - pub horizontal_down: &'static str, - pub horizontal_up: &'static str, - pub cross: &'static str, - } - - pub const NORMAL: Set = Set { - vertical: VERTICAL, - horizontal: HORIZONTAL, - top_right: TOP_RIGHT, - top_left: TOP_LEFT, - bottom_right: BOTTOM_RIGHT, - bottom_left: BOTTOM_LEFT, - vertical_left: VERTICAL_LEFT, - vertical_right: VERTICAL_RIGHT, - horizontal_down: HORIZONTAL_DOWN, - horizontal_up: HORIZONTAL_UP, - cross: CROSS, - }; - - pub const ROUNDED: Set = Set { - top_right: ROUNDED_TOP_RIGHT, - top_left: ROUNDED_TOP_LEFT, - bottom_right: ROUNDED_BOTTOM_RIGHT, - bottom_left: ROUNDED_BOTTOM_LEFT, - ..NORMAL - }; - - pub const DOUBLE: Set = Set { - vertical: DOUBLE_VERTICAL, - horizontal: DOUBLE_HORIZONTAL, - top_right: DOUBLE_TOP_RIGHT, - top_left: DOUBLE_TOP_LEFT, - bottom_right: DOUBLE_BOTTOM_RIGHT, - bottom_left: DOUBLE_BOTTOM_LEFT, - vertical_left: DOUBLE_VERTICAL_LEFT, - vertical_right: DOUBLE_VERTICAL_RIGHT, - horizontal_down: DOUBLE_HORIZONTAL_DOWN, - horizontal_up: DOUBLE_HORIZONTAL_UP, - cross: DOUBLE_CROSS, - }; - - pub const THICK: Set = Set { - vertical: THICK_VERTICAL, - horizontal: THICK_HORIZONTAL, - top_right: THICK_TOP_RIGHT, - top_left: THICK_TOP_LEFT, - bottom_right: THICK_BOTTOM_RIGHT, - bottom_left: THICK_BOTTOM_LEFT, - vertical_left: THICK_VERTICAL_LEFT, - vertical_right: THICK_VERTICAL_RIGHT, - horizontal_down: THICK_HORIZONTAL_DOWN, - horizontal_up: THICK_HORIZONTAL_UP, - cross: THICK_CROSS, - }; -} - -pub const DOT: &str = "•"; - -pub mod braille { - pub const BLANK: u16 = 0x2800; - pub const DOTS: [[u16; 2]; 4] = [ - [0x0001, 0x0008], - [0x0002, 0x0010], - [0x0004, 0x0020], - [0x0040, 0x0080], - ]; -} - -/// Marker to use when plotting data points -#[derive(Debug, Clone, Copy)] -pub enum Marker { - /// One point per cell in shape of dot - Dot, - /// One point per cell in shape of a block - Block, - /// Up to 8 points per cell - Braille, -} diff --git a/src/tui/terminal.rs b/src/tui/terminal.rs deleted file mode 100644 index e64690e9..00000000 --- a/src/tui/terminal.rs +++ /dev/null @@ -1,321 +0,0 @@ -use crate::tui::{ - backend::Backend, - buffer::Buffer, - layout::Rect, - widgets::{StatefulWidget, Widget}, -}; -use std::io; - -#[derive(Debug, Clone, PartialEq)] -/// UNSTABLE -enum ResizeBehavior { - Fixed, - Auto, -} - -#[derive(Debug, Clone, PartialEq)] -/// UNSTABLE -pub struct Viewport { - area: Rect, - resize_behavior: ResizeBehavior, -} - -impl Viewport { - /// UNSTABLE - pub fn fixed(area: Rect) -> Viewport { - Viewport { - area, - resize_behavior: ResizeBehavior::Fixed, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -/// Options to pass to [`Terminal::with_options`] -pub struct TerminalOptions { - /// Viewport used to draw to the terminal - pub viewport: Viewport, -} - -/// Interface to the terminal backed by Termion -#[derive(Debug)] -pub struct Terminal<B> -where - B: Backend, -{ - backend: B, - /// Holds the results of the current and previous draw calls. The two are compared at the end - /// of each draw pass to output the necessary updates to the terminal - buffers: [Buffer; 2], - /// Index of the current buffer in the previous array - current: usize, - /// Whether the cursor is currently hidden - hidden_cursor: bool, - /// Viewport - viewport: Viewport, -} - -/// Represents a consistent terminal interface for rendering. -pub struct Frame<'a, B: 'a + Backend> { - terminal: &'a mut Terminal<B>, - - /// Where should the cursor be after drawing this frame? - /// - /// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x, - /// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`. - cursor_position: Option<(u16, u16)>, -} - -impl<'a, B> Frame<'a, B> -where - B: Backend, -{ - /// Terminal size, guaranteed not to change when rendering. - pub fn size(&self) -> Rect { - self.terminal.viewport.area - } - - /// Render a [`Widget`] to the current buffer using [`Widget::render`]. - /// - /// # Examples - /// - /// ```rust - /// # use tui::Terminal; - /// # use tui::backend::TestBackend; - /// # use tui::layout::Rect; - /// # use tui::widgets::Block; - /// # let backend = TestBackend::new(5, 5); - /// # let mut terminal = Terminal::new(backend).unwrap(); - /// let block = Block::default(); - /// let area = Rect::new(0, 0, 5, 5); - /// let mut frame = terminal.get_frame(); - /// frame.render_widget(block, area); - /// ``` - pub fn render_widget<W>(&mut self, widget: W, area: Rect) - where - W: Widget, - { - widget.render(area, self.terminal.current_buffer_mut()); - } - - /// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`]. - /// - /// The last argument should be an instance of the [`StatefulWidget::State`] associated to the - /// given [`StatefulWidget`]. - /// - /// # Examples - /// - /// ```rust - /// # use tui::Terminal; - /// # use tui::backend::TestBackend; - /// # use tui::layout::Rect; - /// # use tui::widgets::{List, ListItem, ListState}; - /// # let backend = TestBackend::new(5, 5); - /// # let mut terminal = Terminal::new(backend).unwrap(); - /// let mut state = ListState::default(); - /// state.select(Some(1)); - /// let items = vec![ - /// ListItem::new("Item 1"), - /// ListItem::new("Item 2"), - /// ]; - /// let list = List::new(items); - /// let area = Rect::new(0, 0, 5, 5); - /// let mut frame = terminal.get_frame(); - /// frame.render_stateful_widget(list, area, &mut state); - /// ``` - pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State) - where - W: StatefulWidget, - { - widget.render(area, self.terminal.current_buffer_mut(), state); - } - - /// After drawing this frame, make the cursor visible and put it at the specified (x, y) - /// coordinates. If this method is not called, the cursor will be hidden. - /// - /// Note that this will interfere with calls to `Terminal::hide_cursor()`, - /// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick - /// with it. - pub fn set_cursor(&mut self, x: u16, y: u16) { - self.cursor_position = Some((x, y)); - } -} - -/// `CompletedFrame` represents the state of the terminal after all changes performed in the last -/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to -/// [`Terminal::draw`]. -pub struct CompletedFrame<'a> { - pub buffer: &'a Buffer, - pub area: Rect, -} - -impl<B> Drop for Terminal<B> -where - B: Backend, -{ - fn drop(&mut self) { - // Attempt to restore the cursor state - if self.hidden_cursor { - if let Err(err) = self.show_cursor() { - eprintln!("Failed to show the cursor: {err}"); - } - } - } -} - -impl<B> Terminal<B> -where - B: Backend, -{ - /// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and - /// default colors for the foreground and the background - pub fn new(backend: B) -> io::Result<Terminal<B>> { - let size = backend.size()?; - Ok(Terminal::with_options( - backend, - TerminalOptions { - viewport: Viewport { - area: size, - resize_behavior: ResizeBehavior::Auto, - }, - }, - )) - } - - /// UNSTABLE - pub fn with_options(backend: B, options: TerminalOptions) -> Terminal<B> { - Terminal { - backend, - buffers: [ - Buffer::empty(options.viewport.area), - Buffer::empty(options.viewport.area), - ], - current: 0, - hidden_cursor: false, - viewport: options.viewport, - } - } - - /// Get a Frame object which provides a consistent view into the terminal state for rendering. - pub fn get_frame(&mut self) -> Frame<B> { - Frame { - terminal: self, - cursor_position: None, - } - } - - pub fn current_buffer_mut(&mut self) -> &mut Buffer { - &mut self.buffers[self.current] - } - - pub fn backend(&self) -> &B { - &self.backend - } - - pub fn backend_mut(&mut self) -> &mut B { - &mut self.backend - } - - /// Obtains a difference between the previous and the current buffer and passes it to the - /// current backend for drawing. - pub fn flush(&mut self) -> io::Result<()> { - let previous_buffer = &self.buffers[1 - self.current]; - let current_buffer = &self.buffers[self.current]; - let updates = previous_buffer.diff(current_buffer); - self.backend.draw(updates.into_iter()) - } - - /// Updates the Terminal so that internal buffers match the requested size. Requested size will - /// be saved so the size can remain consistent when rendering. - /// This leads to a full clear of the screen. - pub fn resize(&mut self, area: Rect) -> io::Result<()> { - self.buffers[self.current].resize(area); - self.buffers[1 - self.current].resize(area); - self.viewport.area = area; - self.clear() - } - - /// Queries the backend for size and resizes if it doesn't match the previous size. - pub fn autoresize(&mut self) -> io::Result<()> { - if self.viewport.resize_behavior == ResizeBehavior::Auto { - let size = self.size()?; - if size != self.viewport.area { - self.resize(size)?; - } - }; - Ok(()) - } - - /// Synchronizes terminal size, calls the rendering closure, flushes the current internal state - /// and prepares for the next draw call. - pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame> - where - F: FnOnce(&mut Frame<B>), - { - // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets - // and the terminal (if growing), which may OOB. - self.autoresize()?; - - let mut frame = self.get_frame(); - f(&mut frame); - // We can't change the cursor position right away because we have to flush the frame to - // stdout first. But we also can't keep the frame around, since it holds a &mut to - // Terminal. Thus, we're taking the important data out of the Frame and dropping it. - let cursor_position = frame.cursor_position; - - // Draw to stdout - self.flush()?; - - match cursor_position { - None => self.hide_cursor()?, - Some((x, y)) => { - self.show_cursor()?; - self.set_cursor(x, y)?; - } - } - - // Swap buffers - self.buffers[1 - self.current].reset(); - self.current = 1 - self.current; - - // Flush - self.backend.flush()?; - Ok(CompletedFrame { - buffer: &self.buffers[1 - self.current], - area: self.viewport.area, - }) - } - - pub fn hide_cursor(&mut self) -> io::Result<()> { - self.backend.hide_cursor()?; - self.hidden_cursor = true; - Ok(()) - } - - pub fn show_cursor(&mut self) -> io::Result<()> { - self.backend.show_cursor()?; - self.hidden_cursor = false; - Ok(()) - } - - pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> { - self.backend.get_cursor() - } - - pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { - self.backend.set_cursor(x, y) - } - - /// Clear the terminal and force a full redraw on the next draw call. - pub fn clear(&mut self) -> io::Result<()> { - self.backend.clear()?; - // Reset the back buffer to make sure the next update will redraw everything. - self.buffers[1 - self.current].reset(); - Ok(()) - } - - /// Queries the real size of the backend. - pub fn size(&self) -> io::Result<Rect> { - self.backend.size() - } -} diff --git a/src/tui/text.rs b/src/tui/text.rs deleted file mode 100644 index e8a5aa73..00000000 --- a/src/tui/text.rs +++ /dev/null @@ -1,428 +0,0 @@ -//! Primitives for styled text. -//! -//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish, -//! those strings may be associated to a set of styles. `tui` has three ways to represent them: -//! - A single line string where all graphemes have the same style is represented by a [`Span`]. -//! - A single line string where each grapheme may have its own style is represented by [`Spans`]. -//! - A multiple line string where each grapheme may have its own style is represented by a -//! [`Text`]. -//! -//! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`] -//! is a [`Spans`]. -//! -//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is -//! supported for their properties. Moreover, `tui` provides convenient `From` implementations so -//! that you can start by using simple `String` or `&str` and then promote them to the previous -//! primitives when you need additional styling capabilities. -//! -//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set -//! its `title` property (which is a [`Spans`] under the hood): -//! -//! ```rust -//! # use tui::widgets::Block; -//! # use tui::text::{Span, Spans}; -//! # use tui::style::{Color, Style}; -//! // A simple string with no styling. -//! // Converted to Spans(vec![ -//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } } -//! // ]) -//! let block = Block::default().title("My title"); -//! -//! // A simple string with a unique style. -//! // Converted to Spans(vec![ -//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. } -//! // ]) -//! let block = Block::default().title( -//! Span::styled("My title", Style::default().fg(Color::Yellow)) -//! ); -//! -//! // A string with multiple styles. -//! // Converted to Spans(vec![ -//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } }, -//! // Span { content: Cow::Borrowed(" title"), .. } -//! // ]) -//! let block = Block::default().title(vec![ -//! Span::styled("My", Style::default().fg(Color::Yellow)), -//! Span::raw(" title"), -//! ]); -//! ``` -use crate::tui::style::Style; -use std::borrow::Cow; -use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; - -/// A grapheme associated to a style. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct StyledGrapheme<'a> { - pub symbol: &'a str, - pub style: Style, -} - -/// A string where all graphemes have the same style. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Span<'a> { - pub content: Cow<'a, str>, - pub style: Style, -} - -impl<'a> Span<'a> { - /// Create a span with no style. - /// - /// ## Examples - /// - /// ```rust - /// # use tui::text::Span; - /// Span::raw("My text"); - /// Span::raw(String::from("My text")); - /// ``` - pub fn raw<T>(content: T) -> Span<'a> - where - T: Into<Cow<'a, str>>, - { - Span { - content: content.into(), - style: Style::default(), - } - } - - /// Create a span with a style. - /// - /// # Examples - /// - /// ```rust - /// # use tui::text::Span; - /// # use tui::style::{Color, Modifier, Style}; - /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC); - /// Span::styled("My text", style); - /// Span::styled(String::from("My text"), style); - /// ``` - pub fn styled<T>(content: T, style: Style) -> Span<'a> - where - T: Into<Cow<'a, str>>, - { - Span { - content: content.into(), - style, - } - } - - /// Returns the width of the content held by this span. - pub fn width(&self) -> usize { - self.content.width() - } - - /// Returns an iterator over the graphemes held by this span. - /// - /// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get - /// the resulting [`Style`]. - /// - /// ## Examples - /// - /// ```rust - /// # use tui::text::{Span, StyledGrapheme}; - /// # use tui::style::{Color, Modifier, Style}; - /// # use std::iter::Iterator; - /// let style = Style::default().fg(Color::Yellow); - /// let span = Span::styled("Text", style); - /// let style = Style::default().fg(Color::Green).bg(Color::Black); - /// let styled_graphemes = span.styled_graphemes(style); - /// assert_eq!( - /// vec![ - /// StyledGrapheme { - /// symbol: "T", - /// style: Style { - /// fg: Some(Color::Yellow), - /// bg: Some(Color::Black), - /// add_modifier: Modifier::empty(), - /// sub_modifier: Modifier::empty(), - /// }, - /// }, - /// StyledGrapheme { - /// symbol: "e", - /// style: Style { - /// fg: Some(Color::Yellow), - /// bg: Some(Color::Black), - /// add_modifier: Modifier::empty(), - /// sub_modifier: Modifier::empty(), - /// }, - /// }, - /// StyledGrapheme { - /// symbol: "x", - /// style: Style { - /// fg: Some(Color::Yellow), - /// bg: Some(Color::Black), - /// add_modifier: Modifier::empty(), - /// sub_modifier: Modifier::empty(), - /// }, - /// }, - /// StyledGrapheme { - /// symbol: "t", - /// style: Style { - /// fg: Some(Color::Yellow), - /// bg: Some(Color::Black), - /// add_modifier: Modifier::empty(), - /// sub_modifier: Modifier::empty(), - /// }, - /// }, - /// ], - /// styled_graphemes.collect::<Vec<StyledGrapheme>>() - /// ); - /// ``` - pub fn styled_graphemes( - &'a self, - base_style: Style, - ) -> impl Iterator<Item = StyledGrapheme<'a>> { - UnicodeSegmentation::graphemes(self.content.as_ref(), true) - .map(move |g| StyledGrapheme { - symbol: g, - style: base_style.patch(self.style), - }) - .filter(|s| s.symbol != "\n") - } -} - -impl<'a> From<String> for Span<'a> { - fn from(s: String) -> Span<'a> { - Span::raw(s) - } -} - -impl<'a> From<&'a str> for Span<'a> { - fn from(s: &'a str) -> Span<'a> { - Span::raw(s) - } -} - -/// A string composed of clusters of graphemes, each with their own style. -#[derive(Debug, Clone, PartialEq, Default, Eq)] -pub struct Spans<'a>(pub Vec<Span<'a>>); - -impl<'a> Spans<'a> { - /// Returns the width of the underlying string. - /// - /// ## Examples - /// - /// ```rust - /// # use tui::text::{Span, Spans}; - /// # use tui::style::{Color, Style}; - /// let spans = Spans::from(vec![ - /// Span::styled("My", Style::default().fg(Color::Yellow)), - /// Span::raw(" text"), - /// ]); - /// assert_eq!(7, spans.width()); - /// ``` - pub fn width(&self) -> usize { - self.0.iter().map(Span::width).sum() - } -} - -impl<'a> From<String> for Spans<'a> { - fn from(s: String) -> Spans<'a> { - Spans(vec![Span::from(s)]) - } -} - -impl<'a> From<&'a str> for Spans<'a> { - fn from(s: &'a str) -> Spans<'a> { - Spans(vec![Span::from(s)]) - } -} - -impl<'a> From<Vec<Span<'a>>> for Spans<'a> { - fn from(spans: Vec<Span<'a>>) -> Spans<'a> { - Spans(spans) - } -} - -impl<'a> From<Span<'a>> for Spans<'a> { - fn from(span: Span<'a>) -> Spans<'a> { - Spans(vec![span]) - } -} - -impl<'a> From<Spans<'a>> for String { - fn from(line: Spans<'a>) -> String { - line.0.iter().fold(String::new(), |mut acc, s| { - acc.push_str(s.content.as_ref()); - acc - }) - } -} - -/// A string split over multiple lines where each line is composed of several clusters, each with -/// their own style. -/// -/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations -/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements -/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks. -/// -/// ```rust -/// # use tui::text::Text; -/// # use tui::style::{Color, Modifier, Style}; -/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC); -/// -/// // An initial two lines of `Text` built from a `&str` -/// let mut text = Text::from("The first line\nThe second line"); -/// assert_eq!(2, text.height()); -/// -/// // Adding two more unstyled lines -/// text.extend(Text::raw("These are two\nmore lines!")); -/// assert_eq!(4, text.height()); -/// -/// // Adding a final two styled lines -/// text.extend(Text::styled("Some more lines\nnow with more style!", style)); -/// assert_eq!(6, text.height()); -/// ``` -#[derive(Debug, Clone, PartialEq, Default, Eq)] -pub struct Text<'a> { - pub lines: Vec<Spans<'a>>, -} - -impl<'a> Text<'a> { - /// Create some text (potentially multiple lines) with no style. - /// - /// ## Examples - /// - /// ```rust - /// # use tui::text::Text; - /// Text::raw("The first line\nThe second line"); - /// Text::raw(String::from("The first line\nThe second line")); - /// ``` - pub fn raw<T>(content: T) -> Text<'a> - where - T: Into<Cow<'a, str>>, - { - Text { - lines: match content.into() { - Cow::Borrowed(s) => s.lines().map(Spans::from).collect(), - Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(), - }, - } - } - - /// Create some text (potentially multiple lines) with a style. - /// - /// # Examples - /// - /// ```rust - /// # use tui::text::Text; - /// # use tui::style::{Color, Modifier, Style}; - /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC); - /// Text::styled("The first line\nThe second line", style); - /// Text::styled(String::from("The first line\nThe second line"), style); - /// ``` - pub fn styled<T>(content: T, style: Style) -> Text<'a> - where - T: Into<Cow<'a, str>>, - { - let mut text = Text::raw(content); - text.patch_style(style); - text - } - - /// Returns the max width of all the lines. - /// - /// ## Examples - /// - /// ```rust - /// use tui::text::Text; - /// let text = Text::from("The first line\nThe second line"); - /// assert_eq!(15, text.width()); - /// ``` - pub fn width(&self) -> usize { - self.lines - .iter() - .map(Spans::width) - .max() - .unwrap_or_default() - } - - /// Returns the height. - /// - /// ## Examples - /// - /// ```rust - /// use tui::text::Text; - /// let text = Text::from("The first line\nThe second line"); - /// assert_eq!(2, text.height()); - /// ``` - pub fn height(&self) -> usize { - self.lines.len() - } - - /// Apply a new style to existing text. - /// - /// # Examples - /// - /// ```rust - /// # use tui::text::Text; - /// # use tui::style::{Color, Modifier, Style}; - /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC); - /// let mut raw_text = Text::raw("The first line\nThe second line"); - /// let styled_text = Text::styled(String::from("The first line\nThe second line"), style); - /// assert_ne!(raw_text, styled_text); - /// - /// raw_text.patch_style(style); - /// assert_eq!(raw_text, styled_text); - /// ``` - pub fn patch_style(&mut self, style: Style) { - for line in &mut self.lines { - for span in &mut line.0 { - span.style = span.style.patch(style); - } - } - } -} - -impl<'a> From<String> for Text<'a> { - fn from(s: String) -> Text<'a> { - Text::raw(s) - } -} - -impl<'a> From<&'a str> for Text<'a> { - fn from(s: &'a str) -> Text<'a> { - Text::raw(s) - } -} - -impl<'a> From<Cow<'a, str>> for Text<'a> { - fn from(s: Cow<'a, str>) -> Text<'a> { - Text::raw(s) - } -} - -impl<'a> From<Span<'a>> for Text<'a> { - fn from(span: Span<'a>) -> Text<'a> { - Text { - lines: vec![Spans::from(span)], - } - } -} - -impl<'a> From<Spans<'a>> for Text<'a> { - fn from(spans: Spans<'a>) -> Text<'a> { - Text { lines: vec![spans] } - } -} - -impl<'a> From<Vec<Spans<'a>>> for Text<'a> { - fn from(lines: Vec<Spans<'a>>) -> Text<'a> { - Text { lines } - } -} - -impl<'a> IntoIterator for Text<'a> { - type Item = Spans<'a>; - type IntoIter = std::vec::IntoIter<Self::Item>; - - fn into_iter(self) -> Self::IntoIter { - self.lines.into_iter() - } -} - -impl<'a> Extend<Spans<'a>> for Text<'a> { - fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) { - self.lines.extend(iter); - } -} diff --git a/src/tui/widgets/block.rs b/src/tui/widgets/block.rs deleted file mode 100644 index eb59f9c3..00000000 --- a/src/tui/widgets/block.rs +++ /dev/null @@ -1,562 +0,0 @@ -use crate::tui::{ - buffer::Buffer, - layout::{Alignment, Rect}, - style::Style, - symbols::line, - text::Spans, - widgets::{Borders, Widget}, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BorderType { - Plain, - Rounded, - Double, - Thick, -} - -impl BorderType { - pub fn line_symbols(border_type: BorderType) -> line::Set { - match border_type { - BorderType::Plain => line::NORMAL, - BorderType::Rounded => line::ROUNDED, - BorderType::Double => line::DOUBLE, - BorderType::Thick => line::THICK, - } - } -} - -/// Base widget to be used with all upper level ones. It may be used to display a box border around -/// the widget and/or add a title. -/// -/// # Examples -/// -/// ``` -/// # use tui::widgets::{Block, BorderType, Borders}; -/// # use tui::style::{Style, Color}; -/// Block::default() -/// .title("Block") -/// .borders(Borders::LEFT | Borders::RIGHT) -/// .border_style(Style::default().fg(Color::White)) -/// .border_type(BorderType::Rounded) -/// .style(Style::default().bg(Color::Black)); -/// ``` -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Block<'a> { - /// Optional title place on the upper left of the block - title: Option<Spans<'a>>, - /// Title alignment. The default is top left of the block, but one can choose to place - /// title in the top middle, or top right of the block - title_alignment: Alignment, - /// Visible borders - borders: Borders, - /// Border style - border_style: Style, - /// Type of the border. The default is plain lines but one can choose to have rounded corners - /// or doubled lines instead. - border_type: BorderType, - /// Widget style - style: Style, -} - -impl<'a> Default for Block<'a> { - fn default() -> Block<'a> { - Block { - title: None, - title_alignment: Alignment::Left, - borders: Borders::NONE, - border_style: Style::default(), - border_type: BorderType::Plain, - style: Style::default(), - } - } -} - -impl<'a> Block<'a> { - pub fn title<T>(mut self, title: T) -> Block<'a> - where - T: Into<Spans<'a>>, - { - self.title = Some(title.into()); - self - } - - pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> { - self.title_alignment = alignment; - self - } - - pub fn border_style(mut self, style: Style) -> Block<'a> { - self.border_style = style; - self - } - - pub fn style(mut self, style: Style) -> Block<'a> { - self.style = style; - self - } - - pub fn borders(mut self, flag: Borders) -> Block<'a> { - self.borders = flag; - self - } - - pub fn border_type(mut self, border_type: BorderType) -> Block<'a> { - self.border_type = border_type; - self - } - - /// Compute the inner area of a block based on its border visibility rules. - pub fn inner(&self, area: Rect) -> Rect { - let mut inner = area; - if self.borders.intersects(Borders::LEFT) { - inner.x = inner.x.saturating_add(1).min(inner.right()); - inner.width = inner.width.saturating_sub(1); - } - if self.borders.intersects(Borders::TOP) || self.title.is_some() { - inner.y = inner.y.saturating_add(1).min(inner.bottom()); - inner.height = inner.height.saturating_sub(1); - } - if self.borders.intersects(Borders::RIGHT) { - inner.width = inner.width.saturating_sub(1); - } - if self.borders.intersects(Borders::BOTTOM) { - inner.height = inner.height.saturating_sub(1); - } - inner - } -} - -impl<'a> Widget for Block<'a> { - fn render(self, area: Rect, buf: &mut Buffer) { - if area.area() == 0 { - return; - } - buf.set_style(area, self.style); - let symbols = BorderType::line_symbols(self.border_type); - - // Sides - if self.borders.intersects(Borders::LEFT) { - for y in area.top()..area.bottom() { - buf.get_mut(area.left(), y) - .set_symbol(symbols.vertical) - .set_style(self.border_style); - } - } - if self.borders.intersects(Borders::TOP) { - for x in area.left()..area.right() { - buf.get_mut(x, area.top()) - .set_symbol(symbols.horizontal) - .set_style(self.border_style); - } - } - if self.borders.intersects(Borders::RIGHT) { - let x = area.right() - 1; - for y in area.top()..area.bottom() { - buf.get_mut(x, y) - .set_symbol(symbols.vertical) - .set_style(self.border_style); - } - } - if self.borders.intersects(Borders::BOTTOM) { - let y = area.bottom() - 1; - for x in area.left()..area.right() { - buf.get_mut(x, y) - .set_symbol(symbols.horizontal) - .set_style(self.border_style); - } - } - - // Corners - if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) { - buf.get_mut(area.right() - 1, area.bottom() - 1) - .set_symbol(symbols.bottom_right) - .set_style(self.border_style); - } - if self.borders.contains(Borders::RIGHT | Borders::TOP) { - buf.get_mut(area.right() - 1, area.top()) - .set_symbol(symbols.top_right) - .set_style(self.border_style); - } - if self.borders.contains(Borders::LEFT | Borders::BOTTOM) { - buf.get_mut(area.left(), area.bottom() - 1) - .set_symbol(symbols.bottom_left) - .set_style(self.border_style); - } - if self.borders.contains(Borders::LEFT | Borders::TOP) { - buf.get_mut(area.left(), area.top()) - .set_symbol(symbols.top_left) - .set_style(self.border_style); - } - - // Title - if let Some(title) = self.title { - let left_border_dx = if self.borders.intersects(Borders::LEFT) { - 1 - } else { - 0 - }; - - let right_border_dx = if self.borders.intersects(Borders::RIGHT) { - 1 - } else { - 0 - }; - - let title_area_width = area - .width - .saturating_sub(left_border_dx) - .saturating_sub(right_border_dx); - - let title_dx = match self.title_alignment { - Alignment::Left => left_border_dx, - Alignment::Center => area.width.saturating_sub(title.width() as u16) / 2, - Alignment::Right => area - .width - .saturating_sub(title.width() as u16) - .saturating_sub(right_border_dx), - }; - - let title_x = area.left() + title_dx; - let title_y = area.top(); - - buf.set_spans(title_x, title_y, &title, title_area_width); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tui::layout::Rect; - - #[test] - #[allow(clippy::too_many_lines)] - fn inner_takes_into_account_the_borders() { - // No borders - assert_eq!( - Block::default().inner(Rect::default()), - Rect { - x: 0, - y: 0, - width: 0, - height: 0 - }, - "no borders, width=0, height=0" - ); - assert_eq!( - Block::default().inner(Rect { - x: 0, - y: 0, - width: 1, - height: 1 - }), - Rect { - x: 0, - y: 0, - width: 1, - height: 1 - }, - "no borders, width=1, height=1" - ); - - // Left border - assert_eq!( - Block::default().borders(Borders::LEFT).inner(Rect { - x: 0, - y: 0, - width: 0, - height: 1 - }), - Rect { - x: 0, - y: 0, - width: 0, - height: 1 - }, - "left, width=0" - ); - assert_eq!( - Block::default().borders(Borders::LEFT).inner(Rect { - x: 0, - y: 0, - width: 1, - height: 1 - }), - Rect { - x: 1, - y: 0, - width: 0, - height: 1 - }, - "left, width=1" - ); - assert_eq!( - Block::default().borders(Borders::LEFT).inner(Rect { - x: 0, - y: 0, - width: 2, - height: 1 - }), - Rect { - x: 1, - y: 0, - width: 1, - height: 1 - }, - "left, width=2" - ); - - // Top border - assert_eq!( - Block::default().borders(Borders::TOP).inner(Rect { - x: 0, - y: 0, - width: 1, - height: 0 - }), - Rect { - x: 0, - y: 0, - width: 1, - height: 0 - }, - "top, height=0" - ); - assert_eq!( - Block::default().borders(Borders::TOP).inner(Rect { - x: 0, - y: 0, - width: 1, - height: 1 - }), - Rect { - x: 0, - y: 1, - width: 1, - height: 0 - }, - "top, height=1" - ); - assert_eq!( - Block::default().borders(Borders::TOP).inner(Rect { - x: 0, - y: 0, - width: 1, - height: 2 - }), - Rect { - x: 0, - y: 1, - width: 1, - height: 1 - }, - "top, height=2" - ); - - // Right border - assert_eq!( - Block::default().borders(Borders::RIGHT).inner(Rect { - x: 0, - y: 0, - width: 0, - height: 1 - }), - Rect { - x: 0, - y: 0, - width: 0, - height: 1 - }, - "right, width=0" - ); - assert_eq!( - Block::default().borders(Borders::RIGHT).inner(Rect { - x: 0, - y: 0, - width: 1, - height: 1 - }), - Rect { - x: 0, - y: 0, - width: 0, - height: 1 - }, - "right, width=1" - ); - assert_eq!( - Block::default().borders(Borders::RIGHT).inner(Rect { - x: 0, - y: 0, - width: 2, - height: 1 - }), - Rect { - x: 0, - y: 0, - width: 1, - height: 1 - }, - "right, width=2" - ); - - // Bottom border - assert_eq!( - Block::default().borders(Borders::BOTTOM).inner(Rect { - x: 0, - y: 0, - width: 1, - height: 0 - }), - Rect { - x: 0, - y: 0, - width: 1, - height: 0 - }, - "bottom, height=0" - ); - assert_eq!( - Block::default().borders(Borders::BOTTOM).inner(Rect { - x: 0, - y: 0, - width: 1, - height: 1 - }), - Rect { - x: 0, - y: 0, - width: 1, - height: 0 - }, - "bottom, height=1" - ); - assert_eq!( - Block::default().borders(Borders::BOTTOM).inner(Rect { - x: 0, - y: 0, - width: 1, - height: 2 - }), - Rect { - x: 0, - y: 0, - width: 1, - height: 1 - }, - "bottom, height=2" - ); - - // All borders - assert_eq!( - Block::default() - .borders(Borders::ALL) - .inner(Rect::default()), - Rect { - x: 0, - y: 0, - width: 0, - height: 0 - }, - "all borders, width=0, height=0" - ); - assert_eq!( - Block::default().borders(Borders::ALL).inner(Rect { - x: 0, - y: 0, - width: 1, - height: 1 - }), - Rect { - x: 1, - y: 1, - width: 0, - height: 0, - }, - "all borders, width=1, height=1" - ); - assert_eq!( - Block::default().borders(Borders::ALL).inner(Rect { - x: 0, - y: 0, - width: 2, - height: 2, - }), - Rect { - x: 1, - y: 1, - width: 0, - height: 0, - }, - "all borders, width=2, height=2" - ); - assert_eq!( - Block::default().borders(Borders::ALL).inner(Rect { - x: 0, - y: 0, - width: 3, - height: 3, - }), - Rect { - x: 1, - y: 1, - width: 1, - height: 1, - }, - "all borders, width=3, height=3" - ); - } - - #[test] - fn inner_takes_into_account_the_title() { - assert_eq!( - Block::default().title("Test").inner(Rect { - x: 0, - y: 0, - width: 0, - height: 1, - }), - Rect { - x: 0, - y: 1, - width: 0, - height: 0, - }, - ); - assert_eq!( - Block::default() - .title("Test") - .title_alignment(Alignment::Center) - .inner(Rect { - x: 0, - y: 0, - width: 0, - height: 1, - }), - Rect { - x: 0, - y: 1, - width: 0, - height: 0, - }, - ); - assert_eq!( - Block::default() - .title("Test") - .title_alignment(Alignment::Right) - .inner(Rect { - x: 0, - y: 0, - width: 0, - height: 1, - }), - Rect { - x: 0, - y: 1, - width: 0, - height: 0, - }, - ); - } -} diff --git a/src/tui/widgets/mod.rs b/src/tui/widgets/mod.rs deleted file mode 100644 index 635185e5..00000000 --- a/src/tui/widgets/mod.rs +++ /dev/null @@ -1,159 +0,0 @@ -//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both. -//! -//! All widgets are implemented using the builder pattern and are consumable objects. They are not -//! meant to be stored but used as *commands* to draw common figures in the UI. -//! -//! The available widgets are: -//! - [`Block`] -//! - [`Paragraph`] - -mod block; -mod paragraph; -mod reflow; - -pub use self::block::{Block, BorderType}; -pub use self::paragraph::{Paragraph, Wrap}; - -use crate::tui::{buffer::Buffer, layout::Rect}; -use bitflags::bitflags; - -bitflags! { - /// Bitflags that can be composed to set the visible borders essentially on the block widget. - pub struct Borders: u32 { - /// Show no border (default) - const NONE = 0b0000_0001; - /// Show the top border - const TOP = 0b0000_0010; - /// Show the right border - const RIGHT = 0b0000_0100; - /// Show the bottom border - const BOTTOM = 0b000_1000; - /// Show the left border - const LEFT = 0b0001_0000; - /// Show all borders - const ALL = Self::TOP.bits | Self::RIGHT.bits | Self::BOTTOM.bits | Self::LEFT.bits; - } -} - -/// Base requirements for a Widget -pub trait Widget { - /// Draws the current state of the widget in the given buffer. That is the only method required - /// to implement a custom widget. - fn render(self, area: Rect, buf: &mut Buffer); -} - -/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things -/// between two draw calls. -/// -/// Most widgets can be drawn directly based on the input parameters. However, some features may -/// require some kind of associated state to be implemented. -/// -/// For example, the [`List`] widget can highlight the item currently selected. This can be -/// translated in an offset, which is the number of elements to skip in order to have the selected -/// item within the viewport currently allocated to this widget. The widget can therefore only -/// provide the following behavior: whenever the selected item is out of the viewport scroll to a -/// predefined position (making the selected item the last viewable item or the one in the middle -/// for example). Nonetheless, if the widget has access to the last computed offset then it can -/// implement a natural scrolling experience where the last offset is reused until the selected -/// item is out of the viewport. -/// -/// ## Examples -/// -/// ```rust,no_run -/// # use std::io; -/// # use tui::Terminal; -/// # use tui::backend::{Backend, TestBackend}; -/// # use tui::widgets::{Widget, List, ListItem, ListState}; -/// -/// // Let's say we have some events to display. -/// struct Events { -/// // `items` is the state managed by your application. -/// items: Vec<String>, -/// // `state` is the state that can be modified by the UI. It stores the index of the selected -/// // item as well as the offset computed during the previous draw call (used to implement -/// // natural scrolling). -/// state: ListState -/// } -/// -/// impl Events { -/// fn new(items: Vec<String>) -> Events { -/// Events { -/// items, -/// state: ListState::default(), -/// } -/// } -/// -/// pub fn set_items(&mut self, items: Vec<String>) { -/// self.items = items; -/// // We reset the state as the associated items have changed. This effectively reset -/// // the selection as well as the stored offset. -/// self.state = ListState::default(); -/// } -/// -/// // Select the next item. This will not be reflected until the widget is drawn in the -/// // `Terminal::draw` callback using `Frame::render_stateful_widget`. -/// pub fn next(&mut self) { -/// let i = match self.state.selected() { -/// Some(i) => { -/// if i >= self.items.len() - 1 { -/// 0 -/// } else { -/// i + 1 -/// } -/// } -/// None => 0, -/// }; -/// self.state.select(Some(i)); -/// } -/// -/// // Select the previous item. This will not be reflected until the widget is drawn in the -/// // `Terminal::draw` callback using `Frame::render_stateful_widget`. -/// pub fn previous(&mut self) { -/// let i = match self.state.selected() { -/// Some(i) => { -/// if i == 0 { -/// self.items.len() - 1 -/// } else { -/// i - 1 -/// } -/// } -/// None => 0, -/// }; -/// self.state.select(Some(i)); -/// } -/// -/// // Unselect the currently selected item if any. The implementation of `ListState` makes -/// // sure that the stored offset is also reset. -/// pub fn unselect(&mut self) { -/// self.state.select(None); -/// } -/// } -/// -/// # let backend = TestBackend::new(5, 5); -/// # let mut terminal = Terminal::new(backend).unwrap(); -/// -/// let mut events = Events::new(vec![ -/// String::from("Item 1"), -/// String::from("Item 2") -/// ]); -/// -/// loop { -/// terminal.draw(|f| { -/// // The items managed by the application are transformed to something -/// // that is understood by tui. -/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_ref())).collect(); -/// // The `List` widget is then built with those items. -/// let list = List::new(items); -/// // Finally the widget is rendered using the associated state. `events.state` is -/// // effectively the only thing that we will "remember" from this draw call. -/// f.render_stateful_widget(list, f.size(), &mut events.state); -/// }); -/// -/// // In response to some input events or an external http request or whatever: -/// events.next(); -/// } -/// ``` -pub trait StatefulWidget { - type State; - fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State); -} diff --git a/src/tui/widgets/paragraph.rs b/src/tui/widgets/paragraph.rs deleted file mode 100644 index a5036148..00000000 --- a/src/tui/widgets/paragraph.rs +++ /dev/null @@ -1,194 +0,0 @@ -use crate::tui::{ - buffer::Buffer, - layout::{Alignment, Rect}, - style::Style, - text::{StyledGrapheme, Text}, - widgets::{ - reflow::{LineComposer, LineTruncator, WordWrapper}, - Block, Widget, - }, -}; -use std::iter; -use unicode_width::UnicodeWidthStr; - -fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 { - match alignment { - Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2), - Alignment::Right => text_area_width.saturating_sub(line_width), - Alignment::Left => 0, - } -} - -/// A widget to display some text. -/// -/// # Examples -/// -/// ``` -/// # use tui::text::{Text, Spans, Span}; -/// # use tui::widgets::{Block, Borders, Paragraph, Wrap}; -/// # use tui::style::{Style, Color, Modifier}; -/// # use tui::layout::{Alignment}; -/// let text = vec![ -/// Spans::from(vec![ -/// Span::raw("First"), -/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)), -/// Span::raw("."), -/// ]), -/// Spans::from(Span::styled("Second line", Style::default().fg(Color::Red))), -/// ]; -/// Paragraph::new(text) -/// .block(Block::default().title("Paragraph").borders(Borders::ALL)) -/// .style(Style::default().fg(Color::White).bg(Color::Black)) -/// .alignment(Alignment::Center) -/// .wrap(Wrap { trim: true }); -/// ``` -#[derive(Debug, Clone)] -pub struct Paragraph<'a> { - /// A block to wrap the widget in - block: Option<Block<'a>>, - /// Widget style - style: Style, - /// How to wrap the text - wrap: Option<Wrap>, - /// The text to display - text: Text<'a>, - /// Scroll - scroll: (u16, u16), - /// Alignment of the text - alignment: Alignment, -} - -/// Describes how to wrap text across lines. -/// -/// ## Examples -/// -/// ``` -/// # use tui::widgets::{Paragraph, Wrap}; -/// # use tui::text::Text; -/// let bullet_points = Text::from(r#"Some indented points: -/// - First thing goes here and is long so that it wraps -/// - Here is another point that is long enough to wrap"#); -/// -/// // With leading spaces trimmed (window width of 30 chars): -/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true }); -/// // Some indented points: -/// // - First thing goes here and is -/// // long so that it wraps -/// // - Here is another point that -/// // is long enough to wrap -/// -/// // But without trimming, indentation is preserved: -/// Paragraph::new(bullet_points).wrap(Wrap { trim: false }); -/// // Some indented points: -/// // - First thing goes here -/// // and is long so that it wraps -/// // - Here is another point -/// // that is long enough to wrap -/// ``` -#[derive(Debug, Clone, Copy)] -pub struct Wrap { - /// Should leading whitespace be trimmed - pub trim: bool, -} - -impl<'a> Paragraph<'a> { - pub fn new<T>(text: T) -> Paragraph<'a> - where - T: Into<Text<'a>>, - { - Paragraph { - block: None, - style: Style::default(), - wrap: None, - text: text.into(), - scroll: (0, 0), - alignment: Alignment::Left, - } - } - - pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> { - self.block = Some(block); - self - } - - pub fn style(mut self, style: Style) -> Paragraph<'a> { - self.style = style; - self - } - - pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> { - self.wrap = Some(wrap); - self - } - - pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> { - self.scroll = offset; - self - } - - pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> { - self.alignment = alignment; - self - } -} - -impl<'a> Widget for Paragraph<'a> { - fn render(mut self, area: Rect, buf: &mut Buffer) { - buf.set_style(area, self.style); - let text_area = self.block.take().map_or(area, |b| { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - }); - - if text_area.height < 1 { - return; - } - - let style = self.style; - let mut styled = self.text.lines.iter().flat_map(|spans| { - spans - .0 - .iter() - .flat_map(|span| span.styled_graphemes(style)) - // Required given the way composers work but might be refactored out if we change - // composers to operate on lines instead of a stream of graphemes. - .chain(iter::once(StyledGrapheme { - symbol: "\n", - style: self.style, - })) - }); - - let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap { - Box::new(WordWrapper::new(&mut styled, text_area.width, trim)) - } else { - let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width)); - if self.alignment == Alignment::Left { - line_composer.set_horizontal_offset(self.scroll.1); - } - line_composer - }; - let mut y = 0; - while let Some((current_line, current_line_width)) = line_composer.next_line() { - if y >= self.scroll.0 { - let mut x = get_line_offset(current_line_width, text_area.width, self.alignment); - for StyledGrapheme { symbol, style } in current_line { - buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0) - .set_symbol(if symbol.is_empty() { - // If the symbol is empty, the last char which rendered last time will - // leave on the line. It's a quick fix. - " " - } else { - symbol - }) - .set_style(*style); - x += symbol.width() as u16; - } - } - y += 1; - if y >= text_area.height + self.scroll.0 { - break; - } - } - } -} diff --git a/src/tui/widgets/reflow.rs b/src/tui/widgets/reflow.rs deleted file mode 100644 index d174932b..00000000 --- a/src/tui/widgets/reflow.rs +++ /dev/null @@ -1,537 +0,0 @@ -use crate::tui::text::StyledGrapheme; -use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; - -const NBSP: &str = "\u{00a0}"; - -/// A state machine to pack styled symbols into lines. -/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming -/// iterators for that). -pub trait LineComposer<'a> { - fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>; -} - -/// A state machine that wraps lines on word boundaries. -pub struct WordWrapper<'a, 'b> { - symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>, - max_line_width: u16, - current_line: Vec<StyledGrapheme<'a>>, - next_line: Vec<StyledGrapheme<'a>>, - /// Removes the leading whitespace from lines - trim: bool, -} - -impl<'a, 'b> WordWrapper<'a, 'b> { - pub fn new( - symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>, - max_line_width: u16, - trim: bool, - ) -> WordWrapper<'a, 'b> { - WordWrapper { - symbols, - max_line_width, - current_line: vec![], - next_line: vec![], - trim, - } - } -} - -impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> { - fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> { - if self.max_line_width == 0 { - return None; - } - std::mem::swap(&mut self.current_line, &mut self.next_line); - self.next_line.truncate(0); - - let mut current_line_width = self - .current_line - .iter() - .map(|StyledGrapheme { symbol, .. }| symbol.width() as u16) - .sum(); - - let mut symbols_to_last_word_end: usize = 0; - let mut width_to_last_word_end: u16 = 0; - let mut prev_whitespace = false; - let mut symbols_exhausted = true; - for StyledGrapheme { symbol, style } in &mut self.symbols { - symbols_exhausted = false; - let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP; - - // Ignore characters wider that the total max width. - if symbol.width() as u16 > self.max_line_width - // Skip leading whitespace when trim is enabled. - || self.trim && symbol_whitespace && symbol != "\n" && current_line_width == 0 - { - continue; - } - - // Break on newline and discard it. - if symbol == "\n" { - if prev_whitespace { - current_line_width = width_to_last_word_end; - self.current_line.truncate(symbols_to_last_word_end); - } - break; - } - - // Mark the previous symbol as word end. - if symbol_whitespace && !prev_whitespace { - symbols_to_last_word_end = self.current_line.len(); - width_to_last_word_end = current_line_width; - } - - self.current_line.push(StyledGrapheme { symbol, style }); - current_line_width += symbol.width() as u16; - - if current_line_width > self.max_line_width { - // If there was no word break in the text, wrap at the end of the line. - let (truncate_at, truncated_width) = if symbols_to_last_word_end == 0 { - (self.current_line.len() - 1, self.max_line_width) - } else { - (symbols_to_last_word_end, width_to_last_word_end) - }; - - // Push the remainder to the next line but strip leading whitespace: - { - let remainder = &self.current_line[truncate_at..]; - if let Some(remainder_nonwhite) = - remainder.iter().position(|StyledGrapheme { symbol, .. }| { - !symbol.chars().all(&char::is_whitespace) - }) - { - self.next_line - .extend_from_slice(&remainder[remainder_nonwhite..]); - } - } - self.current_line.truncate(truncate_at); - current_line_width = truncated_width; - break; - } - - prev_whitespace = symbol_whitespace; - } - - // Even if the iterator is exhausted, pass the previous remainder. - if symbols_exhausted && self.current_line.is_empty() { - None - } else { - Some((&self.current_line[..], current_line_width)) - } - } -} - -/// A state machine that truncates overhanging lines. -pub struct LineTruncator<'a, 'b> { - symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>, - max_line_width: u16, - current_line: Vec<StyledGrapheme<'a>>, - /// Record the offet to skip render - horizontal_offset: u16, -} - -impl<'a, 'b> LineTruncator<'a, 'b> { - pub fn new( - symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>, - max_line_width: u16, - ) -> LineTruncator<'a, 'b> { - LineTruncator { - symbols, - max_line_width, - horizontal_offset: 0, - current_line: vec![], - } - } - - pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) { - self.horizontal_offset = horizontal_offset; - } -} - -impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> { - fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> { - if self.max_line_width == 0 { - return None; - } - - self.current_line.truncate(0); - let mut current_line_width = 0; - - let mut skip_rest = false; - let mut symbols_exhausted = true; - let mut horizontal_offset = self.horizontal_offset as usize; - for StyledGrapheme { symbol, style } in &mut self.symbols { - symbols_exhausted = false; - - // Ignore characters wider that the total max width. - if symbol.width() as u16 > self.max_line_width { - continue; - } - - // Break on newline and discard it. - if symbol == "\n" { - break; - } - - if current_line_width + symbol.width() as u16 > self.max_line_width { - // Exhaust the remainder of the line. - skip_rest = true; - break; - } - - let symbol = if horizontal_offset == 0 { - symbol - } else { - let w = symbol.width(); - if w > horizontal_offset { - let t = trim_offset(symbol, horizontal_offset); - horizontal_offset = 0; - t - } else { - horizontal_offset -= w; - "" - } - }; - current_line_width += symbol.width() as u16; - self.current_line.push(StyledGrapheme { symbol, style }); - } - - if skip_rest { - for StyledGrapheme { symbol, .. } in &mut self.symbols { - if symbol == "\n" { - break; - } - } - } - - if symbols_exhausted && self.current_line.is_empty() { - None - } else { - Some((&self.current_line[..], current_line_width)) - } - } -} - -/// This function will return a str slice which start at specified offset. -/// As src is a unicode str, start offset has to be calculated with each character. -fn trim_offset(src: &str, mut offset: usize) -> &str { - let mut start = 0; - for c in UnicodeSegmentation::graphemes(src, true) { - let w = c.width(); - if w <= offset { - offset -= w; - start += c.len(); - } else { - break; - } - } - &src[start..] -} - -#[cfg(test)] -mod test { - use crate::tui::style::Style; - - use super::*; - use unicode_segmentation::UnicodeSegmentation; - - #[derive(Clone, Copy)] - enum Composer { - WordWrapper { trim: bool }, - LineTruncator, - } - - fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) { - let style = Style::default(); - let mut styled = - UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style }); - let mut composer: Box<dyn LineComposer> = match which { - Composer::WordWrapper { trim } => { - Box::new(WordWrapper::new(&mut styled, text_area_width, trim)) - } - Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)), - }; - let mut lines = vec![]; - let mut widths = vec![]; - while let Some((styled, width)) = composer.next_line() { - let line = styled - .iter() - .map(|StyledGrapheme { symbol, .. }| *symbol) - .collect::<String>(); - assert!(width <= text_area_width); - lines.push(line); - widths.push(width); - } - (lines, widths) - } - - #[test] - fn line_composer_one_line() { - let width = 40; - for i in 1..width { - let text = "a".repeat(i); - let (word_wrapper, _) = - run_composer(Composer::WordWrapper { trim: true }, &text, width as u16); - let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16); - let expected = vec![text]; - assert_eq!(word_wrapper, expected); - assert_eq!(line_truncator, expected); - } - } - - #[test] - fn line_composer_short_lines() { - let width = 20; - let text = - "abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno"; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width); - let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); - - let wrapped: Vec<&str> = text.split('\n').collect(); - assert_eq!(word_wrapper, wrapped); - assert_eq!(line_truncator, wrapped); - } - - #[test] - fn line_composer_long_word() { - let width = 20; - let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno"; - let (word_wrapper, _) = - run_composer(Composer::WordWrapper { trim: true }, text, width as u16); - let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16); - - let wrapped = vec![ - &text[..width], - &text[width..width * 2], - &text[width * 2..width * 3], - &text[width * 3..], - ]; - assert_eq!( - word_wrapper, wrapped, - "WordWrapper should detect the line cannot be broken on word boundary and \ - break it at line width limit." - ); - assert_eq!(line_truncator, vec![&text[..width]]); - } - - #[test] - fn line_composer_long_sentence() { - let width = 20; - let text = - "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o"; - let text_multi_space = - "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \ - m n o"; - let (word_wrapper_single_space, _) = - run_composer(Composer::WordWrapper { trim: true }, text, width as u16); - let (word_wrapper_multi_space, _) = run_composer( - Composer::WordWrapper { trim: true }, - text_multi_space, - width as u16, - ); - let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16); - - let word_wrapped = vec![ - "abcd efghij", - "klmnopabcd efgh", - "ijklmnopabcdefg", - "hijkl mnopab c d e f", - "g h i j k l m n o", - ]; - assert_eq!(word_wrapper_single_space, word_wrapped); - assert_eq!(word_wrapper_multi_space, word_wrapped); - - assert_eq!(line_truncator, vec![&text[..width]]); - } - - #[test] - fn line_composer_zero_width() { - let width = 0; - let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab "; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width); - let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); - - let expected: Vec<&str> = Vec::new(); - assert_eq!(word_wrapper, expected); - assert_eq!(line_truncator, expected); - } - - #[test] - fn line_composer_max_line_width_of_1() { - let width = 1; - let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab "; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width); - let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); - - let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true) - .filter(|g| g.chars().any(|c| !c.is_whitespace())) - .collect(); - assert_eq!(word_wrapper, expected); - assert_eq!(line_truncator, vec!["a"]); - } - - #[test] - fn line_composer_max_line_width_of_1_double_width_characters() { - let width = 1; - let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\ - 両端点では、"; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width); - let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); - assert_eq!(word_wrapper, vec!["", "a", "a", "a"]); - assert_eq!(line_truncator, vec!["", "a"]); - } - - /// Tests `WordWrapper` with words some of which exceed line length and some not. - #[test] - fn line_composer_word_wrapper_mixed_length() { - let width = 20; - let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno"; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width); - assert_eq!( - word_wrapper, - vec![ - "abcd efghij", - "klmnopabcdefghijklmn", - "opabcdefghijkl", - "mnopab cdefghi j", - "klmno", - ] - ); - } - - #[test] - fn line_composer_double_width_chars() { - let width = 20; - let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\ - では、"; - let (word_wrapper, word_wrapper_width) = - run_composer(Composer::WordWrapper { trim: true }, text, width); - let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); - assert_eq!(line_truncator, vec!["コンピュータ上で文字"]); - let wrapped = vec![ - "コンピュータ上で文字", - "を扱う場合、典型的に", - "は文字による通信を行", - "う場合にその両端点で", - "は、", - ]; - assert_eq!(word_wrapper, wrapped); - assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]); - } - - #[test] - fn line_composer_leading_whitespace_removal() { - let width = 20; - let text = "AAAAAAAAAAAAAAAAAAAA AAA"; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width); - let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); - assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]); - assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]); - } - - /// Tests truncation of leading whitespace. - #[test] - fn line_composer_lots_of_spaces() { - let width = 20; - let text = " "; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width); - let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); - assert_eq!(word_wrapper, vec![""]); - assert_eq!(line_truncator, vec![" "]); - } - - /// Tests an input starting with a letter, folowed by spaces - some of the behaviour is - /// incidental. - #[test] - fn line_composer_char_plus_lots_of_spaces() { - let width = 20; - let text = "a "; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width); - let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); - // What's happening below is: the first line gets consumed, trailing spaces discarded, - // after 20 of which a word break occurs (probably shouldn't). The second line break - // discards all whitespace. The result should probably be vec!["a"] but it doesn't matter - // that much. - assert_eq!(word_wrapper, vec!["a", ""]); - assert_eq!(line_truncator, vec!["a "]); - } - - #[test] - fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() { - let width = 20; - // Japanese seems not to use spaces but we should break on spaces anyway... We're using it - // to test double-width chars. - // You are more than welcome to add word boundary detection based of alterations of - // hiragana and katakana... - // This happens to also be a test case for mixed width because regular spaces are single width. - let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、"; - let (word_wrapper, word_wrapper_width) = - run_composer(Composer::WordWrapper { trim: true }, text, width); - assert_eq!( - word_wrapper, - vec![ - "コンピュ", - "ータ上で文字を扱う場", - "合、 典型的には文", - "字による 通信を行", - "う場合にその両端点で", - "は、", - ] - ); - // Odd-sized lines have a space in them. - assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]); - } - - /// Ensure words separated by nbsp are wrapped as if they were a single one. - #[test] - fn line_composer_word_wrapper_nbsp() { - let width = 20; - let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA"; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width); - assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]); - - // Ensure that if the character was a regular space, it would be wrapped differently. - let text_space = text.replace('\u{00a0}', " "); - let (word_wrapper_space, _) = - run_composer(Composer::WordWrapper { trim: true }, &text_space, width); - assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]); - } - - #[test] - fn line_composer_word_wrapper_preserve_indentation() { - let width = 20; - let text = "AAAAAAAAAAAAAAAAAAAA AAA"; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width); - assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]); - } - - #[test] - fn line_composer_word_wrapper_preserve_indentation_with_wrap() { - let width = 10; - let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D"; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width); - assert_eq!( - word_wrapper, - vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"] - ); - } - - #[test] - fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() { - let width = 10; - let text = " 4 Indent\n must wrap!"; - let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width); - assert_eq!( - word_wrapper, - vec![ - " ", - " 4", - "Indent", - " ", - " must", - "wrap!" - ] - ); - } -} |
