diff --git a/examples/demo/ui.rs b/examples/demo/ui.rs index 600ba975..c1c74f0e 100644 --- a/examples/demo/ui.rs +++ b/examples/demo/ui.rs @@ -274,7 +274,7 @@ where .fg(Color::Magenta) .add_modifier(Modifier::BOLD), )); - let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); + let paragraph = Paragraph::new(text).block(block).wrap(Wrap::default()); f.render_widget(paragraph, area); } diff --git a/examples/paragraph.rs b/examples/paragraph.rs index 587046c9..f3b7f5b4 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -81,20 +81,20 @@ fn main() -> Result<(), Box> { .style(Style::default().bg(Color::White).fg(Color::Black)) .block(create_block("Left, wrap")) .alignment(Alignment::Left) - .wrap(Wrap { trim: true }); + .wrap(Wrap::default()); f.render_widget(paragraph, chunks[1]); let paragraph = Paragraph::new(text.clone()) .style(Style::default().bg(Color::White).fg(Color::Black)) .block(create_block("Center, wrap")) .alignment(Alignment::Center) - .wrap(Wrap { trim: true }) + .wrap(Wrap::default()) .scroll((scroll, 0)); f.render_widget(paragraph, chunks[2]); let paragraph = Paragraph::new(text) .style(Style::default().bg(Color::White).fg(Color::Black)) .block(create_block("Right, wrap")) .alignment(Alignment::Right) - .wrap(Wrap { trim: true }); + .wrap(Wrap::default()); f.render_widget(paragraph, chunks[3]); })?; diff --git a/examples/paragraph2.rs b/examples/paragraph2.rs new file mode 100644 index 00000000..9cedd18d --- /dev/null +++ b/examples/paragraph2.rs @@ -0,0 +1,81 @@ +#[allow(dead_code)] +mod util; + +use crate::util::event::{Event, Events}; +use std::{error::Error, io}; +use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; +use tui::{ + backend::TermionBackend, + layout::{Alignment, Margin}, + style::{Color, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, Paragraph, Wrap}, + Terminal, +}; + +const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"; + +fn main() -> Result<(), Box> { + // Terminal initialization + let stdout = io::stdout().into_raw_mode()?; + let stdout = MouseTerminal::from(stdout); + let stdout = AlternateScreen::from(stdout); + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let events = Events::new(); + + let mut i = 0; + let mut lines = Vec::with_capacity(100); + while i < 100 { + lines.push((i, format!("{}: {}", i, LOREM_IPSUM))); + i += 1; + } + loop { + terminal.draw(|f| { + let size = f.size(); + let text: Vec = lines + .iter() + .cloned() + .map(|(j, l)| { + let span = if i == j + 1 { + Span::styled(l, Style::default().bg(Color::Yellow)) + } else { + Span::raw(l) + }; + Spans::from(span) + }) + .collect(); + let mut wrap = Wrap::default(); + wrap.scroll_callback = Some(Box::new(|text_area, lines| { + let len = lines.len() as u16; + (len.saturating_sub(text_area.height), 0) + })); + let paragraph = Paragraph::new(text) + .block(Block::default().borders(Borders::ALL)) + .wrap(wrap) + .alignment(Alignment::Left); + f.render_widget( + paragraph, + size.inner(&Margin { + vertical: 2, + horizontal: 2, + }), + ); + })?; + + match events.next()? { + Event::Tick => { + lines.push((i, format!("{}: {}", i, LOREM_IPSUM))); + lines.remove(0); + i += 1; + } + Event::Input(key) => { + if key == Key::Char('q') { + break; + } + } + } + } + Ok(()) +} diff --git a/examples/popup.rs b/examples/popup.rs index ca58bc5a..bd81f0f4 100644 --- a/examples/popup.rs +++ b/examples/popup.rs @@ -81,12 +81,12 @@ fn main() -> Result<(), Box> { let paragraph = Paragraph::new(text.clone()) .block(Block::default().title("Left Block").borders(Borders::ALL)) - .alignment(Alignment::Left).wrap(Wrap { trim: true }); + .alignment(Alignment::Left).wrap(Wrap::default()); f.render_widget(paragraph, chunks[0]); let paragraph = Paragraph::new(text) .block(Block::default().title("Right Block").borders(Borders::ALL)) - .alignment(Alignment::Left).wrap(Wrap { trim: true }); + .alignment(Alignment::Left).wrap(Wrap::default()); f.render_widget(paragraph, chunks[1]); let block = Block::default().title("Popup").borders(Borders::ALL); diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index f4ebd8d5..6aa3f6a1 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -40,9 +40,8 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) /// .block(Block::default().title("Paragraph").borders(Borders::ALL)) /// .style(Style::default().fg(Color::White).bg(Color::Black)) /// .alignment(Alignment::Center) -/// .wrap(Wrap { trim: true }); +/// .wrap(Wrap::default()); /// ``` -#[derive(Debug, Clone)] pub struct Paragraph<'a> { /// A block to wrap the widget in block: Option>, @@ -70,7 +69,7 @@ pub struct Paragraph<'a> { /// - Here is another point that is long enough to wrap"#); /// /// // With leading spaces trimmed (window width of 30 chars): -/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true }); +/// Paragraph::new(bullet_points.clone()).wrap(Wrap::default()); /// // Some indented points: /// // - First thing goes here and is /// // long so that it wraps @@ -78,19 +77,30 @@ pub struct Paragraph<'a> { /// // is long enough to wrap /// /// // But without trimming, indentation is preserved: -/// Paragraph::new(bullet_points).wrap(Wrap { trim: false }); +/// Paragraph::new(bullet_points).wrap(Wrap { trim: false, ..Wrap::default() }); /// // Some indented points: /// // - First thing goes here /// // and is long so that it wraps /// // - Here is another point /// // that is long enough to wrap /// ``` -#[derive(Debug, Clone, Copy)] pub struct Wrap { /// Should leading whitespace be trimmed pub trim: bool, + pub scroll_callback: Option>, } +impl Default for Wrap { + fn default() -> Wrap { + Wrap { + trim: true, + scroll_callback: None, + } + } +} + +pub type ScrollCallback = dyn FnOnce(Rect, &[(Vec>, u16)]) -> (u16, u16); + impl<'a> Paragraph<'a> { pub fn new(text: T) -> Paragraph<'a> where @@ -130,6 +140,42 @@ impl<'a> Paragraph<'a> { self.alignment = alignment; self } + + fn draw_lines<'b, T>( + &self, + text_area: Rect, + buf: &mut Buffer, + mut line_composer: T, + scroll: (u16, u16), + ) where + T: LineComposer<'b>, + { + let mut y = 0; + let mut i = 0; + while let Some((current_line, current_line_width)) = line_composer.next_line() { + if i >= scroll.0 { + let cell_y = text_area.top().saturating_add(y); + let mut x = get_line_offset(current_line_width, text_area.width, self.alignment); + for StyledGrapheme { symbol, style } in current_line { + buf.get_mut(text_area.left() + x, cell_y) + .set_symbol(if symbol.is_empty() { + // If the symbol is empty, the last char which rendered last time will + // leave on the line. It's a quick fix. + " " + } else { + symbol + }) + .set_style(*style); + x += symbol.width() as u16; + } + y += 1; + } + i += 1; + if y >= text_area.height { + break; + } + } + } } impl<'a> Widget for Paragraph<'a> { @@ -158,40 +204,54 @@ impl<'a> Widget for Paragraph<'a> { // composers to operate on lines instead of a stream of graphemes. .chain(iter::once(StyledGrapheme { symbol: "\n", - style: self.style, + style, })) }); - let mut line_composer: Box = if let Some(Wrap { trim }) = self.wrap { - Box::new(WordWrapper::new(&mut styled, text_area.width, trim)) - } else { - let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width)); - if let Alignment::Left = self.alignment { - line_composer.set_horizontal_offset(self.scroll.1); - } - line_composer - }; - let mut y = 0; - while let Some((current_line, current_line_width)) = line_composer.next_line() { - if y >= self.scroll.0 { - let mut x = get_line_offset(current_line_width, text_area.width, self.alignment); - for StyledGrapheme { symbol, style } in current_line { - buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0) - .set_symbol(if symbol.is_empty() { - // If the symbol is empty, the last char which rendered last time will - // leave on the line. It's a quick fix. - " " - } else { - symbol - }) - .set_style(*style); - x += symbol.width() as u16; + match self.wrap { + None => { + let mut line_composer = LineTruncator::new(&mut styled, text_area.width); + if let Alignment::Left = self.alignment { + line_composer.set_horizontal_offset(self.scroll.1); } + self.draw_lines(text_area, buf, line_composer, self.scroll); } - y += 1; - if y >= text_area.height + self.scroll.0 { - break; + Some(Wrap { + trim, + scroll_callback: None, + }) => { + let line_composer = WordWrapper::new(&mut styled, text_area.width, trim); + self.draw_lines(text_area, buf, line_composer, self.scroll); } - } + Some(Wrap { + trim, + ref mut scroll_callback, + }) => { + let mut line_composer = WordWrapper::new(&mut styled, text_area.width, trim); + let mut lines = Vec::new(); + while let Some((current_line, current_line_width)) = line_composer.next_line() { + lines.push((Vec::from(current_line), current_line_width)); + } + let f = scroll_callback.take().unwrap(); + let scroll = f(text_area, lines.as_ref()); + self.draw_lines(text_area, buf, WrappedLines { lines, index: 0 }, scroll); + } + }; + } +} + +struct WrappedLines<'a> { + lines: Vec<(Vec>, u16)>, + index: usize, +} + +impl<'a> LineComposer<'a> for WrappedLines<'a> { + fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> { + if self.index >= self.lines.len() { + return None; + } + let (line, width) = &self.lines[self.index]; + self.index += 1; + Some((&line, *width)) } } diff --git a/tests/widgets_paragraph.rs b/tests/widgets_paragraph.rs index 9c556eb4..2e6c3d87 100644 --- a/tests/widgets_paragraph.rs +++ b/tests/widgets_paragraph.rs @@ -25,7 +25,7 @@ fn widgets_paragraph_can_wrap_its_content() { let paragraph = Paragraph::new(text) .block(Block::default().borders(Borders::ALL)) .alignment(alignment) - .wrap(Wrap { trim: true }); + .wrap(Wrap::default()); f.render_widget(paragraph, size); }) .unwrap(); @@ -91,7 +91,7 @@ fn widgets_paragraph_renders_double_width_graphemes() { let text = vec![Spans::from(s)]; let paragraph = Paragraph::new(text) .block(Block::default().borders(Borders::ALL)) - .wrap(Wrap { trim: true }); + .wrap(Wrap::default()); f.render_widget(paragraph, size); }) .unwrap(); @@ -123,7 +123,7 @@ fn widgets_paragraph_renders_mixed_width_graphemes() { let text = vec![Spans::from(s)]; let paragraph = Paragraph::new(text) .block(Block::default().borders(Borders::ALL)) - .wrap(Wrap { trim: true }); + .wrap(Wrap::default()); f.render_widget(paragraph, size); }) .unwrap();