From 32a0b265253cb83cbf1d51784abf150fed7ef82f Mon Sep 17 00:00:00 2001 From: tranzystorekk Date: Tue, 25 Jun 2024 21:14:32 +0200 Subject: [PATCH] refactor: simplify WordWrapper implementation (#1193) --- src/text/grapheme.rs | 8 ++ src/widgets/reflow.rs | 284 ++++++++++++++++++++---------------------- 2 files changed, 143 insertions(+), 149 deletions(-) diff --git a/src/text/grapheme.rs b/src/text/grapheme.rs index 7978481b..7a14ca6a 100644 --- a/src/text/grapheme.rs +++ b/src/text/grapheme.rs @@ -1,5 +1,8 @@ use crate::{prelude::*, style::Styled}; +const NBSP: &str = "\u{00a0}"; +const ZWSP: &str = "\u{200b}"; + /// A grapheme associated to a style. /// Note that, although `StyledGrapheme` is the smallest divisible unit of text, /// it actually is not a member of the text type hierarchy (`Text` -> `Line` -> `Span`). @@ -22,6 +25,11 @@ impl<'a> StyledGrapheme<'a> { style: style.into(), } } + + pub(crate) fn is_whitespace(&self) -> bool { + let symbol = self.symbol; + symbol == ZWSP || symbol.chars().all(char::is_whitespace) && symbol != NBSP + } } impl<'a> Styled for StyledGrapheme<'a> { diff --git a/src/widgets/reflow.rs b/src/widgets/reflow.rs index 16f878ce..133a94a2 100644 --- a/src/widgets/reflow.rs +++ b/src/widgets/reflow.rs @@ -5,9 +5,6 @@ use unicode_width::UnicodeWidthStr; use crate::{layout::Alignment, text::StyledGrapheme}; -const NBSP: &str = "\u{00a0}"; -const ZWSP: &str = "\u{200b}"; - /// A state machine to pack styled symbols into lines. /// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming /// iterators for that). @@ -59,6 +56,124 @@ where trim, } } + + fn next_cached_line(&mut self) -> Option>> { + self.wrapped_lines.as_mut()?.next() + } + + /// Split an input line (`line_symbols`) into wrapped lines + /// and cache them to be emitted later + fn process_input(&mut self, line_symbols: impl IntoIterator>) { + let mut result_lines = vec![]; + let mut pending_line = vec![]; + let mut line_width = 0; + let mut pending_word = vec![]; + let mut word_width = 0; + let mut pending_whitespace: VecDeque = VecDeque::new(); + let mut whitespace_width = 0; + let mut non_whitespace_previous = false; + + for grapheme in line_symbols { + let is_whitespace = grapheme.is_whitespace(); + let symbol_width = grapheme.symbol.width() as u16; + + // ignore symbols wider than line limit + if symbol_width > self.max_line_width { + continue; + } + + let word_found = non_whitespace_previous && is_whitespace; + // current word would overflow after removing whitespace + let trimmed_overflow = pending_line.is_empty() + && self.trim + && word_width + symbol_width > self.max_line_width; + // separated whitespace would overflow on its own + let whitespace_overflow = pending_line.is_empty() + && self.trim + && whitespace_width + symbol_width > self.max_line_width; + // current full word (including whitespace) would overflow + let untrimmed_overflow = pending_line.is_empty() + && !self.trim + && word_width + whitespace_width + symbol_width > self.max_line_width; + + // append finished segment to current line + if word_found || trimmed_overflow || whitespace_overflow || untrimmed_overflow { + if !pending_line.is_empty() || !self.trim { + pending_line.extend(pending_whitespace.drain(..)); + line_width += whitespace_width; + } + + pending_line.append(&mut pending_word); + line_width += word_width; + + pending_whitespace.clear(); + whitespace_width = 0; + word_width = 0; + } + + // pending line fills up limit + let line_full = line_width >= self.max_line_width; + // pending word would overflow line limit + let pending_word_overflow = symbol_width > 0 + && line_width + whitespace_width + word_width >= self.max_line_width; + + // add finished wrapped line to remaining lines + if line_full || pending_word_overflow { + let mut remaining_width = u16::saturating_sub(self.max_line_width, line_width); + + result_lines.push(std::mem::take(&mut pending_line)); + line_width = 0; + + // remove whitespace up to the end of line + while let Some(grapheme) = pending_whitespace.front() { + let width = grapheme.symbol.width() as u16; + + if width > remaining_width { + break; + } + + whitespace_width -= width; + remaining_width -= width; + pending_whitespace.pop_front(); + } + + // don't count first whitespace toward next word + if is_whitespace && pending_whitespace.is_empty() { + continue; + } + } + + // append symbol to a pending buffer + if is_whitespace { + whitespace_width += symbol_width; + pending_whitespace.push_back(grapheme); + } else { + word_width += symbol_width; + pending_word.push(grapheme); + } + + non_whitespace_previous = !is_whitespace; + } + + // append remaining text parts + if pending_line.is_empty() && pending_word.is_empty() && !pending_whitespace.is_empty() { + result_lines.push(vec![]); + } + if !pending_line.is_empty() || !self.trim { + pending_line.extend(pending_whitespace); + } + pending_line.extend(pending_word); + + if !pending_line.is_empty() { + result_lines.push(pending_line); + } + if result_lines.is_empty() { + result_lines.push(vec![]); + } + + // save processed lines for emitting later + self.wrapped_lines = Some(result_lines.into_iter()); + } } impl<'a, O, I> LineComposer<'a> for WordWrapper<'a, O, I> @@ -72,155 +187,26 @@ where return None; } - let mut current_line: Option>> = None; - let mut line_width: u16 = 0; + loop { + // emit next cached line if present + if let Some(line) = self.next_cached_line() { + let line_width = line + .iter() + .map(|grapheme| grapheme.symbol.width() as u16) + .sum(); - // Try to repeatedly retrieve next line - while current_line.is_none() { - // Retrieve next preprocessed wrapped line - if let Some(line_iterator) = &mut self.wrapped_lines { - if let Some(line) = line_iterator.next() { - line_width = line - .iter() - .map(|grapheme| grapheme.symbol.width()) - .sum::() as u16; - current_line = Some(line); - } + self.current_line = line; + return Some(WrappedLine { + line: &self.current_line, + width: line_width, + alignment: self.current_alignment, + }); } - // When no more preprocessed wrapped lines - if current_line.is_none() { - // Try to calculate next wrapped lines based on current whole line - if let Some((line_symbols, line_alignment)) = &mut self.input_lines.next() { - // Save the whole line's alignment - self.current_alignment = *line_alignment; - let mut wrapped_lines = vec![]; // Saves the wrapped lines - // Saves the unfinished wrapped line - let (mut current_line, mut current_line_width) = (vec![], 0); - // Saves the partially processed word - let (mut unfinished_word, mut word_width) = (vec![], 0); - // Saves the whitespaces of the partially unfinished word - let (mut unfinished_whitespaces, mut whitespace_width) = - (VecDeque::::new(), 0); - - let mut has_seen_non_whitespace = false; - for StyledGrapheme { symbol, style } in line_symbols { - let symbol_whitespace = symbol == ZWSP - || (symbol.chars().all(&char::is_whitespace) && symbol != NBSP); - let symbol_width = symbol.width() as u16; - // Ignore characters wider than the total max width - if symbol_width > self.max_line_width { - continue; - } - - // Append finished word to current line - if has_seen_non_whitespace && symbol_whitespace - // Append if trimmed (whitespaces removed) word would overflow - || word_width + symbol_width > self.max_line_width && current_line.is_empty() && self.trim - // Append if removed whitespace would overflow -> reset whitespace counting to prevent overflow - || whitespace_width + symbol_width > self.max_line_width && current_line.is_empty() && self.trim - // Append if complete word would overflow - || word_width + whitespace_width + symbol_width > self.max_line_width && current_line.is_empty() && !self.trim - { - if !current_line.is_empty() || !self.trim { - // Also append whitespaces if not trimming or current line is not - // empty - current_line.extend( - std::mem::take(&mut unfinished_whitespaces).into_iter(), - ); - current_line_width += whitespace_width; - } - // Append trimmed word - current_line.append(&mut unfinished_word); - current_line_width += word_width; - - // Clear whitespace buffer - unfinished_whitespaces.clear(); - whitespace_width = 0; - word_width = 0; - } - - // Append the unfinished wrapped line to wrapped lines if it is as wide as - // max line width - if current_line_width >= self.max_line_width - // or if it would be too long with the current partially processed word added - || current_line_width + whitespace_width + word_width >= self.max_line_width && symbol_width > 0 - { - let mut remaining_width = (i32::from(self.max_line_width) - - i32::from(current_line_width)) - .max(0) as u16; - wrapped_lines.push(std::mem::take(&mut current_line)); - current_line_width = 0; - - // Remove all whitespaces till end of just appended wrapped line + next - // whitespace - let mut first_whitespace = unfinished_whitespaces.pop_front(); - while let Some(grapheme) = first_whitespace.as_ref() { - let symbol_width = grapheme.symbol.width() as u16; - whitespace_width -= symbol_width; - - if symbol_width > remaining_width { - break; - } - remaining_width -= symbol_width; - first_whitespace = unfinished_whitespaces.pop_front(); - } - // In case all whitespaces have been exhausted - if symbol_whitespace && first_whitespace.is_none() { - // Prevent first whitespace to count towards next word - continue; - } - } - - // Append symbol to unfinished, partially processed word - if symbol_whitespace { - whitespace_width += symbol_width; - unfinished_whitespaces.push_back(StyledGrapheme { symbol, style }); - } else { - word_width += symbol_width; - unfinished_word.push(StyledGrapheme { symbol, style }); - } - - has_seen_non_whitespace = !symbol_whitespace; - } - - // Append remaining text parts - if !unfinished_word.is_empty() || !unfinished_whitespaces.is_empty() { - if current_line.is_empty() && unfinished_word.is_empty() { - wrapped_lines.push(vec![]); - } else if !self.trim || !current_line.is_empty() { - current_line.extend(unfinished_whitespaces.into_iter()); - } else { - // TODO: explain why this else branch is ok. - // See clippy::else_if_without_else - } - current_line.append(&mut unfinished_word); - } - if !current_line.is_empty() { - wrapped_lines.push(current_line); - } - if wrapped_lines.is_empty() { - // Append empty line if there was nothing to wrap in the first place - wrapped_lines.push(vec![]); - } - - self.wrapped_lines = Some(wrapped_lines.into_iter()); - } else { - // No more whole lines available -> stop repeatedly retrieving next wrapped line - break; - } - } - } - - if let Some(line) = current_line { - self.current_line = line; - Some(WrappedLine { - line: &self.current_line, - width: line_width, - alignment: self.current_alignment, - }) - } else { - None + // otherwise, process pending wrapped lines from input + let (line_symbols, line_alignment) = self.input_lines.next()?; + self.current_alignment = line_alignment; + self.process_input(line_symbols); } } }