Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Improve event callback
Merged did:key:z6MkgFq6...nBGz opened 1 year ago
8 files changed +155 -144 f6ae35af 0d43859b
modified bin/commands/inbox/select/ui.rs
@@ -1,7 +1,6 @@
use std::collections::HashMap;
use std::str::FromStr;

-
use ratatui::widgets::TableState;
use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;
@@ -19,9 +18,9 @@ use tui::ui::widget::container::{
    Column, Container, ContainerProps, Footer, FooterProps, Header, HeaderProps, SectionGroup,
    SectionGroupProps,
};
-
use tui::ui::widget::input::{TextField, TextFieldProps, TextFieldState};
+
use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps, TableUtils};
-
use tui::ui::widget::text::{Paragraph, ParagraphProps, ParagraphState};
+
use tui::ui::widget::text::{Paragraph, ParagraphProps};
use tui::ui::widget::window::{Shortcuts, ShortcutsProps};
use tui::ui::widget::{BaseView, BoxedAny, Properties, RenderProps, Widget};

@@ -152,14 +151,13 @@ impl<'a: 'static> Widget for Browser<'a> {
                )
                .content(Box::<Table<State, Action, NotificationItem, 9>>::new(
                    Table::new(state, action_tx.clone())
-
                        .on_event(|table, action_tx| {
-
                            TableState::from_boxed_any(table).and_then(|table| {
-
                                action_tx
-
                                    .send(Action::Select {
-
                                        selected: table.selected(),
-
                                    })
-
                                    .ok()
-
                            });
+
                        .on_event(|table| {
+
                            table
+
                                .downcast_mut::<Table<State, Action, NotificationItem, 9>>()
+
                                .and_then(|table| {
+
                                    let selected = table.selected();
+
                                    table.base_mut().send(Action::Select { selected }).ok()
+
                                });
                        })
                        .on_update(|state| {
                            let props = BrowserProps::from(state);
@@ -198,7 +196,7 @@ impl<'a: 'static> Widget for Browser<'a> {
        } else {
            match key {
                Key::Char('/') => {
-
                    let _ = self.base.action_tx.send(Action::OpenSearch);
+
                    let _ = self.base.send(Action::OpenSearch);
                }
                Key::Char('\n') => {
                    self.props
@@ -213,7 +211,6 @@ impl<'a: 'static> Widget for Browser<'a> {
                            };

                            self.base
-
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(selection),
                                })
@@ -226,7 +223,6 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.notifications.get(selected))
                        .and_then(|notif| {
                            self.base
-
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(
                                        Selection::default()
@@ -354,10 +350,10 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
        if self.props.handle_keys {
            match key {
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.base.action_tx.send(Action::Exit { selection: None });
+
                    let _ = self.base.send(Action::Exit { selection: None });
                }
                Key::Char('?') => {
-
                    let _ = self.base.action_tx.send(Action::OpenHelp);
+
                    let _ = self.base.send(Action::OpenHelp);
                }
                _ => {}
            }
@@ -388,7 +384,7 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

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

@@ -419,14 +415,16 @@ impl Widget for Search {
        Self: Sized,
    {
        let input = TextField::new(state, action_tx.clone())
-
            .on_event(|field, action_tx| {
-
                TextFieldState::from_boxed_any(field).and_then(|field| {
-
                    action_tx
-
                        .send(Action::UpdateSearch {
-
                            value: field.text.clone().unwrap_or_default(),
-
                        })
-
                        .ok()
-
                });
+
            .on_event(|field| {
+
                field
+
                    .downcast_mut::<TextField<State, Action>>()
+
                    .and_then(|field| {
+
                        let text = field.text().unwrap_or(&String::new()).to_string();
+
                        field
+
                            .base_mut()
+
                            .send(Action::UpdateSearch { value: text })
+
                            .ok()
+
                    });
            })
            .on_update(|state| {
                TextFieldProps::default()
@@ -450,10 +448,10 @@ impl Widget for Search {
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
-
                let _ = self.base.action_tx.send(Action::CloseSearch);
+
                let _ = self.base.send(Action::CloseSearch);
            }
            Key::Char('\n') => {
-
                let _ = self.base.action_tx.send(Action::ApplySearch);
+
                let _ = self.base.send(Action::ApplySearch);
            }
            _ => {
                self.input.handle_event(key);
@@ -539,6 +537,17 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                )
                .content(
                    Paragraph::new(state, action_tx.clone())
+
                        .on_event(|paragraph| {
+
                            paragraph
+
                                .downcast_mut::<Paragraph<'_, State, Action>>()
+
                                .and_then(|paragraph| {
+
                                    let progress = paragraph.progress();
+
                                    paragraph
+
                                        .base_mut()
+
                                        .send(Action::ScrollHelp { progress })
+
                                        .ok()
+
                                });
+
                        })
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -547,15 +556,6 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                                .page_size(props.page_size)
                                .to_boxed()
                        })
-
                        .on_event(|paragraph, action_tx| {
-
                            ParagraphState::from_boxed_any(paragraph).and_then(|paragraph| {
-
                                action_tx
-
                                    .send(Action::ScrollHelp {
-
                                        progress: paragraph.progress,
-
                                    })
-
                                    .ok()
-
                            });
-
                        })
                        .to_boxed(),
                )
                .footer(
@@ -593,10 +593,10 @@ impl<'a: 'static> Widget for HelpPage<'a> {
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
-
                let _ = self.base.action_tx.send(Action::Exit { selection: None });
+
                let _ = self.base.send(Action::Exit { selection: None });
            }
            Key::Char('?') => {
-
                let _ = self.base.action_tx.send(Action::LeavePage);
+
                let _ = self.base.send(Action::LeavePage);
            }
            _ => {
                self.content.handle_event(key);
@@ -624,7 +624,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

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

modified bin/commands/issue/select/ui.rs
@@ -3,7 +3,6 @@ use std::str::FromStr;
use std::vec;

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

use termion::event::Key;
@@ -21,9 +20,9 @@ use tui::ui::widget::container::{
    Column, Container, ContainerProps, Footer, FooterProps, Header, HeaderProps, SectionGroup,
    SectionGroupProps,
};
-
use tui::ui::widget::input::{TextField, TextFieldProps, TextFieldState};
+
use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps, TableUtils};
-
use tui::ui::widget::text::{Paragraph, ParagraphProps, ParagraphState};
+
use tui::ui::widget::text::{Paragraph, ParagraphProps};
use tui::ui::widget::window::{Shortcuts, ShortcutsProps};
use tui::ui::widget::{BaseView, BoxedAny, Properties, RenderProps, Widget};

@@ -166,14 +165,13 @@ impl<'a: 'static> Widget for Browser<'a> {
                )
                .content(Box::<Table<State, Action, IssueItem, 8>>::new(
                    Table::new(state, action_tx.clone())
-
                        .on_event(|table, action_tx| {
-
                            TableState::from_boxed_any(table).and_then(|table| {
-
                                action_tx
-
                                    .send(Action::Select {
-
                                        selected: table.selected(),
-
                                    })
-
                                    .ok()
-
                            });
+
                        .on_event(|table| {
+
                            table
+
                                .downcast_mut::<Table<State, Action, IssueItem, 8>>()
+
                                .and_then(|table| {
+
                                    let selected = table.selected();
+
                                    table.base_mut().send(Action::Select { selected }).ok()
+
                                });
                        })
                        .on_update(|state| {
                            let props = BrowserProps::from(state);
@@ -214,7 +212,7 @@ impl<'a: 'static> Widget for Browser<'a> {
        } else {
            match key {
                Key::Char('/') => {
-
                    let _ = self.base.action_tx.send(Action::OpenSearch);
+
                    let _ = self.base.send(Action::OpenSearch);
                }
                Key::Char('\n') => {
                    let operation = match self.props.mode {
@@ -227,7 +225,6 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.issues.get(selected))
                        .and_then(|issue| {
                            self.base
-
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(Selection {
                                        operation,
@@ -244,7 +241,6 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.issues.get(selected))
                        .and_then(|issue| {
                            self.base
-
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(Selection {
                                        operation: Some(IssueOperation::Edit.to_string()),
@@ -373,10 +369,10 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
        if self.props.handle_keys {
            match key {
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.base.action_tx.send(Action::Exit { selection: None });
+
                    let _ = self.base.send(Action::Exit { selection: None });
                }
                Key::Char('?') => {
-
                    let _ = self.base.action_tx.send(Action::OpenHelp);
+
                    let _ = self.base.send(Action::OpenHelp);
                }
                _ => {}
            }
@@ -407,7 +403,7 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

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

@@ -438,14 +434,16 @@ impl Widget for Search {
        Self: Sized,
    {
        let input = TextField::new(state, action_tx.clone())
-
            .on_event(|field, action_tx| {
-
                TextFieldState::from_boxed_any(field).and_then(|field| {
-
                    action_tx
-
                        .send(Action::UpdateSearch {
-
                            value: field.text.clone().unwrap_or_default(),
-
                        })
-
                        .ok()
-
                });
+
            .on_event(|field| {
+
                field
+
                    .downcast_mut::<TextField<State, Action>>()
+
                    .and_then(|field| {
+
                        let text = field.text().unwrap_or(&String::new()).to_string();
+
                        field
+
                            .base_mut()
+
                            .send(Action::UpdateSearch { value: text })
+
                            .ok()
+
                    });
            })
            .on_update(|state| {
                TextFieldProps::default()
@@ -469,10 +467,10 @@ impl Widget for Search {
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
-
                let _ = self.base.action_tx.send(Action::CloseSearch);
+
                let _ = self.base.send(Action::CloseSearch);
            }
            Key::Char('\n') => {
-
                let _ = self.base.action_tx.send(Action::ApplySearch);
+
                let _ = self.base.send(Action::ApplySearch);
            }
            _ => {
                self.input.handle_event(key);
@@ -563,14 +561,16 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                                .page_size(props.page_size)
                                .to_boxed()
                        })
-
                        .on_event(|paragraph, action_tx| {
-
                            ParagraphState::from_boxed_any(paragraph).and_then(|paragraph| {
-
                                action_tx
-
                                    .send(Action::ScrollHelp {
-
                                        progress: paragraph.progress,
-
                                    })
-
                                    .ok()
-
                            });
+
                        .on_event(|paragraph| {
+
                            paragraph
+
                                .downcast_mut::<Paragraph<'_, State, Action>>()
+
                                .and_then(|paragraph| {
+
                                    let progress = paragraph.progress();
+
                                    paragraph
+
                                        .base_mut()
+
                                        .send(Action::ScrollHelp { progress })
+
                                        .ok()
+
                                });
                        })
                        .to_boxed(),
                )
@@ -609,10 +609,10 @@ impl<'a: 'static> Widget for HelpPage<'a> {
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
-
                let _ = self.base.action_tx.send(Action::Exit { selection: None });
+
                let _ = self.base.send(Action::Exit { selection: None });
            }
            Key::Char('?') => {
-
                let _ = self.base.action_tx.send(Action::LeavePage);
+
                let _ = self.base.send(Action::LeavePage);
            }
            _ => {
                self.content.handle_event(key);
@@ -640,7 +640,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

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

modified bin/commands/patch/select/ui.rs
@@ -2,7 +2,6 @@ use std::collections::HashMap;
use std::str::FromStr;
use std::vec;

-
use ratatui::widgets::TableState;
use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;
@@ -22,9 +21,9 @@ use tui::ui::widget::container::{
    Column, Container, ContainerProps, Footer, FooterProps, Header, HeaderProps, SectionGroup,
    SectionGroupProps,
};
-
use tui::ui::widget::input::{TextField, TextFieldProps, TextFieldState};
+
use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps, TableUtils};
-
use tui::ui::widget::text::{Paragraph, ParagraphProps, ParagraphState};
+
use tui::ui::widget::text::{Paragraph, ParagraphProps};
use tui::ui::widget::window::{Shortcuts, ShortcutsProps};
use tui::ui::widget::{BaseView, BoxedAny, Properties, RenderProps, Widget};

@@ -166,14 +165,13 @@ impl<'a: 'static> Widget for Browser<'a> {
                )
                .content(Box::<Table<State, Action, PatchItem, 9>>::new(
                    Table::new(state, action_tx.clone())
-
                        .on_event(|table, action_tx| {
-
                            TableState::from_boxed_any(table).and_then(|table| {
-
                                action_tx
-
                                    .send(Action::Select {
-
                                        selected: table.selected(),
-
                                    })
-
                                    .ok()
-
                            });
+
                        .on_event(|table| {
+
                            table
+
                                .downcast_mut::<Table<State, Action, PatchItem, 9>>()
+
                                .and_then(|table| {
+
                                    let selected = table.selected();
+
                                    table.base_mut().send(Action::Select { selected }).ok()
+
                                });
                        })
                        .on_update(|state| {
                            let props = BrowserProps::from(state);
@@ -214,13 +212,13 @@ impl<'a: 'static> Widget for Browser<'a> {
        } else {
            match key {
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.base.action_tx.send(Action::Exit { selection: None });
+
                    let _ = self.base.send(Action::Exit { selection: None });
                }
                Key::Char('?') => {
-
                    let _ = self.base.action_tx.send(Action::OpenHelp);
+
                    let _ = self.base.send(Action::OpenHelp);
                }
                Key::Char('/') => {
-
                    let _ = self.base.action_tx.send(Action::OpenSearch);
+
                    let _ = self.base.send(Action::OpenSearch);
                }
                Key::Char('\n') => {
                    let operation = match self.props.mode {
@@ -233,7 +231,6 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.patches.get(selected))
                        .and_then(|patch| {
                            self.base
-
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(Selection {
                                        operation,
@@ -250,7 +247,6 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.patches.get(selected))
                        .and_then(|patch| {
                            self.base
-
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(Selection {
                                        operation: Some(PatchOperation::Checkout.to_string()),
@@ -267,7 +263,6 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.patches.get(selected))
                        .and_then(|patch| {
                            self.base
-
                                .action_tx
                                .send(Action::Exit {
                                    selection: Some(Selection {
                                        operation: Some(PatchOperation::Diff.to_string()),
@@ -397,10 +392,10 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
        if self.props.handle_keys {
            match key {
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.base.action_tx.send(Action::Exit { selection: None });
+
                    let _ = self.base.send(Action::Exit { selection: None });
                }
                Key::Char('?') => {
-
                    let _ = self.base.action_tx.send(Action::OpenHelp);
+
                    let _ = self.base.send(Action::OpenHelp);
                }
                _ => {}
            }
@@ -431,7 +426,7 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

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

@@ -462,14 +457,16 @@ impl Widget for Search {
        Self: Sized,
    {
        let input = TextField::new(state, action_tx.clone())
-
            .on_event(|field, action_tx| {
-
                TextFieldState::from_boxed_any(field).and_then(|field| {
-
                    action_tx
-
                        .send(Action::UpdateSearch {
-
                            value: field.text.clone().unwrap_or_default(),
-
                        })
-
                        .ok()
-
                });
+
            .on_event(|field| {
+
                field
+
                    .downcast_mut::<TextField<State, Action>>()
+
                    .and_then(|field| {
+
                        let text = field.text().unwrap_or(&String::new()).to_string();
+
                        field
+
                            .base_mut()
+
                            .send(Action::UpdateSearch { value: text })
+
                            .ok()
+
                    });
            })
            .on_update(|state| {
                TextFieldProps::default()
@@ -493,10 +490,10 @@ impl Widget for Search {
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
-
                let _ = self.base.action_tx.send(Action::CloseSearch);
+
                let _ = self.base.send(Action::CloseSearch);
            }
            Key::Char('\n') => {
-
                let _ = self.base.action_tx.send(Action::ApplySearch);
+
                let _ = self.base.send(Action::ApplySearch);
            }
            _ => {
                self.input.handle_event(key);
@@ -579,6 +576,17 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                )
                .content(
                    Paragraph::new(state, action_tx.clone())
+
                        .on_event(|paragraph| {
+
                            paragraph
+
                                .downcast_mut::<Paragraph<'_, State, Action>>()
+
                                .and_then(|paragraph| {
+
                                    let progress = paragraph.progress();
+
                                    paragraph
+
                                        .base_mut()
+
                                        .send(Action::ScrollHelp { progress })
+
                                        .ok()
+
                                });
+
                        })
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -587,15 +595,6 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                                .page_size(props.page_size)
                                .to_boxed()
                        })
-
                        .on_event(|paragraph, action_tx| {
-
                            ParagraphState::from_boxed_any(paragraph).and_then(|paragraph| {
-
                                action_tx
-
                                    .send(Action::ScrollHelp {
-
                                        progress: paragraph.progress,
-
                                    })
-
                                    .ok()
-
                            });
-
                        })
                        .to_boxed(),
                )
                .footer(
@@ -633,10 +632,10 @@ impl<'a: 'static> Widget for HelpPage<'a> {
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
-
                let _ = self.base.action_tx.send(Action::Exit { selection: None });
+
                let _ = self.base.send(Action::Exit { selection: None });
            }
            Key::Char('?') => {
-
                let _ = self.base.action_tx.send(Action::LeavePage);
+
                let _ = self.base.send(Action::LeavePage);
            }
            _ => {
                self.content.handle_event(key);
@@ -663,7 +662,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

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

modified src/ui/widget.rs
@@ -6,6 +6,7 @@ pub mod window;

use std::any::Any;

+
use tokio::sync::mpsc::error::SendError;
use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;
@@ -16,7 +17,7 @@ use ratatui::widgets::Cell;
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(Box<dyn Any>, UnboundedSender<A>);
+
pub type EventCallback = fn(&mut dyn Any);

/// A `View`s common fields.
pub struct BaseView<S, A> {
@@ -25,7 +26,13 @@ pub struct BaseView<S, A> {
    /// Custom update handler
    pub on_update: Option<UpdateCallback<S>>,
    /// Additional custom event handler
-
    pub on_event: Option<EventCallback<A>>,
+
    pub on_event: Option<EventCallback>,
+
}
+

+
impl<S, A> BaseView<S, A> {
+
    pub fn send(&self, action: A) -> Result<(), SendError<A>> {
+
        self.action_tx.send(action)
+
    }
}

/// General properties that specify how a `Widget` is rendered.
@@ -104,7 +111,7 @@ pub trait Widget {
    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<Self::Action>) -> Self
+
    fn on_event(mut self, callback: EventCallback) -> Self
    where
        Self: Sized,
    {
@@ -128,6 +135,13 @@ pub trait Widget {
    {
        Box::new(self)
    }
+

+
    fn downcast_ref(any: &dyn Any) -> Option<&Self>
+
    where
+
        Self: Sized + 'static,
+
    {
+
        any.downcast_ref::<Self>()
+
    }
}

/// Needs to be implemented for items that are supposed to be rendered in tables.
modified src/ui/widget/container.rs
@@ -516,7 +516,7 @@ impl<S, A> SectionGroup<S, A> {
    }
}

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

@@ -555,10 +555,7 @@ impl<S, A> Widget for SectionGroup<S, A> {
        }

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

modified src/ui/widget/input.rs
@@ -49,7 +49,7 @@ impl Default for TextFieldProps {
impl Properties for TextFieldProps {}

#[derive(Clone)]
-
pub struct TextFieldState {
+
struct TextFieldState {
    pub text: Option<String>,
    pub cursor_position: usize,
}
@@ -66,6 +66,10 @@ pub struct TextField<S, A> {
}

impl<S, A> TextField<S, A> {
+
    pub fn text(&self) -> Option<&String> {
+
        self.state.text.as_ref()
+
    }
+

    fn move_cursor_left(&mut self) {
        let cursor_moved_left = self.state.cursor_position.saturating_sub(1);
        self.state.cursor_position = self.clamp_cursor(cursor_moved_left);
@@ -127,7 +131,7 @@ impl<S, A> TextField<S, A> {
    }
}

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

@@ -168,10 +172,7 @@ impl<S, A> Widget for TextField<S, A> {
        }

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

@@ -241,3 +242,5 @@ impl<S, A> Widget for TextField<S, A> {
        &mut self.base
    }
}
+

+
impl<S, A> BoxedAny for TextField<S, A> {}
modified src/ui/widget/list.rs
@@ -104,6 +104,10 @@ impl<'a, S, A, R, const W: usize> Table<'a, S, A, R, W>
where
    R: ToRow<W>,
{
+
    pub fn selected(&self) -> Option<usize> {
+
        self.state.selected()
+
    }
+

    fn prev(&mut self) -> Option<usize> {
        let selected = self
            .state
@@ -155,7 +159,7 @@ where
    }
}

-
impl<'a: 'static, S, A, R, const W: usize> Widget for Table<'a, S, A, R, W>
+
impl<'a: 'static, S: 'static, A: 'static, R, const W: usize> Widget for Table<'a, S, A, R, W>
where
    R: ToRow<W> + Clone + 'static,
{
@@ -200,10 +204,7 @@ where
        self.props.selected = self.state.selected();

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

modified src/ui/widget/text.rs
@@ -44,7 +44,7 @@ impl<'a: 'static> Properties for ParagraphProps<'a> {}
impl<'a: 'static> BoxedAny for ParagraphProps<'a> {}

#[derive(Clone)]
-
pub struct ParagraphState {
+
struct ParagraphState {
    /// Internal offset
    pub offset: usize,
    /// Internal progress
@@ -136,7 +136,7 @@ impl<'a, S, A> Paragraph<'a, S, A> {
    }
}

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

@@ -185,10 +185,7 @@ impl<'a: 'static, S, A> Widget for Paragraph<'a, S, A> {
        }

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