diff --git a/examples/paragraph.rs b/examples/paragraph.rs index 788f5fb..ae71bd1 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -82,7 +82,7 @@ fn main() -> Result<(), Box> { .block(block.clone().title("Center, wrap")) .alignment(Alignment::Center) .wrap(true) - .scroll(scroll); + .scroll((scroll, 0)); f.render_widget(paragraph, chunks[2]); let paragraph = Paragraph::new(text.iter()) .block(block.clone().title("Right, wrap")) diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index b5a57ce..e763cb1 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -50,7 +50,7 @@ where /// Should we parse the text for embedded commands raw: bool, /// Scroll - scroll: u16, + scroll: (u16, u16), /// Aligenment of the text alignment: Alignment, } @@ -66,7 +66,7 @@ where wrapping: false, raw: false, text, - scroll: 0, + scroll: (0, 0), alignment: Alignment::Left, } } @@ -91,7 +91,7 @@ where self } - pub fn scroll(mut self, offset: u16) -> Paragraph<'a, 't, T> { + pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a, 't, T> { self.scroll = offset; self } @@ -136,21 +136,31 @@ where let mut line_composer: Box = if self.wrapping { Box::new(WordWrapper::new(&mut styled, text_area.width)) } else { - Box::new(LineTruncator::new(&mut styled, text_area.width)) + 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 { + if y >= self.scroll.0 { let mut x = get_line_offset(current_line_width, text_area.width, self.alignment); for Styled(symbol, style) in current_line { - buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll) - .set_symbol(symbol) + 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; } } y += 1; - if y >= text_area.height + self.scroll { + if y >= text_area.height + self.scroll.0 { break; } } diff --git a/src/widgets/reflow.rs b/src/widgets/reflow.rs index 4d39a11..c5db940 100644 --- a/src/widgets/reflow.rs +++ b/src/widgets/reflow.rs @@ -1,4 +1,5 @@ use crate::style::Style; +use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; const NBSP: &str = "\u{00a0}"; @@ -124,6 +125,8 @@ pub struct LineTruncator<'a, 'b> { symbols: &'b mut dyn Iterator>, max_line_width: u16, current_line: Vec>, + /// Record the offet to skip render + horizontal_offset: u16, } impl<'a, 'b> LineTruncator<'a, 'b> { @@ -134,9 +137,14 @@ impl<'a, 'b> LineTruncator<'a, 'b> { LineTruncator { symbols, max_line_width, + horizontal_offset: 0, current_line: vec![], } } + + pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) { + self.horizontal_offset = horizontal_offset; + } } impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> { @@ -150,6 +158,7 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> { let mut skip_rest = false; let mut symbols_exhausted = true; + let mut horizontal_offset = self.horizontal_offset as usize; for Styled(symbol, style) in &mut self.symbols { symbols_exhausted = false; @@ -169,6 +178,19 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> { break; } + let symbol = if horizontal_offset == 0 { + symbol + } else { + let w = symbol.width(); + if w > horizontal_offset { + let t = trim_offset(symbol, horizontal_offset); + horizontal_offset = 0; + t + } else { + horizontal_offset -= w; + "" + } + }; current_line_width += symbol.width() as u16; self.current_line.push(Styled(symbol, style)); } @@ -189,6 +211,22 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> { } } +/// This function will return a str slice which start at specified offset. +/// As src is a unicode str, start offset has to be calculated with each character. +fn trim_offset(src: &str, mut offset: usize) -> &str { + let mut start = 0; + for c in UnicodeSegmentation::graphemes(src, true) { + let w = c.width(); + if w <= offset { + offset -= w; + start += c.len(); + } else { + break; + } + } + &src[start..] +} + #[cfg(test)] mod test { use super::*; diff --git a/tests/widgets_paragraph.rs b/tests/widgets_paragraph.rs index 180f94b..3fb0b3e 100644 --- a/tests/widgets_paragraph.rs +++ b/tests/widgets_paragraph.rs @@ -139,3 +139,63 @@ fn widgets_paragraph_renders_mixed_width_graphemes() { ]); terminal.backend().assert_buffer(&expected); } + +#[test] +fn widgets_paragraph_can_scroll_horizontally() { + let test_case = |alignment, scroll, expected| { + let backend = TestBackend::new(20, 10); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|f| { + let size = f.size(); + let text = [Text::raw( + "段落现在可以水平滚动了! +Paragraph can scroll horizontally! +Short line +", + )]; + let paragraph = Paragraph::new(text.iter()) + .block(Block::default().borders(Borders::ALL)) + .alignment(alignment) + .scroll(scroll); + f.render_widget(paragraph, size); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); + }; + + test_case( + Alignment::Left, + (0, 7), + Buffer::with_lines(vec![ + "┌──────────────────┐", + "│在可以水平滚动了!│", + "│ph can scroll hori│", + "│ine │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "└──────────────────┘", + ]), + ); + // only support Alignment::Left + test_case( + Alignment::Right, + (0, 7), + Buffer::with_lines(vec![ + "┌──────────────────┐", + "│段落现在可以水平滚│", + "│Paragraph can scro│", + "│ Short line│", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "└──────────────────┘", + ]), + ); +}