mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-25 22:20:31 +00:00
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:
parent
3aa8b9a259
commit
d999c1b434
4 changed files with 117 additions and 9 deletions
|
@ -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"))
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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│",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└──────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue