use std::ops::Range;
use textwrap::{
core::{break_words, Word},
word_splitters::split_words,
WordSeparator, WordSplitter, WrapAlgorithm,
};
use crate::{backend, layout::Layout, Widget};
#[derive(Debug, Clone)]
pub struct Text<S> {
pub text: S,
lines: Vec<Range<usize>>,
line_offset: u16,
width: u16,
}
impl<S: PartialEq> PartialEq for Text<S> {
fn eq(&self, other: &Self) -> bool {
self.text == other.text
}
}
impl<S: Eq> Eq for Text<S> {}
impl<S: AsRef<str>> Text<S> {
pub fn new(text: S) -> Self {
Self {
text,
lines: Vec::new(),
width: 0,
line_offset: 0,
}
}
pub fn force_recompute(&mut self) {
self.line_offset = u16::MAX;
self.width = u16::MAX;
}
fn max_height(&mut self, layout: Layout) -> u16 {
let width = layout.available_width();
if self.width != width || self.line_offset != layout.line_offset {
wrap(self.text.as_ref(), layout, &mut self.lines);
self.width = width;
self.line_offset = layout.line_offset;
}
self.lines.len() as u16
}
}
impl<S: AsRef<str>> Widget for Text<S> {
fn render<B: backend::Backend>(
&mut self,
layout: &mut Layout,
backend: &mut B,
) -> std::io::Result<()> {
let height = self.max_height(*layout);
let text = self.text.as_ref();
if height == 1 {
backend.write_all(text[self.lines[0].clone()].as_bytes())?;
layout.offset_y += 1;
backend.move_cursor_to(layout.offset_x, layout.offset_y)?;
} else {
let start = layout.get_start(height) as usize;
let nlines = height.min(layout.max_height);
for (i, line) in self
.lines
.iter()
.skip(start)
.take(nlines as usize)
.enumerate()
{
backend.write_all(text[line.clone()].as_bytes())?;
backend.move_cursor_to(layout.offset_x, layout.offset_y + i as u16 + 1)?;
}
layout.offset_y += nlines;
}
layout.line_offset = 0;
Ok(())
}
fn height(&mut self, layout: &mut Layout) -> u16 {
let height = self.max_height(*layout).min(layout.max_height);
layout.offset_y += height;
height
}
fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
layout.offset_cursor((layout.line_offset, 0))
}
fn handle_key(&mut self, _: crate::events::KeyEvent) -> bool {
false
}
}
impl<S: AsRef<str>> AsRef<str> for Text<S> {
fn as_ref(&self) -> &str {
self.text.as_ref()
}
}
impl<S: AsRef<str>> From<S> for Text<S> {
fn from(text: S) -> Self {
Self::new(text)
}
}
fn wrap(text: &str, layout: Layout, lines: &mut Vec<Range<usize>>) {
lines.clear();
let width = layout.available_width() as usize;
let line_widths = [width.saturating_sub(layout.line_offset as usize), width];
let mut start = 0;
for line in text.split_inclusive('\n') {
let next_start = start + line.len();
let line = line.strip_suffix('\n').unwrap_or(line);
let line = line.strip_suffix('\r').unwrap_or(line);
let first_line = lines.is_empty();
if line.len() < width && !(first_line && layout.line_offset > 0) {
lines.push(start..start + line.trim_end_matches(' ').len());
} else {
wrap_line(line, start, &line_widths, first_line, lines);
}
start = next_start;
}
}
fn wrap_line(
line: &str,
start: usize,
line_widths: &[usize; 2],
first_line: bool,
lines: &mut Vec<Range<usize>>,
) {
let words = WordSeparator::new().find_words(line);
let words = split_words(words, &WordSplitter::HyphenSplitter);
let mut words = break_words(words, line_widths[1]);
if first_line && line_widths[0] != line_widths[1] {
words.insert(0, Word::from(""));
}
let mut idx = start;
for words in WrapAlgorithm::new().wrap(&words, line_widths) {
let last_word = match words.last() {
Some(word) => word,
None => {
lines.push(idx..idx);
continue;
}
};
debug_assert!(last_word.penalty.is_empty());
let len = words
.iter()
.map(|word| word.len() + word.whitespace.len())
.sum::<usize>();
lines.push(idx..idx + len - last_word.whitespace.len());
idx += len;
}
}
#[cfg(test)]
mod tests {
use crate::{backend::TestBackend, test_consts::*};
use super::*;
#[test]
fn test_wrap() {
fn test(text: &str, indent: usize, max_width: usize, nlines: usize) {
let layout = Layout::new(indent as u16, (max_width as u16, 100).into());
let mut wrapped = Vec::new();
wrap(text, layout, &mut wrapped);
let indent_str = " ".repeat(indent);
let filled = textwrap::fill(
text,
textwrap::Options::new(max_width).initial_indent(&indent_str),
);
let expected: Vec<_> = filled[indent..].lines().collect();
let actual: Vec<_> = wrapped.iter().map(|r| &text[r.clone()]).collect();
assert_eq!(expected, actual);
assert_eq!(nlines, wrapped.len());
let mut lines = actual.into_iter();
assert!(lines.next().unwrap().chars().count() <= max_width - indent);
for line in lines {
assert!(line.chars().count() <= max_width);
}
}
test("Hello World", 0, 80, 1);
test("Hello World", 0, 6, 2);
test(LOREM, 40, 80, 7);
test(UNICODE, 40, 80, 7);
test("Hello\n\nWorld \n", 0, 80, 3);
test("Hello\r\nWorld", 3, 80, 2);
test(
"a-very-long-hyphenated-word and a supercalifragilisticexpialidocious one",
5,
12,
8,
);
}
#[test]
fn test_text_height() {
let mut layout = Layout::new(40, (80, 100).into());
let mut text = Text::new(LOREM);
assert_eq!(text.max_height(layout), 7);
assert_eq!(text.height(&mut layout.with_max_height(5)), 5);
layout.line_offset = 0;
layout.width = 110;
assert_eq!(text.height(&mut layout.clone()), text.max_height(layout));
assert_eq!(text.height(&mut layout.clone()), 5);
let mut layout = Layout::new(40, (80, 100).into());
let mut text = Text::new(UNICODE);
assert_eq!(text.max_height(layout), 7);
assert_eq!(text.height(&mut layout.with_max_height(5)), 5);
layout.line_offset = 0;
layout.width = 110;
assert_eq!(text.height(&mut layout.clone()), text.max_height(layout));
assert_eq!(text.height(&mut layout.clone()), 5);
}
#[test]
fn test_render_single_line() {
let size = (100, 20).into();
let mut layout = Layout::new(0, size);
let mut backend = TestBackend::new(size);
let mut text = Text::new("Hello, World!");
text.render(&mut layout, &mut backend).unwrap();
crate::assert_backend_snapshot!(backend);
assert_eq!(layout, layout.with_offset(0, 1));
}
#[test]
fn test_render_multiline() {
let size = (100, 20).into();
let mut layout = Layout::new(0, size);
let mut backend = TestBackend::new(size);
let mut text = Text::new(LOREM);
text.render(&mut layout, &mut backend).unwrap();
crate::assert_backend_snapshot!(backend);
assert_eq!(layout, Layout::new(0, size).with_offset(0, 5));
layout = Layout::new(0, size).with_offset(10, 10);
backend.reset_with_layout(layout);
let mut text = Text::new(UNICODE);
text.render(&mut layout, &mut backend).unwrap();
crate::assert_backend_snapshot!(backend);
assert_eq!(layout, Layout::new(0, size).with_offset(10, 16));
}
}