diff --git a/src/flux/ui/widget.rs b/src/flux/ui/widget.rs index 485d962d..4323b474 100644 --- a/src/flux/ui/widget.rs +++ b/src/flux/ui/widget.rs @@ -1,5 +1,6 @@ pub mod container; pub mod input; +pub mod text; use std::cmp; use std::fmt::Debug; diff --git a/src/flux/ui/widget/text.rs b/src/flux/ui/widget/text.rs new file mode 100644 index 00000000..c1a40d12 --- /dev/null +++ b/src/flux/ui/widget/text.rs @@ -0,0 +1,137 @@ +use tokio::sync::mpsc::UnboundedSender; + +use termion::event::Key; + +use ratatui::backend::Backend; +use ratatui::layout::{Constraint, Layout, Rect}; +use ratatui::text::Text; +use ratatui::widgets::{Block, BorderType, Borders}; + +use crate::flux::ui::theme::style; + +use super::{Render, Widget}; + +pub struct ParagraphProps<'a> { + pub content: Text<'a>, + pub focus: bool, + pub has_header: bool, + pub has_footer: bool, +} + +pub struct Paragraph { + /// Sending actions to the state store + pub action_tx: UnboundedSender, + /// Internal offset + offset: usize, + /// Internal progress + progress: usize, +} + +impl Paragraph { + pub fn scroll(&self) -> (u16, u16) { + (self.offset as u16, 0) + } + + pub fn prev(&mut self, len: usize, page_size: usize) -> (u16, u16) { + self.offset = self.offset.saturating_sub(1); + self.progress = Self::scroll_percent(self.offset, len, page_size); + self.scroll() + } + + pub fn next(&mut self, len: usize, page_size: usize) -> (u16, u16) { + if self.progress < 100 { + self.offset = self.offset.saturating_add(1); + self.progress = Self::scroll_percent(self.offset, len, page_size); + } + + self.scroll() + } + + pub fn prev_page(&mut self, len: usize, page_size: usize) -> (u16, u16) { + self.offset = self.offset.saturating_sub(page_size); + self.progress = Self::scroll_percent(self.offset, len, page_size); + self.scroll() + } + + pub fn next_page(&mut self, len: usize, page_size: usize) -> (u16, u16) { + let end = len.saturating_sub(page_size); + + self.offset = std::cmp::min(self.offset.saturating_add(page_size), end); + self.progress = Self::scroll_percent(self.offset, len, page_size); + self.scroll() + } + + pub fn begin(&mut self, len: usize, page_size: usize) -> (u16, u16) { + self.offset = 0; + self.progress = Self::scroll_percent(self.offset, len, page_size); + self.scroll() + } + + pub fn end(&mut self, len: usize, page_size: usize) -> (u16, u16) { + self.offset = len.saturating_sub(page_size); + self.progress = Self::scroll_percent(self.offset, len, page_size); + self.scroll() + } + + pub fn progress(&self) -> usize { + self.progress + } + + fn scroll_percent(offset: usize, len: usize, height: usize) -> usize { + if height >= len { + 100 + } else { + let y = offset as f64; + let h = height as f64; + let t = len.saturating_sub(1) as f64; + let v = y / (t - h) * 100_f64; + + std::cmp::max(0, std::cmp::min(100, v as usize)) + } + } +} + +impl Widget for Paragraph { + fn new(state: &S, action_tx: UnboundedSender) -> Self + where + Self: Sized, + { + Self { + action_tx: action_tx.clone(), + offset: 0, + progress: 0, + } + .move_with_state(state) + } + + fn move_with_state(self, _state: &S) -> Self + where + Self: Sized, + { + Self { ..self } + } + + fn name(&self) -> &str { + "paragraph" + } + + fn handle_key_event(&mut self, _key: Key) {} +} + +impl<'a, A> Render> for Paragraph { + fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: ParagraphProps) { + let block = Block::default() + .borders(Borders::LEFT | Borders::RIGHT) + .border_type(BorderType::Rounded) + .border_style(style::border(props.focus)); + frame.render_widget(block, area); + + let [content_area] = Layout::horizontal([Constraint::Min(1)]) + .horizontal_margin(2) + .areas(area); + let content = + ratatui::widgets::Paragraph::new(props.content.clone()).scroll((self.offset as u16, 0)); + + frame.render_widget(content, content_area); + } +}