Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Introduce and use base view
Erik Kundt committed 2 years ago
commit f7c90ac425322cf03ec16b92eb05eeb33873d33b
parent 73f29c94cacef16c234ca797bb1d2057d35d772f
4 files changed +108 -107
modified src/ui/widget.rs
@@ -23,6 +23,20 @@ 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>);

+
pub struct BaseView<S, A, P>
+
where
+
    P: Properties,
+
{
+
    /// Internal properties
+
    pub props: P,
+
    /// 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`.
@@ -138,14 +152,8 @@ pub struct Window<B, S, A, Id>
where
    B: Backend,
{
-
    /// 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>>,
+
    /// Internal base
+
    base: BaseView<S, A, WindowProps<Id>>,
    /// All pages known
    pages: HashMap<Id, BoxedWidget<B, S, A>>,
}
@@ -172,29 +180,32 @@ where
        Self: Sized,
    {
        Self {
-
            _action_tx: action_tx.clone(),
-
            props: WindowProps::default(),
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                props: WindowProps::default(),
+
                on_update: None,
+
                on_event: None,
+
            },
            pages: HashMap::new(),
-
            on_update: None,
-
            on_event: None,
        }
    }

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

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

        let page = self
+
            .base
            .props
            .current_page
            .as_ref()
@@ -207,6 +218,7 @@ where

    fn handle_key_event(&mut self, key: termion::event::Key) {
        let page = self
+
            .base
            .props
            .current_page
            .as_ref()
@@ -226,11 +238,12 @@ where
    fn render(&self, frame: &mut ratatui::Frame, _area: Rect, props: Option<Box<dyn Any>>) {
        let _props = props
            .and_then(WindowProps::from_boxed_any)
-
            .unwrap_or(self.props.clone());
+
            .unwrap_or(self.base.props.clone());

        let area = frame.size();

        let page = self
+
            .base
            .props
            .current_page
            .as_ref()
@@ -275,26 +288,21 @@ impl Default for ShortcutsProps {
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, ShortcutsProps>,
}

impl<S, A> Shortcuts<S, A> {
    pub fn divider(mut self, divider: char) -> Self {
-
        self.props.divider = divider;
+
        self.base.props.divider = divider;
        self
    }

    pub fn shortcuts(mut self, shortcuts: &[(&str, &str)]) -> Self {
-
        self.props.shortcuts.clear();
+
        self.base.props.shortcuts.clear();
        for (short, long) in shortcuts {
-
            self.props
+
            self.base
+
                .props
                .shortcuts
                .push((short.to_string(), long.to_string()));
        }
@@ -305,28 +313,30 @@ 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(),
-
            props: ShortcutsProps::default(),
-
            on_update: None,
-
            on_event: None,
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                props: ShortcutsProps::default(),
+
                on_update: None,
+
                on_event: None,
+
            },
        }
    }

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

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

    fn handle_key_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());
+
        self.base.props = ShortcutsProps::from_callback(self.base.on_update, state)
+
            .unwrap_or(self.base.props.clone());
    }
}

@@ -339,7 +349,7 @@ where

        let props = props
            .and_then(ShortcutsProps::from_boxed_any)
-
            .unwrap_or(self.props.clone());
+
            .unwrap_or(self.base.props.clone());

        let mut shortcuts = props.shortcuts.iter().peekable();
        let mut row = vec![];
modified src/ui/widget/container.rs
@@ -11,7 +11,9 @@ 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, EventCallback, Properties, UpdateCallback, View, Widget,
+
};

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

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

-
pub struct Header<'a, 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>>,
+
pub struct Header<'a: 'static, S, A> {
+
    /// Internal base
+
    base: BaseView<S, A, HeaderProps<'a>>,
}

impl<'a, S, A> Header<'a, S, A> {
    pub fn columns(mut self, columns: Vec<Column<'a>>) -> Self {
-
        self.props.columns = columns;
+
        self.base.props.columns = columns;
        self
    }

    pub fn focus(mut self, focus: bool) -> Self {
-
        self.props.focus = focus;
+
        self.base.props.focus = focus;
        self
    }

    pub fn cutoff(mut self, cutoff: usize, cutoff_after: usize) -> Self {
-
        self.props.cutoff = cutoff;
-
        self.props.cutoff_after = cutoff_after;
+
        self.base.props.cutoff = cutoff;
+
        self.base.props.cutoff_after = cutoff_after;
        self
    }
}
@@ -84,33 +80,36 @@ 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(),
-
            props: HeaderProps::default(),
-
            on_update: None,
-
            on_event: None,
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                props: HeaderProps::default(),
+
                on_update: None,
+
                on_event: None,
+
            },
        }
    }

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

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

    fn update(&mut self, state: &S) {
-
        self.props = self
+
        self.base.props = self
+
            .base
            .on_update
            .and_then(|on_update| HeaderProps::from_boxed_any((on_update)(state)))
-
            .unwrap_or(self.props.clone());
+
            .unwrap_or(self.base.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());
+
        if let Some(on_event) = self.base.on_event {
+
            (on_event)(&self.base.props, self.base.action_tx.clone());
        }
    }
}
@@ -122,7 +121,7 @@ where
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(HeaderProps::from_boxed_any)
-
            .unwrap_or(self.props.clone());
+
            .unwrap_or(self.base.props.clone());

        let widths: Vec<Constraint> = props
            .columns
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, EventCallback, Properties, UpdateCallback, View, Widget};

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

pub struct TextField<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 base
+
    base: BaseView<S, A, TextFieldProps>,
    /// Internal state
    state: TextFieldState,
}
@@ -133,10 +127,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,
-
            props: TextFieldProps::default(),
-
            on_update: None,
-
            on_event: None,
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                props: TextFieldProps::default(),
+
                on_update: None,
+
                on_event: None,
+
            },
            state: TextFieldState {
                text: None,
                cursor_position: 0,
@@ -145,19 +141,19 @@ 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.base.on_update = Some(callback);
        self
    }

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

    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();
+
                self.base.props = props.clone();

                if self.state.text.is_none() {
                    self.state.cursor_position = props.text.len().saturating_sub(1);
@@ -188,8 +184,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());
        }
    }
}
@@ -201,7 +197,7 @@ where
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(TextFieldProps::from_boxed_any)
-
            .unwrap_or(self.props.clone());
+
            .unwrap_or(self.base.props.clone());

        let layout = Layout::vertical(Constraint::from_lengths([1, 1])).split(area);

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, EventCallback, Properties, UpdateCallback, View, Widget};

#[derive(Clone)]
pub struct ParagraphProps<'a> {
@@ -59,15 +59,9 @@ pub struct ParagraphState {
    pub progress: usize,
}

-
pub struct Paragraph<'a, S, A> {
-
    /// Internal properties
-
    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>>,
+
pub struct Paragraph<'a: 'static, S, A> {
+
    /// Internal base
+
    base: BaseView<S, A, ParagraphProps<'a>>,
    /// Internal state
    state: ParagraphState,
}
@@ -78,12 +72,12 @@ impl<'a, S, A> Paragraph<'a, S, A> {
    }

    pub fn page_size(mut self, page_size: usize) -> Self {
-
        self.props.page_size = page_size;
+
        self.base.props.page_size = page_size;
        self
    }

    pub fn text(mut self, text: &Text<'a>) -> Self {
-
        self.props.content = text.clone();
+
        self.base.props.content = text.clone();
        self
    }

@@ -152,10 +146,12 @@ impl<'a: 'static, S, A> View<S, A> for Paragraph<'a, S, A> {
        Self: Sized,
    {
        Self {
-
            action_tx: action_tx.clone(),
-
            props: ParagraphProps::default(),
-
            on_update: None,
-
            on_event: None,
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                props: ParagraphProps::default(),
+
                on_update: None,
+
                on_event: None,
+
            },
            state: ParagraphState {
                offset: 0,
                progress: 0,
@@ -164,23 +160,23 @@ 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.base.on_event = Some(callback);
        self
    }

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

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

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

        match key {
            Key::Up | Key::Char('k') => {
@@ -204,8 +200,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());
        }
    }
}
@@ -217,7 +213,7 @@ where
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<Box<dyn Any>>) {
        let props = props
            .and_then(ParagraphProps::from_boxed_any)
-
            .unwrap_or(self.props.clone());
+
            .unwrap_or(self.base.props.clone());

        let [content_area] = Layout::horizontal([Constraint::Min(1)])
            .horizontal_margin(1)