From 26a8936cf50ee4b775fd4d1bf96f1b3077421e5c Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 1 Aug 2022 19:02:57 +0200 Subject: [PATCH] Store Styled with contiguous string The previous implementation of Styled used chunks that consisted of a String and a ContentStyle. The current implementation instead stores a single String and chunks consisting of a ContentStyle and an ending index. This implementation may reduce allocations and makes width-related operations easier, for example getting the width of a Styled with its whitespace trimmed. --- examples/text_wrapping.rs | 2 +- src/buffer.rs | 2 +- src/frame.rs | 9 -- src/styled.rs | 282 ++++++++++++++++++++------------------ 4 files changed, 149 insertions(+), 146 deletions(-) diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index ecf49aa..bc5d569 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -31,7 +31,7 @@ fn draw(f: &mut Frame) { ); let breaks = f.wrap(text, f.size().width.into()); - let lines = Styled::default().then(text).split_at_indices(&breaks); + let lines = Styled::new_plain(text).split_at_indices(&breaks); for (i, mut line) in lines.into_iter().enumerate() { line.trim_end(); f.write(Pos::new(0, i as i32), line); diff --git a/src/buffer.rs b/src/buffer.rs index c02ca2a..e25035a 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -344,7 +344,7 @@ impl Buffer { let y = pos.y as u16; let mut col: usize = 0; - for styled_grapheme in styled.styled_graphemes() { + for (_, styled_grapheme) in styled.styled_grapheme_indices() { let x = pos.x + col as i32; let g = *styled_grapheme.content(); let style = *styled_grapheme.style(); diff --git a/src/frame.rs b/src/frame.rs index 84cb240..2943c8e 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -76,15 +76,6 @@ impl Frame { self.widthdb.width(s) } - /// Like [`Self::width`] for [`Styled`]. - pub fn width_styled(&mut self, s: &Styled) -> usize { - let mut total: usize = 0; - for grapheme in s.graphemes() { - total += self.widthdb.grapheme_width(grapheme) as usize; - } - total - } - pub fn tab_width_at_column(&self, col: usize) -> u8 { wrap::tab_width_at_column(self.tab_width, col) } diff --git a/src/styled.rs b/src/styled.rs index 1a19540..68f32c4 100644 --- a/src/styled.rs +++ b/src/styled.rs @@ -1,147 +1,81 @@ +use std::iter::Peekable; +use std::{slice, vec}; + use crossterm::style::{ContentStyle, StyledContent}; -use unicode_segmentation::UnicodeSegmentation; - -#[derive(Debug, Clone)] -pub struct Chunk { - string: String, - style: ContentStyle, -} - -impl Chunk { - pub fn new(string: S, style: ContentStyle) -> Self { - Self { - string: string.to_string(), - style, - } - } - - pub fn string(&self) -> &str { - &self.string - } - - pub fn style(&self) -> ContentStyle { - self.style - } - - pub fn plain(string: S) -> Self { - Self::new(string, ContentStyle::default()) - } - - pub fn split_at(&self, mid: usize) -> (Self, Self) { - let (lstr, rstr) = self.string.split_at(mid); - let left = Self { - string: lstr.to_string(), - style: self.style, - }; - let right = Self { - string: rstr.to_string(), - style: self.style, - }; - (left, right) - } -} - -impl From<&str> for Chunk { - fn from(str: &str) -> Self { - Self::plain(str) - } -} - -impl From for Chunk { - fn from(string: String) -> Self { - Self::plain(string) - } -} - -impl From<&String> for Chunk { - fn from(string: &String) -> Self { - Self::plain(string) - } -} - -impl From<(S,)> for Chunk { - fn from(tuple: (S,)) -> Self { - Self::plain(tuple.0) - } -} - -impl From<(S, ContentStyle)> for Chunk { - fn from(tuple: (S, ContentStyle)) -> Self { - Self::new(tuple.0, tuple.1) - } -} +use unicode_segmentation::{GraphemeIndices, Graphemes, UnicodeSegmentation}; #[derive(Debug, Default, Clone)] -pub struct Styled(Vec); +pub struct Styled { + text: String, + /// List of `(style, until)` tuples. The style should be applied to all + /// chars in the range `prev_until..until`. + styles: Vec<(ContentStyle, usize)>, +} impl Styled { - pub fn new>(chunk: C) -> Self { - Self::default().then(chunk) + pub fn new>(text: S, style: ContentStyle) -> Self { + Self::default().then(text, style) } - pub fn then>(mut self, chunk: C) -> Self { - self.0.push(chunk.into()); + pub fn new_plain>(text: S) -> Self { + Self::default().then_plain(text) + } + + pub fn then>(mut self, text: S, style: ContentStyle) -> Self { + let text = text.as_ref(); + if !text.is_empty() { + self.text.push_str(text); + self.styles.push((style, self.text.len())); + } self } - pub fn and_then(mut self, other: Styled) -> Self { - self.0.extend(other.0); + pub fn then_plain>(self, text: S) -> Self { + self.then(text, ContentStyle::default()) + } + + pub fn and_then(mut self, mut other: Styled) -> Self { + let delta = self.text.len(); + for (_, until) in &mut other.styles { + *until += delta; + } + + self.text.push_str(&other.text); + self.styles.extend(other.styles); self } - pub fn chunks(&self) -> &[Chunk] { - &self.0 - } - - pub fn text(&self) -> String { - self.0.iter().flat_map(|c| c.string.chars()).collect() - } - - pub fn graphemes(&self) -> impl Iterator { - self.0.iter().flat_map(|c| c.string.graphemes(true)) - } - - pub fn grapheme_indices(&self) -> impl Iterator { - self.0 - .iter() - .scan(0, |s, c| { - let offset = *s; - *s += c.string.len(); - Some((offset, c)) - }) - .flat_map(|(o, c)| { - c.string - .grapheme_indices(true) - .map(move |(gi, g)| (o + gi, g)) - }) - } - - pub fn styled_graphemes(&self) -> impl Iterator> { - self.0.iter().flat_map(|c| { - c.string - .graphemes(true) - .map(|g| StyledContent::new(c.style, g)) - }) + pub fn text(&self) -> &str { + &self.text } pub fn split_at(self, mid: usize) -> (Self, Self) { - let mut left = vec![]; - let mut right = vec![]; - let mut offset = 0; - for chunk in self.0 { - let len = chunk.string.len(); - if offset >= mid { - right.push(chunk); - } else if offset + len > mid { - let (lchunk, rchunk) = chunk.split_at(mid - offset); - left.push(lchunk); - right.push(rchunk); - } else { - left.push(chunk); + let (left_text, right_text) = self.text.split_at(mid); + + let mut left_styles = vec![]; + let mut right_styles = vec![]; + let mut from = 0; + for (style, until) in self.styles { + if from < mid { + left_styles.push((style, until.max(mid))); } - offset += len; + if mid < until { + right_styles.push((style, until.saturating_sub(mid))); + } + from = until; } - (Self(left), Self(right)) + + let left = Self { + text: left_text.to_string(), + styles: left_styles, + }; + + let right = Self { + text: right_text.to_string(), + styles: right_styles, + }; + + (left, right) } pub fn split_at_indices(self, indices: &[usize]) -> Vec { @@ -163,20 +97,98 @@ impl Styled { } pub fn trim_end(&mut self) { - while let Some(last) = self.0.last_mut() { - let trimmed = last.string.trim_end(); - if trimmed.is_empty() { - self.0.pop(); - } else { - last.string = trimmed.to_string(); + self.text = self.text.trim_end().to_string(); + + let text_len = self.text.len(); + let mut styles_len = 0; + for (_, until) in &mut self.styles { + styles_len += 1; + if *until >= text_len { + *until = text_len; break; } } + + while self.styles.len() > styles_len { + self.styles.pop(); + } } } -impl> From for Styled { - fn from(chunk: C) -> Self { - Self::new(chunk) +////////////////////////////// +// Iterating over graphemes // +////////////////////////////// + +pub struct StyledGraphemeIndices<'a> { + text: GraphemeIndices<'a>, + styles: Peekable>, +} + +impl<'a> Iterator for StyledGraphemeIndices<'a> { + type Item = (usize, StyledContent<&'a str>); + + fn next(&mut self) -> Option { + let (gi, grapheme) = self.text.next()?; + let (mut style, mut until) = **self.styles.peek().expect("styles cover entire text"); + while gi >= until { + self.styles.next(); + (style, until) = **self.styles.peek().expect("styles cover entire text"); + } + Some((gi, StyledContent::new(style, grapheme))) + } +} + +impl Styled { + pub fn graphemes(&self) -> Graphemes<'_> { + self.text.graphemes(true) + } + + pub fn grapheme_indices(&self) -> GraphemeIndices<'_> { + self.text.grapheme_indices(true) + } + + pub fn styled_grapheme_indices(&self) -> StyledGraphemeIndices<'_> { + StyledGraphemeIndices { + text: self.grapheme_indices(), + styles: self.styles.iter().peekable(), + } + } +} + +////////////////////////// +// Converting to Styled // +////////////////////////// + +impl From<&str> for Styled { + fn from(text: &str) -> Self { + Self::new_plain(text) + } +} + +impl From for Styled { + fn from(text: String) -> Self { + Self::new_plain(&text) + } +} + +impl> From<(S,)> for Styled { + fn from((text,): (S,)) -> Self { + Self::new_plain(text) + } +} + +impl> From<(S, ContentStyle)> for Styled { + fn from((text, style): (S, ContentStyle)) -> Self { + Self::new(text, style) + } +} + +impl> From<&[(S, ContentStyle)]> for Styled { + fn from(segments: &[(S, ContentStyle)]) -> Self { + let mut result = Self::default(); + for (text, style) in segments { + result = result.then(text, *style); + } + result } }