Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Improve widget state conversions
Merged did:key:z6MkswQE...2C1V opened 2 years ago

Introduce WidgetState that provides conversions to and from Any.

7 files changed +71 -46 e76c69b5 beaf1af2
modified bin/commands/inbox/select/ui.rs
@@ -20,7 +20,7 @@ use tui::ui::widget::container::{
};
use tui::ui::widget::input::{TextField, TextFieldProps, TextFieldState};
use tui::ui::widget::text::{Paragraph, ParagraphProps, ParagraphState};
-
use tui::ui::widget::{self, BaseView, TableUtils};
+
use tui::ui::widget::{self, BaseView, TableUtils, WidgetState};
use tui::ui::widget::{Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, Widget};
use tui::Selection;

@@ -151,11 +151,11 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                )
                .content(Box::<Table<State, Action, NotificationItem>>::new(
                    Table::new(state, action_tx.clone())
-
                        .on_event(|state, action_tx| {
-
                            state.downcast_ref::<TableState>().and_then(|state| {
+
                        .on_event(|table, action_tx| {
+
                            TableState::from_boxed_any(table).and_then(|table| {
                                action_tx
                                    .send(Action::Select {
-
                                        selected: state.selected(),
+
                                        selected: table.selected(),
                                    })
                                    .ok()
                            });
@@ -334,11 +334,11 @@ impl Widget for Search {
        Self: Sized,
    {
        let input = TextField::new(state, action_tx.clone())
-
            .on_event(|state, action_tx| {
-
                state.downcast_ref::<TextFieldState>().and_then(|state| {
+
            .on_event(|field, action_tx| {
+
                TextFieldState::from_boxed_any(field).and_then(|field| {
                    action_tx
                        .send(Action::UpdateSearch {
-
                            value: state.text.clone().unwrap_or_default(),
+
                            value: field.text.clone().unwrap_or_default(),
                        })
                        .ok()
                });
@@ -464,11 +464,11 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                                .focus(props.focus)
                                .to_boxed()
                        })
-
                        .on_event(|state, action_tx| {
-
                            state.downcast_ref::<ParagraphState>().and_then(|state| {
+
                        .on_event(|paragraph, action_tx| {
+
                            ParagraphState::from_boxed_any(paragraph).and_then(|paragraph| {
                                action_tx
                                    .send(Action::ScrollHelp {
-
                                        progress: state.progress,
+
                                        progress: paragraph.progress,
                                    })
                                    .ok()
                            });
modified bin/commands/issue/select/ui.rs
@@ -22,7 +22,7 @@ use tui::ui::widget::container::{
};
use tui::ui::widget::input::{TextField, TextFieldProps, TextFieldState};
use tui::ui::widget::text::{Paragraph, ParagraphProps, ParagraphState};
-
use tui::ui::widget::{self, BaseView};
+
use tui::ui::widget::{self, BaseView, WidgetState};
use tui::ui::widget::{
    Column, Properties, Shortcuts, ShortcutsProps, Table, TableProps, TableUtils, Widget,
};
@@ -156,11 +156,11 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                )
                .content(Box::<Table<State, Action, IssueItem>>::new(
                    Table::new(state, action_tx.clone())
-
                        .on_event(|state, action_tx| {
-
                            state.downcast_ref::<TableState>().and_then(|state| {
+
                        .on_event(|table, action_tx| {
+
                            TableState::from_boxed_any(table).and_then(|table| {
                                action_tx
                                    .send(Action::Select {
-
                                        selected: state.selected(),
+
                                        selected: table.selected(),
                                    })
                                    .ok()
                            });
@@ -341,11 +341,11 @@ impl Widget for Search {
        Self: Sized,
    {
        let input = TextField::new(state, action_tx.clone())
-
            .on_event(|state, action_tx| {
-
                state.downcast_ref::<TextFieldState>().and_then(|state| {
+
            .on_event(|field, action_tx| {
+
                TextFieldState::from_boxed_any(field).and_then(|field| {
                    action_tx
                        .send(Action::UpdateSearch {
-
                            value: state.text.clone().unwrap_or_default(),
+
                            value: field.text.clone().unwrap_or_default(),
                        })
                        .ok()
                });
@@ -471,11 +471,11 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                                .focus(props.focus)
                                .to_boxed()
                        })
-
                        .on_event(|state, action_tx| {
-
                            state.downcast_ref::<ParagraphState>().and_then(|state| {
+
                        .on_event(|paragraph, action_tx| {
+
                            ParagraphState::from_boxed_any(paragraph).and_then(|paragraph| {
                                action_tx
                                    .send(Action::ScrollHelp {
-
                                        progress: state.progress,
+
                                        progress: paragraph.progress,
                                    })
                                    .ok()
                            });
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::{TableUtils, WidgetState};
use tui::Selection;

use crate::tui_patch::common::Mode;
@@ -155,11 +155,11 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                )
                .content(Box::<Table<State, Action, PatchItem>>::new(
                    Table::new(state, action_tx.clone())
-
                        .on_event(|state, action_tx| {
-
                            state.downcast_ref::<TableState>().and_then(|state| {
+
                        .on_event(|table, action_tx| {
+
                            TableState::from_boxed_any(table).and_then(|table| {
                                action_tx
                                    .send(Action::Select {
-
                                        selected: state.selected(),
+
                                        selected: table.selected(),
                                    })
                                    .ok()
                            });
@@ -357,11 +357,11 @@ impl Widget for Search {
        Self: Sized,
    {
        let input = TextField::new(state, action_tx.clone())
-
            .on_event(|state, action_tx| {
-
                state.downcast_ref::<TextFieldState>().and_then(|state| {
+
            .on_event(|field, action_tx| {
+
                TextFieldState::from_boxed_any(field).and_then(|field| {
                    action_tx
                        .send(Action::UpdateSearch {
-
                            value: state.text.clone().unwrap_or_default(),
+
                            value: field.text.clone().unwrap_or_default(),
                        })
                        .ok()
                });
@@ -487,11 +487,11 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                                .focus(props.focus)
                                .to_boxed()
                        })
-
                        .on_event(|state, action_tx| {
-
                            state.downcast_ref::<ParagraphState>().and_then(|state| {
+
                        .on_event(|paragraph, action_tx| {
+
                            ParagraphState::from_boxed_any(paragraph).and_then(|paragraph| {
                                action_tx
                                    .send(Action::ScrollHelp {
-
                                        progress: state.progress,
+
                                        progress: paragraph.progress,
                                    })
                                    .ok()
                            });
modified src/ui/widget.rs
@@ -21,7 +21,7 @@ use super::{layout, span};
pub type BoxedWidget<S, A> = Box<dyn Widget<State = S, Action = A>>;

pub type UpdateCallback<S> = fn(&S) -> Box<dyn Any>;
-
pub type EventCallback<A> = fn(&dyn Any, UnboundedSender<A>);
+
pub type EventCallback<A> = fn(Box<dyn Any>, UnboundedSender<A>);

/// A `View`s common fields.
pub struct BaseView<S, A> {
@@ -130,6 +130,22 @@ pub trait Properties {
    }
}

+
pub trait WidgetState {
+
    fn to_boxed_any(self) -> Box<dyn Any>
+
    where
+
        Self: Sized + Clone + 'static,
+
    {
+
        Box::new(self)
+
    }
+

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

#[derive(Clone)]
pub struct WindowProps<Id> {
    current_page: Option<Id>,
@@ -457,6 +473,7 @@ where
}

impl<'a: 'static, R> Properties for TableProps<'a, R> where R: ToRow + 'static {}
+
impl WidgetState for TableState {}

pub struct Table<'a, S, A, R>
where
@@ -570,7 +587,10 @@ where
        self.props.selected = self.state.selected();

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

modified src/ui/widget/container.rs
@@ -92,11 +92,7 @@ impl<'a: 'static, S, A> Widget for Header<'a, S, A> {
        }
    }

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

    fn update(&mut self, state: &S) {
        self.props = self
@@ -269,11 +265,7 @@ impl<'a: 'static, S, A> Widget for Footer<'a, S, A> {
        }
    }

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

    fn update(&mut self, state: &S) {
        self.props = self
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, Widget, WidgetState};

#[derive(Clone)]
pub struct TextFieldProps {
@@ -50,11 +50,15 @@ impl Default for TextFieldProps {
}

impl Properties for TextFieldProps {}
+

+
#[derive(Clone)]
pub struct TextFieldState {
    pub text: Option<String>,
    pub cursor_position: usize,
}

+
impl WidgetState for TextFieldState {}
+

pub struct TextField<S, A> {
    /// Internal base
    base: BaseView<S, A>,
@@ -167,7 +171,10 @@ impl<S, A> Widget for TextField<S, A> {
        }

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

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, Widget, WidgetState};

#[derive(Clone)]
pub struct ParagraphProps<'a> {
@@ -51,6 +51,7 @@ impl<'a> Default for ParagraphProps<'a> {

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

+
#[derive(Clone)]
pub struct ParagraphState {
    /// Internal offset
    pub offset: usize,
@@ -58,6 +59,8 @@ pub struct ParagraphState {
    pub progress: usize,
}

+
impl WidgetState for ParagraphState {}
+

pub struct Paragraph<'a, S, A> {
    /// Internal base
    base: BaseView<S, A>,
@@ -190,7 +193,10 @@ impl<'a: 'static, S, A> Widget for Paragraph<'a, S, A> {
        }

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