mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-21 20:23:11 +00:00
feat(text): add a way to wrap text without Paragraph
This commit is contained in:
parent
25ff2e5e61
commit
3a9f07a53e
1 changed files with 184 additions and 0 deletions
184
src/text.rs
184
src/text.rs
|
@ -51,6 +51,8 @@ use std::borrow::Cow;
|
|||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
const NBSP: &str = "\u{00a0}";
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct StyledGrapheme<'a> {
|
||||
|
@ -179,6 +181,29 @@ impl<'a> Span<'a> {
|
|||
})
|
||||
.filter(|s| s.symbol != "\n")
|
||||
}
|
||||
|
||||
fn split_at_in_place(&mut self, mid: usize) -> Span<'a> {
|
||||
let content = match self.content {
|
||||
Cow::Owned(ref mut s) => {
|
||||
let s2 = s[mid..].to_string();
|
||||
s.truncate(mid);
|
||||
Cow::Owned(s2)
|
||||
}
|
||||
Cow::Borrowed(s) => {
|
||||
let (s1, s2) = s.split_at(mid);
|
||||
self.content = Cow::Borrowed(s1);
|
||||
Cow::Borrowed(s2)
|
||||
}
|
||||
};
|
||||
Span {
|
||||
content,
|
||||
style: self.style,
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_start(&mut self) {
|
||||
self.content = Cow::Owned(String::from(self.content.trim_start()));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Span<'a> {
|
||||
|
@ -255,6 +280,15 @@ impl<'a> From<Spans<'a>> for String {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Spans<'a> {
|
||||
type Item = Span<'a>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
///
|
||||
|
@ -432,3 +466,153 @@ impl<'a> Extend<Spans<'a>> for Text<'a> {
|
|||
self.lines.extend(iter);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct WrappedText<'a> {
|
||||
text: Text<'a>,
|
||||
trim: bool,
|
||||
width: u16,
|
||||
column: u16,
|
||||
last_word_end: u16,
|
||||
was_whitespace: bool,
|
||||
}
|
||||
|
||||
impl<'a> WrappedText<'a> {
|
||||
pub fn new(width: u16) -> Self {
|
||||
Self {
|
||||
text: Text::default(),
|
||||
width,
|
||||
trim: true,
|
||||
column: 0,
|
||||
last_word_end: 0,
|
||||
was_whitespace: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trim(mut self, trim: bool) -> Self {
|
||||
self.trim = trim;
|
||||
self
|
||||
}
|
||||
|
||||
fn push_span(&mut self, span: Span<'a>) {
|
||||
if self.text.lines.is_empty() {
|
||||
self.text.lines.push(Spans::default());
|
||||
}
|
||||
let last_line = self.text.lines.len() - 1;
|
||||
self.text.lines[last_line].0.push(span);
|
||||
}
|
||||
|
||||
fn push_spans<T>(&mut self, spans: T)
|
||||
where
|
||||
T: IntoIterator<Item = Span<'a>>,
|
||||
{
|
||||
let mut iter = spans.into_iter();
|
||||
let mut pending_span = iter.next();
|
||||
while let Some(mut span) = pending_span.take() {
|
||||
let span_position = self.column;
|
||||
let mut breakpoint = None;
|
||||
// Skip leading whitespaces when trim is enabled
|
||||
if self.column == 0 && self.trim {
|
||||
span.trim_start();
|
||||
}
|
||||
for grapheme in UnicodeSegmentation::graphemes(span.content.as_ref(), true) {
|
||||
let grapheme_width = grapheme.width() as u16;
|
||||
// Ignore grapheme that are larger than the allowed width
|
||||
if grapheme_width > self.width {
|
||||
continue;
|
||||
}
|
||||
let is_whitespace = grapheme.chars().all(&char::is_whitespace);
|
||||
if is_whitespace && !self.was_whitespace && grapheme != NBSP {
|
||||
self.last_word_end = self.column;
|
||||
}
|
||||
let next_column = self.column.saturating_add(grapheme_width);
|
||||
if next_column > self.width {
|
||||
let width = self.last_word_end.saturating_sub(span_position) as usize;
|
||||
breakpoint = Some(width);
|
||||
break;
|
||||
}
|
||||
self.column = next_column;
|
||||
self.was_whitespace = is_whitespace;
|
||||
}
|
||||
if let Some(b) = breakpoint {
|
||||
pending_span = if b > 0 {
|
||||
let new_span = span.split_at_in_place(b);
|
||||
self.push_span(span);
|
||||
Some(new_span)
|
||||
} else {
|
||||
Some(span)
|
||||
};
|
||||
self.start_new_line();
|
||||
} else {
|
||||
self.push_span(span);
|
||||
pending_span = iter.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_new_line(&mut self) {
|
||||
self.column = 0;
|
||||
self.last_word_end = 0;
|
||||
self.text.lines.push(Spans::default());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extend<Spans<'a>> for WrappedText<'a> {
|
||||
fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
|
||||
for spans in iter {
|
||||
self.start_new_line();
|
||||
self.push_spans(spans);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<WrappedText<'a>> for Text<'a> {
|
||||
fn from(text: WrappedText<'a>) -> Text<'a> {
|
||||
text.text
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Style};
|
||||
|
||||
#[test]
|
||||
fn text_can_be_wrapped() {
|
||||
let mut t = WrappedText::new(10);
|
||||
t.extend(Text::from(Spans::from(vec![
|
||||
Span::raw("This is "),
|
||||
Span::styled("a test.", Style::default().fg(Color::Red)),
|
||||
])));
|
||||
t.extend(Text::from("It should wrap."));
|
||||
let t = Text::from(t);
|
||||
let expected = Text::from(vec![
|
||||
Spans::from(vec![
|
||||
Span::raw("This is "),
|
||||
Span::styled("a", Style::default().fg(Color::Red)),
|
||||
]),
|
||||
Spans::from(Span::styled("test.", Style::default().fg(Color::Red))),
|
||||
Spans::from("It should"),
|
||||
Spans::from("wrap."),
|
||||
]);
|
||||
assert_eq!(expected, t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_with_trailing_nbsp_can_be_wrapped() {
|
||||
let mut t = WrappedText::new(10);
|
||||
t.extend(Text::from(Spans::from(vec![
|
||||
Span::raw("Line1"),
|
||||
Span::styled(NBSP, Style::default().add_modifier(Modifier::UNDERLINED)),
|
||||
Span::raw("Line2"),
|
||||
])));
|
||||
let expected = Text::from(vec![
|
||||
Spans::from(vec![
|
||||
Span::raw("Line1"),
|
||||
Span::styled(NBSP, Style::default().add_modifier(Modifier::UNDERLINED)),
|
||||
]),
|
||||
Spans::from(vec![Span::raw("Line2")]),
|
||||
]);
|
||||
assert_eq!(expected, Text::from(t));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue