Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Stateless and stateful widgets
Draft did:key:z6MkswQE...2C1V opened 2 years ago
11 files changed +221 -186 d54f20f1 a79608db
modified bin/commands/inbox/select.rs
@@ -14,12 +14,13 @@ use radicle::storage::git::Repository;
use radicle::Profile;
use radicle_tui as tui;

-
use tui::cob::inbox::{self};
+
use tui::cob::inbox;
use tui::store;
use tui::store::StateValue;
-
use tui::task::{self, Interrupted};
+
use tui::task;
+
use tui::task::Interrupted;
use tui::ui::items::{Filter, NotificationItem, NotificationItemFilter};
-
use tui::ui::widget::{Properties, Widget, Window, WindowProps};
+
use tui::ui::widget::{Properties, StatefulWidget, StatelessWidget, Widget, Window, WindowProps};
use tui::ui::Frontend;
use tui::Exit;

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

-
        let window: Window<State, Action, Page> = Window::new(&state, action_tx.clone())
+
        let window: Window<State, Action, Page> = Window::new(action_tx.clone())
            .page(
                Page::Browse,
                BrowsePage::new(&state, action_tx.clone()).to_boxed(),
@@ -297,7 +298,7 @@ impl App {

        tokio::try_join!(
            store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
-
            frontend.main_loop(Some(window), state_rx, interrupt_rx.resubscribe()),
+
            frontend.main_loop(window, state_rx, interrupt_rx.resubscribe()),
        )?;

        if let Ok(reason) = interrupt_rx.recv().await {
modified bin/commands/inbox/select/ui.rs
@@ -15,13 +15,16 @@ use radicle_tui as tui;

use tui::ui::items::{NotificationItem, NotificationItemFilter, NotificationState};
use tui::ui::span;
+
use tui::ui::widget;
use tui::ui::widget::container::{
    Container, ContainerProps, Footer, FooterProps, Header, HeaderProps,
};
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, Widget};
+
use tui::ui::widget::{
+
    BaseView, Column, Properties, Shortcuts, ShortcutsProps, StatefulWidget, StatelessWidget,
+
    Table, TableProps, TableUtils, Widget,
+
};
use tui::Selection;

use crate::tui_inbox::common::{InboxOperation, Mode, RepositoryMode, SelectionMode};
@@ -116,10 +119,7 @@ pub struct BrowsePage<'a> {
    shortcuts: BoxedWidget,
}

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

+
impl<'a: 'static> StatefulWidget for BrowsePage<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
        let props = BrowsePageProps::from(state);
        let name = match state.mode.repository() {
@@ -135,9 +135,9 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                on_event: None,
            },
            props: props.clone(),
-
            notifications: Container::new(state, action_tx.clone())
+
            notifications: Container::new(action_tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(action_tx.clone())
                        .columns(
                            [
                                Column::new("", Constraint::Length(0)),
@@ -150,7 +150,7 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                        .to_boxed(),
                )
                .content(Box::<Table<State, Action, NotificationItem>>::new(
-
                    Table::new(state, action_tx.clone())
+
                    Table::new(action_tx.clone())
                        .on_event(|state, action_tx| {
                            state.downcast_ref::<TableState>().and_then(|state| {
                                action_tx
@@ -173,7 +173,7 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                        }),
                ))
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(action_tx.clone())
                        .on_update(|state| {
                            let props = BrowsePageProps::from(state);

@@ -184,10 +184,15 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                        .to_boxed(),
                )
                .to_boxed(),
-
            search: Search::new(state, action_tx.clone()).to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
+
            search: Search::new(action_tx.clone()).to_boxed(),
+
            shortcuts: Shortcuts::new(action_tx.clone()).to_boxed(),
        }
    }
+
}
+

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

    fn handle_event(&mut self, key: Key) {
        if self.props.show_search {
@@ -325,15 +330,9 @@ pub struct Search {
    input: BoxedWidget,
}

-
impl Widget for Search {
-
    type Action = Action;
-
    type State = State;
-

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        let input = TextField::new(state, action_tx.clone())
+
impl StatelessWidget for Search {
+
    fn new(action_tx: UnboundedSender<Action>) -> Self {
+
        let input = TextField::new(action_tx.clone())
            .on_event(|state, action_tx| {
                state.downcast_ref::<TextFieldState>().and_then(|state| {
                    action_tx
@@ -343,7 +342,7 @@ impl Widget for Search {
                        .ok()
                });
            })
-
            .on_update(|state| {
+
            .on_update(|state: &State| {
                TextFieldProps::default()
                    .text(&state.browser.search.read().to_string())
                    .title("Search")
@@ -361,6 +360,11 @@ impl Widget for Search {
            input,
        }
    }
+
}
+

+
impl Widget for Search {
+
    type Action = Action;
+
    type State = State;

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
@@ -425,10 +429,7 @@ pub struct HelpPage<'a> {
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static> Widget for HelpPage<'a> {
-
    type Action = Action;
-
    type State = State;
-

+
impl<'a: 'static> StatefulWidget for HelpPage<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -440,9 +441,9 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                on_event: None,
            },
            props: HelpPageProps::from(state),
-
            content: Container::new(state, action_tx.clone())
+
            content: Container::new(action_tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(action_tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -454,7 +455,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .content(
-
                    Paragraph::new(state, action_tx.clone())
+
                    Paragraph::new(action_tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -476,7 +477,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(action_tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -498,9 +499,14 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
+
            shortcuts: Shortcuts::new(action_tx.clone()).to_boxed(),
        }
    }
+
}
+

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

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
modified bin/commands/issue/select.rs
@@ -16,7 +16,7 @@ use tui::store::StateValue;
use tui::task;
use tui::task::Interrupted;
use tui::ui::items::{Filter, IssueItem, IssueItemFilter};
-
use tui::ui::widget::{Properties, Widget, Window, WindowProps};
+
use tui::ui::widget::{Properties, StatefulWidget, StatelessWidget, Widget, Window, WindowProps};
use tui::ui::Frontend;
use tui::Exit;
use tui::{store, PageStack};
@@ -199,7 +199,7 @@ impl App {
        let (frontend, action_tx, action_rx) = Frontend::new();
        let state = State::try_from(&self.context)?;

-
        let window: Window<State, Action, Page> = Window::new(&state, action_tx.clone())
+
        let window: Window<State, Action, Page> = Window::new(action_tx.clone())
            .page(
                Page::Browse,
                BrowsePage::new(&state, action_tx.clone()).to_boxed(),
@@ -216,7 +216,7 @@ impl App {

        tokio::try_join!(
            store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
-
            frontend.main_loop(Some(window), state_rx, interrupt_rx.resubscribe()),
+
            frontend.main_loop(window, state_rx, interrupt_rx.resubscribe()),
        )?;

        if let Ok(reason) = interrupt_rx.recv().await {
modified bin/commands/issue/select/ui.rs
@@ -3,7 +3,8 @@ use std::collections::HashMap;
use std::str::FromStr;
use std::vec;

-
use radicle::issue::{self, CloseReason};
+
use radicle::issue;
+
use radicle::issue::CloseReason;
use ratatui::widgets::TableState;
use tokio::sync::mpsc::UnboundedSender;

@@ -17,12 +18,13 @@ use radicle_tui as tui;

use tui::ui::items::{IssueItem, IssueItemFilter};
use tui::ui::span;
+
use tui::ui::widget;
use tui::ui::widget::container::{
    Container, ContainerProps, Footer, FooterProps, Header, HeaderProps,
};
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::{BaseView, StatefulWidget, StatelessWidget};
use tui::ui::widget::{
    Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, TableUtils, Widget,
};
@@ -132,10 +134,7 @@ pub struct BrowsePage<'a> {
    shortcuts: BoxedWidget,
}

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

+
impl<'a: 'static> StatefulWidget for BrowsePage<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
        let props = BrowsePageProps::from(state);

@@ -146,16 +145,16 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                on_event: None,
            },
            props: BrowsePageProps::from(state),
-
            issues: Container::new(state, action_tx.clone())
+
            issues: Container::new(action_tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(action_tx.clone())
                        .columns(props.columns.clone())
                        .cutoff(props.cutoff, props.cutoff_after)
                        .focus(props.focus)
                        .to_boxed(),
                )
                .content(Box::<Table<State, Action, IssueItem>>::new(
-
                    Table::new(state, action_tx.clone())
+
                    Table::new(action_tx.clone())
                        .on_event(|state, action_tx| {
                            state.downcast_ref::<TableState>().and_then(|state| {
                                action_tx
@@ -178,7 +177,7 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                        }),
                ))
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(action_tx.clone())
                        .on_update(|state| {
                            let props = BrowsePageProps::from(state);

@@ -189,10 +188,15 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                        .to_boxed(),
                )
                .to_boxed(),
-
            search: Search::new(state, action_tx.clone()).to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
+
            search: Search::new(action_tx.clone()).to_boxed(),
+
            shortcuts: Shortcuts::new(action_tx.clone()).to_boxed(),
        }
    }
+
}
+

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

    fn handle_event(&mut self, key: Key) {
        if self.props.show_search {
@@ -332,15 +336,9 @@ pub struct Search {
    input: BoxedWidget,
}

-
impl Widget for Search {
-
    type Action = Action;
-
    type State = State;
-

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        let input = TextField::new(state, action_tx.clone())
+
impl StatelessWidget for Search {
+
    fn new(action_tx: UnboundedSender<Action>) -> Self {
+
        let input = TextField::new(action_tx.clone())
            .on_event(|state, action_tx| {
                state.downcast_ref::<TextFieldState>().and_then(|state| {
                    action_tx
@@ -350,7 +348,7 @@ impl Widget for Search {
                        .ok()
                });
            })
-
            .on_update(|state| {
+
            .on_update(|state: &State| {
                TextFieldProps::default()
                    .text(&state.browser.search.read().to_string())
                    .title("Search")
@@ -368,6 +366,11 @@ impl Widget for Search {
            input,
        }
    }
+
}
+

+
impl Widget for Search {
+
    type Action = Action;
+
    type State = State;

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
@@ -432,10 +435,7 @@ pub struct HelpPage<'a> {
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static> Widget for HelpPage<'a> {
-
    type Action = Action;
-
    type State = State;
-

+
impl<'a: 'static> StatefulWidget for HelpPage<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -447,9 +447,9 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                on_event: None,
            },
            props: HelpPageProps::from(state),
-
            content: Container::new(state, action_tx.clone())
+
            content: Container::new(action_tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(action_tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -461,7 +461,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .content(
-
                    Paragraph::new(state, action_tx.clone())
+
                    Paragraph::new(action_tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -483,7 +483,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(action_tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -505,9 +505,14 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
+
            shortcuts: Shortcuts::new(action_tx.clone()).to_boxed(),
        }
    }
+
}
+

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

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
modified bin/commands/patch/select.rs
@@ -16,10 +16,7 @@ use tui::store;
use tui::task;
use tui::task::Interrupted;
use tui::ui::items::{Filter, PatchItem, PatchItemFilter};
-
use tui::ui::widget::Properties;
-
use tui::ui::widget::Widget;
-
use tui::ui::widget::Window;
-
use tui::ui::widget::WindowProps;
+
use tui::ui::widget::{Properties, StatefulWidget, StatelessWidget, Widget, Window, WindowProps};
use tui::ui::Frontend;
use tui::Exit;

@@ -201,10 +198,10 @@ impl App {
    pub async fn run(&self) -> Result<Option<Selection>> {
        let (terminator, mut interrupt_rx) = task::create_termination();
        let (store, state_rx) = store::Store::<Action, State, Selection>::new();
-
        let (frontend, action_tx, action_rx) = Frontend::<Action>::new();
+
        let (frontend, action_tx, action_rx) = Frontend::new();
        let state = State::try_from(&self.context)?;

-
        let window: Window<State, Action, Page> = Window::new(&state, action_tx.clone())
+
        let window: Window<State, Action, Page> = Window::new(action_tx.clone())
            .page(
                Page::Browse,
                BrowsePage::new(&state, action_tx.clone()).to_boxed(),
@@ -221,7 +218,7 @@ impl App {

        tokio::try_join!(
            store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
-
            frontend.main_loop(Some(window), state_rx, interrupt_rx.resubscribe()),
+
            frontend.main_loop(window, state_rx, interrupt_rx.resubscribe()),
        )?;

        if let Ok(reason) = interrupt_rx.recv().await {
modified bin/commands/patch/select/ui.rs
@@ -23,9 +23,9 @@ 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::TableUtils;
use tui::ui::widget::{self, BaseView};
use tui::ui::widget::{Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, Widget};
+
use tui::ui::widget::{StatefulWidget, StatelessWidget, TableUtils};
use tui::Selection;

use crate::tui_patch::common::Mode;
@@ -131,10 +131,7 @@ pub struct BrowsePage<'a> {
    shortcuts: BoxedWidget,
}

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

+
impl<'a: 'static> StatefulWidget for BrowsePage<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
        let props = BrowsePageProps::from(state);

@@ -145,16 +142,16 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                on_event: None,
            },
            props: props.clone(),
-
            patches: Container::new(state, action_tx.clone())
+
            patches: Container::new(action_tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(action_tx.clone())
                        .columns(props.columns.clone())
                        .cutoff(props.cutoff, props.cutoff_after)
                        .focus(props.focus)
                        .to_boxed(),
                )
                .content(Box::<Table<State, Action, PatchItem>>::new(
-
                    Table::new(state, action_tx.clone())
+
                    Table::new(action_tx.clone())
                        .on_event(|state, action_tx| {
                            state.downcast_ref::<TableState>().and_then(|state| {
                                action_tx
@@ -177,7 +174,7 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                        }),
                ))
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(action_tx.clone())
                        .on_update(|state| {
                            let props = BrowsePageProps::from(state);

@@ -188,10 +185,15 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                        .to_boxed(),
                )
                .to_boxed(),
-
            search: Search::new(state, action_tx.clone()).to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
+
            search: Search::new(action_tx.clone()).to_boxed(),
+
            shortcuts: Shortcuts::new(action_tx.clone()).to_boxed(),
        }
    }
+
}
+

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

    fn handle_event(&mut self, key: Key) {
        if self.props.show_search {
@@ -348,15 +350,9 @@ pub struct Search {
    input: BoxedWidget,
}

-
impl Widget for Search {
-
    type Action = Action;
-
    type State = State;
-

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        let input = TextField::new(state, action_tx.clone())
+
impl StatelessWidget for Search {
+
    fn new(action_tx: UnboundedSender<Action>) -> Self {
+
        let input = TextField::new(action_tx.clone())
            .on_event(|state, action_tx| {
                state.downcast_ref::<TextFieldState>().and_then(|state| {
                    action_tx
@@ -366,7 +362,7 @@ impl Widget for Search {
                        .ok()
                });
            })
-
            .on_update(|state| {
+
            .on_update(|state: &State| {
                TextFieldProps::default()
                    .text(&state.browser.search.read().to_string())
                    .title("Search")
@@ -384,6 +380,11 @@ impl Widget for Search {
            input,
        }
    }
+
}
+

+
impl Widget for Search {
+
    type Action = Action;
+
    type State = State;

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
@@ -448,10 +449,7 @@ pub struct HelpPage<'a> {
    shortcuts: BoxedWidget,
}

-
impl<'a: 'static> Widget for HelpPage<'a> {
-
    type Action = Action;
-
    type State = State;
-

+
impl<'a: 'static> StatefulWidget for HelpPage<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -463,9 +461,9 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                on_event: None,
            },
            props: HelpPageProps::from(state),
-
            content: Container::new(state, action_tx.clone())
+
            content: Container::new(action_tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(action_tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -477,7 +475,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .content(
-
                    Paragraph::new(state, action_tx.clone())
+
                    Paragraph::new(action_tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -499,7 +497,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(action_tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -521,9 +519,14 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
+
            shortcuts: Shortcuts::new(action_tx.clone()).to_boxed(),
        }
    }
+
}
+

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

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
modified src/ui.rs
@@ -10,13 +10,16 @@ use std::fmt::Debug;
use std::time::Duration;

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

+
use tokio::sync::mpsc;
+
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
+

+
use self::widget::Widget;

use super::event::Event;
use super::store::State;
use super::task::Interrupted;
use super::terminal;
-
use super::ui::widget::Widget;

const RENDERING_TICK_RATE: Duration = Duration::from_millis(250);
const INLINE_HEIGHT: usize = 20;
@@ -26,22 +29,14 @@ const INLINE_HEIGHT: usize = 20;
///
/// Once created and run with `main_loop`, the `Frontend` will wait for new messages
/// being sent on either the terminal event, the state or the interrupt message channel.
-
pub struct Frontend<A> {
-
    action_tx: mpsc::UnboundedSender<A>,
-
}
+
pub struct Frontend {}

-
impl<A> Frontend<A> {
+
impl Frontend {
    /// Create a new `Frontend` storing the sending end of a message channel.
-
    pub fn new() -> (Self, UnboundedSender<A>, UnboundedReceiver<A>) {
+
    pub fn new<A>() -> (Self, UnboundedSender<A>, UnboundedReceiver<A>) {
        let (action_tx, action_rx) = mpsc::unbounded_channel();

-
        (
-
            Self {
-
                action_tx: action_tx.clone(),
-
            },
-
            action_tx,
-
            action_rx,
-
        )
+
        (Self {}, action_tx, action_rx)
    }

    /// By calling `main_loop`, the `Frontend` will wait for new messages being sent
@@ -58,9 +53,9 @@ impl<A> Frontend<A> {
    ///
    /// Interrupt messages are being sent to broadcast channel for retrieving the
    /// application kill signal.
-
    pub async fn main_loop<S, W, P>(
+
    pub async fn main_loop<S, A, W, P>(
        self,
-
        root: Option<W>,
+
        mut root: W,
        mut state_rx: UnboundedReceiver<S>,
        mut interrupt_rx: broadcast::Receiver<Interrupted<P>>,
    ) -> anyhow::Result<Interrupted<P>>
@@ -74,17 +69,8 @@ impl<A> Frontend<A> {
        let mut terminal = terminal::setup(INLINE_HEIGHT)?;
        let mut events_rx = terminal::events();

-
        let mut root = {
-
            let state = state_rx.recv().await.unwrap();
-

-
            match root {
-
                Some(mut root) => {
-
                    root.update(&state);
-
                    root
-
                }
-
                None => W::new(&state, self.action_tx.clone()),
-
            }
-
        };
+
        let state = state_rx.recv().await.unwrap();
+
        root.update(&state);

        let result: anyhow::Result<Interrupted<P>> = loop {
            tokio::select! {
modified src/ui/widget.rs
@@ -40,11 +40,11 @@ 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: &Self::State, action_tx: UnboundedSender<Self::Action>) -> Self
-
    where
-
        Self: Sized;
+
    // /// Should return a new view with props build from state (if type is known) and a
+
    // /// message sender set.
+
    // 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.
    ///
@@ -99,6 +99,22 @@ pub trait Widget {
    }
}

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

+
pub trait StatelessWidget: Widget {
+
    /// Should return a new view with props build from state (if type is known) and a
+
    /// message sender set.
+
    fn new(action_tx: UnboundedSender<Self::Action>) -> Self
+
    where
+
        Self: Sized;
+
}
+

/// Needs to be implemented for items that are supposed to be rendered in tables.
pub trait ToRow {
    fn to_row(&self) -> Vec<Cell>;
@@ -169,14 +185,11 @@ where
    }
}

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

-
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self
+
    fn new(action_tx: UnboundedSender<A>) -> Self
    where
        Self: Sized,
    {
@@ -190,6 +203,14 @@ where
            pages: HashMap::new(),
        }
    }
+
}
+

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

    fn handle_event(&mut self, key: termion::event::Key) {
        let page = self
@@ -297,11 +318,11 @@ impl<S, A> 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 {
+
impl<'a: 'static, S, A> StatelessWidget for Shortcuts<S, A> {
+
    fn new(action_tx: UnboundedSender<A>) -> Self
+
    where
+
        Self: Sized,
+
    {
        Self {
            base: BaseView {
                action_tx: action_tx.clone(),
@@ -311,6 +332,11 @@ impl<S, A> Widget for Shortcuts<S, A> {
            props: ShortcutsProps::default(),
        }
    }
+
}
+

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

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

@@ -525,14 +551,11 @@ where
    }
}

-
impl<'a: 'static, S, A, R> Widget for Table<'a, S, A, R>
+
impl<'a: 'static, S, A, R> StatelessWidget 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 {
+
    fn new(action_tx: UnboundedSender<A>) -> Self {
        Self {
            base: BaseView {
                action_tx: action_tx.clone(),
@@ -543,6 +566,14 @@ where
            state: TableState::default().with_selected(Some(0)),
        }
    }
+
}
+

+
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 handle_event(&mut self, key: Key) {
        match key {
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, Widget};
+
use super::{BaseView, BoxedWidget, Column, Properties, StatelessWidget, Widget};

#[derive(Clone, Debug)]
pub struct HeaderProps<'a> {
@@ -77,11 +77,8 @@ impl<'a, S, A> 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 {
+
impl<'a: 'static, S, A> StatelessWidget for Header<'a, S, A> {
+
    fn new(action_tx: UnboundedSender<A>) -> Self {
        Self {
            base: BaseView {
                action_tx: action_tx.clone(),
@@ -91,6 +88,11 @@ impl<'a: 'static, S, A> Widget for Header<'a, S, A> {
            props: HeaderProps::default(),
        }
    }
+
}
+

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

    fn handle_event(&mut self, _key: Key) {
        if let Some(on_event) = self.base.on_event {
@@ -254,11 +256,8 @@ impl<'a, S, A> Footer<'a, S, A> {
    }
}

-
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 {
+
impl<'a: 'static, S, A> StatelessWidget for Footer<'a, S, A> {
+
    fn new(action_tx: UnboundedSender<A>) -> Self {
        Self {
            base: BaseView {
                action_tx: action_tx.clone(),
@@ -268,6 +267,11 @@ impl<'a: 'static, S, A> Widget for Footer<'a, S, A> {
            props: FooterProps::default(),
        }
    }
+
}
+

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

    fn handle_event(&mut self, _key: Key) {
        if let Some(on_event) = self.base.on_event {
@@ -374,18 +378,11 @@ impl<S, A> Container<S, A> {
    }
}

-
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,
-
    {
+
impl<S, A> StatelessWidget for Container<S, A> {
+
    fn new(action_tx: UnboundedSender<A>) -> Self {
        Self {
            base: BaseView {
                action_tx: action_tx.clone(),
-

                on_update: None,
                on_event: None,
            },
@@ -395,6 +392,11 @@ impl<S, A> Widget for Container<S, A> {
            footer: None,
        }
    }
+
}
+

+
impl<S, A> Widget for Container<S, A> {
+
    type Action = A;
+
    type State = S;

    fn handle_event(&mut self, key: termion::event::Key) {
        if let Some(content) = &mut self.content {
modified src/ui/widget/input.rs
@@ -9,7 +9,7 @@ use ratatui::prelude::Rect;
use ratatui::style::Stylize;
use ratatui::text::{Line, Span};

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

#[derive(Clone)]
pub struct TextFieldProps {
@@ -126,11 +126,8 @@ impl<S, A> 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 {
+
impl<S, A> StatelessWidget for TextField<S, A> {
+
    fn new(action_tx: UnboundedSender<A>) -> Self {
        Self {
            base: BaseView {
                action_tx: action_tx.clone(),
@@ -144,6 +141,11 @@ impl<S, A> Widget for TextField<S, A> {
            },
        }
    }
+
}
+

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

    fn handle_event(&mut self, key: Key) {
        match key {
modified src/ui/widget/text.rs
@@ -7,7 +7,7 @@ use termion::event::Key;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Text;

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

#[derive(Clone)]
pub struct ParagraphProps<'a> {
@@ -141,11 +141,8 @@ impl<'a, S, A> 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
+
impl<'a: 'static, S, A> StatelessWidget for Paragraph<'a, S, A> {
+
    fn new(action_tx: UnboundedSender<A>) -> Self
    where
        Self: Sized,
    {
@@ -162,6 +159,11 @@ impl<'a: 'static, S, A> Widget for Paragraph<'a, S, A> {
            },
        }
    }
+
}
+

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

    fn handle_event(&mut self, key: Key) {
        let len = self.props.content.lines.len() + 1;