diff --git a/src/components/backlog.rs b/src/components/backlog.rs index d2f9fdd..c12906f 100644 --- a/src/components/backlog.rs +++ b/src/components/backlog.rs @@ -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> { + 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(()) } diff --git a/src/components/home.rs b/src/components/home.rs index bdd0ca8..b3fc4de 100644 --- a/src/components/home.rs +++ b/src/components/home.rs @@ -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)) }, diff --git a/src/widgets/bottom_aligned_paragraph.rs b/src/widgets/bottom_aligned_paragraph.rs index b7eb9c0..d2a929b 100644 --- a/src/widgets/bottom_aligned_paragraph.rs +++ b/src/widgets/bottom_aligned_paragraph.rs @@ -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>> { 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::>() + .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();