Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-tui/src/buffer.rs')
| -rw-r--r-- | helix-tui/src/buffer.rs | 410 |
1 files changed, 77 insertions, 333 deletions
diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 4f57e8e5..377e3e39 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -1,50 +1,42 @@ -//! Contents of a terminal screen. A [Buffer] is made up of [Cell]s. use crate::text::{Span, Spans}; use helix_core::unicode::width::UnicodeWidthStr; use std::cmp::min; use unicode_segmentation::UnicodeSegmentation; -use helix_view::graphics::{Color, Modifier, Rect, Style, UnderlineStyle}; +use helix_view::graphics::{Color, Modifier, Rect, Style}; -/// One cell of the terminal. Contains one stylized grapheme. -#[derive(Debug, Clone, PartialEq, Eq)] +/// A buffer cell +#[derive(Debug, Clone, PartialEq)] pub struct Cell { pub symbol: String, pub fg: Color, pub bg: Color, - pub underline_color: Color, - pub underline_style: UnderlineStyle, pub modifier: Modifier, } impl Cell { - /// Set the cell's grapheme pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell { self.symbol.clear(); self.symbol.push_str(symbol); self } - /// Set the cell's grapheme to a [char] pub fn set_char(&mut self, ch: char) -> &mut Cell { self.symbol.clear(); self.symbol.push(ch); self } - /// Set the foreground [Color] pub fn set_fg(&mut self, color: Color) -> &mut Cell { self.fg = color; self } - /// Set the background [Color] pub fn set_bg(&mut self, color: Color) -> &mut Cell { self.bg = color; self } - /// Set the [Style] of the cell pub fn set_style(&mut self, style: Style) -> &mut Cell { if let Some(c) = style.fg { self.fg = c; @@ -52,36 +44,23 @@ impl Cell { if let Some(c) = style.bg { self.bg = c; } - if let Some(c) = style.underline_color { - self.underline_color = c; - } - if let Some(style) = style.underline_style { - self.underline_style = style; - } - self.modifier.insert(style.add_modifier); self.modifier.remove(style.sub_modifier); self } - /// Returns the current style of the cell pub fn style(&self) -> Style { Style::default() .fg(self.fg) .bg(self.bg) - .underline_color(self.underline_color) - .underline_style(self.underline_style) .add_modifier(self.modifier) } - /// Resets the cell to a default blank state pub fn reset(&mut self) { self.symbol.clear(); self.symbol.push(' '); self.fg = Color::Reset; self.bg = Color::Reset; - self.underline_color = Color::Reset; - self.underline_style = UnderlineStyle::Reset; self.modifier = Modifier::empty(); } } @@ -92,8 +71,6 @@ impl Default for Cell { symbol: " ".into(), fg: Color::Reset, bg: Color::Reset, - underline_color: Color::Reset, - underline_style: UnderlineStyle::Reset, modifier: Modifier::empty(), } } @@ -110,24 +87,22 @@ impl Default for Cell { /// /// ``` /// use helix_tui::buffer::{Buffer, Cell}; -/// use helix_view::graphics::{Rect, Color, UnderlineStyle, Style, Modifier}; +/// use helix_view::graphics::{Rect, Color, Style, Modifier}; /// /// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5}); -/// buf[(0, 2)].set_symbol("x"); -/// assert_eq!(buf[(0, 2)].symbol, "x"); +/// 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[(5, 0)], Cell{ +/// assert_eq!(buf.get(5, 0), &Cell{ /// symbol: String::from("r"), /// fg: Color::Red, /// bg: Color::White, -/// underline_color: Color::Reset, -/// underline_style: UnderlineStyle::Reset, -/// modifier: Modifier::empty(), +/// modifier: Modifier::empty() /// }); -/// buf[(5, 0)].set_char('x'); -/// assert_eq!(buf[(5, 0)].symbol, "x"); +/// buf.get_mut(5, 0).set_char('x'); +/// assert_eq!(buf.get(5, 0).symbol, "x"); /// ``` -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct Buffer { /// The area represented by this buffer pub area: Rect, @@ -136,23 +111,33 @@ pub struct Buffer { pub content: Vec<Cell>, } +impl Default for Buffer { + fn default() -> Buffer { + Buffer { + area: Default::default(), + content: Vec::new(), + } + } +} + impl Buffer { /// Returns a Buffer with all cells set to the default one - #[must_use] pub fn empty(area: Rect) -> Buffer { - Buffer::filled(area, &Cell::default()) + let cell: Cell = Default::default(); + Buffer::filled(area, &cell) } /// Returns a Buffer with all cells initialized with the attributes of the given Cell - #[must_use] pub fn filled(area: Rect, cell: &Cell) -> Buffer { - let size = area.area(); - let content = vec![cell.clone(); size]; + 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 - #[must_use] pub fn with_lines<S>(lines: Vec<S>) -> Buffer where S: AsRef<str>, @@ -186,41 +171,18 @@ impl Buffer { } /// Returns a reference to Cell at the given coordinates - pub fn get(&self, x: u16, y: u16) -> Option<&Cell> { - self.index_of_opt(x, y).map(|i| &self.content[i]) + 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) -> Option<&mut Cell> { - self.index_of_opt(x, y).map(|i| &mut self.content[i]) - } - - /// Tells whether the global (x, y) coordinates are inside the Buffer's area. - /// - /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). - /// - /// # Examples - /// - /// ``` - /// # use helix_tui::buffer::Buffer; - /// # use helix_view::graphics::Rect; - /// let rect = Rect::new(200, 100, 10, 10); - /// let buffer = Buffer::empty(rect); - /// // Global coordinates inside the Buffer's area - /// assert!(buffer.in_bounds(209, 100)); - /// // Global coordinates outside the Buffer's area - /// assert!(!buffer.in_bounds(210, 100)); - /// ``` - /// - /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). - pub fn in_bounds(&self, x: u16, y: u16) -> bool { - x >= self.area.left() - && x < self.area.right() - && y >= self.area.top() - && y < self.area.bottom() + 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. + /// 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`). /// @@ -231,7 +193,7 @@ impl Buffer { /// # use helix_view::graphics::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 + /// // Global coordinates to the top corner of this buffer's area /// assert_eq!(buffer.index_of(200, 100), 0); /// ``` /// @@ -240,23 +202,16 @@ impl Buffer { /// Panics when given an coordinate that is outside of this Buffer's area. pub fn index_of(&self, x: u16, y: u16) -> usize { debug_assert!( - self.in_bounds(x, y), + 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) as usize) * (self.area.width as usize) + ((x - self.area.x) as usize) - } - - /// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates, - /// or `None` if the coordinates are outside the buffer's area. - fn index_of_opt(&self, x: u16, y: u16) -> Option<usize> { - if self.in_bounds(x, y) { - Some(self.index_of(x, y)) - } else { - None - } + ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize } /// Returns the (global) coordinates of a cell given its index @@ -285,8 +240,8 @@ impl Buffer { self.content.len() ); ( - (self.area.x as usize + (i % self.area.width as usize)) as u16, - (self.area.y as usize + (i / self.area.width as usize)) as u16, + self.area.x + i as u16 % self.area.width, + self.area.y + i as u16 / self.area.width, ) } @@ -311,182 +266,37 @@ impl Buffer { where S: AsRef<str>, { - self.set_string_truncated_at_end(x, y, string.as_ref(), width, style) - } - - /// Print at most the first `width` characters of a string if enough space is available - /// until the end of the line. - /// If `ellipsis` is true appends a `…` at the end of truncated lines. - /// If `truncate_start` is `true`, adds a `…` at the beginning of truncated lines. - #[allow(clippy::too_many_arguments)] - pub fn set_string_anchored( - &mut self, - x: u16, - y: u16, - truncate_start: bool, - truncate_end: bool, - string: &str, - width: usize, - style: impl Fn(usize) -> Style, // Map a grapheme's string offset to a style - ) -> (u16, u16) { - // prevent panic if out of range - if !self.in_bounds(x, y) || width == 0 { - return (x, y); - } - - let mut index = self.index_of(x, y); - let mut rendered_width = 0; - let mut graphemes = string.grapheme_indices(true); - - if truncate_start { - for _ in 0..graphemes.next().map(|(_, g)| g.width()).unwrap_or_default() { - self.content[index].set_symbol("…"); - index += 1; - rendered_width += 1; - } - } - - for (byte_offset, s) in graphemes { - let grapheme_width = s.width(); - if truncate_end && rendered_width + grapheme_width >= width { - break; - } - if grapheme_width == 0 { - continue; - } - - self.content[index].set_symbol(s); - self.content[index].set_style(style(byte_offset)); - - // Reset following cells if multi-width (they would be hidden by the grapheme): - for i in index + 1..index + grapheme_width { - self.content[i].reset(); - } - - index += grapheme_width; - rendered_width += grapheme_width; - } - - if truncate_end { - for _ in 0..width.saturating_sub(rendered_width) { - self.content[index].set_symbol("…"); - index += 1; - } - } - - (x, y) + self.set_string_truncated(x, y, string, width, style, false) } /// Print at most the first `width` characters of a string if enough space is available - /// until the end of the line. If `ellipsis` is true appends a `…` at the end of - /// truncated lines. If `truncate_start` is `true`, truncate the beginning of the string - /// instead of the end. - #[allow(clippy::too_many_arguments)] - pub fn set_string_truncated( + /// until the end of the line. If `markend` is true appends a `…` at the end of + /// truncated lines. + pub fn set_string_truncated<S>( &mut self, x: u16, y: u16, - string: &str, + string: S, width: usize, - style: impl Fn(usize) -> Style, // Map a grapheme's string offset to a style + style: Style, ellipsis: bool, - truncate_start: bool, - ) -> (u16, u16) { - // prevent panic if out of range - if !self.in_bounds(x, y) || width == 0 { - return (x, y); - } - + ) -> (u16, u16) + where + S: AsRef<str>, + { let mut index = self.index_of(x, y); let mut x_offset = x as usize; let width = if ellipsis { width - 1 } else { width }; - let graphemes = string.grapheme_indices(true); + let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true); let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize)); - if !truncate_start { - for (byte_offset, 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 dimensions 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(byte_offset)); - // 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; - } - if ellipsis && x_offset - (x as usize) < string.width() { - self.content[index].set_symbol("…"); - } - } else { - let mut start_index = self.index_of(x, y); - let mut index = self.index_of(max_offset as u16, y); - - let content_width = string.width(); - let truncated = content_width > width; - if ellipsis && truncated { - self.content[start_index].set_symbol("…"); - start_index += 1; - } - if !truncated { - index -= width - content_width; - } - for (byte_offset, s) in graphemes.rev() { - let width = s.width(); - if width == 0 { - continue; - } - let start = index - width; - if start < start_index { - break; - } - self.content[start].set_symbol(s); - self.content[start].set_style(style(byte_offset)); - for i in start + 1..index { - self.content[i].reset(); - } - index -= width; - x_offset += width; - } - } - (x_offset as u16, y) - } - - /// Print at most the first `width` characters of a string if enough space is available - /// until the end of the line. - pub fn set_string_truncated_at_end( - &mut self, - x: u16, - y: u16, - string: &str, - width: usize, - style: Style, - ) -> (u16, u16) { - // prevent panic if out of range - if !self.in_bounds(x, y) { - return (x, y); - } - - let mut index = self.index_of(x, y); - let mut x_offset = x as usize; - let max_x_offset = min(self.area.right() as usize, width.saturating_add(x as usize)); - - for s in string.graphemes(true) { + 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 dimensions to usize or u32 and someone resizes the terminal to 1x2^32. - if width > max_x_offset.saturating_sub(x_offset) { + // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32. + if width > max_offset.saturating_sub(x_offset) { break; } @@ -499,56 +309,13 @@ impl Buffer { index += width; x_offset += width; } - - (x_offset as u16, y) - } - - /// Print at most the first `width` characters of a [Spans] if enough space is available - /// until the end of the line. Appends a `…` at the end of truncated lines. - pub fn set_spans_truncated(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) { - // prevent panic if out of range - if !self.in_bounds(x, y) || width == 0 { - return (x, y); - } - - let mut x_offset = x as usize; - let max_offset = min(self.area.right(), width.saturating_add(x)); - let mut start_index = self.index_of(x, y); - let mut index = self.index_of(max_offset, y); - - let content_width = spans.width(); - let truncated = content_width > width as usize; - if truncated { - self.content[start_index].set_symbol("…"); - start_index += 1; - } else { - index -= width as usize - content_width; - } - for span in spans.0.iter().rev() { - for s in span.content.graphemes(true).rev() { - let width = s.width(); - if width == 0 { - continue; - } - let start = index - width; - if start < start_index { - break; - } - self.content[start].set_symbol(s); - self.content[start].set_style(span.style); - for i in start + 1..index { - self.content[i].reset(); - } - index -= width; - x_offset += width; - } + if ellipsis && x_offset - (x as usize) < string.as_ref().width() { + self.content[index].set_symbol("…"); } (x_offset as u16, y) } - /// Print at most the first `width` characters of a [Spans] if enough space is available - /// until the end of the line - pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) { + pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) { let mut remaining_width = width; let mut x = x; for span in &spans.0 { @@ -569,9 +336,7 @@ impl Buffer { (x, y) } - /// Print at most the first `width` characters of a [Span] if enough space is available - /// until the end of the line - pub fn set_span(&mut self, x: u16, y: u16, span: &Span, width: u16) -> (u16, u16) { + pub fn set_span<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) { self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style) } @@ -582,16 +347,15 @@ impl Buffer { 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[(x, y)].set_bg(color); + self.get_mut(x, y).set_bg(color); } } } - /// Set all cells in the [area](Rect) to the given [Style] 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[(x, y)].set_style(style); + self.get_mut(x, y).set_style(style); } } } @@ -599,7 +363,7 @@ impl Buffer { /// 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(); + let length = area.area() as usize; if self.content.len() > length { self.content.truncate(length); } else { @@ -619,7 +383,7 @@ impl Buffer { pub fn clear(&mut self, area: Rect) { for x in area.left()..area.right() { for y in area.top()..area.bottom() { - self[(x, y)].reset(); + self.get_mut(x, y).reset(); } } } @@ -628,7 +392,7 @@ impl Buffer { pub fn clear_with(&mut self, area: Rect, style: Style) { for x in area.left()..area.right() { for y in area.top()..area.bottom() { - let cell = &mut self[(x, y)]; + let cell = self.get_mut(x, y); cell.reset(); cell.set_style(style); } @@ -639,10 +403,10 @@ impl Buffer { pub fn merge(&mut self, other: &Buffer) { let area = self.area.union(other.area); let cell: Cell = Default::default(); - self.content.resize(area.area(), cell.clone()); + self.content.resize(area.area() as usize, cell.clone()); // Move original content to the appropriate space - let size = self.area.area(); + let size = self.area.area() as usize; for i in (0..size).rev() { let (x, y) = self.pos_of(i); // New index in content @@ -655,7 +419,7 @@ impl Buffer { // Push content of the other buffer into this one (may erase previous // data) - let size = other.area.area(); + let size = other.area.area() as usize; for i in 0..size { let (x, y) = other.pos_of(i); // New index in content @@ -699,44 +463,27 @@ impl Buffer { let width = self.area.width; let mut updates: Vec<(u16, u16, &Cell)> = vec![]; - // Cells invalidated by drawing/replacing preceding multi-width characters: + // Cells invalidated by drawing/replacing preceeding multi-width characters: let mut invalidated: usize = 0; - // Cells from the current buffer to skip due to preceding multi-width characters taking their + // 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 % width as usize) as u16; - let y = (i / width as usize) as u16; + let x = i as u16 % width; + let y = i as u16 / width; updates.push((x, y, &next_buffer[i])); } - let current_width = current.symbol.width(); - to_skip = current_width.saturating_sub(1); + to_skip = current.symbol.width().saturating_sub(1); - let affected_width = std::cmp::max(current_width, previous.symbol.width()); + let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width()); invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1); } updates } } -impl std::ops::Index<(u16, u16)> for Buffer { - type Output = Cell; - - fn index(&self, (x, y): (u16, u16)) -> &Self::Output { - let i = self.index_of(x, y); - &self.content[i] - } -} - -impl std::ops::IndexMut<(u16, u16)> for Buffer { - fn index_mut(&mut self, (x, y): (u16, u16)) -> &mut Self::Output { - let i = self.index_of(x, y); - &mut self.content[i] - } -} - #[cfg(test)] mod tests { use super::*; @@ -812,16 +559,13 @@ mod tests { let area = Rect::new(0, 0, 1, 1); let mut buffer = Buffer::empty(area); - // U+200B is the zero-width space codepoint - assert_eq!("\u{200B}".width(), 0); - // Leading grapheme with zero width - let s = "\u{200B}a"; + let s = "\u{1}a"; buffer.set_stringn(0, 0, s, 1, Style::default()); assert_eq!(buffer, Buffer::with_lines(vec!["a"])); - // Trailing grapheme with zero width - let s = "a\u{200B}"; + // Trailing grapheme with zero with + let s = "a\u{1}"; buffer.set_stringn(0, 0, s, 1, Style::default()); assert_eq!(buffer, Buffer::with_lines(vec!["a"])); } |