feat(widgets/paragraph): Add horizontal scroll (#329)

* `Paragraph:scroll` takes a tuple of offsets instead of a single vertical offset.
* `LineTruncator` takes this new horizontal offset into account to let the paragraph scroll horizontally.
This commit is contained in:
Xiaopeng Li 2020-07-07 05:47:52 +08:00 committed by GitHub
parent 3aa8b9a259
commit d999c1b434
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 117 additions and 9 deletions

View file

@ -82,7 +82,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.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"))

View file

@ -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<dyn LineComposer> = 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;
}
}

View file

@ -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<Item = Styled<'a>>,
max_line_width: u16,
current_line: Vec<Styled<'a>>,
/// 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::*;

View file

@ -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│",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└──────────────────┘",
]),
);
}