Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Widget trait simplification
Merged did:key:z6MkgFq6...nBGz opened 2 years ago

This merges the view into the widget trait and removes unnecessary generic backend parameter.

12 files changed +596 -689 956ca70f 5e71e1d7
modified bin/commands/inbox/select.rs
@@ -18,9 +18,8 @@ use tui::cob::inbox::{self};
use tui::store;
use tui::store::StateValue;
use tui::task::{self, Interrupted};
-
use tui::terminal::Backend;
use tui::ui::items::{Filter, NotificationItem, NotificationItemFilter};
-
use tui::ui::widget::{Properties, View, Window, WindowProps};
+
use tui::ui::widget::{Properties, Widget, Window, WindowProps};
use tui::ui::Frontend;
use tui::Exit;

@@ -210,7 +209,9 @@ pub enum Action {
    ScrollHelp { progress: usize },
}

-
impl store::State<Action, Selection> for State {
+
impl store::State<Selection> for State {
+
    type Action = Action;
+

    fn tick(&self) {}

    fn handle_action(&mut self, action: Action) -> Option<Exit<Selection>> {
@@ -279,7 +280,7 @@ impl App {
        let (frontend, action_tx, action_rx) = Frontend::new();
        let state = State::try_from(&self.context)?;

-
        let window: Window<Backend, State, Action, Page> = Window::new(&state, action_tx.clone())
+
        let window: Window<State, Action, Page> = Window::new(&state, action_tx.clone())
            .page(
                Page::Browse,
                BrowsePage::new(&state, action_tx.clone()).to_boxed(),
modified bin/commands/inbox/select/ui.rs
@@ -7,7 +7,6 @@ use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;

-
use ratatui::backend::Backend;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::Stylize;
use ratatui::text::{Line, Span, Text};
@@ -22,16 +21,14 @@ use tui::ui::widget::container::{
use tui::ui::widget::input::{TextField, TextFieldProps, TextFieldState};
use tui::ui::widget::text::{Paragraph, ParagraphProps, ParagraphState};
use tui::ui::widget::{self, BaseView, TableUtils};
-
use tui::ui::widget::{
-
    Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, View, Widget,
-
};
+
use tui::ui::widget::{Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, Widget};
use tui::Selection;

use crate::tui_inbox::common::{InboxOperation, Mode, RepositoryMode, SelectionMode};

use super::{Action, State};

-
type BoxedWidget<B> = widget::BoxedWidget<B, State, Action>;
+
type BoxedWidget = widget::BoxedWidget<State, Action>;

#[derive(Clone)]
struct BrowsePageProps<'a> {
@@ -106,23 +103,86 @@ impl<'a> From<&State> for BrowsePageProps<'a> {

impl<'a> Properties for BrowsePageProps<'a> {}

-
pub struct BrowsePage<'a, B> {
+
pub struct BrowsePage<'a> {
    /// Internal base
    base: BaseView<State, Action>,
    /// Internal props
    props: BrowsePageProps<'a>,
    /// Notifications widget
-
    notifications: BoxedWidget<B>,
+
    notifications: BoxedWidget,
    /// Search widget
-
    search: BoxedWidget<B>,
+
    search: BoxedWidget,
    /// Shortcut widget
-
    shortcuts: BoxedWidget<B>,
+
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static, B> View<State, Action> for BrowsePage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
+
impl<'a> BrowsePage<'a> {
+
    fn build_footer(props: &BrowsePageProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
+
        let search = Line::from(vec![
+
            span::default(" Search ").cyan().dim().reversed(),
+
            span::default(" "),
+
            span::default(&props.search.to_string()).gray().dim(),
+
        ]);
+

+
        let seen = Line::from(vec![
+
            span::positive(&props.stats.get("Seen").unwrap_or(&0).to_string()).dim(),
+
            span::default(" Seen").dim(),
+
        ]);
+
        let unseen = Line::from(vec![
+
            span::positive(&props.stats.get("Unseen").unwrap_or(&0).to_string())
+
                .magenta()
+
                .dim(),
+
            span::default(" Unseen").dim(),
+
        ]);
+

+
        let progress = selected
+
            .map(|selected| {
+
                TableUtils::progress(selected, props.notifications.len(), props.page_size)
+
            })
+
            .unwrap_or_default();
+
        let progress = span::default(&format!("{}%", progress)).dim();
+

+
        match NotificationItemFilter::from_str(&props.search)
+
            .unwrap_or_default()
+
            .state()
+
        {
+
            Some(state) => {
+
                let block = match state {
+
                    NotificationState::Seen => seen,
+
                    NotificationState::Unseen => unseen,
+
                };
+

+
                [
+
                    Column::new(Text::from(search), Constraint::Fill(1)),
+
                    Column::new(
+
                        Text::from(block.clone()),
+
                        Constraint::Min(block.width() as u16),
+
                    ),
+
                    Column::new(Text::from(progress), Constraint::Min(4)),
+
                ]
+
                .to_vec()
+
            }
+
            None => [
+
                Column::new(Text::from(search), Constraint::Fill(1)),
+
                Column::new(
+
                    Text::from(seen.clone()),
+
                    Constraint::Min(seen.width() as u16),
+
                ),
+
                Column::new(
+
                    Text::from(unseen.clone()),
+
                    Constraint::Min(unseen.width() as u16),
+
                ),
+
                Column::new(Text::from(progress), Constraint::Min(4)),
+
            ]
+
            .to_vec(),
+
        }
+
    }
+
}
+

+
impl<'a: 'static> Widget for BrowsePage<'a> {
+
    type Action = Action;
+
    type State = State;
+

    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
        let props = BrowsePageProps::from(state);
        let name = match state.mode.repository() {
@@ -192,19 +252,6 @@ where
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &State) {
-
        self.props = BrowsePageProps::from_callback(self.base.on_update, state)
-
            .unwrap_or(BrowsePageProps::from(state));
-

-
        self.notifications.update(state);
-
        self.search.update(state);
-
        self.shortcuts.update(state);
-
    }
-

    fn handle_event(&mut self, key: Key) {
        if self.props.show_search {
            self.search.handle_event(key);
@@ -262,75 +309,16 @@ where
            }
        }
    }
-
}

-
impl<'a, B: Backend> BrowsePage<'a, B> {
-
    fn build_footer(props: &BrowsePageProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
-
        let search = Line::from(vec![
-
            span::default(" Search ").cyan().dim().reversed(),
-
            span::default(" "),
-
            span::default(&props.search.to_string()).gray().dim(),
-
        ]);
-

-
        let seen = Line::from(vec![
-
            span::positive(&props.stats.get("Seen").unwrap_or(&0).to_string()).dim(),
-
            span::default(" Seen").dim(),
-
        ]);
-
        let unseen = Line::from(vec![
-
            span::positive(&props.stats.get("Unseen").unwrap_or(&0).to_string())
-
                .magenta()
-
                .dim(),
-
            span::default(" Unseen").dim(),
-
        ]);
-

-
        let progress = selected
-
            .map(|selected| {
-
                TableUtils::progress(selected, props.notifications.len(), props.page_size)
-
            })
-
            .unwrap_or_default();
-
        let progress = span::default(&format!("{}%", progress)).dim();
-

-
        match NotificationItemFilter::from_str(&props.search)
-
            .unwrap_or_default()
-
            .state()
-
        {
-
            Some(state) => {
-
                let block = match state {
-
                    NotificationState::Seen => seen,
-
                    NotificationState::Unseen => unseen,
-
                };
+
    fn update(&mut self, state: &State) {
+
        self.props = BrowsePageProps::from_callback(self.base.on_update, state)
+
            .unwrap_or(BrowsePageProps::from(state));

-
                [
-
                    Column::new(Text::from(search), Constraint::Fill(1)),
-
                    Column::new(
-
                        Text::from(block.clone()),
-
                        Constraint::Min(block.width() as u16),
-
                    ),
-
                    Column::new(Text::from(progress), Constraint::Min(4)),
-
                ]
-
                .to_vec()
-
            }
-
            None => [
-
                Column::new(Text::from(search), Constraint::Fill(1)),
-
                Column::new(
-
                    Text::from(seen.clone()),
-
                    Constraint::Min(seen.width() as u16),
-
                ),
-
                Column::new(
-
                    Text::from(unseen.clone()),
-
                    Constraint::Min(unseen.width() as u16),
-
                ),
-
                Column::new(Text::from(progress), Constraint::Min(4)),
-
            ]
-
            .to_vec(),
-
        }
+
        self.notifications.update(state);
+
        self.search.update(state);
+
        self.shortcuts.update(state);
    }
-
}

-
impl<'a: 'static, B> Widget<B, State, Action> for BrowsePage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(BrowsePageProps::from_boxed_any)
@@ -381,22 +369,29 @@ where
            let _ = self.base.action_tx.send(Action::BrowserPageSize(page_size));
        }
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
+
    }
}

pub struct SearchProps {}

impl Properties for SearchProps {}

-
pub struct Search<B: Backend> {
+
pub struct Search {
    /// Internal base
    base: BaseView<State, Action>,
    /// Internal props
    _props: SearchProps,
    /// Search input field
-
    input: BoxedWidget<B>,
+
    input: BoxedWidget,
}

-
impl<B: Backend> View<State, Action> for Search<B> {
+
impl Widget for Search {
+
    type Action = Action;
+
    type State = State;
+

    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -430,14 +425,6 @@ impl<B: Backend> View<State, Action> for Search<B> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &State) {
-
        self.input.update(state);
-
    }
-

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
@@ -451,12 +438,11 @@ impl<B: Backend> View<State, Action> for Search<B> {
            }
        }
    }
-
}

-
impl<B> Widget<B, State, Action> for Search<B>
-
where
-
    B: Backend,
-
{
+
    fn update(&mut self, state: &State) {
+
        self.input.update(state);
+
    }
+

    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: Option<Box<dyn Any>>) {
        let layout = Layout::horizontal(Constraint::from_mins([0]))
            .horizontal_margin(1)
@@ -464,6 +450,10 @@ where

        self.input.render(frame, layout[0], None);
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
+
    }
}

#[derive(Clone)]
@@ -487,24 +477,21 @@ impl<'a> From<&State> for HelpPageProps<'a> {

impl<'a> Properties for HelpPageProps<'a> {}

-
pub struct HelpPage<'a, B>
-
where
-
    B: Backend,
-
{
+
pub struct HelpPage<'a> {
    /// Internal base
    base: BaseView<State, Action>,
    /// Internal props
    props: HelpPageProps<'a>,
    /// Content widget
-
    content: BoxedWidget<B>,
+
    content: BoxedWidget,
    /// Shortcut widget
-
    shortcuts: BoxedWidget<B>,
+
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static, B> View<State, Action> for HelpPage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
+
impl<'a: 'static> Widget for HelpPage<'a> {
+
    type Action = Action;
+
    type State = State;
+

    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -578,17 +565,6 @@ where
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &State) {
-
        self.props = HelpPageProps::from_callback(self.base.on_update, state)
-
            .unwrap_or(HelpPageProps::from(state));
-

-
        self.content.update(state);
-
    }
-

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
@@ -602,12 +578,14 @@ where
            }
        }
    }
-
}

-
impl<'a: 'static, B> Widget<B, State, Action> for HelpPage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
+
    fn update(&mut self, state: &State) {
+
        self.props = HelpPageProps::from_callback(self.base.on_update, state)
+
            .unwrap_or(HelpPageProps::from(state));
+

+
        self.content.update(state);
+
    }
+

    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(HelpPageProps::from_boxed_any)
@@ -633,6 +611,10 @@ where
            let _ = self.base.action_tx.send(Action::HelpPageSize(page_size));
        }
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
+
    }
}

fn help_text() -> Text<'static> {
modified bin/commands/issue/select.rs
@@ -15,9 +15,8 @@ use tui::cob::issue;
use tui::store::StateValue;
use tui::task;
use tui::task::Interrupted;
-
use tui::terminal::Backend;
use tui::ui::items::{Filter, IssueItem, IssueItemFilter};
-
use tui::ui::widget::{Properties, View, Window, WindowProps};
+
use tui::ui::widget::{Properties, Widget, Window, WindowProps};
use tui::ui::Frontend;
use tui::Exit;
use tui::{store, PageStack};
@@ -129,8 +128,8 @@ pub enum Action {
    ScrollHelp { progress: usize },
}

-
impl store::State<Action, Selection> for State {
-
    fn tick(&self) {}
+
impl store::State<Selection> for State {
+
    type Action = Action;

    fn handle_action(&mut self, action: Action) -> Option<Exit<Selection>> {
        match action {
@@ -185,6 +184,8 @@ impl store::State<Action, Selection> for State {
            }
        }
    }
+

+
    fn tick(&self) {}
}

impl App {
@@ -198,7 +199,7 @@ impl App {
        let (frontend, action_tx, action_rx) = Frontend::new();
        let state = State::try_from(&self.context)?;

-
        let window: Window<Backend, State, Action, Page> = Window::new(&state, action_tx.clone())
+
        let window: Window<State, Action, Page> = Window::new(&state, action_tx.clone())
            .page(
                Page::Browse,
                BrowsePage::new(&state, action_tx.clone()).to_boxed(),
modified bin/commands/issue/select/ui.rs
@@ -9,7 +9,6 @@ use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;

-
use ratatui::backend::Backend;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::Stylize;
use ratatui::text::{Line, Span, Text};
@@ -25,7 +24,7 @@ use tui::ui::widget::input::{TextField, TextFieldProps, TextFieldState};
use tui::ui::widget::text::{Paragraph, ParagraphProps, ParagraphState};
use tui::ui::widget::{self, BaseView};
use tui::ui::widget::{
-
    Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, TableUtils, View, Widget,
+
    Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, TableUtils, Widget,
};
use tui::Selection;

@@ -34,7 +33,7 @@ use crate::tui_issue::common::Mode;

use super::{Action, State};

-
type BoxedWidget<B> = widget::BoxedWidget<B, State, Action>;
+
type BoxedWidget = widget::BoxedWidget<State, Action>;

#[derive(Clone)]
struct BrowsePageProps<'a> {
@@ -120,23 +119,100 @@ impl<'a> From<&State> for BrowsePageProps<'a> {

impl<'a> Properties for BrowsePageProps<'a> {}

-
pub struct BrowsePage<'a, B> {
+
pub struct BrowsePage<'a> {
    /// Internal base
    base: BaseView<State, Action>,
    /// Internal props
    props: BrowsePageProps<'a>,
    /// Notifications widget
-
    issues: BoxedWidget<B>,
+
    issues: BoxedWidget,
    /// Search widget
-
    search: BoxedWidget<B>,
+
    search: BoxedWidget,
    /// Shortcut widget
-
    shortcuts: BoxedWidget<B>,
+
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static, B> View<State, Action> for BrowsePage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
+
impl<'a> BrowsePage<'a> {
+
    fn build_footer(props: &BrowsePageProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
+
        let search = Line::from(vec![
+
            span::default(" Search ").cyan().dim().reversed(),
+
            span::default(" "),
+
            span::default(&props.search).gray().dim(),
+
        ]);
+

+
        let open = Line::from(vec![
+
            span::positive(&props.stats.get("Open").unwrap_or(&0).to_string()).dim(),
+
            span::default(" Open").dim(),
+
        ]);
+
        let solved = Line::from(vec![
+
            span::default(&props.stats.get("Solved").unwrap_or(&0).to_string())
+
                .magenta()
+
                .dim(),
+
            span::default(" Solved").dim(),
+
        ]);
+
        let closed = Line::from(vec![
+
            span::default(&props.stats.get("Closed").unwrap_or(&0).to_string())
+
                .magenta()
+
                .dim(),
+
            span::default(" Closed").dim(),
+
        ]);
+
        let sum = Line::from(vec![
+
            span::default("Σ ").dim(),
+
            span::default(&props.issues.len().to_string()).dim(),
+
        ]);
+

+
        let progress = selected
+
            .map(|selected| TableUtils::progress(selected, props.issues.len(), props.page_size))
+
            .unwrap_or_default();
+
        let progress = span::default(&format!("{}%", progress)).dim();
+

+
        match IssueItemFilter::from_str(&props.search)
+
            .unwrap_or_default()
+
            .state()
+
        {
+
            Some(state) => {
+
                let block = match state {
+
                    issue::State::Open => open,
+
                    issue::State::Closed {
+
                        reason: issue::CloseReason::Other,
+
                    } => closed,
+
                    issue::State::Closed {
+
                        reason: issue::CloseReason::Solved,
+
                    } => solved,
+
                };
+

+
                [
+
                    Column::new(Text::from(search), Constraint::Fill(1)),
+
                    Column::new(
+
                        Text::from(block.clone()),
+
                        Constraint::Min(block.width() as u16),
+
                    ),
+
                    Column::new(Text::from(progress), Constraint::Min(4)),
+
                ]
+
                .to_vec()
+
            }
+
            None => [
+
                Column::new(Text::from(search), Constraint::Fill(1)),
+
                Column::new(
+
                    Text::from(open.clone()),
+
                    Constraint::Min(open.width() as u16),
+
                ),
+
                Column::new(
+
                    Text::from(closed.clone()),
+
                    Constraint::Min(closed.width() as u16),
+
                ),
+
                Column::new(Text::from(sum.clone()), Constraint::Min(sum.width() as u16)),
+
                Column::new(Text::from(progress), Constraint::Min(4)),
+
            ]
+
            .to_vec(),
+
        }
+
    }
+
}
+

+
impl<'a: 'static> Widget for BrowsePage<'a> {
+
    type Action = Action;
+
    type State = State;
+

    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
        let props = BrowsePageProps::from(state);

@@ -195,19 +271,6 @@ where
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &State) {
-
        self.props = BrowsePageProps::from_callback(self.base.on_update, state)
-
            .unwrap_or(BrowsePageProps::from(state));
-

-
        self.issues.update(state);
-
        self.search.update(state);
-
        self.shortcuts.update(state);
-
    }
-

    fn handle_event(&mut self, key: Key) {
        if self.props.show_search {
            self.search.handle_event(key);
@@ -267,89 +330,16 @@ where
            }
        }
    }
-
}
-

-
impl<'a, B: Backend> BrowsePage<'a, B> {
-
    fn build_footer(props: &BrowsePageProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
-
        let search = Line::from(vec![
-
            span::default(" Search ").cyan().dim().reversed(),
-
            span::default(" "),
-
            span::default(&props.search).gray().dim(),
-
        ]);
-

-
        let open = Line::from(vec![
-
            span::positive(&props.stats.get("Open").unwrap_or(&0).to_string()).dim(),
-
            span::default(" Open").dim(),
-
        ]);
-
        let solved = Line::from(vec![
-
            span::default(&props.stats.get("Solved").unwrap_or(&0).to_string())
-
                .magenta()
-
                .dim(),
-
            span::default(" Solved").dim(),
-
        ]);
-
        let closed = Line::from(vec![
-
            span::default(&props.stats.get("Closed").unwrap_or(&0).to_string())
-
                .magenta()
-
                .dim(),
-
            span::default(" Closed").dim(),
-
        ]);
-
        let sum = Line::from(vec![
-
            span::default("Σ ").dim(),
-
            span::default(&props.issues.len().to_string()).dim(),
-
        ]);

-
        let progress = selected
-
            .map(|selected| TableUtils::progress(selected, props.issues.len(), props.page_size))
-
            .unwrap_or_default();
-
        let progress = span::default(&format!("{}%", progress)).dim();
-

-
        match IssueItemFilter::from_str(&props.search)
-
            .unwrap_or_default()
-
            .state()
-
        {
-
            Some(state) => {
-
                let block = match state {
-
                    issue::State::Open => open,
-
                    issue::State::Closed {
-
                        reason: issue::CloseReason::Other,
-
                    } => closed,
-
                    issue::State::Closed {
-
                        reason: issue::CloseReason::Solved,
-
                    } => solved,
-
                };
+
    fn update(&mut self, state: &State) {
+
        self.props = BrowsePageProps::from_callback(self.base.on_update, state)
+
            .unwrap_or(BrowsePageProps::from(state));

-
                [
-
                    Column::new(Text::from(search), Constraint::Fill(1)),
-
                    Column::new(
-
                        Text::from(block.clone()),
-
                        Constraint::Min(block.width() as u16),
-
                    ),
-
                    Column::new(Text::from(progress), Constraint::Min(4)),
-
                ]
-
                .to_vec()
-
            }
-
            None => [
-
                Column::new(Text::from(search), Constraint::Fill(1)),
-
                Column::new(
-
                    Text::from(open.clone()),
-
                    Constraint::Min(open.width() as u16),
-
                ),
-
                Column::new(
-
                    Text::from(closed.clone()),
-
                    Constraint::Min(closed.width() as u16),
-
                ),
-
                Column::new(Text::from(sum.clone()), Constraint::Min(sum.width() as u16)),
-
                Column::new(Text::from(progress), Constraint::Min(4)),
-
            ]
-
            .to_vec(),
-
        }
+
        self.issues.update(state);
+
        self.search.update(state);
+
        self.shortcuts.update(state);
    }
-
}

-
impl<'a: 'static, B> Widget<B, State, Action> for BrowsePage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(BrowsePageProps::from_boxed_any)
@@ -400,22 +390,29 @@ where
            let _ = self.base.action_tx.send(Action::BrowserPageSize(page_size));
        }
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
+
    }
}

pub struct SearchProps {}

impl Properties for SearchProps {}

-
pub struct Search<B: Backend> {
+
pub struct Search {
    /// Internal base
    base: BaseView<State, Action>,
    /// Internal props
    _props: SearchProps,
    /// Search input field
-
    input: BoxedWidget<B>,
+
    input: BoxedWidget,
}

-
impl<B: Backend> View<State, Action> for Search<B> {
+
impl Widget for Search {
+
    type Action = Action;
+
    type State = State;
+

    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -449,14 +446,6 @@ impl<B: Backend> View<State, Action> for Search<B> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &State) {
-
        self.input.update(state);
-
    }
-

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
@@ -470,12 +459,11 @@ impl<B: Backend> View<State, Action> for Search<B> {
            }
        }
    }
-
}

-
impl<B> Widget<B, State, Action> for Search<B>
-
where
-
    B: Backend,
-
{
+
    fn update(&mut self, state: &State) {
+
        self.input.update(state);
+
    }
+

    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: Option<Box<dyn Any>>) {
        let layout = Layout::horizontal(Constraint::from_mins([0]))
            .horizontal_margin(1)
@@ -483,6 +471,10 @@ where

        self.input.render(frame, layout[0], None);
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
+
    }
}

#[derive(Clone)]
@@ -506,24 +498,21 @@ impl<'a> From<&State> for HelpPageProps<'a> {

impl<'a> Properties for HelpPageProps<'a> {}

-
pub struct HelpPage<'a, B>
-
where
-
    B: Backend,
-
{
+
pub struct HelpPage<'a> {
    /// Internal base
    base: BaseView<State, Action>,
    /// Internal props
    props: HelpPageProps<'a>,
    /// Content widget
-
    content: BoxedWidget<B>,
+
    content: BoxedWidget,
    /// Shortcut widget
-
    shortcuts: BoxedWidget<B>,
+
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static, B> View<State, Action> for HelpPage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
+
impl<'a: 'static> Widget for HelpPage<'a> {
+
    type Action = Action;
+
    type State = State;
+

    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -597,17 +586,6 @@ where
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &State) {
-
        self.props = HelpPageProps::from_callback(self.base.on_update, state)
-
            .unwrap_or(HelpPageProps::from(state));
-

-
        self.content.update(state);
-
    }
-

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
@@ -621,12 +599,14 @@ where
            }
        }
    }
-
}

-
impl<'a: 'static, B> Widget<B, State, Action> for HelpPage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
+
    fn update(&mut self, state: &State) {
+
        self.props = HelpPageProps::from_callback(self.base.on_update, state)
+
            .unwrap_or(HelpPageProps::from(state));
+

+
        self.content.update(state);
+
    }
+

    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(HelpPageProps::from_boxed_any)
@@ -652,6 +632,10 @@ where
            let _ = self.base.action_tx.send(Action::HelpPageSize(page_size));
        }
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
+
    }
}

fn help_text() -> Text<'static> {
modified bin/commands/patch/select.rs
@@ -15,10 +15,9 @@ use tui::cob::patch;
use tui::store;
use tui::task;
use tui::task::Interrupted;
-
use tui::terminal::Backend;
use tui::ui::items::{Filter, PatchItem, PatchItemFilter};
use tui::ui::widget::Properties;
-
use tui::ui::widget::View;
+
use tui::ui::widget::Widget;
use tui::ui::widget::Window;
use tui::ui::widget::WindowProps;
use tui::ui::Frontend;
@@ -134,8 +133,8 @@ pub enum Action {
    ScrollHelp { progress: usize },
}

-
impl store::State<Action, Selection> for State {
-
    fn tick(&self) {}
+
impl store::State<Selection> for State {
+
    type Action = Action;

    fn handle_action(&mut self, action: Action) -> Option<Exit<Selection>> {
        match action {
@@ -190,6 +189,8 @@ impl store::State<Action, Selection> for State {
            }
        }
    }
+

+
    fn tick(&self) {}
}

impl App {
@@ -203,7 +204,7 @@ impl App {
        let (frontend, action_tx, action_rx) = Frontend::<Action>::new();
        let state = State::try_from(&self.context)?;

-
        let window: Window<Backend, State, Action, Page> = Window::new(&state, action_tx.clone())
+
        let window: Window<State, Action, Page> = Window::new(&state, action_tx.clone())
            .page(
                Page::Browse,
                BrowsePage::new(&state, action_tx.clone()).to_boxed(),
modified bin/commands/patch/select/ui.rs
@@ -8,7 +8,6 @@ use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;

-
use ratatui::backend::Backend;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::Stylize;
use ratatui::text::{Line, Span, Text};
@@ -26,9 +25,7 @@ use tui::ui::widget::input::{TextField, TextFieldProps, TextFieldState};
use tui::ui::widget::text::{Paragraph, ParagraphProps, ParagraphState};
use tui::ui::widget::TableUtils;
use tui::ui::widget::{self, BaseView};
-
use tui::ui::widget::{
-
    Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, View, Widget,
-
};
+
use tui::ui::widget::{Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, Widget};
use tui::Selection;

use crate::tui_patch::common::Mode;
@@ -36,7 +33,7 @@ use crate::tui_patch::common::PatchOperation;

use super::{Action, State};

-
type BoxedWidget<B> = widget::BoxedWidget<B, State, Action>;
+
type BoxedWidget = widget::BoxedWidget<State, Action>;

#[derive(Clone)]
pub struct BrowsePageProps<'a> {
@@ -121,23 +118,110 @@ impl<'a> From<&State> for BrowsePageProps<'a> {

impl<'a: 'static> Properties for BrowsePageProps<'a> {}

-
pub struct BrowsePage<'a, B> {
+
pub struct BrowsePage<'a> {
    /// Internal base
    base: BaseView<State, Action>,
    /// Internal props
    props: BrowsePageProps<'a>,
    /// Notifications widget
-
    patches: BoxedWidget<B>,
+
    patches: BoxedWidget,
    /// Search widget
-
    search: BoxedWidget<B>,
+
    search: BoxedWidget,
    /// Shortcut widget
-
    shortcuts: BoxedWidget<B>,
+
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static, B> View<State, Action> for BrowsePage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
+
impl<'a> BrowsePage<'a> {
+
    fn build_footer(props: &BrowsePageProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
+
        let filter = PatchItemFilter::from_str(&props.search).unwrap_or_default();
+

+
        let search = Line::from(vec![
+
            span::default(" Search ").cyan().dim().reversed(),
+
            span::default(" "),
+
            span::default(&props.search.to_string()).gray().dim(),
+
        ]);
+

+
        let draft = Line::from(vec![
+
            span::default(&props.stats.get("Draft").unwrap_or(&0).to_string()).dim(),
+
            span::default(" Draft").dim(),
+
        ]);
+

+
        let open = Line::from(vec![
+
            span::positive(&props.stats.get("Open").unwrap_or(&0).to_string()).dim(),
+
            span::default(" Open").dim(),
+
        ]);
+

+
        let merged = Line::from(vec![
+
            span::default(&props.stats.get("Merged").unwrap_or(&0).to_string())
+
                .magenta()
+
                .dim(),
+
            span::default(" Merged").dim(),
+
        ]);
+

+
        let archived = Line::from(vec![
+
            span::default(&props.stats.get("Archived").unwrap_or(&0).to_string())
+
                .yellow()
+
                .dim(),
+
            span::default(" Archived").dim(),
+
        ]);
+

+
        let sum = Line::from(vec![
+
            span::default("Σ ").dim(),
+
            span::default(&props.patches.len().to_string()).dim(),
+
        ]);
+

+
        let progress = selected
+
            .map(|selected| TableUtils::progress(selected, props.patches.len(), props.page_size))
+
            .unwrap_or_default();
+
        let progress = span::default(&format!("{}%", progress)).dim();
+

+
        match filter.status() {
+
            Some(state) => {
+
                let block = match state {
+
                    Status::Draft => draft,
+
                    Status::Open => open,
+
                    Status::Merged => merged,
+
                    Status::Archived => archived,
+
                };
+

+
                vec![
+
                    Column::new(Text::from(search), Constraint::Fill(1)),
+
                    Column::new(
+
                        Text::from(block.clone()),
+
                        Constraint::Min(block.width() as u16),
+
                    ),
+
                    Column::new(Text::from(progress), Constraint::Min(4)),
+
                ]
+
            }
+
            None => vec![
+
                Column::new(Text::from(search), Constraint::Fill(1)),
+
                Column::new(
+
                    Text::from(draft.clone()),
+
                    Constraint::Min(draft.width() as u16),
+
                ),
+
                Column::new(
+
                    Text::from(open.clone()),
+
                    Constraint::Min(open.width() as u16),
+
                ),
+
                Column::new(
+
                    Text::from(merged.clone()),
+
                    Constraint::Min(merged.width() as u16),
+
                ),
+
                Column::new(
+
                    Text::from(archived.clone()),
+
                    Constraint::Min(archived.width() as u16),
+
                ),
+
                Column::new(Text::from(sum.clone()), Constraint::Min(sum.width() as u16)),
+
                Column::new(Text::from(progress), Constraint::Min(4)),
+
            ],
+
        }
+
    }
+
}
+

+
impl<'a: 'static> Widget for BrowsePage<'a> {
+
    type Action = Action;
+
    type State = State;
+

    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
        let props = BrowsePageProps::from(state);

@@ -196,19 +280,6 @@ where
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &State) {
-
        self.props = BrowsePageProps::from_callback(self.base.on_update, state)
-
            .unwrap_or(BrowsePageProps::from(state));
-

-
        self.patches.update(state);
-
        self.search.update(state);
-
        self.shortcuts.update(state);
-
    }
-

    fn handle_event(&mut self, key: Key) {
        if self.props.show_search {
            self.search.handle_event(key);
@@ -285,99 +356,16 @@ where
            }
        }
    }
-
}
-

-
impl<'a, B: Backend> BrowsePage<'a, B> {
-
    fn build_footer(props: &BrowsePageProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
-
        let filter = PatchItemFilter::from_str(&props.search).unwrap_or_default();

-
        let search = Line::from(vec![
-
            span::default(" Search ").cyan().dim().reversed(),
-
            span::default(" "),
-
            span::default(&props.search.to_string()).gray().dim(),
-
        ]);
-

-
        let draft = Line::from(vec![
-
            span::default(&props.stats.get("Draft").unwrap_or(&0).to_string()).dim(),
-
            span::default(" Draft").dim(),
-
        ]);
-

-
        let open = Line::from(vec![
-
            span::positive(&props.stats.get("Open").unwrap_or(&0).to_string()).dim(),
-
            span::default(" Open").dim(),
-
        ]);
-

-
        let merged = Line::from(vec![
-
            span::default(&props.stats.get("Merged").unwrap_or(&0).to_string())
-
                .magenta()
-
                .dim(),
-
            span::default(" Merged").dim(),
-
        ]);
-

-
        let archived = Line::from(vec![
-
            span::default(&props.stats.get("Archived").unwrap_or(&0).to_string())
-
                .yellow()
-
                .dim(),
-
            span::default(" Archived").dim(),
-
        ]);
-

-
        let sum = Line::from(vec![
-
            span::default("Σ ").dim(),
-
            span::default(&props.patches.len().to_string()).dim(),
-
        ]);
-

-
        let progress = selected
-
            .map(|selected| TableUtils::progress(selected, props.patches.len(), props.page_size))
-
            .unwrap_or_default();
-
        let progress = span::default(&format!("{}%", progress)).dim();
-

-
        match filter.status() {
-
            Some(state) => {
-
                let block = match state {
-
                    Status::Draft => draft,
-
                    Status::Open => open,
-
                    Status::Merged => merged,
-
                    Status::Archived => archived,
-
                };
+
    fn update(&mut self, state: &State) {
+
        self.props = BrowsePageProps::from_callback(self.base.on_update, state)
+
            .unwrap_or(BrowsePageProps::from(state));

-
                vec![
-
                    Column::new(Text::from(search), Constraint::Fill(1)),
-
                    Column::new(
-
                        Text::from(block.clone()),
-
                        Constraint::Min(block.width() as u16),
-
                    ),
-
                    Column::new(Text::from(progress), Constraint::Min(4)),
-
                ]
-
            }
-
            None => vec![
-
                Column::new(Text::from(search), Constraint::Fill(1)),
-
                Column::new(
-
                    Text::from(draft.clone()),
-
                    Constraint::Min(draft.width() as u16),
-
                ),
-
                Column::new(
-
                    Text::from(open.clone()),
-
                    Constraint::Min(open.width() as u16),
-
                ),
-
                Column::new(
-
                    Text::from(merged.clone()),
-
                    Constraint::Min(merged.width() as u16),
-
                ),
-
                Column::new(
-
                    Text::from(archived.clone()),
-
                    Constraint::Min(archived.width() as u16),
-
                ),
-
                Column::new(Text::from(sum.clone()), Constraint::Min(sum.width() as u16)),
-
                Column::new(Text::from(progress), Constraint::Min(4)),
-
            ],
-
        }
+
        self.patches.update(state);
+
        self.search.update(state);
+
        self.shortcuts.update(state);
    }
-
}

-
impl<'a: 'static, B> Widget<B, State, Action> for BrowsePage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(BrowsePageProps::from_boxed_any)
@@ -428,22 +416,29 @@ where
            let _ = self.base.action_tx.send(Action::BrowserPageSize(page_size));
        }
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
+
    }
}

pub struct SearchProps {}

impl Properties for SearchProps {}

-
pub struct Search<B: Backend> {
+
pub struct Search {
    /// Internal base
    base: BaseView<State, Action>,
    /// Internal props
    _props: SearchProps,
    /// Search input field
-
    input: BoxedWidget<B>,
+
    input: BoxedWidget,
}

-
impl<B: Backend> View<State, Action> for Search<B> {
+
impl Widget for Search {
+
    type Action = Action;
+
    type State = State;
+

    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -477,14 +472,6 @@ impl<B: Backend> View<State, Action> for Search<B> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &State) {
-
        self.input.update(state);
-
    }
-

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
@@ -498,12 +485,11 @@ impl<B: Backend> View<State, Action> for Search<B> {
            }
        }
    }
-
}

-
impl<B> Widget<B, State, Action> for Search<B>
-
where
-
    B: Backend,
-
{
+
    fn update(&mut self, state: &State) {
+
        self.input.update(state);
+
    }
+

    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: Option<Box<dyn Any>>) {
        let layout = Layout::horizontal(Constraint::from_mins([0]))
            .horizontal_margin(1)
@@ -511,6 +497,10 @@ where

        self.input.render(frame, layout[0], None);
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
+
    }
}

#[derive(Clone)]
@@ -534,24 +524,21 @@ impl<'a> From<&State> for HelpPageProps<'a> {

impl<'a> Properties for HelpPageProps<'a> {}

-
pub struct HelpPage<'a, B>
-
where
-
    B: Backend,
-
{
+
pub struct HelpPage<'a> {
    /// Internal base
    base: BaseView<State, Action>,
    /// Internal props
    props: HelpPageProps<'a>,
    /// Content widget
-
    content: BoxedWidget<B>,
+
    content: BoxedWidget,
    /// Shortcut widget
-
    shortcuts: BoxedWidget<B>,
+
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static, B> View<State, Action> for HelpPage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
+
impl<'a: 'static> Widget for HelpPage<'a> {
+
    type Action = Action;
+
    type State = State;
+

    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -625,17 +612,6 @@ where
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &State) {
-
        self.props = HelpPageProps::from_callback(self.base.on_update, state)
-
            .unwrap_or(HelpPageProps::from(state));
-

-
        self.content.update(state);
-
    }
-

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
@@ -649,12 +625,14 @@ where
            }
        }
    }
-
}

-
impl<'a: 'static, B> Widget<B, State, Action> for HelpPage<'a, B>
-
where
-
    B: Backend + 'a,
-
{
+
    fn update(&mut self, state: &State) {
+
        self.props = HelpPageProps::from_callback(self.base.on_update, state)
+
            .unwrap_or(HelpPageProps::from(state));
+

+
        self.content.update(state);
+
    }
+

    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(HelpPageProps::from_boxed_any)
@@ -680,6 +658,10 @@ where
            let _ = self.base.action_tx.send(Action::HelpPageSize(page_size));
        }
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
+
    }
}

fn help_text() -> Text<'static> {
modified src/store.rs
@@ -13,13 +13,15 @@ const STORE_TICK_RATE: Duration = Duration::from_millis(1000);

/// The `State` known to the application store. It handles user-defined
/// application messages as well as ticks.
-
pub trait State<A, P>
+
pub trait State<P>
where
    P: Clone + Debug + Send + Sync,
{
+
    type Action;
+

    /// Handle a user-defined application message and return an `Exit` object
    /// in case the received message requested the application to also quit.
-
    fn handle_action(&mut self, action: A) -> Option<Exit<P>>;
+
    fn handle_action(&mut self, action: Self::Action) -> Option<Exit<P>>;

    /// Handle recurring tick.
    fn tick(&self);
@@ -29,7 +31,7 @@ where
/// messages coming from the frontend and updates the state accordingly.
pub struct Store<A, S, P>
where
-
    S: State<A, P> + Clone + Send + Sync,
+
    S: State<P> + Clone + Send + Sync,
    P: Clone + Debug + Send + Sync,
{
    state_tx: UnboundedSender<S>,
@@ -38,7 +40,7 @@ where

impl<A, S, P> Store<A, S, P>
where
-
    S: State<A, P> + Clone + Send + Sync,
+
    S: State<P> + Clone + Send + Sync,
    P: Clone + Debug + Send + Sync,
{
    pub fn new() -> (Self, UnboundedReceiver<S>) {
@@ -56,7 +58,7 @@ where

impl<A, S, P> Store<A, S, P>
where
-
    S: State<A, P> + Clone + Debug + Send + Sync + 'static,
+
    S: State<P, Action = A> + Clone + Debug + Send + Sync + 'static,
    P: Clone + Debug + Send + Sync + 'static,
{
    /// By calling `main_loop`, the store will wait for new messages coming
modified src/ui.rs
@@ -7,11 +7,8 @@ pub mod theme;
pub mod widget;

use std::fmt::Debug;
-
use std::io::{self};
use std::time::Duration;

-
use termion::raw::RawTerminal;
-

use tokio::sync::broadcast;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};

@@ -19,11 +16,8 @@ use super::event::Event;
use super::store::State;
use super::task::Interrupted;
use super::terminal;
-
use super::terminal::TermionBackendExt;
use super::ui::widget::Widget;

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

const RENDERING_TICK_RATE: Duration = Duration::from_millis(250);
const INLINE_HEIGHT: usize = 20;

@@ -71,8 +65,8 @@ impl<A> Frontend<A> {
        mut interrupt_rx: broadcast::Receiver<Interrupted<P>>,
    ) -> anyhow::Result<Interrupted<P>>
    where
-
        S: State<A, P>,
-
        W: Widget<Backend, S, A>,
+
        S: State<P>,
+
        W: Widget<State = S, Action = A>,
        P: Clone + Send + Sync + Debug,
    {
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
modified src/ui/widget.rs
@@ -18,7 +18,7 @@ use ratatui::widgets::{Cell, Row, TableState};
use super::theme::style;
use super::{layout, span};

-
pub type BoxedWidget<B, S, A> = Box<dyn Widget<B, S, A>>;
+
pub type BoxedWidget<S, A> = Box<dyn Widget<State = S, Action = A>>;

pub type UpdateCallback<S> = fn(&S) -> Box<dyn Any>;
pub type EventCallback<A> = fn(&dyn Any, UnboundedSender<A>);
@@ -33,18 +33,47 @@ pub struct BaseView<S, A> {
    pub on_event: Option<EventCallback<A>>,
}

-
/// Main trait defining a `View` behaviour.
+
/// Main trait defining a `Widget` behaviour.
///
-
/// This is the first trait that you should implement to define a custom `Widget`.
-
pub trait View<S, A> {
+
/// This is the trait that you should implement to define a custom `Widget`.
+
pub trait Widget {
+
    type State;
+
    type Action;
+

    /// Should return a new view with props build from state (if type is known) and a
    /// message sender set.
-
    fn new(state: &S, action_tx: UnboundedSender<A>) -> Self
+
    fn new(state: &Self::State, action_tx: UnboundedSender<Self::Action>) -> Self
    where
        Self: Sized;

+
    /// Should handle key events and call `handle_event` on all children.
+
    ///
+
    /// After key events have been handled, the custom event handler `on_event` should
+
    /// be called
+
    fn handle_event(&mut self, key: Key);
+

+
    /// Should update the internal props of this and all children.
+
    ///
+
    /// Applications are usually defined by app-specific widgets that do know
+
    /// the type of `state`. These can use widgets from the library that do not know the
+
    /// type of `state`.
+
    ///
+
    /// If `on_update` is set, implementations of this function should call it to
+
    /// construct and update the internal props. If it is not set, app widgets can construct
+
    /// props directly via their state converters, whereas library widgets can just fallback
+
    /// to their current props.
+
    fn update(&mut self, state: &Self::State);
+

+
    /// Renders a widget to the given frame in the given area.
+
    ///
+
    /// Optional props take precedence over the internal ones.
+
    fn render(&self, frame: &mut Frame, area: Rect, props: Option<Box<dyn Any>>);
+

+
    /// Return a mutable reference to this widgets' base view.
+
    fn base_mut(&mut self) -> &mut BaseView<Self::State, Self::Action>;
+

    /// Should set the optional custom event handler.
-
    fn on_event(mut self, callback: EventCallback<A>) -> Self
+
    fn on_event(mut self, callback: EventCallback<Self::Action>) -> Self
    where
        Self: Sized,
    {
@@ -53,7 +82,7 @@ pub trait View<S, A> {
    }

    /// Should set the optional update handler.
-
    fn on_update(mut self, callback: UpdateCallback<S>) -> Self
+
    fn on_update(mut self, callback: UpdateCallback<Self::State>) -> Self
    where
        Self: Sized,
    {
@@ -61,47 +90,13 @@ pub trait View<S, A> {
        self
    }

-
    /// Returns a boxed `View`
+
    /// Returns a boxed `Widget`
    fn to_boxed(self) -> Box<Self>
    where
        Self: Sized,
    {
        Box::new(self)
    }
-

-
    /// Return a mutable reference to this widgets' base view.
-
    fn base_mut(&mut self) -> &mut BaseView<S, A>;
-

-
    /// Should handle key events and call `handle_event` on all children.
-
    ///
-
    /// After key events have been handled, the custom event handler `on_event` should
-
    /// be called
-
    fn handle_event(&mut self, key: Key);
-

-
    /// Should update the internal props of this and all children.
-
    ///
-
    /// Applications are usually defined by app-specific widgets that do know
-
    /// the type of `state`. These can use widgets from the library that do not know the
-
    /// type of `state`.
-
    ///
-
    /// If `on_update` is set, implementations of this function should call it to
-
    /// construct and update the internal props. If it is not set, app widgets can construct
-
    /// props directly via their state converters, whereas library widgets can just fallback
-
    /// to their current props.
-
    fn update(&mut self, state: &S);
-
}
-

-
/// A `Widget` is a `View` that can be rendered using a specific backend.
-
///
-
/// This is the second trait that you should implement to define a custom `Widget`.
-
pub trait Widget<B, S, A>: View<S, A>
-
where
-
    B: Backend,
-
{
-
    /// Renders a widget to the given frame in the given area.
-
    ///
-
    /// Optional props take precedence over the internal ones.
-
    fn render(&self, frame: &mut Frame, area: Rect, props: Option<Box<dyn Any>>);
}

/// Needs to be implemented for items that are supposed to be rendered in tables.
@@ -155,35 +150,32 @@ impl<Id> Default for WindowProps<Id> {

impl<Id> Properties for WindowProps<Id> {}

-
pub struct Window<B, S, A, Id>
-
where
-
    B: Backend,
-
{
+
pub struct Window<S, A, Id> {
    /// Internal base
    base: BaseView<S, A>,
    /// Internal properties
    props: WindowProps<Id>,
    /// All pages known
-
    pages: HashMap<Id, BoxedWidget<B, S, A>>,
+
    pages: HashMap<Id, BoxedWidget<S, A>>,
}

-
impl<B, S, A, Id> Window<B, S, A, Id>
+
impl<S, A, Id> Window<S, A, Id>
where
-
    B: Backend,
    Id: Clone + Hash + Eq + PartialEq,
{
-
    pub fn page(mut self, id: Id, page: BoxedWidget<B, S, A>) -> Self {
-
        // self.pages.inse
+
    pub fn page(mut self, id: Id, page: BoxedWidget<S, A>) -> Self {
        self.pages.insert(id, page);
        self
    }
}

-
impl<'a: 'static, B, S, A, Id> View<S, A> for Window<B, S, A, Id>
+
impl<'a: 'static, S, A, Id> Widget for Window<S, A, Id>
where
-
    B: Backend + 'a,
    Id: Clone + Hash + Eq + PartialEq + 'a,
{
+
    type Action = A;
+
    type State = S;
+

    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self
    where
        Self: Sized,
@@ -199,13 +191,7 @@ where
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
-
        &mut self.base
-
    }
-
    fn update(&mut self, state: &S) {
-
        self.props =
-
            WindowProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
-

+
    fn handle_event(&mut self, key: termion::event::Key) {
        let page = self
            .props
            .current_page
@@ -213,11 +199,14 @@ where
            .and_then(|id| self.pages.get_mut(id));

        if let Some(page) = page {
-
            page.update(state);
+
            page.handle_event(key);
        }
    }

-
    fn handle_event(&mut self, key: termion::event::Key) {
+
    fn update(&mut self, state: &S) {
+
        self.props =
+
            WindowProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
+

        let page = self
            .props
            .current_page
@@ -225,16 +214,10 @@ where
            .and_then(|id| self.pages.get_mut(id));

        if let Some(page) = page {
-
            page.handle_event(key);
+
            page.update(state);
        }
    }
-
}

-
impl<'a: 'static, B, S, A, Id> Widget<B, S, A> for Window<B, S, A, Id>
-
where
-
    B: Backend + 'a,
-
    Id: Clone + Hash + Eq + PartialEq + 'a,
-
{
    fn render(&self, frame: &mut ratatui::Frame, _area: Rect, props: Option<Box<dyn Any>>) {
        let _props = props
            .and_then(WindowProps::from_boxed_any)
@@ -252,6 +235,10 @@ where
            page.render(frame, area, None);
        }
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
+
    }
}

#[derive(Clone)]
@@ -310,7 +297,10 @@ impl<S, A> Shortcuts<S, A> {
    }
}

-
impl<S, A> View<S, A> for Shortcuts<S, A> {
+
impl<S, A> Widget for Shortcuts<S, A> {
+
    type Action = A;
+
    type State = S;
+

    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
        Self {
            base: BaseView {
@@ -322,22 +312,13 @@ impl<S, A> View<S, A> for Shortcuts<S, A> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
-
        &mut self.base
-
    }
-

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

    fn update(&mut self, state: &S) {
        self.props =
            ShortcutsProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
    }
-
}

-
impl<B, S, A> Widget<B, S, A> for Shortcuts<S, A>
-
where
-
    B: Backend,
-
{
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        use ratatui::widgets::Table;

@@ -378,6 +359,10 @@ where
        let table = Table::new([Row::new(row)], widths).column_spacing(0);
        frame.render_widget(table, area);
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
+
    }
}

#[derive(Clone, Debug)]
@@ -540,10 +525,13 @@ where
    }
}

-
impl<'a: 'static, S, A, R> View<S, A> for Table<'a, S, A, R>
+
impl<'a: 'static, S, A, R> Widget for Table<'a, S, A, R>
where
    R: ToRow + Clone + 'static,
{
+
    type Action = A;
+
    type State = S;
+

    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
        Self {
            base: BaseView {
@@ -556,22 +544,6 @@ where
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &S) {
-
        self.props =
-
            TableProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
-

-
        // TODO: Move to state reducer
-
        if let Some(selected) = self.state.selected() {
-
            if selected > self.props.items.len() {
-
                self.begin();
-
            }
-
        }
-
    }
-

    fn handle_event(&mut self, key: Key) {
        match key {
            Key::Up | Key::Char('k') => {
@@ -601,13 +573,19 @@ where
            (on_event)(&self.state, self.base.action_tx.clone());
        }
    }
-
}

-
impl<'a: 'static, B, S, A, R> Widget<B, S, A> for Table<'a, S, A, R>
-
where
-
    B: Backend,
-
    R: ToRow + Clone + Debug + 'static,
-
{
+
    fn update(&mut self, state: &S) {
+
        self.props =
+
            TableProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
+

+
        // TODO: Move to state reducer
+
        if let Some(selected) = self.state.selected() {
+
            if selected > self.props.items.len() {
+
                self.begin();
+
            }
+
        }
+
    }
+

    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(TableProps::from_boxed_any)
@@ -664,6 +642,10 @@ where
            frame.render_widget(hint, center);
        }
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
+
    }
}

pub struct TableUtils {}
modified src/ui/widget/container.rs
@@ -11,7 +11,7 @@ use ratatui::widgets::{Block, BorderType, Borders, Row};
use crate::ui::ext::{FooterBlock, FooterBlockType, HeaderBlock};
use crate::ui::theme::style;

-
use super::{BaseView, BoxedWidget, Column, Properties, View, Widget};
+
use super::{BaseView, BoxedWidget, Column, Properties, Widget};

#[derive(Clone, Debug)]
pub struct HeaderProps<'a> {
@@ -77,7 +77,10 @@ impl<'a, S, A> Header<'a, S, A> {
    }
}

-
impl<'a: 'static, S, A> View<S, A> for Header<'a, S, A> {
+
impl<'a: 'static, S, A> Widget for Header<'a, S, A> {
+
    type Action = A;
+
    type State = S;
+

    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
        Self {
            base: BaseView {
@@ -89,8 +92,10 @@ impl<'a: 'static, S, A> View<S, A> for Header<'a, S, A> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
-
        &mut self.base
+
    fn handle_event(&mut self, _key: Key) {
+
        if let Some(on_event) = self.base.on_event {
+
            (on_event)(&self.props, self.base.action_tx.clone());
+
        }
    }

    fn update(&mut self, state: &S) {
@@ -101,17 +106,6 @@ impl<'a: 'static, S, A> View<S, A> for Header<'a, S, A> {
            .unwrap_or(self.props.clone());
    }

-
    fn handle_event(&mut self, _key: Key) {
-
        if let Some(on_event) = self.base.on_event {
-
            (on_event)(&self.props, self.base.action_tx.clone());
-
        }
-
    }
-
}
-

-
impl<'a: 'static, B, S, A> Widget<B, S, A> for Header<'a, S, A>
-
where
-
    B: Backend,
-
{
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(HeaderProps::from_boxed_any)
@@ -168,6 +162,10 @@ where
        frame.render_widget(block, area);
        frame.render_widget(header, header_layout[0]);
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
+
    }
}

#[derive(Clone, Debug)]
@@ -232,40 +230,7 @@ impl<'a, S, A> Footer<'a, S, A> {
        self.props.focus = focus;
        self
    }
-
}
-

-
impl<'a: 'static, S, A> View<S, A> for Footer<'a, S, A> {
-
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
-
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
-
            props: FooterProps::default(),
-
        }
-
    }
-

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &S) {
-
        self.props = self
-
            .base
-
            .on_update
-
            .and_then(|on_update| FooterProps::from_boxed_any((on_update)(state)))
-
            .unwrap_or(self.props.clone());
-
    }
-

-
    fn handle_event(&mut self, _key: Key) {
-
        if let Some(on_event) = self.base.on_event {
-
            (on_event)(&self.props, self.base.action_tx.clone());
-
        }
-
    }
-
}

-
impl<'a, S, A> Footer<'a, S, A> {
    fn render_cell(
        &self,
        frame: &mut ratatui::Frame,
@@ -289,10 +254,35 @@ impl<'a, S, A> Footer<'a, S, A> {
    }
}

-
impl<'a: 'static, B, S, A> Widget<B, S, A> for Footer<'a, S, A>
-
where
-
    B: Backend,
-
{
+
impl<'a: 'static, S, A> Widget for Footer<'a, S, A> {
+
    type Action = A;
+
    type State = S;
+

+
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
+
        Self {
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
+
            props: FooterProps::default(),
+
        }
+
    }
+

+
    fn handle_event(&mut self, _key: Key) {
+
        if let Some(on_event) = self.base.on_event {
+
            (on_event)(&self.props, self.base.action_tx.clone());
+
        }
+
    }
+

+
    fn update(&mut self, state: &S) {
+
        self.props = self
+
            .base
+
            .on_update
+
            .and_then(|on_update| FooterProps::from_boxed_any((on_update)(state)))
+
            .unwrap_or(self.props.clone());
+
    }
+

    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(FooterProps::from_boxed_any)
@@ -328,6 +318,10 @@ where
            self.render_cell(frame, *area, block_type, cell.clone(), props.focus);
        }
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
+
    }
}

#[derive(Clone, Default)]
@@ -350,46 +344,40 @@ impl ContainerProps {

impl Properties for ContainerProps {}

-
pub struct Container<B, S, A>
-
where
-
    B: Backend,
-
{
+
pub struct Container<S, A> {
    /// Internal base
    base: BaseView<S, A>,
    /// Internal props
    props: ContainerProps,
    /// Container header
-
    header: Option<BoxedWidget<B, S, A>>,
+
    header: Option<BoxedWidget<S, A>>,
    /// Content widget
-
    content: Option<BoxedWidget<B, S, A>>,
+
    content: Option<BoxedWidget<S, A>>,
    /// Container footer
-
    footer: Option<BoxedWidget<B, S, A>>,
+
    footer: Option<BoxedWidget<S, A>>,
}

-
impl<B, S, A> Container<B, S, A>
-
where
-
    B: Backend,
-
{
-
    pub fn header(mut self, header: BoxedWidget<B, S, A>) -> Self {
+
impl<S, A> Container<S, A> {
+
    pub fn header(mut self, header: BoxedWidget<S, A>) -> Self {
        self.header = Some(header);
        self
    }

-
    pub fn content(mut self, content: BoxedWidget<B, S, A>) -> Self {
+
    pub fn content(mut self, content: BoxedWidget<S, A>) -> Self {
        self.content = Some(content);
        self
    }

-
    pub fn footer(mut self, footer: BoxedWidget<B, S, A>) -> Self {
+
    pub fn footer(mut self, footer: BoxedWidget<S, A>) -> Self {
        self.footer = Some(footer);
        self
    }
}

-
impl<B, S, A> View<S, A> for Container<B, S, A>
-
where
-
    B: Backend,
-
{
+
impl<S, A> Widget for Container<S, A> {
+
    type Action = A;
+
    type State = S;
+

    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self
    where
        Self: Sized,
@@ -408,8 +396,10 @@ where
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
-
        &mut self.base
+
    fn handle_event(&mut self, key: termion::event::Key) {
+
        if let Some(content) = &mut self.content {
+
            content.handle_event(key);
+
        }
    }

    fn update(&mut self, state: &S) {
@@ -429,17 +419,6 @@ where
        }
    }

-
    fn handle_event(&mut self, key: termion::event::Key) {
-
        if let Some(content) = &mut self.content {
-
            content.handle_event(key);
-
        }
-
    }
-
}
-

-
impl<'a: 'static, B, S, A> Widget<B, S, A> for Container<B, S, A>
-
where
-
    B: Backend,
-
{
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(ContainerProps::from_boxed_any)
@@ -487,4 +466,8 @@ where
            footer.render(frame, footer_area, None);
        }
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
+
    }
}
modified src/ui/widget/input.rs
@@ -5,11 +5,11 @@ use termion::event::Key;
use tokio::sync::mpsc::UnboundedSender;

use ratatui::layout::{Constraint, Layout};
-
use ratatui::prelude::{Backend, Rect};
+
use ratatui::prelude::Rect;
use ratatui::style::Stylize;
use ratatui::text::{Line, Span};

-
use super::{BaseView, Properties, View, Widget};
+
use super::{BaseView, Properties, Widget};

#[derive(Clone)]
pub struct TextFieldProps {
@@ -126,7 +126,10 @@ impl<S, A> TextField<S, A> {
    }
}

-
impl<S, A> View<S, A> for TextField<S, A> {
+
impl<S, A> Widget for TextField<S, A> {
+
    type Action = A;
+
    type State = S;
+

    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
        Self {
            base: BaseView {
@@ -142,23 +145,6 @@ impl<S, A> View<S, A> for TextField<S, A> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &S) {
-
        if let Some(on_update) = self.base.on_update {
-
            if let Some(props) = (on_update)(state).downcast_ref::<TextFieldProps>() {
-
                self.props = props.clone();
-

-
                if self.state.text.is_none() {
-
                    self.state.cursor_position = props.text.len().saturating_sub(1);
-
                }
-
                self.state.text = Some(props.text.clone());
-
            }
-
        }
-
    }
-

    fn handle_event(&mut self, key: Key) {
        match key {
            Key::Char(to_insert)
@@ -184,12 +170,20 @@ impl<S, A> View<S, A> for TextField<S, A> {
            (on_event)(&self.state, self.base.action_tx.clone());
        }
    }
-
}

-
impl<B, S, A> Widget<B, S, A> for TextField<S, A>
-
where
-
    B: Backend,
-
{
+
    fn update(&mut self, state: &S) {
+
        if let Some(on_update) = self.base.on_update {
+
            if let Some(props) = (on_update)(state).downcast_ref::<TextFieldProps>() {
+
                self.props = props.clone();
+

+
                if self.state.text.is_none() {
+
                    self.state.cursor_position = props.text.len().saturating_sub(1);
+
                }
+
                self.state.text = Some(props.text.clone());
+
            }
+
        }
+
    }
+

    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(TextFieldProps::from_boxed_any)
@@ -241,4 +235,8 @@ where
            }
        }
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
+
    }
}
modified src/ui/widget/text.rs
@@ -4,11 +4,10 @@ use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;

-
use ratatui::backend::Backend;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Text;

-
use super::{BaseView, Properties, View, Widget};
+
use super::{BaseView, Properties, Widget};

#[derive(Clone)]
pub struct ParagraphProps<'a> {
@@ -142,7 +141,10 @@ impl<'a, S, A> Paragraph<'a, S, A> {
    }
}

-
impl<'a: 'static, S, A> View<S, A> for Paragraph<'a, S, A> {
+
impl<'a: 'static, S, A> Widget for Paragraph<'a, S, A> {
+
    type Action = A;
+
    type State = S;
+

    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self
    where
        Self: Sized,
@@ -161,15 +163,6 @@ impl<'a: 'static, S, A> View<S, A> for Paragraph<'a, S, A> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
-
        &mut self.base
-
    }
-

-
    fn update(&mut self, state: &S) {
-
        self.props =
-
            ParagraphProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
-
    }
-

    fn handle_event(&mut self, key: Key) {
        let len = self.props.content.lines.len() + 1;
        let page_size = self.props.page_size;
@@ -200,12 +193,12 @@ impl<'a: 'static, S, A> View<S, A> for Paragraph<'a, S, A> {
            (on_event)(&self.state, self.base.action_tx.clone());
        }
    }
-
}

-
impl<'a: 'static, B, S, A> Widget<B, S, A> for Paragraph<'a, S, A>
-
where
-
    B: Backend,
-
{
+
    fn update(&mut self, state: &S) {
+
        self.props =
+
            ParagraphProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
+
    }
+

    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(ParagraphProps::from_boxed_any)
@@ -219,4 +212,8 @@ where

        frame.render_widget(content, content_area);
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
+
    }
}