Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
all: Improve footer
Archived did:key:z6MkswQE...2C1V opened 2 years ago

Implements new footer block widget and changes progress rendering.

9 files changed +293 -98 70208c35 b6ff4ddc
modified bin/commands/inbox/flux/select.rs
@@ -38,11 +38,23 @@ pub struct App {
}

#[derive(Clone, Debug)]
+
pub struct UIState {
+
    page_size: usize,
+
}
+

+
impl Default for UIState {
+
    fn default() -> Self {
+
        Self { page_size: 1 }
+
    }
+
}
+

+
#[derive(Clone, Debug)]
pub struct InboxState {
    notifications: Vec<NotificationItem>,
    selected: Option<NotificationItem>,
    mode: Mode,
    project: Project,
+
    ui: UIState,
}

impl TryFrom<&Context> for InboxState {
@@ -136,6 +148,7 @@ impl TryFrom<&Context> for InboxState {
            selected,
            mode: mode.clone(),
            project,
+
            ui: UIState::default(),
        })
    }
}
@@ -143,6 +156,7 @@ impl TryFrom<&Context> for InboxState {
pub enum Action {
    Exit { selection: Option<Selection> },
    Select { item: NotificationItem },
+
    PageSize(usize),
}

impl State<Action, Selection> for InboxState {
@@ -150,11 +164,15 @@ impl State<Action, Selection> for InboxState {

    fn handle_action(&mut self, action: Action) -> Option<Exit<Selection>> {
        match action {
+
            Action::Exit { selection } => Some(Exit { value: selection }),
            Action::Select { item } => {
                self.selected = Some(item);
                None
            }
-
            Action::Exit { selection } => Some(Exit { value: selection }),
+
            Action::PageSize(size) => {
+
                self.ui.page_size = size;
+
                None
+
            }
        }
    }
}
modified bin/commands/inbox/flux/select/ui.rs
@@ -1,12 +1,11 @@
use std::collections::HashMap;
-
use std::vec;

use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;

use ratatui::backend::Backend;
-
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
+
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Stylize;
use ratatui::text::Line;

@@ -16,7 +15,6 @@ use radicle_tui as tui;

use tui::flux::ui::cob::NotificationItem;
use tui::flux::ui::span;
-
use tui::flux::ui::theme::style;
use tui::flux::ui::widget::container::{Footer, FooterProps, Header, HeaderProps};
use tui::flux::ui::widget::{
    Render, Shortcut, Shortcuts, ShortcutsProps, Table, TableProps, Widget,
@@ -153,6 +151,7 @@ struct NotificationsProps {
    cutoff: usize,
    cutoff_after: usize,
    focus: bool,
+
    page_size: usize,
}

impl From<&InboxState> for NotificationsProps {
@@ -177,6 +176,7 @@ impl From<&InboxState> for NotificationsProps {
            cutoff: 200,
            cutoff_after: 5,
            focus: false,
+
            page_size: state.ui.page_size,
        }
    }
}
@@ -337,42 +337,52 @@ impl Notifications {

    fn render_footer<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect) {
        let filter = Line::from([span::blank()].to_vec());
-
        let stats = Line::from(
+
        let seen = Line::from(
            [
                span::positive(self.props.stats.get("Seen").unwrap_or(&0).to_string()).dim(),
                span::default(" Seen".to_string()).dim(),
-
                span::default(" | ".to_string()).style(style::border(self.props.focus)),
-
                span::default(self.props.stats.get("Unseen").unwrap_or(&0).to_string())
+
            ]
+
            .to_vec(),
+
        );
+
        let unseen = Line::from(
+
            [
+
                span::positive(self.props.stats.get("Unseen").unwrap_or(&0).to_string())
                    .magenta()
                    .dim(),
                span::default(" Unseen".to_string()).dim(),
-
                span::default(" | ".to_string()).style(style::border(self.props.focus)),
-
                span::default("Σ ".to_string()).dim(),
-
                span::default(self.props.notifications.len().to_string()).dim(),
            ]
            .to_vec(),
-
        )
-
        .alignment(Alignment::Right);
-

-
        let (step, len) = self.table.progress(self.props.notifications.len());
-
        let progress = Line::from(
+
        );
+
        let sum = Line::from(
            [
-
                span::default("| ".to_string()).style(style::border(self.props.focus)),
-
                span::progress(step, len),
+
                span::default("Σ ".to_string()).dim(),
+
                span::default(self.props.notifications.len().to_string()).dim(),
            ]
            .to_vec(),
-
        )
-
        .alignment(Alignment::Left);
+
        );
+

+
        let progress = self
+
            .table
+
            .progress_percentage(self.props.notifications.len(), self.props.page_size);
+
        let progress = span::default(format!("{}%", progress)).dim();

        self.footer.render::<B>(
            frame,
            area,
            FooterProps {
-
                cells: [filter.into(), stats.into(), progress.clone().into()],
+
                cells: [
+
                    filter.into(),
+
                    seen.clone().into(),
+
                    unseen.clone().into(),
+
                    sum.clone().into(),
+
                    progress.clone().into(),
+
                ],
                widths: [
                    Constraint::Fill(1),
-
                    Constraint::Fill(1),
-
                    Constraint::Length(7),
+
                    Constraint::Min(seen.width() as u16),
+
                    Constraint::Min(unseen.width() as u16),
+
                    Constraint::Min(sum.width() as u16),
+
                    Constraint::Min(4),
                ],
                focus: self.props.focus,
                cutoff: self.props.cutoff,
@@ -396,5 +406,12 @@ impl Render<()> for Notifications {
        self.render_header::<B>(frame, layout[0]);
        self.render_list::<B>(frame, layout[1]);
        self.render_footer::<B>(frame, layout[2]);
+

+
        let page_size = layout[1].height as usize;
+
        if page_size != self.props.page_size {
+
            let _ = self
+
                .action_tx
+
                .send(Action::PageSize(layout[1].height as usize));
+
        }
    }
}
modified bin/commands/issue/flux/select.rs
@@ -34,11 +34,23 @@ pub struct App {
}

#[derive(Clone, Debug)]
+
pub struct UIState {
+
    page_size: usize,
+
}
+

+
impl Default for UIState {
+
    fn default() -> Self {
+
        Self { page_size: 1 }
+
    }
+
}
+

+
#[derive(Clone, Debug)]
pub struct IssuesState {
    issues: Vec<IssueItem>,
    selected: Option<IssueItem>,
    mode: Mode,
    filter: Filter,
+
    ui: UIState,
}

impl TryFrom<&Context> for IssuesState {
@@ -68,6 +80,7 @@ impl TryFrom<&Context> for IssuesState {
            selected,
            mode: context.mode.clone(),
            filter: context.filter.clone(),
+
            ui: UIState::default(),
        })
    }
}
@@ -75,6 +88,7 @@ impl TryFrom<&Context> for IssuesState {
pub enum Action {
    Exit { selection: Option<Selection> },
    Select { item: IssueItem },
+
    PageSize(usize),
}

impl State<Action, Selection> for IssuesState {
@@ -86,6 +100,10 @@ impl State<Action, Selection> for IssuesState {
                self.selected = Some(item);
                None
            }
+
            Action::PageSize(size) => {
+
                self.ui.page_size = size;
+
                None
+
            }
            Action::Exit { selection } => Some(Exit { value: selection }),
        }
    }
modified bin/commands/issue/flux/select/ui.rs
@@ -9,14 +9,13 @@ use tokio::sync::mpsc::UnboundedSender;
use termion::event::Key;

use ratatui::backend::Backend;
-
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
+
use ratatui::layout::{Constraint, Direction, Layout, Rect};

use radicle_tui as tui;

use tui::common::cob::issue::Filter;
use tui::flux::ui::cob::IssueItem;
use tui::flux::ui::span;
-
use tui::flux::ui::theme::style;
use tui::flux::ui::widget::container::{Footer, FooterProps, Header, HeaderProps};
use tui::flux::ui::widget::{
    Render, Shortcut, Shortcuts, ShortcutsProps, Table, TableProps, Widget,
@@ -178,6 +177,7 @@ struct IssuesProps {
    cutoff: usize,
    cutoff_after: usize,
    focus: bool,
+
    page_size: usize,
}

impl From<&IssuesState> for IssuesProps {
@@ -210,6 +210,7 @@ impl From<&IssuesState> for IssuesProps {
            cutoff_after: 5,
            focus: false,
            stats,
+
            page_size: state.ui.page_size,
        }
    }
}
@@ -340,43 +341,52 @@ impl Issues {
            ]
            .to_vec(),
        );
-

-
        let stats = Line::from(
+
        let open = Line::from(
            [
                span::positive(self.props.stats.get("Open").unwrap_or(&0).to_string()).dim(),
                span::default(" Open".to_string()).dim(),
-
                span::default(" | ".to_string()).style(style::border(false)),
+
            ]
+
            .to_vec(),
+
        );
+
        let closed = Line::from(
+
            [
                span::default(self.props.stats.get("Closed").unwrap_or(&0).to_string())
                    .magenta()
                    .dim(),
                span::default(" Closed".to_string()).dim(),
-
                span::default(" | ".to_string()).style(style::border(self.props.focus)),
-
                span::default("Σ ".to_string()).dim(),
-
                span::default(self.props.issues.len().to_string()).dim(),
            ]
            .to_vec(),
-
        )
-
        .alignment(Alignment::Right);
-

-
        let (step, len) = self.table.progress(self.props.issues.len());
-
        let progress = Line::from(
+
        );
+
        let sum = Line::from(
            [
-
                span::default("| ".to_string()).style(style::border(self.props.focus)),
-
                span::progress(step, len),
+
                span::default("Σ ".to_string()).dim(),
+
                span::default(self.props.issues.len().to_string()).dim(),
            ]
            .to_vec(),
-
        )
-
        .alignment(Alignment::Left);
+
        );
+

+
        let progress = self
+
            .table
+
            .progress_percentage(self.props.issues.len(), self.props.page_size);
+
        let progress = span::default(format!("{}%", progress)).dim();

        self.footer.render::<B>(
            frame,
            area,
            FooterProps {
-
                cells: [filter.into(), stats.into(), progress.clone().into()],
+
                cells: [
+
                    filter.into(),
+
                    open.clone().into(),
+
                    closed.clone().into(),
+
                    sum.clone().into(),
+
                    progress.clone().into(),
+
                ],
                widths: [
                    Constraint::Fill(1),
-
                    Constraint::Fill(1),
-
                    Constraint::Length(7),
+
                    Constraint::Min(open.width() as u16),
+
                    Constraint::Min(closed.width() as u16),
+
                    Constraint::Min(sum.width() as u16),
+
                    Constraint::Min(4),
                ],
                focus: self.props.focus,
                cutoff: self.props.cutoff,
@@ -400,5 +410,12 @@ impl Render<()> for Issues {
        self.render_header::<B>(frame, layout[0]);
        self.render_list::<B>(frame, layout[1]);
        self.render_footer::<B>(frame, layout[2]);
+

+
        let page_size = layout[1].height as usize;
+
        if page_size != self.props.page_size {
+
            let _ = self
+
                .action_tx
+
                .send(Action::PageSize(layout[1].height as usize));
+
        }
    }
}
modified bin/commands/patch/flux/select.rs
@@ -34,11 +34,23 @@ pub struct App {
}

#[derive(Clone, Debug)]
+
pub struct UIState {
+
    page_size: usize,
+
}
+

+
impl Default for UIState {
+
    fn default() -> Self {
+
        Self { page_size: 1 }
+
    }
+
}
+

+
#[derive(Clone, Debug)]
pub struct PatchesState {
    patches: Vec<PatchItem>,
    selected: Option<PatchItem>,
    mode: Mode,
    filter: Filter,
+
    ui: UIState,
}

impl TryFrom<&Context> for PatchesState {
@@ -68,6 +80,7 @@ impl TryFrom<&Context> for PatchesState {
            selected,
            mode: context.mode.clone(),
            filter: context.filter.clone(),
+
            ui: UIState::default(),
        })
    }
}
@@ -75,6 +88,7 @@ impl TryFrom<&Context> for PatchesState {
pub enum Action {
    Exit { selection: Option<Selection> },
    Select { item: PatchItem },
+
    PageSize(usize),
}

impl State<Action, Selection> for PatchesState {
@@ -82,11 +96,15 @@ impl State<Action, Selection> for PatchesState {

    fn handle_action(&mut self, action: Action) -> Option<Exit<Selection>> {
        match action {
+
            Action::Exit { selection } => Some(Exit { value: selection }),
            Action::Select { item } => {
                self.selected = Some(item);
                None
            }
-
            Action::Exit { selection } => Some(Exit { value: selection }),
+
            Action::PageSize(size) => {
+
                self.ui.page_size = size;
+
                None
+
            }
        }
    }
}
modified bin/commands/patch/flux/select/ui.rs
@@ -8,7 +8,6 @@ use tokio::sync::mpsc::UnboundedSender;
use termion::event::Key;

use ratatui::backend::Backend;
-
use ratatui::layout::Alignment;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Stylize;
use ratatui::text::Line;
@@ -18,7 +17,6 @@ use radicle_tui as tui;
use tui::common::cob::patch::Filter;
use tui::flux::ui::cob::PatchItem;
use tui::flux::ui::span;
-
use tui::flux::ui::theme::style;
use tui::flux::ui::widget::container::{Footer, FooterProps, Header, HeaderProps};
use tui::flux::ui::widget::{
    Render, Shortcut, Shortcuts, ShortcutsProps, Table, TableProps, Widget,
@@ -196,6 +194,7 @@ struct PatchesProps {
    cutoff: usize,
    cutoff_after: usize,
    focus: bool,
+
    page_size: usize,
}

impl From<&PatchesState> for PatchesProps {
@@ -242,6 +241,7 @@ impl From<&PatchesState> for PatchesProps {
            cutoff_after: 5,
            focus: false,
            stats,
+
            page_size: state.ui.page_size,
        }
    }
}
@@ -298,9 +298,9 @@ impl Widget<PatchesState, Action> for Patches {
                    .and_then(|selected| self.props.patches.get(selected));

                // TODO: propagate error
-
                if let Some(notif) = selected {
+
                if let Some(patch) = selected {
                    let _ = self.action_tx.send(Action::Select {
-
                        item: notif.clone(),
+
                        item: patch.clone(),
                    });
                }
            }
@@ -313,9 +313,9 @@ impl Widget<PatchesState, Action> for Patches {
                    .and_then(|selected| self.props.patches.get(selected));

                // TODO: propagate error
-
                if let Some(notif) = selected {
+
                if let Some(patch) = selected {
                    let _ = self.action_tx.send(Action::Select {
-
                        item: notif.clone(),
+
                        item: patch.clone(),
                    });
                }
            }
@@ -373,51 +373,72 @@ impl Patches {
            ]
            .to_vec(),
        );
-

-
        let stats = Line::from(
+
        let draft = Line::from(
            [
                span::default(self.props.stats.get("Draft").unwrap_or(&0).to_string()).dim(),
                span::default(" Draft".to_string()).dim(),
-
                span::default(" | ".to_string()).style(style::border(false)),
+
            ]
+
            .to_vec(),
+
        );
+
        let open = Line::from(
+
            [
                span::positive(self.props.stats.get("Open").unwrap_or(&0).to_string()).dim(),
                span::default(" Open".to_string()).dim(),
-
                span::default(" | ".to_string()).style(style::border(false)),
+
            ]
+
            .to_vec(),
+
        );
+
        let merged = Line::from(
+
            [
                span::default(self.props.stats.get("Merged").unwrap_or(&0).to_string())
                    .magenta()
                    .dim(),
                span::default(" Merged".to_string()).dim(),
-
                span::default(" | ".to_string()).style(style::border(false)),
+
            ]
+
            .to_vec(),
+
        );
+
        let archived = Line::from(
+
            [
                span::default(self.props.stats.get("Archived").unwrap_or(&0).to_string())
                    .yellow()
                    .dim(),
                span::default(" Archived".to_string()).dim(),
-
                span::default(" | ".to_string()).style(style::border(self.props.focus)),
-
                span::default("Σ ".to_string()).dim(),
-
                span::default(self.props.patches.len().to_string()).dim(),
            ]
            .to_vec(),
-
        )
-
        .alignment(Alignment::Right);
-

-
        let (step, len) = self.table.progress(self.props.patches.len());
-
        let progress = Line::from(
+
        );
+
        let sum = Line::from(
            [
-
                span::default("| ".to_string()).style(style::border(self.props.focus)),
-
                span::progress(step, len),
+
                span::default("Σ ".to_string()).dim(),
+
                span::default(self.props.patches.len().to_string()).dim(),
            ]
            .to_vec(),
-
        )
-
        .alignment(Alignment::Left);
+
        );
+

+
        let progress = self
+
            .table
+
            .progress_percentage(self.props.patches.len(), self.props.page_size);
+
        let progress = span::default(format!("{}%", progress)).dim();

        self.footer.render::<B>(
            frame,
            area,
            FooterProps {
-
                cells: [filter.into(), stats.into(), progress.clone().into()],
+
                cells: [
+
                    filter.into(),
+
                    draft.clone().into(),
+
                    open.clone().into(),
+
                    merged.clone().into(),
+
                    archived.clone().into(),
+
                    sum.clone().into(),
+
                    progress.clone().into(),
+
                ],
                widths: [
                    Constraint::Fill(1),
-
                    Constraint::Fill(1),
-
                    Constraint::Length(7),
+
                    Constraint::Min(draft.width() as u16),
+
                    Constraint::Min(open.width() as u16),
+
                    Constraint::Min(merged.width() as u16),
+
                    Constraint::Min(archived.width() as u16),
+
                    Constraint::Min(sum.width() as u16),
+
                    Constraint::Min(4),
                ],
                focus: self.props.focus,
                cutoff: self.props.cutoff,
@@ -441,5 +462,12 @@ impl Render<()> for Patches {
        self.render_header::<B>(frame, layout[0]);
        self.render_list::<B>(frame, layout[1]);
        self.render_footer::<B>(frame, layout[2]);
+

+
        let page_size = layout[1].height as usize;
+
        if page_size != self.props.page_size {
+
            let _ = self
+
                .action_tx
+
                .send(Action::PageSize(layout[1].height as usize));
+
        }
    }
}
modified src/flux/ui/ext.rs
@@ -113,6 +113,14 @@ impl Widget for HeaderBlock {
    }
}

+
#[derive(Clone)]
+
pub enum FooterBlockType {
+
    Single,
+
    Begin,
+
    End,
+
    Repeat,
+
}
+

pub struct FooterBlock {
    /// Visible borders
    borders: Borders,
@@ -121,6 +129,8 @@ pub struct FooterBlock {
    /// Type of the border. The default is plain lines but one can choose to have rounded corners
    /// or doubled lines instead.
    border_type: BorderType,
+
    ///
+
    block_type: FooterBlockType,
    /// Widget style
    style: Style,
}
@@ -128,7 +138,8 @@ pub struct FooterBlock {
impl Default for FooterBlock {
    fn default() -> Self {
        Self {
-
            borders: Borders::NONE,
+
            block_type: FooterBlockType::Single,
+
            borders: Self::borders(FooterBlockType::Single),
            border_style: Default::default(),
            border_type: BorderType::Rounded,
            style: Default::default(),
@@ -137,25 +148,35 @@ impl Default for FooterBlock {
}

impl FooterBlock {
-
    pub fn border_style(mut self, style: Style) -> FooterBlock {
+
    pub fn border_style(mut self, style: Style) -> Self {
        self.border_style = style;
        self
    }

-
    pub fn style(mut self, style: Style) -> FooterBlock {
+
    pub fn style(mut self, style: Style) -> Self {
        self.style = style;
        self
    }

-
    pub fn borders(mut self, flag: Borders) -> FooterBlock {
-
        self.borders = flag;
+
    pub fn block_type(mut self, block_type: FooterBlockType) -> Self {
+
        self.block_type = block_type.clone();
+
        self.borders = Self::borders(block_type);
        self
    }

-
    pub fn border_type(mut self, border_type: BorderType) -> FooterBlock {
+
    pub fn border_type(mut self, border_type: BorderType) -> Self {
        self.border_type = border_type;
        self
    }
+

+
    fn borders(block_type: FooterBlockType) -> Borders {
+
        match block_type {
+
            FooterBlockType::Single | FooterBlockType::Begin => Borders::ALL,
+
            FooterBlockType::End | FooterBlockType::Repeat => {
+
                Borders::TOP | Borders::RIGHT | Borders::BOTTOM
+
            }
+
        }
+
    }
}

impl Widget for FooterBlock {
@@ -200,13 +221,21 @@ impl Widget for FooterBlock {

        // Corners
        if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
+
            let symbol = match self.block_type {
+
                FooterBlockType::Begin | FooterBlockType::Repeat => symbols::line::HORIZONTAL_UP,
+
                _ => symbols.bottom_right,
+
            };
            buf.get_mut(area.right() - 1, area.bottom() - 1)
-
                .set_symbol(symbols.bottom_right)
+
                .set_symbol(symbol)
                .set_style(self.border_style);
        }
        if self.borders.contains(Borders::RIGHT | Borders::TOP) {
+
            let symbol = match self.block_type {
+
                FooterBlockType::Begin | FooterBlockType::Repeat => symbols::line::HORIZONTAL_DOWN,
+
                _ => symbols::line::VERTICAL_LEFT,
+
            };
            buf.get_mut(area.right() - 1, area.top())
-
                .set_symbol(symbols::line::VERTICAL_LEFT)
+
                .set_symbol(symbol)
                .set_style(self.border_style);
        }
        if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
modified src/flux/ui/widget.rs
@@ -169,6 +169,25 @@ impl<A> Table<A> {

        (cmp::min(step, len), len)
    }
+

+
    pub fn progress_percentage(&self, len: usize, page_size: usize) -> usize {
+
        let step = self.selected().unwrap_or_default();
+
        let page_size = page_size as f64;
+
        let len = len as f64;
+

+
        let lines = page_size + step.saturating_sub(page_size as usize) as f64;
+
        let progress = (lines / len * 100_f64).ceil() as usize;
+

+
        if progress > 97 {
+
            Self::map_range((0, progress), (0, 100), progress)
+
        } else {
+
            progress
+
        }
+
    }
+

+
    fn map_range(from: (usize, usize), to: (usize, usize), value: usize) -> usize {
+
        to.0 + (value - from.0) * (to.1 - to.0) / (from.1 - from.0)
+
    }
}

impl<S, A> Widget<S, A> for Table<A> {
modified src/flux/ui/widget/container.rs
@@ -7,7 +7,7 @@ use termion::event::Key;
use ratatui::prelude::*;
use ratatui::widgets::{BorderType, Borders, Row};

-
use crate::flux::ui::ext::{FooterBlock, HeaderBlock};
+
use crate::flux::ui::ext::{FooterBlock, FooterBlockType, HeaderBlock};
use crate::flux::ui::theme::style;

use super::{Render, Widget};
@@ -51,20 +51,15 @@ impl<S, A> Widget<S, A> for Footer<A> {
    fn handle_key_event(&mut self, _key: Key) {}
}

-
impl<'a, A, const W: usize> Render<FooterProps<'a, W>> for Footer<A> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, props: FooterProps<W>) {
-
        let widths = props.widths.to_vec();
-
        let widths = if area.width < props.cutoff as u16 {
-
            widths.iter().take(props.cutoff_after).collect::<Vec<_>>()
-
        } else {
-
            widths.iter().collect::<Vec<_>>()
-
        };
-

-
        let footer_block = FooterBlock::default()
-
            .borders(Borders::ALL)
-
            .border_style(style::border(props.focus))
-
            .border_type(BorderType::Rounded);
-

+
impl<A> Footer<A> {
+
    fn render_cell<'a>(
+
        &self,
+
        frame: &mut ratatui::Frame,
+
        area: Rect,
+
        block_type: FooterBlockType,
+
        text: impl Into<Text<'a>>,
+
        focus: bool,
+
    ) {
        let footer_layout = Layout::default()
            .direction(Direction::Vertical)
            .constraints(vec![Constraint::Min(1)])
@@ -72,13 +67,49 @@ impl<'a, A, const W: usize> Render<FooterProps<'a, W>> for Footer<A> {
            .horizontal_margin(1)
            .split(area);

-
        let footer = ratatui::widgets::Table::default()
-
            .column_spacing(1)
-
            .header(Row::new(props.cells))
-
            .widths(widths);
-

+
        let footer_block = FooterBlock::default()
+
            .border_style(style::border(focus))
+
            .block_type(block_type);
        frame.render_widget(footer_block, area);
-
        frame.render_widget(footer, footer_layout[0]);
+
        frame.render_widget(text.into(), footer_layout[0]);
+
    }
+
}
+

+
impl<'a, A, const W: usize> Render<FooterProps<'a, W>> for Footer<A> {
+
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, props: FooterProps<W>) {
+
        let widths = props.widths.to_vec();
+
        let widths = if area.width < props.cutoff as u16 {
+
            widths
+
                .into_iter()
+
                .take(props.cutoff_after)
+
                .collect::<Vec<_>>()
+
        } else {
+
            widths.into_iter().collect::<Vec<_>>()
+
        };
+
        let widths = widths
+
            .into_iter()
+
            .map(|c| match c {
+
                Constraint::Min(min) => Constraint::Length(min.saturating_add(3)),
+
                _ => c,
+
            })
+
            .collect::<Vec<_>>();
+

+
        let layout = Layout::horizontal(widths).split(area);
+
        let cells = props.cells.iter().zip(layout.iter()).collect::<Vec<_>>();
+

+
        let last = cells.len().saturating_sub(1);
+
        let len = cells.len();
+

+
        for (i, (cell, area)) in cells.into_iter().enumerate() {
+
            // let last = cells.len().saturating_sub(1);
+
            let block_type = match i {
+
                0 if len == 1 => FooterBlockType::Single,
+
                0 => FooterBlockType::Begin,
+
                _ if i == last => FooterBlockType::End,
+
                _ => FooterBlockType::Repeat,
+
            };
+
            self.render_cell(frame, *area, block_type, cell.clone(), props.focus);
+
        }
    }
}