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.rs410
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"]));
}