diff --git a/termwiz/src/cell.rs b/termwiz/src/cell.rs index 8f6118bf6..fa4a42d6e 100644 --- a/termwiz/src/cell.rs +++ b/termwiz/src/cell.rs @@ -539,7 +539,12 @@ impl Cell { /// Returns the number of cells visually occupied by this grapheme pub fn width(&self) -> usize { - grapheme_column_width(self.str()) + let s = self.str(); + if s.len() == 1 { + 1 + } else { + grapheme_column_width(s) + } } /// Returns the attributes of the cell diff --git a/termwiz/src/cellcluster.rs b/termwiz/src/cellcluster.rs index 5bbbb2be5..d0a4825cd 100644 --- a/termwiz/src/cellcluster.rs +++ b/termwiz/src/cellcluster.rs @@ -1,4 +1,5 @@ use crate::cell::{Cell, CellAttributes}; +use std::borrow::Cow; /// A `CellCluster` is another representation of a Line. /// A `Vec` is produced by walking through the Cells in @@ -9,34 +10,97 @@ use crate::cell::{Cell, CellAttributes}; pub struct CellCluster { pub attrs: CellAttributes, pub text: String, - pub byte_to_cell_idx: Vec, + byte_to_cell_idx: Vec, + first_cell_idx: usize, } impl CellCluster { + /// Given a byte index into `self.text`, return the corresponding + /// cell index in the originating line. + pub fn byte_to_cell_idx(&self, byte_idx: usize) -> usize { + if self.byte_to_cell_idx.is_empty() { + self.first_cell_idx + byte_idx + } else { + self.byte_to_cell_idx[byte_idx] + } + } + /// Compute the list of CellClusters from a set of visible cells. /// The input is typically the result of calling `Line::visible_cells()`. - pub fn make_cluster<'a>(iter: impl Iterator) -> Vec { + pub fn make_cluster<'a>( + hint: usize, + iter: impl Iterator, + ) -> Vec { let mut last_cluster = None; let mut clusters = Vec::new(); + let mut whitespace_run = 0; + let mut only_whitespace = false; for (cell_idx, c) in iter { let cell_str = c.str(); - let normalized_attr = c.attrs().clone().set_wrapped(false).clone(); + let normalized_attr = if c.attrs().wrapped() { + let mut attr_storage = c.attrs().clone(); + attr_storage.set_wrapped(false); + Cow::Owned(attr_storage) + } else { + Cow::Borrowed(c.attrs()) + }; last_cluster = match last_cluster.take() { None => { // Start new cluster - Some(CellCluster::new(c.attrs().clone(), cell_str, cell_idx)) + only_whitespace = cell_str == " "; + whitespace_run = if only_whitespace { 1 } else { 0 }; + Some(CellCluster::new( + hint, + normalized_attr.into_owned(), + cell_str, + cell_idx, + )) } Some(mut last) => { - if last.attrs != normalized_attr { + if last.attrs != *normalized_attr { // Flush pending cluster and start a new one clusters.push(last); - Some(CellCluster::new(normalized_attr, cell_str, cell_idx)) + + only_whitespace = cell_str == " "; + whitespace_run = if only_whitespace { 1 } else { 0 }; + Some(CellCluster::new( + hint, + normalized_attr.into_owned(), + cell_str, + cell_idx, + )) } else { - // Add to current cluster - last.add(cell_str, cell_idx); - Some(last) + // Add to current cluster. + + // Force cluster to break when we get a run of 2 whitespace + // characters following non-whitespace. + // This reduces the amount of shaping work for scenarios where + // the terminal is wide and a long series of short lines are printed; + // the shaper can cache the few variations of trailing whitespace + // and focus on shaping the shorter cluster sequences. + if cell_str == " " { + whitespace_run += 1; + } else { + whitespace_run = 0; + only_whitespace = false; + } + if !only_whitespace && whitespace_run > 2 { + clusters.push(last); + + only_whitespace = cell_str == " "; + whitespace_run = 1; + Some(CellCluster::new( + hint, + normalized_attr.into_owned(), + cell_str, + cell_idx, + )) + } else { + last.add(cell_str, cell_idx); + Some(last) + } } } }; @@ -51,22 +115,43 @@ impl CellCluster { } /// Start off a new cluster with some initial data - fn new(attrs: CellAttributes, text: &str, cell_idx: usize) -> CellCluster { + fn new(hint: usize, attrs: CellAttributes, text: &str, cell_idx: usize) -> CellCluster { let mut idx = Vec::new(); - for _ in 0..text.len() { - idx.push(cell_idx); + if text.len() > 1 { + // Prefer to avoid pushing any index data; this saves + // allocating any storage until we have any cells that + // are multibyte + for _ in 0..text.len() { + idx.push(cell_idx); + } } + let mut storage = String::with_capacity(hint); + storage.push_str(text); + CellCluster { attrs, - text: text.into(), + text: storage, byte_to_cell_idx: idx, + first_cell_idx: cell_idx, } } /// Add to this cluster fn add(&mut self, text: &str, cell_idx: usize) { - for _ in 0..text.len() { - self.byte_to_cell_idx.push(cell_idx); + if !self.byte_to_cell_idx.is_empty() { + // We had at least one multi-byte cell in the past + for _ in 0..text.len() { + self.byte_to_cell_idx.push(cell_idx); + } + } else if text.len() > 1 { + // Extrapolate the indices so far + for n in 0..self.text.len() { + self.byte_to_cell_idx.push(n + self.first_cell_idx); + } + // Now add this new multi-byte cell text + for _ in 0..text.len() { + self.byte_to_cell_idx.push(cell_idx); + } } self.text.push_str(text); } diff --git a/termwiz/src/surface/line.rs b/termwiz/src/surface/line.rs index cff1b27e4..f3a6b9004 100644 --- a/termwiz/src/surface/line.rs +++ b/termwiz/src/surface/line.rs @@ -432,7 +432,7 @@ impl Line { } pub fn cluster(&self) -> Vec { - CellCluster::make_cluster(self.visible_cells()) + CellCluster::make_cluster(self.cells.len(), self.visible_cells()) } pub fn cells(&self) -> &[Cell] { diff --git a/wezterm-font/src/shaper/harfbuzz.rs b/wezterm-font/src/shaper/harfbuzz.rs index 8201d1cf9..995cacd0a 100644 --- a/wezterm-font/src/shaper/harfbuzz.rs +++ b/wezterm-font/src/shaper/harfbuzz.rs @@ -365,6 +365,7 @@ impl FontShaper for HarfbuzzShaper { dpi: u32, no_glyphs: &mut Vec, ) -> anyhow::Result> { + log::trace!("shape {} `{}`", text.len(), text); let start = std::time::Instant::now(); let result = self.do_shape(0, text, size, dpi, no_glyphs); metrics::histogram!("shape.harfbuzz", start.elapsed()); diff --git a/wezterm-gui/src/termwindow/render.rs b/wezterm-gui/src/termwindow/render.rs index a59aee423..d57c5286e 100644 --- a/wezterm-gui/src/termwindow/render.rs +++ b/wezterm-gui/src/termwindow/render.rs @@ -720,7 +720,13 @@ impl super::TermWindow { } // Break the line into clusters of cells with the same attributes + let start = Instant::now(); let cell_clusters = params.line.cluster(); + log::trace!( + "cluster -> {} clusters, elapsed {:?}", + cell_clusters.len(), + start.elapsed() + ); let mut last_cell_idx = 0; @@ -811,6 +817,8 @@ impl super::TermWindow { ); // Shape the printable text from this cluster + + let shape_resolve_start = Instant::now(); let glyph_info = { let key = BorrowedShapeCacheKey { style, @@ -858,9 +866,14 @@ impl super::TermWindow { } } }; + log::trace!( + "shape_resolve for cluster len {} -> elapsed {:?}", + cluster.text.len(), + shape_resolve_start.elapsed() + ); for info in glyph_info.iter() { - let cell_idx = cluster.byte_to_cell_idx[info.pos.cluster as usize]; + let cell_idx = cluster.byte_to_cell_idx(info.pos.cluster as usize); let glyph = &info.glyph; let top = ((PixelLength::new(self.render_metrics.cell_size.height as f64) @@ -1015,6 +1028,7 @@ impl super::TermWindow { }, ); + let right_fill_start = Instant::now(); for cell_idx in last_cell_idx + 1..num_cols { // Even though we don't have a cell for these, they still // hold the cursor or the selection so we need to compute @@ -1062,6 +1076,11 @@ impl super::TermWindow { ); quad.set_cursor_color(params.cursor_border_color); } + log::trace!( + "right fill {} -> elapsed {:?}", + num_cols.saturating_sub(last_cell_idx), + right_fill_start.elapsed() + ); Ok(()) } @@ -1277,7 +1296,7 @@ impl super::TermWindow { ) -> anyhow::Result>>> { let mut glyphs = Vec::with_capacity(infos.len()); for info in infos { - let cell_idx = cluster.byte_to_cell_idx[info.cluster as usize]; + let cell_idx = cluster.byte_to_cell_idx(info.cluster as usize); let followed_by_space = match line.cells().get(cell_idx + 1) { Some(cell) => cell.str() == " ", None => false,