Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Rework widget and render traits
Erik Kundt committed 2 years ago
commit 937580af7bea8f5fb6bcc0a90de5cf1576168eba
parent f02369894247aab8d648f6659e1cc0c78350db2b
8 files changed +162 -178
modified bin/commands/inbox/select/ui.rs
@@ -17,7 +17,7 @@ use tui::ui::span;
use tui::ui::widget::container::{Footer, Header};
use tui::ui::widget::input::TextField;
use tui::ui::widget::text::Paragraph;
-
use tui::ui::widget::{Column, Render, Shortcuts, Table, Widget};
+
use tui::ui::widget::{Column, Render, Shortcuts, Table, View};
use tui::Selection;

use crate::tui_inbox::common::{InboxOperation, Mode, RepositoryMode, SelectionMode};
@@ -53,7 +53,7 @@ pub struct ListPage<'a> {
    shortcuts: Shortcuts<Action>,
}

-
impl<'a> Widget<State, Action> for ListPage<'a> {
+
impl<'a> View<State, Action> for ListPage<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -103,9 +103,9 @@ impl<'a> Widget<State, Action> for ListPage<'a> {

    fn handle_key_event(&mut self, key: termion::event::Key) {
        if self.props.show_search {
-
            <Search as Widget<State, Action>>::handle_key_event(&mut self.search, key)
+
            <Search as View<State, Action>>::handle_key_event(&mut self.search, key)
        } else if self.props.show_help {
-
            <Help as Widget<State, Action>>::handle_key_event(&mut self.help, key)
+
            <Help as View<State, Action>>::handle_key_event(&mut self.help, key)
        } else {
            match key {
                Key::Esc | Key::Ctrl('c') => {
@@ -118,7 +118,7 @@ impl<'a> Widget<State, Action> for ListPage<'a> {
                    let _ = self.action_tx.send(Action::OpenHelp);
                }
                _ => {
-
                    <Notifications as Widget<State, Action>>::handle_key_event(
+
                    <Notifications as View<State, Action>>::handle_key_event(
                        &mut self.notifications,
                        key,
                    );
@@ -129,8 +129,8 @@ impl<'a> Widget<State, Action> for ListPage<'a> {
    }
}

-
impl<'a> Render<()> for ListPage<'a> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, _area: Rect, _props: ()) {
+
impl<'a, B: Backend> Render<B, ()> for ListPage<'a> {
+
    fn render(&self, frame: &mut ratatui::Frame, _area: Rect, _props: ()) {
        let area = frame.size();
        let layout = tui::ui::layout::default_page(area, 0u16, 1u16);

@@ -138,16 +138,25 @@ impl<'a> Render<()> for ListPage<'a> {
            let component_layout = Layout::vertical([Constraint::Min(1), Constraint::Length(2)])
                .split(layout.component);

-
            self.notifications
-
                .render::<B>(frame, component_layout[0], ());
-
            self.search.render::<B>(frame, component_layout[1], ());
+
            <Notifications<'_> as Render<B, ()>>::render(
+
                &self.notifications,
+
                frame,
+
                component_layout[0],
+
                (),
+
            );
+
            <Search as Render<B, ()>>::render(&self.search, frame, component_layout[1], ());
        } else if self.props.show_help {
-
            self.help.render::<B>(frame, layout.component, ());
+
            <Help<'_> as Render<B, ()>>::render(&self.help, frame, layout.component, ());
        } else {
-
            self.notifications.render::<B>(frame, layout.component, ());
+
            <Notifications<'_> as Render<B, ()>>::render(
+
                &self.notifications,
+
                frame,
+
                layout.component,
+
                (),
+
            );
        }

-
        self.shortcuts.render::<B>(frame, layout.shortcuts, ());
+
        <Shortcuts<_> as Render<B, ()>>::render(&self.shortcuts, frame, layout.shortcuts, ());
    }
}

@@ -225,7 +234,7 @@ struct Notifications<'a> {
    footer: Footer<'a, Action>,
}

-
impl<'a> Widget<State, Action> for Notifications<'a> {
+
impl<'a> View<State, Action> for Notifications<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
        let props = NotificationsProps::from(state);
        let name = match state.mode.repository() {
@@ -326,7 +335,7 @@ impl<'a> Widget<State, Action> for Notifications<'a> {
                    });
            }
            _ => {
-
                <Table<Action, NotificationItem> as Widget<(), Action>>::handle_key_event(
+
                <Table<Action, NotificationItem> as View<(), Action>>::handle_key_event(
                    &mut self.table,
                    key,
                );
@@ -414,19 +423,19 @@ impl<'a> Notifications<'a> {
    }
}

-
impl<'a> Render<()> for Notifications<'a> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<'a, B: Backend> Render<B, ()> for Notifications<'a> {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let header_height = 3_usize;

        let page_size = if self.props.show_search {
-
            self.table.render::<B>(frame, area, ());
+
            <Table<'_, _, _> as Render<B, ()>>::render(&self.table, frame, area, ());

            (area.height as usize).saturating_sub(header_height)
        } else {
            let layout = Layout::vertical([Constraint::Min(1), Constraint::Length(3)]).split(area);

-
            self.table.render::<B>(frame, layout[0], ());
-
            self.footer.render::<B>(frame, layout[1], ());
+
            <Table<'_, _, _> as Render<B, ()>>::render(&self.table, frame, layout[0], ());
+
            <Footer<'_, _> as Render<B, ()>>::render(&self.footer, frame, layout[1], ());

            (area.height as usize).saturating_sub(header_height)
        };
@@ -442,7 +451,7 @@ pub struct Search {
    pub input: TextField<Action>,
}

-
impl Widget<State, Action> for Search {
+
impl View<State, Action> for Search {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -472,10 +481,7 @@ impl Widget<State, Action> for Search {
                let _ = self.action_tx.send(Action::ApplySearch);
            }
            _ => {
-
                <TextField<Action> as Widget<State, Action>>::handle_key_event(
-
                    &mut self.input,
-
                    key,
-
                );
+
                <TextField<Action> as View<State, Action>>::handle_key_event(&mut self.input, key);
                let _ = self.action_tx.send(Action::UpdateSearch {
                    value: self.input.read().to_string(),
                });
@@ -484,13 +490,13 @@ impl Widget<State, Action> for Search {
    }
}

-
impl Render<()> for Search {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<B: Backend> Render<B, ()> for Search {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let layout = Layout::horizontal(Constraint::from_mins([0]))
            .horizontal_margin(1)
            .split(area);

-
        self.input.render::<B>(frame, layout[0], ());
+
        <TextField<_> as Render<B, ()>>::render(&self.input, frame, layout[0], ());
    }
}

@@ -652,7 +658,7 @@ pub struct Help<'a> {
    footer: Footer<'a, Action>,
}

-
impl<'a> Widget<State, Action> for Help<'a> {
+
impl<'a> View<State, Action> for Help<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -714,14 +720,14 @@ impl<'a> Widget<State, Action> for Help<'a> {
                let _ = self.action_tx.send(Action::CloseHelp);
            }
            _ => {
-
                <Paragraph<_> as Widget<(), _>>::handle_key_event(&mut self.content, key);
+
                <Paragraph<_> as View<(), _>>::handle_key_event(&mut self.content, key);
            }
        }
    }
}

-
impl<'a> Render<()> for Help<'a> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<'a, B: Backend> Render<B, ()> for Help<'a> {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let [header_area, content_area, footer_area] = Layout::vertical([
            Constraint::Length(3),
            Constraint::Min(1),
@@ -729,9 +735,9 @@ impl<'a> Render<()> for Help<'a> {
        ])
        .areas(area);

-
        self.header.render::<B>(frame, header_area, ());
-
        self.content.render::<B>(frame, content_area, ());
-
        self.footer.render::<B>(frame, footer_area, ());
+
        <Header<'_, _> as Render<B, ()>>::render(&self.header, frame, header_area, ());
+
        <Paragraph<'_, _> as Render<B, ()>>::render(&self.content, frame, content_area, ());
+
        <Footer<'_, _> as Render<B, ()>>::render(&self.footer, frame, footer_area, ());

        let page_size = content_area.height as usize;
        if page_size != self.props.page_size {
modified bin/commands/issue/select/ui.rs
@@ -19,7 +19,7 @@ use tui::ui::span;
use tui::ui::widget::container::{Footer, Header};
use tui::ui::widget::input::TextField;
use tui::ui::widget::text::Paragraph;
-
use tui::ui::widget::{Column, Render, Shortcuts, Table, Widget};
+
use tui::ui::widget::{Column, Render, Shortcuts, Table, View};
use tui::Selection;

use crate::tui_issue::common::IssueOperation;
@@ -56,7 +56,7 @@ pub struct ListPage<'a> {
    shortcuts: Shortcuts<Action>,
}

-
impl<'a> Widget<State, Action> for ListPage<'a> {
+
impl<'a> View<State, Action> for ListPage<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -106,9 +106,9 @@ impl<'a> Widget<State, Action> for ListPage<'a> {

    fn handle_key_event(&mut self, key: termion::event::Key) {
        if self.props.show_search {
-
            <Search as Widget<State, Action>>::handle_key_event(&mut self.search, key)
+
            <Search as View<State, Action>>::handle_key_event(&mut self.search, key)
        } else if self.props.show_help {
-
            <Help as Widget<State, Action>>::handle_key_event(&mut self.help, key)
+
            <Help as View<State, Action>>::handle_key_event(&mut self.help, key)
        } else {
            match key {
                Key::Esc | Key::Ctrl('c') => {
@@ -121,7 +121,7 @@ impl<'a> Widget<State, Action> for ListPage<'a> {
                    let _ = self.action_tx.send(Action::OpenHelp);
                }
                _ => {
-
                    <Issues as Widget<State, Action>>::handle_key_event(&mut self.issues, key);
+
                    <Issues as View<State, Action>>::handle_key_event(&mut self.issues, key);
                }
            }
        }
@@ -129,8 +129,8 @@ impl<'a> Widget<State, Action> for ListPage<'a> {
    }
}

-
impl<'a> Render<()> for ListPage<'a> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, _area: Rect, _props: ()) {
+
impl<'a, B: Backend> Render<B, ()> for ListPage<'a> {
+
    fn render(&self, frame: &mut ratatui::Frame, _area: Rect, _props: ()) {
        let area = frame.size();
        let layout = tui::ui::layout::default_page(area, 0u16, 1u16);

@@ -138,15 +138,15 @@ impl<'a> Render<()> for ListPage<'a> {
            let component_layout = Layout::vertical([Constraint::Min(1), Constraint::Length(2)])
                .split(layout.component);

-
            self.issues.render::<B>(frame, component_layout[0], ());
-
            self.search.render::<B>(frame, component_layout[1], ());
+
            <Issues<'_> as Render<B, ()>>::render(&self.issues, frame, component_layout[0], ());
+
            <Search as Render<B, ()>>::render(&self.search, frame, component_layout[1], ());
        } else if self.props.show_help {
-
            self.help.render::<B>(frame, layout.component, ());
+
            <Help<'_> as Render<B, ()>>::render(&self.help, frame, layout.component, ());
        } else {
-
            self.issues.render::<B>(frame, layout.component, ());
+
            <Issues<'_> as Render<B, ()>>::render(&self.issues, frame, layout.component, ());
        }

-
        self.shortcuts.render::<B>(frame, layout.shortcuts, ());
+
        <Shortcuts<_> as Render<B, ()>>::render(&self.shortcuts, frame, layout.shortcuts, ());
    }
}

@@ -236,7 +236,7 @@ struct Issues<'a> {
    footer: Footer<'a, Action>,
}

-
impl<'a> Widget<State, Action> for Issues<'a> {
+
impl<'a> View<State, Action> for Issues<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
        let props = IssuesProps::from(state);

@@ -329,7 +329,7 @@ impl<'a> Widget<State, Action> for Issues<'a> {
                    });
            }
            _ => {
-
                <Table<Action, IssueItem> as Widget<(), Action>>::handle_key_event(
+
                <Table<Action, IssueItem> as View<(), Action>>::handle_key_event(
                    &mut self.table,
                    key,
                );
@@ -435,19 +435,19 @@ impl<'a> Issues<'a> {
    }
}

-
impl<'a> Render<()> for Issues<'a> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<'a, B: Backend> Render<B, ()> for Issues<'a> {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let header_height = 3_usize;

        let page_size = if self.props.show_search {
-
            self.table.render::<B>(frame, area, ());
+
            <Table<'_, _, _> as Render<B, ()>>::render(&self.table, frame, area, ());

            (area.height as usize).saturating_sub(header_height)
        } else {
            let layout = Layout::vertical([Constraint::Min(1), Constraint::Length(3)]).split(area);

-
            self.table.render::<B>(frame, layout[0], ());
-
            self.footer.render::<B>(frame, layout[1], ());
+
            <Table<'_, _, _> as Render<B, ()>>::render(&self.table, frame, layout[0], ());
+
            <Footer<'_, _> as Render<B, ()>>::render(&self.footer, frame, layout[1], ());

            (area.height as usize).saturating_sub(header_height)
        };
@@ -463,7 +463,7 @@ pub struct Search {
    pub input: TextField<Action>,
}

-
impl Widget<State, Action> for Search {
+
impl View<State, Action> for Search {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -493,10 +493,7 @@ impl Widget<State, Action> for Search {
                let _ = self.action_tx.send(Action::ApplySearch);
            }
            _ => {
-
                <TextField<Action> as Widget<State, Action>>::handle_key_event(
-
                    &mut self.input,
-
                    key,
-
                );
+
                <TextField<Action> as View<State, Action>>::handle_key_event(&mut self.input, key);
                let _ = self.action_tx.send(Action::UpdateSearch {
                    value: self.input.read().to_string(),
                });
@@ -505,13 +502,13 @@ impl Widget<State, Action> for Search {
    }
}

-
impl Render<()> for Search {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<B: Backend> Render<B, ()> for Search {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let layout = Layout::horizontal(Constraint::from_mins([0]))
            .horizontal_margin(1)
            .split(area);

-
        self.input.render::<B>(frame, layout[0], ());
+
        <TextField<_> as Render<B, ()>>::render(&self.input, frame, layout[0], ());
    }
}

@@ -672,7 +669,7 @@ pub struct Help<'a> {
    footer: Footer<'a, Action>,
}

-
impl<'a> Widget<State, Action> for Help<'a> {
+
impl<'a> View<State, Action> for Help<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -734,14 +731,14 @@ impl<'a> Widget<State, Action> for Help<'a> {
                let _ = self.action_tx.send(Action::CloseHelp);
            }
            _ => {
-
                <Paragraph<_> as Widget<(), _>>::handle_key_event(&mut self.content, key);
+
                <Paragraph<_> as View<(), _>>::handle_key_event(&mut self.content, key);
            }
        }
    }
}

-
impl<'a> Render<()> for Help<'a> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<'a, B: Backend> Render<B, ()> for Help<'a> {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let [header_area, content_area, footer_area] = Layout::vertical([
            Constraint::Length(3),
            Constraint::Min(1),
@@ -749,9 +746,9 @@ impl<'a> Render<()> for Help<'a> {
        ])
        .areas(area);

-
        self.header.render::<B>(frame, header_area, ());
-
        self.content.render::<B>(frame, content_area, ());
-
        self.footer.render::<B>(frame, footer_area, ());
+
        <Header<'_, _> as Render<B, ()>>::render(&self.header, frame, header_area, ());
+
        <Paragraph<'_, _> as Render<B, ()>>::render(&self.content, frame, content_area, ());
+
        <Footer<'_, _> as Render<B, ()>>::render(&self.footer, frame, footer_area, ());

        let page_size = content_area.height as usize;
        if page_size != self.props.page_size {
modified bin/commands/patch/select/ui.rs
@@ -20,7 +20,7 @@ use tui::ui::span;
use tui::ui::widget::container::{Footer, Header};
use tui::ui::widget::input::TextField;
use tui::ui::widget::text::Paragraph;
-
use tui::ui::widget::{Column, Render, Shortcuts, Table, Widget};
+
use tui::ui::widget::{Column, Render, Shortcuts, Table, View, Widget};
use tui::Selection;

use crate::tui_patch::common::Mode;
@@ -42,6 +42,9 @@ impl From<&State> for ListPageProps {
    }
}

+
impl<'a, B: Backend> Widget<State, Action, B> for Patches<'a> {}
+
impl<B: Backend> Widget<State, Action, B> for Search {}
+

pub struct ListPage<'a> {
    /// Action sender
    pub action_tx: UnboundedSender<Action>,
@@ -57,7 +60,7 @@ pub struct ListPage<'a> {
    shortcuts: Shortcuts<Action>,
}

-
impl<'a> Widget<State, Action> for ListPage<'a> {
+
impl<'a> View<State, Action> for ListPage<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -94,7 +97,7 @@ impl<'a> Widget<State, Action> for ListPage<'a> {
            }
        };

-
        let shortcuts = self.shortcuts.move_with_state(&());
+
        let shortcuts = self.shortcuts.move_with_state(state);
        let shortcuts = shortcuts.shortcuts(&shorts);

        ListPage {
@@ -109,9 +112,9 @@ impl<'a> Widget<State, Action> for ListPage<'a> {

    fn handle_key_event(&mut self, key: termion::event::Key) {
        if self.props.show_search {
-
            <Search as Widget<State, Action>>::handle_key_event(&mut self.search, key)
+
            <Search as View<State, Action>>::handle_key_event(&mut self.search, key)
        } else if self.props.show_help {
-
            <Help as Widget<State, Action>>::handle_key_event(&mut self.help, key);
+
            <Help as View<State, Action>>::handle_key_event(&mut self.help, key);
        } else {
            match key {
                Key::Esc | Key::Ctrl('c') => {
@@ -124,7 +127,7 @@ impl<'a> Widget<State, Action> for ListPage<'a> {
                    let _ = self.action_tx.send(Action::OpenHelp);
                }
                _ => {
-
                    <Patches as Widget<State, Action>>::handle_key_event(&mut self.patches, key);
+
                    <Patches as View<State, Action>>::handle_key_event(&mut self.patches, key);
                }
            }
        }
@@ -132,8 +135,8 @@ impl<'a> Widget<State, Action> for ListPage<'a> {
    }
}

-
impl<'a> Render<()> for ListPage<'a> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, _area: Rect, _props: ()) {
+
impl<'a, B: Backend> Render<B, ()> for ListPage<'a> {
+
    fn render(&self, frame: &mut ratatui::Frame, _area: Rect, _props: ()) {
        let area = frame.size();
        let layout = tui::ui::layout::default_page(area, 0u16, 1u16);

@@ -141,15 +144,15 @@ impl<'a> Render<()> for ListPage<'a> {
            let component_layout = Layout::vertical([Constraint::Min(1), Constraint::Length(2)])
                .split(layout.component);

-
            self.patches.render::<B>(frame, component_layout[0], ());
-
            self.search.render::<B>(frame, component_layout[1], ());
+
            <Patches<'_> as Render<B, ()>>::render(&self.patches, frame, component_layout[0], ());
+
            <Search as Render<B, ()>>::render(&self.search, frame, component_layout[1], ());
        } else if self.props.show_help {
-
            self.help.render::<B>(frame, layout.component, ());
+
            <Help<'_> as Render<B, ()>>::render(&self.help, frame, layout.component, ());
        } else {
-
            self.patches.render::<B>(frame, layout.component, ());
+
            <Patches<'_> as Render<B, ()>>::render(&self.patches, frame, layout.component, ());
        }

-
        self.shortcuts.render::<B>(frame, layout.shortcuts, ());
+
        <Shortcuts<_> as Render<B, ()>>::render(&self.shortcuts, frame, layout.shortcuts, ());
    }
}

@@ -237,7 +240,7 @@ struct Patches<'a> {
    footer: Footer<'a, Action>,
}

-
impl<'a> Widget<State, Action> for Patches<'a> {
+
impl<'a> View<State, Action> for Patches<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
        let props = PatchesProps::from(state);

@@ -260,10 +263,7 @@ impl<'a> Widget<State, Action> for Patches<'a> {
        .move_with_state(state)
    }

-
    fn move_with_state(self, state: &State) -> Self
-
    where
-
        Self: Sized,
-
    {
+
    fn move_with_state(self, state: &State) -> Self {
        let patches: Vec<PatchItem> = state
            .patches
            .iter()
@@ -347,7 +347,7 @@ impl<'a> Widget<State, Action> for Patches<'a> {
                    });
            }
            _ => {
-
                <Table<Action, PatchItem> as Widget<(), Action>>::handle_key_event(
+
                <Table<Action, PatchItem> as View<(), Action>>::handle_key_event(
                    &mut self.table,
                    key,
                );
@@ -468,19 +468,19 @@ impl<'a> Patches<'a> {
    }
}

-
impl<'a> Render<()> for Patches<'a> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<'a, B: Backend> Render<B, ()> for Patches<'a> {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let header_height = 3_usize;

        let page_size = if self.props.show_search {
-
            self.table.render::<B>(frame, area, ());
+
            <Table<'_, _, _> as Render<B, ()>>::render(&self.table, frame, area, ());

            (area.height as usize).saturating_sub(header_height)
        } else {
            let layout = Layout::vertical([Constraint::Min(1), Constraint::Length(3)]).split(area);

-
            self.table.render::<B>(frame, layout[0], ());
-
            self.footer.render::<B>(frame, layout[1], ());
+
            <Table<'_, _, _> as Render<B, ()>>::render(&self.table, frame, layout[0], ());
+
            <Footer<'_, _> as Render<B, ()>>::render(&self.footer, frame, layout[1], ());

            (area.height as usize).saturating_sub(header_height)
        };
@@ -496,7 +496,7 @@ pub struct Search {
    pub input: TextField<Action>,
}

-
impl Widget<State, Action> for Search {
+
impl View<State, Action> for Search {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -526,10 +526,7 @@ impl Widget<State, Action> for Search {
                let _ = self.action_tx.send(Action::ApplySearch);
            }
            _ => {
-
                <TextField<Action> as Widget<State, Action>>::handle_key_event(
-
                    &mut self.input,
-
                    key,
-
                );
+
                <TextField<Action> as View<State, Action>>::handle_key_event(&mut self.input, key);
                let _ = self.action_tx.send(Action::UpdateSearch {
                    value: self.input.read().to_string(),
                });
@@ -538,13 +535,13 @@ impl Widget<State, Action> for Search {
    }
}

-
impl Render<()> for Search {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<B: Backend> Render<B, ()> for Search {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let layout = Layout::horizontal(Constraint::from_mins([0]))
            .horizontal_margin(1)
            .split(area);

-
        self.input.render::<B>(frame, layout[0], ());
+
        <TextField<_> as Render<B, ()>>::render(&self.input, frame, layout[0], ());
    }
}

@@ -714,7 +711,7 @@ pub struct Help<'a> {
    footer: Footer<'a, Action>,
}

-
impl<'a> Widget<State, Action> for Help<'a> {
+
impl<'a> View<State, Action> for Help<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -776,14 +773,14 @@ impl<'a> Widget<State, Action> for Help<'a> {
                let _ = self.action_tx.send(Action::CloseHelp);
            }
            _ => {
-
                <Paragraph<_> as Widget<(), _>>::handle_key_event(&mut self.content, key);
+
                <Paragraph<_> as View<(), _>>::handle_key_event(&mut self.content, key);
            }
        }
    }
}

-
impl<'a> Render<()> for Help<'a> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<'a, B: Backend> Render<B, ()> for Help<'a> {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let [header_area, content_area, footer_area] = Layout::vertical([
            Constraint::Length(3),
            Constraint::Min(1),
@@ -791,9 +788,9 @@ impl<'a> Render<()> for Help<'a> {
        ])
        .areas(area);

-
        self.header.render::<B>(frame, header_area, ());
-
        self.content.render::<B>(frame, content_area, ());
-
        self.footer.render::<B>(frame, footer_area, ());
+
        <Header<'_, _> as Render<B, ()>>::render(&self.header, frame, header_area, ());
+
        <Paragraph<'_, _> as Render<B, ()>>::render(&self.content, frame, content_area, ());
+
        <Footer<'_, _> as Render<B, ()>>::render(&self.footer, frame, footer_area, ());

        let page_size = content_area.height as usize;
        if page_size != self.props.page_size {
modified src/ui.rs
@@ -20,7 +20,7 @@ use super::store::State;
use super::task::Interrupted;
use super::terminal;
use super::terminal::TermionBackendExt;
-
use super::ui::widget::{Render, Widget};
+
use super::ui::widget::{Render, View};

type Backend = TermionBackendExt<RawTerminal<io::Stdout>>;

@@ -45,7 +45,7 @@ impl<A> Frontend<A> {
    ) -> anyhow::Result<Interrupted<P>>
    where
        S: State<A, P>,
-
        W: Widget<S, A> + Render<()>,
+
        W: View<S, A> + Render<Backend, ()>,
        P: Clone + Send + Sync + Debug,
    {
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
@@ -79,7 +79,7 @@ impl<A> Frontend<A> {
                    break Ok(interrupted);
                }
            }
-
            terminal.draw(|frame| root.render::<Backend>(frame, frame.size(), ()))?;
+
            terminal.draw(|frame| root.render(frame, frame.size(), ()))?;
        };

        terminal::restore(&mut terminal)?;
modified src/ui/widget.rs
@@ -17,7 +17,7 @@ use self::container::Header;
use super::theme::style;
use super::{layout, span};

-
pub trait Widget<S, A> {
+
pub trait View<S, A> {
    fn new(state: &S, action_tx: UnboundedSender<A>) -> Self
    where
        Self: Sized;
@@ -29,10 +29,12 @@ pub trait Widget<S, A> {
    fn handle_key_event(&mut self, key: Key);
}

-
pub trait Render<P> {
-
    fn render<B: ratatui::backend::Backend>(&self, frame: &mut Frame, area: Rect, props: P);
+
pub trait Render<B: Backend, P> {
+
    fn render(&self, frame: &mut Frame, area: Rect, props: P);
}

+
pub trait Widget<S, A, B: Backend>: View<S, A> + Render<B, ()> {}
+

pub trait ToRow {
    fn to_row(&self) -> Vec<Cell>;
}
@@ -75,11 +77,8 @@ impl<A> Shortcuts<A> {
    }
}

-
impl<A> Widget<(), A> for Shortcuts<A> {
-
    fn new(state: &(), action_tx: UnboundedSender<A>) -> Self
-
    where
-
        Self: Sized,
-
    {
+
impl<S, A> View<S, A> for Shortcuts<A> {
+
    fn new(state: &S, action_tx: UnboundedSender<A>) -> Self {
        Self {
            action_tx: action_tx.clone(),
            props: ShortcutsProps::default(),
@@ -87,18 +86,15 @@ impl<A> Widget<(), A> for Shortcuts<A> {
        .move_with_state(state)
    }

-
    fn move_with_state(self, _state: &()) -> Self
-
    where
-
        Self: Sized,
-
    {
+
    fn move_with_state(self, _state: &S) -> Self {
        Self { ..self }
    }

    fn handle_key_event(&mut self, _key: termion::event::Key) {}
}

-
impl<A> Render<()> for Shortcuts<A> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<A, B: Backend> Render<B, ()> for Shortcuts<A> {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        use ratatui::widgets::Table;

        let mut shortcuts = self.props.shortcuts.iter().peekable();
@@ -137,6 +133,8 @@ impl<A> Render<()> for Shortcuts<A> {
    }
}

+
impl<S, A, B: Backend> Widget<S, A, B> for Shortcuts<A> {}
+

#[derive(Clone, Debug)]
pub struct Column<'a> {
    pub text: Text<'a>,
@@ -299,14 +297,11 @@ impl<'a, A, R: ToRow> Table<'a, A, R> {
    }
}

-
impl<'a, A, R> Widget<(), A> for Table<'a, A, R>
+
impl<'a, S, A, R> View<S, A> for Table<'a, A, R>
where
    R: ToRow,
{
-
    fn new(state: &(), action_tx: UnboundedSender<A>) -> Self
-
    where
-
        Self: Sized,
-
    {
+
    fn new(state: &S, action_tx: UnboundedSender<A>) -> Self {
        Self {
            action_tx: action_tx.clone(),
            props: TableProps::default(),
@@ -316,12 +311,8 @@ where
        .move_with_state(state)
    }

-
    fn move_with_state(self, _state: &()) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        let mut me = Self { ..self };
-

+
    fn move_with_state(self, _state: &S) -> Self {
+
        let mut me = self;
        if let Some(selected) = me.selected() {
            if selected > me.props.items.len() {
                me.begin();
@@ -356,11 +347,12 @@ where
    }
}

-
impl<'a, A, R> Render<()> for Table<'a, A, R>
+
impl<'a, A, B, R> Render<B, ()> for Table<'a, A, R>
where
+
    B: Backend,
    R: ToRow + Debug,
{
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
    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);
@@ -422,7 +414,7 @@ where
                .highlight_style(style::highlight());

            if let Some(header) = &self.header {
-
                header.render::<B>(frame, header_area, ());
+
                <Header<'_, _> as Render<B, ()>>::render(header, frame, header_area, ());
            }
            frame.render_stateful_widget(rows, table_area, &mut self.state.clone());
        } else {
@@ -432,7 +424,7 @@ where
                .borders(borders);

            if let Some(header) = &self.header {
-
                header.render::<B>(frame, header_area, ());
+
                <Header<'_, _> as Render<B, ()>>::render(header, frame, header_area, ());
            }
            frame.render_widget(block, table_area);

@@ -446,3 +438,10 @@ where
        }
    }
}
+

+
impl<'a, S, A, B, R> Widget<S, A, B> for Table<'a, A, R>
+
where
+
    B: Backend,
+
    R: ToRow + Debug,
+
{
+
}
modified src/ui/widget/container.rs
@@ -10,7 +10,7 @@ use ratatui::widgets::{BorderType, Borders, Row};
use crate::ui::ext::{FooterBlock, FooterBlockType, HeaderBlock};
use crate::ui::theme::style;

-
use super::{Column, Render, Widget};
+
use super::{Column, Render, View};

#[derive(Debug)]
pub struct HeaderProps<'a> {
@@ -56,11 +56,8 @@ impl<'a, A> Header<'a, A> {
    }
}

-
impl<'a, A> Widget<(), A> for Header<'a, A> {
-
    fn new(state: &(), action_tx: UnboundedSender<A>) -> Self
-
    where
-
        Self: Sized,
-
    {
+
impl<'a, A> View<(), A> for Header<'a, A> {
+
    fn new(state: &(), action_tx: UnboundedSender<A>) -> Self {
        Self {
            action_tx: action_tx.clone(),
            props: HeaderProps::default(),
@@ -68,18 +65,15 @@ impl<'a, A> Widget<(), A> for Header<'a, A> {
        .move_with_state(state)
    }

-
    fn move_with_state(self, _state: &()) -> Self
-
    where
-
        Self: Sized,
-
    {
+
    fn move_with_state(self, _state: &()) -> Self {
        Self { ..self }
    }

    fn handle_key_event(&mut self, _key: Key) {}
}

-
impl<'a, A> Render<()> for Header<'a, A> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<'a, A, B: Backend> Render<B, ()> for Header<'a, A> {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let widths: Vec<Constraint> = self
            .props
            .columns
@@ -182,11 +176,8 @@ impl<'a, A> Footer<'a, A> {
    }
}

-
impl<'a, A> Widget<(), A> for Footer<'a, A> {
-
    fn new(_state: &(), action_tx: UnboundedSender<A>) -> Self
-
    where
-
        Self: Sized,
-
    {
+
impl<'a, A> View<(), A> for Footer<'a, A> {
+
    fn new(_state: &(), action_tx: UnboundedSender<A>) -> Self {
        Self {
            action_tx: action_tx.clone(),
            props: FooterProps::default(),
@@ -194,10 +185,7 @@ impl<'a, A> Widget<(), A> for Footer<'a, A> {
        .move_with_state(&())
    }

-
    fn move_with_state(self, _state: &()) -> Self
-
    where
-
        Self: Sized,
-
    {
+
    fn move_with_state(self, _state: &()) -> Self {
        Self { ..self }
    }

@@ -228,8 +216,8 @@ impl<'a, A> Footer<'a, A> {
    }
}

-
impl<'a, A> Render<()> for Footer<'a, A> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<'a, A, B: Backend> Render<B, ()> for Footer<'a, A> {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let widths = self
            .props
            .columns
modified src/ui/widget/input.rs
@@ -7,7 +7,7 @@ use ratatui::prelude::{Backend, Rect};
use ratatui::style::Stylize;
use ratatui::text::{Line, Span};

-
use super::{Render, Widget};
+
use super::{Render, View};

pub struct TextFieldProps {
    title: String,
@@ -101,7 +101,7 @@ impl<A> TextField<A> {
    }
}

-
impl<S, A> Widget<S, A> for TextField<A> {
+
impl<S, A> View<S, A> for TextField<A> {
    fn new(state: &S, action_tx: UnboundedSender<A>) -> Self {
        Self {
            action_tx,
@@ -110,11 +110,8 @@ impl<S, A> Widget<S, A> for TextField<A> {
        .move_with_state(state)
    }

-
    fn move_with_state(self, _state: &S) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        Self { ..self }
+
    fn move_with_state(self, _state: &S) -> Self {
+
        self
    }

    fn handle_key_event(&mut self, key: Key) {
@@ -140,8 +137,8 @@ impl<S, A> Widget<S, A> for TextField<A> {
    }
}

-
impl<A> Render<()> for TextField<A> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<A, B: Backend> Render<B, ()> for TextField<A> {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let layout = Layout::vertical(Constraint::from_lengths([1, 1])).split(area);

        let input = self.props.text.as_str();
modified src/ui/widget/text.rs
@@ -9,7 +9,7 @@ use ratatui::widgets::{Block, BorderType, Borders};

use crate::ui::theme::style;

-
use super::{Render, Widget};
+
use super::{Render, View};

pub struct ParagraphProps<'a> {
    pub content: Text<'a>,
@@ -116,7 +116,7 @@ impl<'a, A> Paragraph<'a, A> {
    }
}

-
impl<'a, S, A> Widget<S, A> for Paragraph<'a, A> {
+
impl<'a, S, A> View<S, A> for Paragraph<'a, A> {
    fn new(state: &S, action_tx: UnboundedSender<A>) -> Self
    where
        Self: Sized,
@@ -165,8 +165,8 @@ impl<'a, S, A> Widget<S, A> for Paragraph<'a, A> {
    }
}

-
impl<'a, A> Render<()> for Paragraph<'a, A> {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
impl<'a, A, B: Backend> Render<B, ()> for Paragraph<'a, A> {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
        let block = Block::default()
            .borders(Borders::LEFT | Borders::RIGHT)
            .border_type(BorderType::Rounded)
@@ -176,8 +176,8 @@ impl<'a, A> Render<()> for Paragraph<'a, A> {
        let [content_area] = Layout::horizontal([Constraint::Min(1)])
            .horizontal_margin(2)
            .areas(area);
-
        let content =
-
            ratatui::widgets::Paragraph::new(self.props.content.clone()).scroll((self.offset as u16, 0));
+
        let content = ratatui::widgets::Paragraph::new(self.props.content.clone())
+
            .scroll((self.offset as u16, 0));

        frame.render_widget(content, content_area);
    }