Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Introduce base view
Merged did:key:z6MkgFq6...nBGz opened 2 years ago

Every view has a base that all views share: a message sender, an update and an event callback.

8 files changed +325 -413 73f29c94 956ca70f
modified bin/commands/inbox/select/ui.rs
@@ -21,10 +21,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::{self, TableUtils};
+
use tui::ui::widget::{self, BaseView, TableUtils};
use tui::ui::widget::{
-
    Column, EventCallback, Properties, Shortcuts, ShortcutsProps, Table, TableProps,
-
    UpdateCallback, View, Widget,
+
    Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, View, Widget,
};
use tui::Selection;

@@ -108,14 +107,10 @@ impl<'a> From<&State> for BrowsePageProps<'a> {
impl<'a> Properties for BrowsePageProps<'a> {}

pub struct BrowsePage<'a, B> {
-
    /// Internal properties
+
    /// Internal base
+
    base: BaseView<State, Action>,
+
    /// Internal props
    props: BrowsePageProps<'a>,
-
    /// Message sender
-
    action_tx: UnboundedSender<Action>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<State>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<Action>>,
    /// Notifications widget
    notifications: BoxedWidget<B>,
    /// Search widget
@@ -137,8 +132,12 @@ where
        };

        Self {
-
            action_tx: action_tx.clone(),
-
            props: BrowsePageProps::from(state),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
+
            props: props.clone(),
            notifications: Container::new(state, action_tx.clone())
                .header(
                    Header::new(state, action_tx.clone())
@@ -190,23 +189,15 @@ where
                .to_boxed(),
            search: Search::new(state, action_tx.clone()).to_boxed(),
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_event(mut self, callback: EventCallback<Action>) -> Self {
-
        self.on_event = Some(callback);
-
        self
-
    }
-

-
    fn on_update(mut self, callback: UpdateCallback<State>) -> Self {
-
        self.on_update = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
    }

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

        self.notifications.update(state);
@@ -214,19 +205,19 @@ where
        self.shortcuts.update(state);
    }

-
    fn handle_key_event(&mut self, key: Key) {
+
    fn handle_event(&mut self, key: Key) {
        if self.props.show_search {
-
            self.search.handle_key_event(key);
+
            self.search.handle_event(key);
        } else {
            match key {
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.action_tx.send(Action::Exit { selection: None });
+
                    let _ = self.base.action_tx.send(Action::Exit { selection: None });
                }
                Key::Char('?') => {
-
                    let _ = self.action_tx.send(Action::OpenHelp);
+
                    let _ = self.base.action_tx.send(Action::OpenHelp);
                }
                Key::Char('/') => {
-
                    let _ = self.action_tx.send(Action::OpenSearch);
+
                    let _ = self.base.action_tx.send(Action::OpenSearch);
                }
                Key::Char('\n') => {
                    self.props
@@ -240,7 +231,8 @@ where
                                SelectionMode::Id => Selection::default().with_id(notif.id),
                            };

-
                            self.action_tx
+
                            self.base
+
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(selection),
                                })
@@ -252,7 +244,8 @@ where
                        .selected
                        .and_then(|selected| self.props.notifications.get(selected))
                        .and_then(|notif| {
-
                            self.action_tx
+
                            self.base
+
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(
                                        Selection::default()
@@ -264,7 +257,7 @@ where
                        });
                }
                _ => {
-
                    self.notifications.handle_key_event(key);
+
                    self.notifications.handle_event(key);
                }
            }
        }
@@ -385,18 +378,20 @@ where
        );

        if page_size != props.page_size {
-
            let _ = self.action_tx.send(Action::BrowserPageSize(page_size));
+
            let _ = self.base.action_tx.send(Action::BrowserPageSize(page_size));
        }
    }
}

+
pub struct SearchProps {}
+

+
impl Properties for SearchProps {}
+

pub struct Search<B: Backend> {
-
    /// Message sender
-
    action_tx: UnboundedSender<Action>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<State>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<Action>>,
+
    /// Internal base
+
    base: BaseView<State, Action>,
+
    /// Internal props
+
    _props: SearchProps,
    /// Search input field
    input: BoxedWidget<B>,
}
@@ -425,37 +420,34 @@ impl<B: Backend> View<State, Action> for Search<B> {
            })
            .to_boxed();
        Self {
-
            action_tx,
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
+
            _props: SearchProps {},
            input,
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<State>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<Action>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
    }

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

-
    fn handle_key_event(&mut self, key: termion::event::Key) {
+
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
-
                let _ = self.action_tx.send(Action::CloseSearch);
+
                let _ = self.base.action_tx.send(Action::CloseSearch);
            }
            Key::Char('\n') => {
-
                let _ = self.action_tx.send(Action::ApplySearch);
+
                let _ = self.base.action_tx.send(Action::ApplySearch);
            }
            _ => {
-
                self.input.handle_key_event(key);
+
                self.input.handle_event(key);
            }
        }
    }
@@ -499,14 +491,10 @@ pub struct HelpPage<'a, B>
where
    B: Backend,
{
-
    /// Internal properties
+
    /// Internal base
+
    base: BaseView<State, Action>,
+
    /// Internal props
    props: HelpPageProps<'a>,
-
    /// Message sender
-
    action_tx: UnboundedSender<Action>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<State>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<Action>>,
    /// Content widget
    content: BoxedWidget<B>,
    /// Shortcut widget
@@ -522,7 +510,11 @@ where
        Self: Sized,
    {
        Self {
-
            action_tx: action_tx.clone(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
            props: HelpPageProps::from(state),
            content: Container::new(state, action_tx.clone())
                .header(
@@ -583,38 +575,30 @@ where
                )
                .to_boxed(),
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<State>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<Action>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
    }

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

        self.content.update(state);
    }

-
    fn handle_key_event(&mut self, key: termion::event::Key) {
+
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
-
                let _ = self.action_tx.send(Action::Exit { selection: None });
+
                let _ = self.base.action_tx.send(Action::Exit { selection: None });
            }
            Key::Char('?') => {
-
                let _ = self.action_tx.send(Action::LeavePage);
+
                let _ = self.base.action_tx.send(Action::LeavePage);
            }
            _ => {
-
                self.content.handle_key_event(key);
+
                self.content.handle_event(key);
            }
        }
    }
@@ -646,7 +630,7 @@ where
        );

        if page_size != props.page_size {
-
            let _ = self.action_tx.send(Action::HelpPageSize(page_size));
+
            let _ = self.base.action_tx.send(Action::HelpPageSize(page_size));
        }
    }
}
modified bin/commands/issue/select/ui.rs
@@ -18,15 +18,14 @@ 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::{
-
    Column, EventCallback, Properties, Shortcuts, ShortcutsProps, Table, TableProps, TableUtils,
-
    UpdateCallback, View, Widget,
+
    Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, TableUtils, View, Widget,
};
use tui::Selection;

@@ -122,15 +121,11 @@ impl<'a> From<&State> for BrowsePageProps<'a> {
impl<'a> Properties for BrowsePageProps<'a> {}

pub struct BrowsePage<'a, B> {
-
    /// Internal properties
+
    /// Internal base
+
    base: BaseView<State, Action>,
+
    /// Internal props
    props: BrowsePageProps<'a>,
-
    /// Message sender
-
    action_tx: UnboundedSender<Action>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<State>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<Action>>,
-
    /// Patches widget
+
    /// Notifications widget
    issues: BoxedWidget<B>,
    /// Search widget
    search: BoxedWidget<B>,
@@ -146,8 +141,12 @@ where
        let props = BrowsePageProps::from(state);

        Self {
-
            action_tx: action_tx.clone(),
-
            props: props.clone(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
+
            props: BrowsePageProps::from(state),
            issues: Container::new(state, action_tx.clone())
                .header(
                    Header::new(state, action_tx.clone())
@@ -193,23 +192,15 @@ where
                .to_boxed(),
            search: Search::new(state, action_tx.clone()).to_boxed(),
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<State>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<Action>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
    }

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

        self.issues.update(state);
@@ -217,19 +208,19 @@ where
        self.shortcuts.update(state);
    }

-
    fn handle_key_event(&mut self, key: Key) {
+
    fn handle_event(&mut self, key: Key) {
        if self.props.show_search {
-
            self.search.handle_key_event(key);
+
            self.search.handle_event(key);
        } else {
            match key {
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.action_tx.send(Action::Exit { selection: None });
+
                    let _ = self.base.action_tx.send(Action::Exit { selection: None });
                }
                Key::Char('?') => {
-
                    let _ = self.action_tx.send(Action::OpenHelp);
+
                    let _ = self.base.action_tx.send(Action::OpenHelp);
                }
                Key::Char('/') => {
-
                    let _ = self.action_tx.send(Action::OpenSearch);
+
                    let _ = self.base.action_tx.send(Action::OpenSearch);
                }
                Key::Char('\n') => {
                    let operation = match self.props.mode {
@@ -241,7 +232,8 @@ where
                        .selected
                        .and_then(|selected| self.props.issues.get(selected))
                        .and_then(|issue| {
-
                            self.action_tx
+
                            self.base
+
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(Selection {
                                        operation,
@@ -257,7 +249,8 @@ where
                        .selected
                        .and_then(|selected| self.props.issues.get(selected))
                        .and_then(|issue| {
-
                            self.action_tx
+
                            self.base
+
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(Selection {
                                        operation: Some(IssueOperation::Edit.to_string()),
@@ -269,7 +262,7 @@ where
                        });
                }
                _ => {
-
                    self.issues.handle_key_event(key);
+
                    self.issues.handle_event(key);
                }
            }
        }
@@ -280,7 +273,7 @@ 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(" ".into()),
+
            span::default(" "),
            span::default(&props.search).gray().dim(),
        ]);

@@ -404,18 +397,20 @@ where
        );

        if page_size != props.page_size {
-
            let _ = self.action_tx.send(Action::BrowserPageSize(page_size));
+
            let _ = self.base.action_tx.send(Action::BrowserPageSize(page_size));
        }
    }
}

+
pub struct SearchProps {}
+

+
impl Properties for SearchProps {}
+

pub struct Search<B: Backend> {
-
    /// Message sender
-
    action_tx: UnboundedSender<Action>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<State>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<Action>>,
+
    /// Internal base
+
    base: BaseView<State, Action>,
+
    /// Internal props
+
    _props: SearchProps,
    /// Search input field
    input: BoxedWidget<B>,
}
@@ -444,37 +439,34 @@ impl<B: Backend> View<State, Action> for Search<B> {
            })
            .to_boxed();
        Self {
-
            action_tx,
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
+
            _props: SearchProps {},
            input,
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<State>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<Action>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
    }

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

-
    fn handle_key_event(&mut self, key: termion::event::Key) {
+
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
-
                let _ = self.action_tx.send(Action::CloseSearch);
+
                let _ = self.base.action_tx.send(Action::CloseSearch);
            }
            Key::Char('\n') => {
-
                let _ = self.action_tx.send(Action::ApplySearch);
+
                let _ = self.base.action_tx.send(Action::ApplySearch);
            }
            _ => {
-
                self.input.handle_key_event(key);
+
                self.input.handle_event(key);
            }
        }
    }
@@ -518,14 +510,10 @@ pub struct HelpPage<'a, B>
where
    B: Backend,
{
-
    /// Internal properties
+
    /// Internal base
+
    base: BaseView<State, Action>,
+
    /// Internal props
    props: HelpPageProps<'a>,
-
    /// Message sender
-
    action_tx: UnboundedSender<Action>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<State>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<Action>>,
    /// Content widget
    content: BoxedWidget<B>,
    /// Shortcut widget
@@ -541,7 +529,11 @@ where
        Self: Sized,
    {
        Self {
-
            action_tx: action_tx.clone(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
            props: HelpPageProps::from(state),
            content: Container::new(state, action_tx.clone())
                .header(
@@ -602,38 +594,30 @@ where
                )
                .to_boxed(),
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<State>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<Action>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
    }

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

        self.content.update(state);
    }

-
    fn handle_key_event(&mut self, key: termion::event::Key) {
+
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
-
                let _ = self.action_tx.send(Action::Exit { selection: None });
+
                let _ = self.base.action_tx.send(Action::Exit { selection: None });
            }
            Key::Char('?') => {
-
                let _ = self.action_tx.send(Action::LeavePage);
+
                let _ = self.base.action_tx.send(Action::LeavePage);
            }
            _ => {
-
                self.content.handle_key_event(key);
+
                self.content.handle_event(key);
            }
        }
    }
@@ -665,7 +649,7 @@ where
        );

        if page_size != props.page_size {
-
            let _ = self.action_tx.send(Action::HelpPageSize(page_size));
+
            let _ = self.base.action_tx.send(Action::HelpPageSize(page_size));
        }
    }
}
modified bin/commands/patch/select/ui.rs
@@ -19,16 +19,15 @@ use radicle_tui as tui;

use tui::ui::items::{PatchItem, PatchItemFilter};
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::TableUtils;
+
use tui::ui::widget::{self, BaseView};
use tui::ui::widget::{
-
    Column, EventCallback, Properties, Shortcuts, ShortcutsProps, Table, TableProps,
-
    UpdateCallback, View, Widget,
+
    Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, View, Widget,
};
use tui::Selection;

@@ -123,15 +122,11 @@ impl<'a> From<&State> for BrowsePageProps<'a> {
impl<'a: 'static> Properties for BrowsePageProps<'a> {}

pub struct BrowsePage<'a, B> {
-
    /// Internal properties
+
    /// Internal base
+
    base: BaseView<State, Action>,
+
    /// Internal props
    props: BrowsePageProps<'a>,
-
    /// Message sender
-
    action_tx: UnboundedSender<Action>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<State>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<Action>>,
-
    /// Patches widget
+
    /// Notifications widget
    patches: BoxedWidget<B>,
    /// Search widget
    search: BoxedWidget<B>,
@@ -147,7 +142,11 @@ where
        let props = BrowsePageProps::from(state);

        Self {
-
            action_tx: action_tx.clone(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
            props: props.clone(),
            patches: Container::new(state, action_tx.clone())
                .header(
@@ -194,23 +193,15 @@ where
                .to_boxed(),
            search: Search::new(state, action_tx.clone()).to_boxed(),
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<State>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<Action>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
    }

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

        self.patches.update(state);
@@ -218,19 +209,19 @@ where
        self.shortcuts.update(state);
    }

-
    fn handle_key_event(&mut self, key: Key) {
+
    fn handle_event(&mut self, key: Key) {
        if self.props.show_search {
-
            self.search.handle_key_event(key);
+
            self.search.handle_event(key);
        } else {
            match key {
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.action_tx.send(Action::Exit { selection: None });
+
                    let _ = self.base.action_tx.send(Action::Exit { selection: None });
                }
                Key::Char('?') => {
-
                    let _ = self.action_tx.send(Action::OpenHelp);
+
                    let _ = self.base.action_tx.send(Action::OpenHelp);
                }
                Key::Char('/') => {
-
                    let _ = self.action_tx.send(Action::OpenSearch);
+
                    let _ = self.base.action_tx.send(Action::OpenSearch);
                }
                Key::Char('\n') => {
                    let operation = match self.props.mode {
@@ -242,7 +233,8 @@ where
                        .selected
                        .and_then(|selected| self.props.patches.get(selected))
                        .and_then(|patch| {
-
                            self.action_tx
+
                            self.base
+
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(Selection {
                                        operation,
@@ -258,7 +250,8 @@ where
                        .selected
                        .and_then(|selected| self.props.patches.get(selected))
                        .and_then(|patch| {
-
                            self.action_tx
+
                            self.base
+
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(Selection {
                                        operation: Some(PatchOperation::Checkout.to_string()),
@@ -274,7 +267,8 @@ where
                        .selected
                        .and_then(|selected| self.props.patches.get(selected))
                        .and_then(|patch| {
-
                            self.action_tx
+
                            self.base
+
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(Selection {
                                        operation: Some(PatchOperation::Diff.to_string()),
@@ -286,7 +280,7 @@ where
                        });
                }
                _ => {
-
                    self.patches.handle_key_event(key);
+
                    self.patches.handle_event(key);
                }
            }
        }
@@ -431,18 +425,20 @@ where
        );

        if page_size != props.page_size {
-
            let _ = self.action_tx.send(Action::BrowserPageSize(page_size));
+
            let _ = self.base.action_tx.send(Action::BrowserPageSize(page_size));
        }
    }
}

+
pub struct SearchProps {}
+

+
impl Properties for SearchProps {}
+

pub struct Search<B: Backend> {
-
    /// Message sender
-
    action_tx: UnboundedSender<Action>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<State>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<Action>>,
+
    /// Internal base
+
    base: BaseView<State, Action>,
+
    /// Internal props
+
    _props: SearchProps,
    /// Search input field
    input: BoxedWidget<B>,
}
@@ -471,37 +467,34 @@ impl<B: Backend> View<State, Action> for Search<B> {
            })
            .to_boxed();
        Self {
-
            action_tx,
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
+
            _props: SearchProps {},
            input,
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<State>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<Action>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
    }

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

-
    fn handle_key_event(&mut self, key: termion::event::Key) {
+
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
-
                let _ = self.action_tx.send(Action::CloseSearch);
+
                let _ = self.base.action_tx.send(Action::CloseSearch);
            }
            Key::Char('\n') => {
-
                let _ = self.action_tx.send(Action::ApplySearch);
+
                let _ = self.base.action_tx.send(Action::ApplySearch);
            }
            _ => {
-
                self.input.handle_key_event(key);
+
                self.input.handle_event(key);
            }
        }
    }
@@ -545,14 +538,10 @@ pub struct HelpPage<'a, B>
where
    B: Backend,
{
-
    /// Internal properties
+
    /// Internal base
+
    base: BaseView<State, Action>,
+
    /// Internal props
    props: HelpPageProps<'a>,
-
    /// Message sender
-
    action_tx: UnboundedSender<Action>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<State>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<Action>>,
    /// Content widget
    content: BoxedWidget<B>,
    /// Shortcut widget
@@ -568,7 +557,11 @@ where
        Self: Sized,
    {
        Self {
-
            action_tx: action_tx.clone(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
            props: HelpPageProps::from(state),
            content: Container::new(state, action_tx.clone())
                .header(
@@ -629,38 +622,30 @@ where
                )
                .to_boxed(),
            shortcuts: Shortcuts::new(state, action_tx.clone()).to_boxed(),
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<State>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<Action>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
    }

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

        self.content.update(state);
    }

-
    fn handle_key_event(&mut self, key: termion::event::Key) {
+
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
-
                let _ = self.action_tx.send(Action::Exit { selection: None });
+
                let _ = self.base.action_tx.send(Action::Exit { selection: None });
            }
            Key::Char('?') => {
-
                let _ = self.action_tx.send(Action::LeavePage);
+
                let _ = self.base.action_tx.send(Action::LeavePage);
            }
            _ => {
-
                self.content.handle_key_event(key);
+
                self.content.handle_event(key);
            }
        }
    }
@@ -692,7 +677,7 @@ where
        );

        if page_size != props.page_size {
-
            let _ = self.action_tx.send(Action::HelpPageSize(page_size));
+
            let _ = self.base.action_tx.send(Action::HelpPageSize(page_size));
        }
    }
}
modified src/ui.rs
@@ -97,7 +97,7 @@ impl<A> Frontend<A> {
                // Tick to terminate the select every N milliseconds
                _ = ticker.tick() => (),
                Some(event) = events_rx.recv() => match event {
-
                    Event::Key(key) => root.handle_key_event(key),
+
                    Event::Key(key) => root.handle_event(key),
                    Event::Resize => (),
                },
                // Handle state updates
modified src/ui/widget.rs
@@ -23,6 +23,16 @@ pub type BoxedWidget<B, S, A> = Box<dyn Widget<B, S, A>>;
pub type UpdateCallback<S> = fn(&S) -> Box<dyn Any>;
pub type EventCallback<A> = fn(&dyn Any, UnboundedSender<A>);

+
/// A `View`s common fields.
+
pub struct BaseView<S, A> {
+
    /// Message sender
+
    pub action_tx: UnboundedSender<A>,
+
    /// Custom update handler
+
    pub on_update: Option<UpdateCallback<S>>,
+
    /// Additional custom event handler
+
    pub on_event: Option<EventCallback<A>>,
+
}
+

/// Main trait defining a `View` behaviour.
///
/// This is the first trait that you should implement to define a custom `Widget`.
@@ -34,14 +44,22 @@ pub trait View<S, A> {
        Self: Sized;

    /// Should set the optional custom event handler.
-
    fn on_event(self, callback: EventCallback<A>) -> Self
+
    fn on_event(mut self, callback: EventCallback<A>) -> Self
    where
-
        Self: Sized;
+
        Self: Sized,
+
    {
+
        self.base_mut().on_event = Some(callback);
+
        self
+
    }

    /// Should set the optional update handler.
-
    fn on_update(self, callback: UpdateCallback<S>) -> Self
+
    fn on_update(mut self, callback: UpdateCallback<S>) -> Self
    where
-
        Self: Sized;
+
        Self: Sized,
+
    {
+
        self.base_mut().on_update = Some(callback);
+
        self
+
    }

    /// Returns a boxed `View`
    fn to_boxed(self) -> Box<Self>
@@ -51,11 +69,14 @@ pub trait View<S, A> {
        Box::new(self)
    }

-
    /// Should handle key events and call `handle_key_event` on all children.
+
    /// 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_key_event(&mut self, key: Key);
+
    fn handle_event(&mut self, key: Key);

    /// Should update the internal props of this and all children.
    ///
@@ -83,7 +104,7 @@ where
    fn render(&self, frame: &mut Frame, area: Rect, props: Option<Box<dyn Any>>);
}

-
/// Needs to be implemented for items that are supposed to be rendering in tables.
+
/// Needs to be implemented for items that are supposed to be rendered in tables.
pub trait ToRow {
    fn to_row(&self) -> Vec<Cell>;
}
@@ -138,14 +159,10 @@ pub struct Window<B, S, A, Id>
where
    B: Backend,
{
+
    /// Internal base
+
    base: BaseView<S, A>,
    /// Internal properties
    props: WindowProps<Id>,
-
    /// Message sender
-
    _action_tx: UnboundedSender<A>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<S>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<A>>,
    /// All pages known
    pages: HashMap<Id, BoxedWidget<B, S, A>>,
}
@@ -172,27 +189,22 @@ where
        Self: Sized,
    {
        Self {
-
            _action_tx: action_tx.clone(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
            props: WindowProps::default(),
            pages: HashMap::new(),
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
-
        self.on_update = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
    }
-

-
    fn on_event(mut self, callback: EventCallback<A>) -> Self {
-
        self.on_event = Some(callback);
-
        self
-
    }
-

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

        let page = self
            .props
@@ -205,7 +217,7 @@ where
        }
    }

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

        if let Some(page) = page {
-
            page.handle_key_event(key);
+
            page.handle_event(key);
        }
    }
}
@@ -277,12 +289,8 @@ impl Properties for ShortcutsProps {}
pub struct Shortcuts<S, A> {
    /// Internal properties
    props: ShortcutsProps,
-
    /// Message sender
-
    _action_tx: UnboundedSender<A>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<S>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<A>>,
+
    /// Internal base
+
    base: BaseView<S, A>,
}

impl<S, A> Shortcuts<S, A> {
@@ -305,28 +313,24 @@ impl<S, A> Shortcuts<S, A> {
impl<S, A> View<S, A> for Shortcuts<S, A> {
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
        Self {
-
            _action_tx: action_tx.clone(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
            props: ShortcutsProps::default(),
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_event(mut self, callback: EventCallback<A>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
    }

-
    fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

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

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

@@ -473,14 +477,10 @@ pub struct Table<'a, S, A, R>
where
    R: ToRow,
{
+
    /// Internal base
+
    base: BaseView<S, A>,
    /// Internal table properties
    props: TableProps<'a, R>,
-
    /// Message sender
-
    action_tx: UnboundedSender<A>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<S>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<A>>,
    /// Internal selection and offset state
    state: TableState,
}
@@ -546,27 +546,23 @@ where
{
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
        Self {
-
            action_tx: action_tx.clone(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
            props: TableProps::default(),
            state: TableState::default().with_selected(Some(0)),
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<A>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
    }

    fn update(&mut self, state: &S) {
        self.props =
-
            TableProps::<'_, R>::from_callback(self.on_update, state).unwrap_or(self.props.clone());
+
            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() {
@@ -576,7 +572,7 @@ where
        }
    }

-
    fn handle_key_event(&mut self, key: Key) {
+
    fn handle_event(&mut self, key: Key) {
        match key {
            Key::Up | Key::Char('k') => {
                self.prev();
@@ -601,8 +597,8 @@ where

        self.props.selected = self.state.selected();

-
        if let Some(on_event) = self.on_event {
-
            (on_event)(&self.state, self.action_tx.clone());
+
        if let Some(on_event) = self.base.on_event {
+
            (on_event)(&self.state, self.base.action_tx.clone());
        }
    }
}
@@ -614,7 +610,7 @@ where
{
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
-
            .and_then(TableProps::<'_, R>::from_boxed_any)
+
            .and_then(TableProps::from_boxed_any)
            .unwrap_or(self.props.clone());

        let widths: Vec<Constraint> = self
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::{BoxedWidget, Column, EventCallback, Properties, UpdateCallback, View, Widget};
+
use super::{BaseView, BoxedWidget, Column, Properties, View, Widget};

#[derive(Clone, Debug)]
pub struct HeaderProps<'a> {
@@ -52,15 +52,11 @@ impl<'a> Default for HeaderProps<'a> {

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

-
pub struct Header<'a, S, A> {
+
pub struct Header<'a: 'static, S, A> {
    /// Internal props
    props: HeaderProps<'a>,
-
    /// Message sender
-
    action_tx: UnboundedSender<A>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<S>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<A>>,
+
    /// Internal base
+
    base: BaseView<S, A>,
}

impl<'a, S, A> Header<'a, S, A> {
@@ -84,33 +80,30 @@ impl<'a, S, A> Header<'a, S, A> {
impl<'a: 'static, S, A> View<S, A> for Header<'a, S, A> {
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
        Self {
-
            action_tx: action_tx.clone(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
            props: HeaderProps::default(),
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<A>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    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| HeaderProps::from_boxed_any((on_update)(state)))
            .unwrap_or(self.props.clone());
    }

-
    fn handle_key_event(&mut self, _key: Key) {
-
        if let Some(on_event) = self.on_event {
-
            (on_event)(&self.props, self.action_tx.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());
        }
    }
}
@@ -217,14 +210,10 @@ impl<'a> Default for FooterProps<'a> {
impl<'a: 'static> Properties for FooterProps<'a> {}

pub struct Footer<'a, S, A> {
-
    /// Internal properties
+
    /// Internal props
    props: FooterProps<'a>,
-
    /// Message sender
-
    action_tx: UnboundedSender<A>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<S>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<A>>,
+
    /// Internal base
+
    base: BaseView<S, A>,
}

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

-
    fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<A>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    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_key_event(&mut self, _key: Key) {
-
        if let Some(on_event) = self.on_event {
-
            (on_event)(&self.props, self.action_tx.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());
        }
    }
}
@@ -368,14 +354,10 @@ pub struct Container<B, S, A>
where
    B: Backend,
{
-
    /// Internal properties
+
    /// Internal base
+
    base: BaseView<S, A>,
+
    /// Internal props
    props: ContainerProps,
-
    /// Message sender
-
    _action_tx: UnboundedSender<A>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<S>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<A>>,
    /// Container header
    header: Option<BoxedWidget<B, S, A>>,
    /// Content widget
@@ -413,29 +395,26 @@ where
        Self: Sized,
    {
        Self {
-
            _action_tx: action_tx.clone(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+

+
                on_update: None,
+
                on_event: None,
+
            },
            props: ContainerProps::default(),
            header: None,
            content: None,
            footer: None,
-
            on_update: None,
-
            on_event: None,
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<A>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
    }

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

        if let Some(header) = &mut self.header {
            header.update(state);
@@ -450,9 +429,9 @@ where
        }
    }

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

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

#[derive(Clone)]
pub struct TextFieldProps {
@@ -56,14 +56,10 @@ pub struct TextFieldState {
}

pub struct TextField<S, A> {
+
    /// Internal base
+
    base: BaseView<S, A>,
    /// Internal props
    props: TextFieldProps,
-
    /// Message sender
-
    action_tx: UnboundedSender<A>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<S>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<A>>,
    /// Internal state
    state: TextFieldState,
}
@@ -133,10 +129,12 @@ impl<S, A> TextField<S, A> {
impl<S, A> View<S, A> for TextField<S, A> {
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
        Self {
-
            action_tx,
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
            props: TextFieldProps::default(),
-
            on_update: None,
-
            on_event: None,
            state: TextFieldState {
                text: None,
                cursor_position: 0,
@@ -144,18 +142,12 @@ impl<S, A> View<S, A> for TextField<S, A> {
        }
    }

-
    fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
-
        self.on_update = Some(callback);
-
        self
-
    }
-

-
    fn on_event(mut self, callback: EventCallback<A>) -> Self {
-
        self.on_event = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
    }

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

@@ -167,7 +159,7 @@ impl<S, A> View<S, A> for TextField<S, A> {
        }
    }

-
    fn handle_key_event(&mut self, key: Key) {
+
    fn handle_event(&mut self, key: Key) {
        match key {
            Key::Char(to_insert)
                if (key != Key::Alt('\n'))
@@ -188,8 +180,8 @@ impl<S, A> View<S, A> for TextField<S, A> {
            _ => {}
        }

-
        if let Some(on_event) = self.on_event {
-
            (on_event)(&self.state, self.action_tx.clone());
+
        if let Some(on_event) = self.base.on_event {
+
            (on_event)(&self.state, self.base.action_tx.clone());
        }
    }
}
modified src/ui/widget/text.rs
@@ -8,7 +8,7 @@ use ratatui::backend::Backend;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Text;

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

#[derive(Clone)]
pub struct ParagraphProps<'a> {
@@ -60,14 +60,10 @@ pub struct ParagraphState {
}

pub struct Paragraph<'a, S, A> {
-
    /// Internal properties
+
    /// Internal base
+
    base: BaseView<S, A>,
+
    /// Internal props
    props: ParagraphProps<'a>,
-
    /// Message sender
-
    action_tx: UnboundedSender<A>,
-
    /// Custom update handler
-
    on_update: Option<UpdateCallback<S>>,
-
    /// Additional custom event handler
-
    on_event: Option<EventCallback<A>>,
    /// Internal state
    state: ParagraphState,
}
@@ -152,10 +148,12 @@ impl<'a: 'static, S, A> View<S, A> for Paragraph<'a, S, A> {
        Self: Sized,
    {
        Self {
-
            action_tx: action_tx.clone(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
            props: ParagraphProps::default(),
-
            on_update: None,
-
            on_event: None,
            state: ParagraphState {
                offset: 0,
                progress: 0,
@@ -163,22 +161,16 @@ impl<'a: 'static, S, A> View<S, A> for Paragraph<'a, S, A> {
        }
    }

-
    fn on_event(mut self, callback: EventCallback<A>) -> Self {
-
        self.on_event = Some(callback);
-
        self
-
    }
-

-
    fn on_update(mut self, callback: UpdateCallback<S>) -> Self {
-
        self.on_update = Some(callback);
-
        self
+
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
        &mut self.base
    }

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

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

@@ -204,8 +196,8 @@ impl<'a: 'static, S, A> View<S, A> for Paragraph<'a, S, A> {
            _ => {}
        }

-
        if let Some(on_event) = self.on_event {
-
            (on_event)(&self.state, self.action_tx.clone());
+
        if let Some(on_event) = self.base.on_event {
+
            (on_event)(&self.state, self.base.action_tx.clone());
        }
    }
}