Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Rework table header
Erik Kundt committed 2 years ago
commit 62ffd3de9cac746e09868dd25e8f43c1da8348cd
parent 2ee9528cc3f6d2be7dce8d7e793d24343477af88
2 files changed +73 -24
modified 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<R: ToRow> {
    pub items: Vec<R>,
    pub focus: bool,
    pub columns: Vec<Column>,
-
    pub has_header: bool,
    pub has_footer: bool,
    pub cutoff: usize,
    pub cutoff_after: usize,
@@ -158,7 +159,6 @@ impl<R: ToRow> Default for TableProps<R> {
            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<A, R: ToRow> {
    pub props: TableProps<R>,
    /// Internal selection state
    state: TableState,
+
    /// Table header widget
+
    header: Option<Header<A>>,
}

impl<A, R: ToRow> Table<A, R> {
@@ -187,11 +189,6 @@ impl<A, R: ToRow> Table<A, R> {
        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<A, R: ToRow> Table<A, R> {
        self
    }

+
    pub fn header(mut self, header: Header<A>) -> Self {
+
        self.header = Some(header);
+
        self
+
    }
+

    fn prev(&mut self) -> Option<usize> {
        let selected = self.selected().map(|current| current.saturating_sub(1));
        self.state.select(selected);
@@ -289,11 +291,11 @@ impl<A, R: ToRow> Table<A, R> {
    }
}

-
impl<S, A, R> Widget<S, A> for Table<A, R>
+
impl<A, R> Widget<(), A> for Table<A, R>
where
    R: ToRow,
{
-
    fn new(state: &S, action_tx: UnboundedSender<A>) -> Self
+
    fn new(state: &(), action_tx: UnboundedSender<A>) -> 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<B: Backend>(&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<Constraint> = self
            .props
            .columns
@@ -366,7 +372,7 @@ where
            widths.iter().collect::<Vec<_>>()
        };

-
        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::<B>(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::<B>(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()
modified 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<A> {
    /// Sending actions to the state store
    pub action_tx: UnboundedSender<A>,
+
    /// Internal props
+
    props: HeaderProps,
}

-
impl<S, A> Widget<S, A> for Header<A> {
-
    fn new(state: &S, action_tx: UnboundedSender<A>) -> Self
+
impl<A> Header<A> {
+
    pub fn columns(mut self, columns: Vec<Column>) -> 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<A> Widget<(), A> for Header<A> {
+
    fn new(state: &(), action_tx: UnboundedSender<A>) -> 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<S, A> Widget<S, A> for Header<A> {
    fn handle_key_event(&mut self, _key: Key) {}
}

-
impl<A> Render<HeaderProps> for Header<A> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, props: HeaderProps) {
-
        let widths: Vec<Constraint> = props
+
impl<A> Render<()> for Header<A> {
+
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
        let widths: Vec<Constraint> = self
+
            .props
            .columns
            .iter()
            .filter_map(|column| {
@@ -147,7 +180,8 @@ impl<A> Render<HeaderProps> for Header<A> {
                }
            })
            .collect();
-
        let cells = props
+
        let cells = self
+
            .props
            .columns
            .iter()
            .filter_map(|column| {
@@ -159,8 +193,11 @@ impl<A> Render<HeaderProps> for Header<A> {
            })
            .collect::<Vec<_>>();

-
        let widths = if area.width < props.cutoff as u16 {
-
            widths.iter().take(props.cutoff_after).collect::<Vec<_>>()
+
        let widths = if area.width < self.props.cutoff as u16 {
+
            widths
+
                .iter()
+
                .take(self.props.cutoff_after)
+
                .collect::<Vec<_>>()
        } else {
            widths.iter().collect::<Vec<_>>()
        };
@@ -168,7 +205,7 @@ impl<A> Render<HeaderProps> for Header<A> {
        // 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()