Add scrolling
This commit is contained in:
@ -16,14 +16,42 @@
|
||||
|
||||
use super::Component;
|
||||
use color_eyre::eyre::{Result, WrapErr};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use crate::components::Action;
|
||||
use crate::widgets::{BottomAlignedParagraph, OverlappableWidget};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Backlog {}
|
||||
pub struct Backlog {
|
||||
scroll: u64,
|
||||
}
|
||||
|
||||
impl Component for Backlog {
|
||||
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
match key {
|
||||
KeyEvent {
|
||||
code: KeyCode::PageUp,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _,
|
||||
} => {
|
||||
self.scroll += 20; // TODO: use the component height
|
||||
},
|
||||
KeyEvent {
|
||||
code: KeyCode::PageDown,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _,
|
||||
} => {
|
||||
self.scroll = self.scroll.saturating_sub(20); // TODO: use the component height
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&mut self,
|
||||
frame: &mut Frame<'_>,
|
||||
@ -37,17 +65,48 @@ impl Component for Backlog {
|
||||
let active_buffer = buffers.active_buffer();
|
||||
let mut items = active_buffer.content();
|
||||
items.reverse();
|
||||
let mut items = items.into_iter();
|
||||
let mut scroll = self.scroll;
|
||||
|
||||
// Skip widgets at the bottom (if scrolled up), and render the first visible one
|
||||
loop {
|
||||
let Some(item) = items.next() else {
|
||||
break;
|
||||
};
|
||||
let widget = BottomAlignedParagraph::new(item);
|
||||
let expected_height = widget.height(text_area.width);
|
||||
|
||||
if scroll.saturating_sub(expected_height) > text_area.height.into() {
|
||||
// Paragraph is too far down, not displayed
|
||||
scroll -= expected_height;
|
||||
continue;
|
||||
}
|
||||
|
||||
let widget = widget.scroll(scroll);
|
||||
let height = widget.render_overlap(text_area, frame.buffer_mut());
|
||||
text_area.height = text_area.height.saturating_sub(height);
|
||||
|
||||
scroll = scroll.saturating_sub(expected_height);
|
||||
}
|
||||
if text_area.height == 0 {
|
||||
// No more room to display other paragraphs, stop now
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Render other widgets
|
||||
for item in items {
|
||||
let widget = BottomAlignedParagraph::new(item);
|
||||
let height = widget.render_overlap(text_area, frame.buffer_mut());
|
||||
assert!(area.height >= height, "{:?} {}", area, height);
|
||||
text_area.height -= height; // Remove lines at the bottom used by this paragraph
|
||||
text_area.height = text_area.height.saturating_sub(height); // Remove lines at the bottom used by this paragraph
|
||||
if text_area.height == 0 {
|
||||
// No more room to display other paragraphs, stop now
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// If there is empty room on screen, ask the buffer to fetch more backlog if it can
|
||||
if text_area.height > 0 {
|
||||
active_buffer.request_back_pagination(100);
|
||||
}
|
||||
// There is empty room on screen, ask the buffer to fetch more backlog if it can
|
||||
active_buffer.request_back_pagination(100);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -99,6 +99,10 @@ impl Component for Home {
|
||||
Ok(Some(Action::Render))
|
||||
},
|
||||
_ => {
|
||||
assert!(
|
||||
self.backlog.handle_key_events(key)?.is_none(),
|
||||
"backlog.handle_key_events returned Some"
|
||||
);
|
||||
self.textarea.input(key);
|
||||
Ok(Some(Action::Render))
|
||||
},
|
||||
|
@ -20,13 +20,15 @@ use ratatui::text::StyledGrapheme;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::OverlappableWidget;
|
||||
use crate::widgets::reflow::WordWrapper;
|
||||
use crate::widgets::reflow::{WordWrapper, WrappedLine};
|
||||
|
||||
/// A variant of [`Paragraph`](ratatui::widgets::Paragraph) that implements [`BottomAlignedWidget`]
|
||||
/// and always wraps
|
||||
#[derive(Debug)]
|
||||
pub struct BottomAlignedParagraph<'a> {
|
||||
text: Text<'a>,
|
||||
scroll: u16,
|
||||
/// Number of lines at the bottom that should not be rendered
|
||||
scroll: u64,
|
||||
}
|
||||
|
||||
impl<'a> BottomAlignedParagraph<'a> {
|
||||
@ -40,21 +42,19 @@ impl<'a> BottomAlignedParagraph<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// How many lines should be skipped at the beginning
|
||||
/// How many lines should be skipped at the bottom
|
||||
///
|
||||
/// This is like [`Paragraph::scroll`](ratatui::widgets::Paragraph::scroll), but it's only vertical.
|
||||
pub fn scroll(mut self, offset: u16) -> BottomAlignedParagraph<'a> {
|
||||
pub fn scroll(mut self, offset: u64) -> BottomAlignedParagraph<'a> {
|
||||
self.scroll = offset;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> OverlappableWidget for BottomAlignedParagraph<'a> {
|
||||
fn render_overlap(self, area: Rect, buf: &mut Buffer) -> u16 {
|
||||
// Inspired by https://github.com/ratatui-org/ratatui/blob/9f371000968044e09545d66068c4ed4ea4b35d8a/src/widgets/paragraph.rs#L214-L275
|
||||
/// Returns lines with at most the given width, starting from the last one
|
||||
fn wrap_lines(&self, width: u16) -> Vec<Vec<StyledGrapheme<'_>>> {
|
||||
let trim = false;
|
||||
let style = Style::default();
|
||||
let lines: Vec<_> = WordWrapper::new(
|
||||
WordWrapper::new(
|
||||
self.text.lines.iter().map(|line| {
|
||||
(
|
||||
line
|
||||
@ -64,24 +64,41 @@ impl<'a> OverlappableWidget for BottomAlignedParagraph<'a> {
|
||||
Alignment::Left,
|
||||
)
|
||||
}),
|
||||
area.width,
|
||||
width,
|
||||
trim,
|
||||
)
|
||||
.skip(self.scroll as usize)
|
||||
.map_into_iter(|line: crate::widgets::reflow::WrappedLine| line.line.to_vec())
|
||||
.collect();
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.skip(self.scroll as usize)
|
||||
.take(u16::MAX as usize) // Avoid overflows in ratatui for excessively long messages
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns how many lines it would actuall draw if rendered with the given height
|
||||
pub fn height(&self, width: u16) -> u64 {
|
||||
self.wrap_lines(width).len() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> OverlappableWidget for BottomAlignedParagraph<'a> {
|
||||
fn render_overlap(self, area: Rect, buf: &mut Buffer) -> u16 {
|
||||
// Inspired by https://github.com/ratatui-org/ratatui/blob/9f371000968044e09545d66068c4ed4ea4b35d8a/src/widgets/paragraph.rs#L214-L275
|
||||
let lines = self.wrap_lines(area.width);
|
||||
|
||||
let text_area = area; // Borders not supported by BottomAlignedParagraph (yet?)
|
||||
let text_area_height = text_area.height as usize;
|
||||
let lines = if lines.len() > text_area_height {
|
||||
// Overflow; keep only the last lines
|
||||
&lines[(lines.len() - text_area_height)..]
|
||||
&lines[..text_area_height]
|
||||
} else {
|
||||
&lines[..]
|
||||
};
|
||||
|
||||
assert!(lines.len() <= text_area_height);
|
||||
|
||||
for (y, line) in lines.into_iter().rev().enumerate() {
|
||||
for (y, line) in lines.into_iter().enumerate() {
|
||||
let mut x = 0;
|
||||
for StyledGrapheme { symbol, style } in line {
|
||||
let width = symbol.width();
|
||||
|
Reference in New Issue
Block a user