diff --git a/src/ui/widget.rs b/src/ui/widget.rs index d317008d..96ca25ba 100644 --- a/src/ui/widget.rs +++ b/src/ui/widget.rs @@ -12,6 +12,8 @@ use termion::event::Key; use ratatui::prelude::*; use ratatui::widgets::{Block, BorderType, Borders, Cell, Row, TableState}; +use self::container::Header; + use super::theme::style; use super::{layout, span}; @@ -145,7 +147,6 @@ pub struct TableProps { pub items: Vec, pub focus: bool, pub columns: Vec, - pub has_header: bool, pub has_footer: bool, pub cutoff: usize, pub cutoff_after: usize, @@ -158,7 +159,6 @@ impl Default for TableProps { items: vec![], focus: false, columns: vec![], - has_header: false, has_footer: false, cutoff: usize::MAX, cutoff_after: usize::MAX, @@ -174,6 +174,8 @@ pub struct Table { pub props: TableProps, /// Internal selection state state: TableState, + /// Table header widget + header: Option>, } impl Table { @@ -187,11 +189,6 @@ impl Table { self } - pub fn header(mut self, has_header: bool) -> Self { - self.props.has_header = has_header; - self - } - pub fn footer(mut self, has_footer: bool) -> Self { self.props.has_footer = has_footer; self @@ -208,6 +205,11 @@ impl Table { self } + pub fn header(mut self, header: Header) -> Self { + self.header = Some(header); + self + } + fn prev(&mut self) -> Option { let selected = self.selected().map(|current| current.saturating_sub(1)); self.state.select(selected); @@ -289,11 +291,11 @@ impl Table { } } -impl Widget for Table +impl Widget<(), A> for Table where R: ToRow, { - fn new(state: &S, action_tx: UnboundedSender) -> Self + fn new(state: &(), action_tx: UnboundedSender) -> Self where Self: Sized, { @@ -301,11 +303,12 @@ where action_tx: action_tx.clone(), props: TableProps::default(), state: TableState::default().with_selected(Some(0)), + header: None, } .move_with_state(state) } - fn move_with_state(self, _state: &S) -> Self + fn move_with_state(self, _state: &()) -> Self where Self: Sized, { @@ -350,6 +353,9 @@ where R: ToRow + Debug, { fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) { + let header_height = if self.header.is_some() { 3 } else { 0 }; + let [header_area, table_area] = + Layout::vertical([Constraint::Length(header_height), Constraint::Min(1)]).areas(area); let widths: Vec = self .props .columns @@ -366,7 +372,7 @@ where widths.iter().collect::>() }; - let borders = match (self.props.has_header, self.props.has_footer) { + let borders = match (self.header.is_some(), self.props.has_footer) { (false, false) => Borders::ALL, (true, false) => Borders::BOTTOM | Borders::LEFT | Borders::RIGHT, (false, true) => Borders::TOP | Borders::LEFT | Borders::RIGHT, @@ -407,16 +413,22 @@ where ) .highlight_style(style::highlight()); - frame.render_stateful_widget(rows, area, &mut self.state.clone()); + if let Some(header) = &self.header { + header.render::(frame, header_area, ()); + } + frame.render_stateful_widget(rows, table_area, &mut self.state.clone()); } else { let block = Block::default() .border_style(style::border(self.props.focus)) .border_type(BorderType::Rounded) .borders(borders); - frame.render_widget(block, area); + if let Some(header) = &self.header { + header.render::(frame, header_area, ()); + } + frame.render_widget(block, table_area); - let center = layout::centered_rect(area, 50, 10); + let center = layout::centered_rect(table_area, 50, 10); let hint = Text::from(span::default("Nothing to show".to_string())) .centered() .light_magenta() diff --git a/src/ui/widget/container.rs b/src/ui/widget/container.rs index ff6f10ed..0ebaa4f7 100644 --- a/src/ui/widget/container.rs +++ b/src/ui/widget/container.rs @@ -108,23 +108,55 @@ pub struct HeaderProps { pub focus: bool, } +impl Default for HeaderProps { + fn default() -> Self { + Self { + columns: vec![], + cutoff: usize::MAX, + cutoff_after: usize::MAX, + focus: false, + } + } +} + pub struct Header { /// Sending actions to the state store pub action_tx: UnboundedSender, + /// Internal props + props: HeaderProps, } -impl Widget for Header { - fn new(state: &S, action_tx: UnboundedSender) -> Self +impl Header { + pub fn columns(mut self, columns: Vec) -> Self { + self.props.columns = columns; + self + } + + pub fn focus(mut self, focus: bool) -> Self { + self.props.focus = focus; + self + } + + pub fn cutoff(mut self, cutoff: usize, cutoff_after: usize) -> Self { + self.props.cutoff = cutoff; + self.props.cutoff_after = cutoff_after; + self + } +} + +impl Widget<(), A> for Header { + fn new(state: &(), action_tx: UnboundedSender) -> Self where Self: Sized, { Self { action_tx: action_tx.clone(), + props: HeaderProps::default(), } .move_with_state(state) } - fn move_with_state(self, _state: &S) -> Self + fn move_with_state(self, _state: &()) -> Self where Self: Sized, { @@ -134,9 +166,10 @@ impl Widget for Header { fn handle_key_event(&mut self, _key: Key) {} } -impl Render for Header { - fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: HeaderProps) { - let widths: Vec = props +impl Render<()> for Header { + fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) { + let widths: Vec = self + .props .columns .iter() .filter_map(|column| { @@ -147,7 +180,8 @@ impl Render for Header { } }) .collect(); - let cells = props + let cells = self + .props .columns .iter() .filter_map(|column| { @@ -159,8 +193,11 @@ impl Render for Header { }) .collect::>(); - let widths = if area.width < props.cutoff as u16 { - widths.iter().take(props.cutoff_after).collect::>() + let widths = if area.width < self.props.cutoff as u16 { + widths + .iter() + .take(self.props.cutoff_after) + .collect::>() } else { widths.iter().collect::>() }; @@ -168,7 +205,7 @@ impl Render for Header { // Render header let block = HeaderBlock::default() .borders(Borders::ALL) - .border_style(style::border(props.focus)) + .border_style(style::border(self.props.focus)) .border_type(BorderType::Rounded); let header_layout = Layout::default()