diff --git a/term/src/line.rs b/term/src/line.rs index a94d22954..5372955ca 100644 --- a/term/src/line.rs +++ b/term/src/line.rs @@ -13,7 +13,7 @@ enum ImplicitHyperlinks { #[derive(Debug, Clone, Eq, PartialEq)] pub struct Line { - pub cells: Vec, + cells: Vec, dirty: bool, has_hyperlink: bool, has_implicit_hyperlinks: ImplicitHyperlinks, @@ -76,6 +76,79 @@ impl Line { } } + fn invalidate_grapheme_at_or_before(&mut self, idx: usize) { + // Assumption: that the width of a grapheme is never > 2. + // This constrains the amount of look-back that we need to do here. + if idx > 0 { + let prior = idx - 1; + let width = self.cells[prior].width(); + if width > 1 { + let attrs = self.cells[prior].attrs().clone(); + for nerf in prior..prior + width { + self.cells[nerf] = Cell::new(' ', attrs.clone()); + } + } + } + } + + /// If we're about to modify a cell obscured by a double-width + /// character ahead of that cell, we need to nerf that sequence + /// of cells to avoid partial rendering concerns. + /// Similarly, when we assign a cell, we need to blank out those + /// occluded successor cells. + pub fn set_cell(&mut self, idx: usize, cell: Cell) -> &Cell { + // if the line isn't wide enough, pad it out with the default attributes + if idx >= self.cells.len() { + self.cells.resize(idx, Cell::default()); + } + + self.invalidate_grapheme_at_or_before(idx); + + // For double-wide or wider chars, ensure that the cells that + // are overlapped by this one are blanked out. + let width = cell.width(); + for i in 1..=width.saturating_sub(1) { + self.cells[idx + i] = Cell::new(' ', cell.attrs().clone()); + } + + self.cells[idx] = cell; + &self.cells[idx] + } + + pub fn insert_cell(&mut self, x: usize, cell: Cell) { + self.invalidate_implicit_links(); + + // If we're inserting a wide cell, we should also insert the overlapped cells. + // We insert them first so that the grapheme winds up left-most. + let width = cell.width(); + for _ in 1..=width.saturating_sub(1) { + self.cells.insert(x, Cell::new(' ', cell.attrs().clone())); + } + + self.cells.insert(x, cell); + } + + pub fn erase_cell(&mut self, x: usize) { + self.invalidate_implicit_links(); + self.invalidate_grapheme_at_or_before(x); + self.cells.remove(x); + self.cells.push(Cell::default()); + } + + pub fn fill_range(&mut self, cols: impl Iterator, cell: &Cell) { + let max_col = self.cells.len(); + for x in cols { + if x >= max_col { + break; + } + self.set_cell(x, cell.clone()); + } + } + + pub fn cells(&self) -> &[Cell] { + &self.cells + } + /// Iterates the visible cells, respecting the width of the cell. /// For instance, a double-width cell overlaps the following (blank) /// cell, so that blank cell is omitted from the iterator results. diff --git a/term/src/screen.rs b/term/src/screen.rs index 164b72ead..5bcc99c6b 100644 --- a/term/src/screen.rs +++ b/term/src/screen.rs @@ -103,16 +103,13 @@ impl Screen { pub fn insert_cell(&mut self, x: usize, y: VisibleRowIndex) { let line_idx = self.phys_row(y); let line = self.line_mut(line_idx); - line.invalidate_implicit_links(); - line.cells.insert(x, Cell::default()); + line.insert_cell(x, Cell::default()); } pub fn erase_cell(&mut self, x: usize, y: VisibleRowIndex) { let line_idx = self.phys_row(y); let line = self.line_mut(line_idx); - line.invalidate_implicit_links(); - line.cells.remove(x); - line.cells.push(Cell::default()); + line.erase_cell(x); } /// Set a cell. the x and y coordinates are relative to the visible screeen @@ -137,18 +134,7 @@ impl Screen { line.set_has_hyperlink(true); } - let width = line.cells.len(); - let cell = Cell::new(c, attr.clone()); - if x == width { - line.cells.push(cell); - } else if x > width { - // if the line isn't wide enough, pad it out with the default attributes - line.cells.resize(x, Cell::default()); - line.cells.push(cell); - } else { - line.cells[x] = cell; - } - &line.cells[x] + line.set_cell(x, Cell::new(c, attr.clone())) } pub fn clear_line( @@ -159,13 +145,7 @@ impl Screen { ) { let line_idx = self.phys_row(y); let line = self.line_mut(line_idx); - let max_col = line.cells.len(); - for x in cols { - if x >= max_col { - break; - } - line.cells[x] = Cell::new(' ', attr.clone()); - } + line.fill_range(cols, &Cell::new(' ', attr.clone())); } /// Translate a VisibleRowIndex into a PhysRowIndex. The resultant index diff --git a/term/src/terminalstate.rs b/term/src/terminalstate.rs index 313c249c7..152e3d79e 100644 --- a/term/src/terminalstate.rs +++ b/term/src/terminalstate.rs @@ -305,7 +305,7 @@ impl TerminalState { match screen.lines.get_mut(idx) { Some(ref mut line) => { line.find_hyperlinks(rules); - match line.cells.get(x) { + match line.cells().get(x) { Some(cell) => cell.attrs().hyperlink.as_ref().cloned(), None => None, } @@ -1460,20 +1460,6 @@ impl<'a> Performer<'a> { cell.width() }; - // for double- or triple-wide cells, the client of the terminal - // expects the cursor to move by the visible width, which means that - // we need to generate non-printing cells to pad out the gap. They - // need to be non-printing rather than space so that that renderer - // doesn't render an actual space between the glyphs. - for non_print_x in 1..print_width { - self.screen_mut().set_cell( - x + non_print_x, - y, - 0 as char, // non-printable - &pen, - ); - } - self.clear_selection_if_intersects(x..x + print_width, y as ScrollbackOrVisibleRowIndex); if x + print_width < width { diff --git a/term/src/test/csi.rs b/term/src/test/csi.rs index a8ea8d04b..84d6c9745 100644 --- a/term/src/test/csi.rs +++ b/term/src/test/csi.rs @@ -151,9 +151,7 @@ fn test_ed() { .set_background(color::AnsiColor::Navy) .clone(); let mut line: Line = " ".into(); - line.cells[0] = Cell::new(' ', attr.clone()); - line.cells[1] = Cell::new(' ', attr.clone()); - line.cells[2] = Cell::new(' ', attr.clone()); + line.fill_range(0..=2, &Cell::new(' ', attr.clone())); assert_lines_equal( &term.screen().visible_lines(), &[line.clone(), line.clone(), line], diff --git a/term/src/test/mod.rs b/term/src/test/mod.rs index 13c3edd0c..8eadd2648 100644 --- a/term/src/test/mod.rs +++ b/term/src/test/mod.rs @@ -256,8 +256,8 @@ fn assert_lines_equal(lines: &[Line], expect_lines: &[Line], compare: Compare) { } if compare.contains(Compare::ATTRS) { - let line_attrs: Vec<_> = line.cells.iter().map(|c| c.attrs().clone()).collect(); - let expect_attrs: Vec<_> = expect.cells.iter().map(|c| c.attrs().clone()).collect(); + let line_attrs: Vec<_> = line.cells().iter().map(|c| c.attrs().clone()).collect(); + let expect_attrs: Vec<_> = expect.cells().iter().map(|c| c.attrs().clone()).collect(); assert_eq!(expect_attrs, line_attrs, "line {} attrs didn't match", idx,); } if compare.contains(Compare::TEXT) { @@ -510,17 +510,23 @@ fn test_hyperlinks() { term.print("00t"); let mut partial_line: Line = "wo00t".into(); - partial_line.cells[0] = Cell::new( - 'w', - CellAttributes::default() - .set_hyperlink(Some(Rc::clone(&otherlink))) - .clone(), + partial_line.set_cell( + 0, + Cell::new( + 'w', + CellAttributes::default() + .set_hyperlink(Some(Rc::clone(&otherlink))) + .clone(), + ), ); - partial_line.cells[1] = Cell::new( - 'o', - CellAttributes::default() - .set_hyperlink(Some(Rc::clone(&otherlink))) - .clone(), + partial_line.set_cell( + 1, + Cell::new( + 'o', + CellAttributes::default() + .set_hyperlink(Some(Rc::clone(&otherlink))) + .clone(), + ), ); assert_lines_equal(