mirror of
https://github.com/ratatui/ratatui.git
synced 2025-10-02 15:25:54 +00:00
refactor: simplify WordWrapper implementation (#1193)
This commit is contained in:
parent
36d49e549b
commit
32a0b26525
@ -1,5 +1,8 @@
|
|||||||
use crate::{prelude::*, style::Styled};
|
use crate::{prelude::*, style::Styled};
|
||||||
|
|
||||||
|
const NBSP: &str = "\u{00a0}";
|
||||||
|
const ZWSP: &str = "\u{200b}";
|
||||||
|
|
||||||
/// A grapheme associated to a style.
|
/// A grapheme associated to a style.
|
||||||
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
|
/// 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`).
|
/// 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(),
|
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> {
|
impl<'a> Styled for StyledGrapheme<'a> {
|
||||||
|
@ -5,9 +5,6 @@ use unicode_width::UnicodeWidthStr;
|
|||||||
|
|
||||||
use crate::{layout::Alignment, text::StyledGrapheme};
|
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.
|
/// A state machine to pack styled symbols into lines.
|
||||||
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
|
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
|
||||||
/// iterators for that).
|
/// iterators for that).
|
||||||
@ -59,6 +56,124 @@ where
|
|||||||
trim,
|
trim,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn next_cached_line(&mut self) -> Option<Vec<StyledGrapheme<'a>>> {
|
||||||
|
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<Item = StyledGrapheme<'a>>) {
|
||||||
|
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<StyledGrapheme> = 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>
|
impl<'a, O, I> LineComposer<'a> for WordWrapper<'a, O, I>
|
||||||
@ -72,155 +187,26 @@ where
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut current_line: Option<Vec<StyledGrapheme<'a>>> = None;
|
loop {
|
||||||
let mut line_width: u16 = 0;
|
// 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
|
self.current_line = line;
|
||||||
while current_line.is_none() {
|
return Some(WrappedLine {
|
||||||
// Retrieve next preprocessed wrapped line
|
line: &self.current_line,
|
||||||
if let Some(line_iterator) = &mut self.wrapped_lines {
|
width: line_width,
|
||||||
if let Some(line) = line_iterator.next() {
|
alignment: self.current_alignment,
|
||||||
line_width = line
|
});
|
||||||
.iter()
|
|
||||||
.map(|grapheme| grapheme.symbol.width())
|
|
||||||
.sum::<usize>() as u16;
|
|
||||||
current_line = Some(line);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When no more preprocessed wrapped lines
|
// otherwise, process pending wrapped lines from input
|
||||||
if current_line.is_none() {
|
let (line_symbols, line_alignment) = self.input_lines.next()?;
|
||||||
// Try to calculate next wrapped lines based on current whole line
|
self.current_alignment = line_alignment;
|
||||||
if let Some((line_symbols, line_alignment)) = &mut self.input_lines.next() {
|
self.process_input(line_symbols);
|
||||||
// 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::<StyledGrapheme>::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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user