Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
inbox: Factor out browser widget on browser page
Merged did:key:z6MkswQE...2C1V opened 1 year ago

New browser widget will be a section soon and needs to incapsulate the search widget for correct key handling.

2 files changed +156 -66 b4d543ed eb80772e
modified bin/commands/inbox/select.rs
@@ -25,7 +25,7 @@ use tui::Exit;

use tui::PageStack;

-
use self::ui::BrowsePage;
+
use self::ui::BrowserPage;
use self::ui::HelpPage;

use super::common::{Mode, RepositoryMode};
@@ -283,7 +283,7 @@ impl App {
        let window: Window<State, Action, Page> = Window::new(&state, action_tx.clone())
            .page(
                Page::Browse,
-
                BrowsePage::new(&state, action_tx.clone()).to_boxed(),
+
                BrowserPage::new(&state, action_tx.clone()).to_boxed(),
            )
            .page(
                Page::Help,
modified bin/commands/inbox/select/ui.rs
@@ -30,28 +30,44 @@ use super::{Action, State};
type BoxedWidget = widget::BoxedWidget<State, Action>;

#[derive(Clone)]
-
struct BrowsePageProps<'a> {
+
pub struct BrowserProps<'a> {
+
    /// Application mode: openation and id or id only.
+
    mode: Mode,
+
    /// Table title
+
    header: String,
+
    /// Filtered notifications.
    notifications: Vec<NotificationItem>,
+
    /// Current (selected) table index
    selected: Option<usize>,
-
    mode: Mode,
+
    /// Notification statistics.
    stats: HashMap<String, usize>,
+
    /// Table columns
    columns: Vec<Column<'a>>,
+
    /// Max. width, before columns are cut-off.
    cutoff: usize,
+
    /// Column index that marks where to cut.
    cutoff_after: usize,
+
    /// Current page size (height of table content).
    page_size: usize,
-
    search: String,
+
    /// If search widget should be shown.
    show_search: bool,
-
    shortcuts: Vec<(&'a str, &'a str)>,
+
    /// Current search string.
+
    search: String,
}

-
impl<'a> From<&State> for BrowsePageProps<'a> {
+
impl<'a> From<&State> for BrowserProps<'a> {
    fn from(state: &State) -> Self {
-
        let mut seen = 0;
-
        let mut unseen = 0;
+
        let header = match state.mode.repository() {
+
            RepositoryMode::Contextual => state.project.name().to_string(),
+
            RepositoryMode::All => "All repositories".to_string(),
+
            RepositoryMode::ByRepo((_, name)) => name.clone().unwrap_or_default(),
+
        };

        let notifications = state.browser.notifications();

        // Compute statistics
+
        let mut seen = 0;
+
        let mut unseen = 0;
        for notification in &notifications {
            if notification.seen {
                seen += 1;
@@ -59,12 +75,13 @@ impl<'a> From<&State> for BrowsePageProps<'a> {
                unseen += 1;
            }
        }
-

        let stats = HashMap::from([("Seen".to_string(), seen), ("Unseen".to_string(), unseen)]);

        Self {
-
            notifications,
            mode: state.mode.clone(),
+
            header,
+
            notifications,
+
            selected: state.browser.selected,
            stats,
            columns: [
                Column::new("", Constraint::Length(5)),
@@ -81,49 +98,32 @@ impl<'a> From<&State> for BrowsePageProps<'a> {
            .to_vec(),
            cutoff: 200,
            cutoff_after: 5,
-
            search: state.browser.search.read(),
            page_size: state.browser.page_size,
+
            search: state.browser.search.read(),
            show_search: state.browser.show_search,
-
            selected: state.browser.selected,
-
            shortcuts: match state.mode.selection() {
-
                SelectionMode::Id => vec![("enter", "select"), ("/", "search")],
-
                SelectionMode::Operation => vec![
-
                    ("enter", "show"),
-
                    ("c", "clear"),
-
                    ("/", "search"),
-
                    ("?", "help"),
-
                ],
-
            },
        }
    }
}

-
impl<'a> Properties for BrowsePageProps<'a> {}
+
impl<'a> Properties for BrowserProps<'a> {}

-
pub struct BrowsePage<'a> {
+
pub struct Browser<'a> {
    /// Internal base
    base: BaseView<State, Action>,
    /// Internal props
-
    props: BrowsePageProps<'a>,
-
    /// Notifications widget
+
    props: BrowserProps<'a>,
+
    /// Notification widget
    notifications: BoxedWidget,
    /// Search widget
    search: BoxedWidget,
-
    /// Shortcut widget
-
    shortcuts: BoxedWidget,
}

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

    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
-
        let props = BrowsePageProps::from(state);
-
        let name = match state.mode.repository() {
-
            RepositoryMode::Contextual => state.project.name().to_string(),
-
            RepositoryMode::All => "All repositories".to_string(),
-
            RepositoryMode::ByRepo((_, name)) => name.clone().unwrap_or_default(),
-
        };
+
        let props = BrowserProps::from(state);

        Self {
            base: BaseView {
@@ -138,7 +138,7 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                        .columns(
                            [
                                Column::new("", Constraint::Length(0)),
-
                                Column::new(Text::from(name), Constraint::Fill(1)),
+
                                Column::new(Text::from(props.header), Constraint::Fill(1)),
                            ]
                            .to_vec(),
                        )
@@ -157,7 +157,7 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                            });
                        })
                        .on_update(|state| {
-
                            let props = BrowsePageProps::from(state);
+
                            let props = BrowserProps::from(state);

                            TableProps::default()
                                .columns(props.columns)
@@ -171,7 +171,7 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                .footer(
                    Footer::new(state, action_tx.clone())
                        .on_update(|state| {
-
                            let props = BrowsePageProps::from(state);
+
                            let props = BrowserProps::from(state);

                            FooterProps::default()
                                .columns(browse_footer(&props, props.selected))
@@ -181,18 +181,11 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                )
                .on_update(|state| {
                    ContainerProps::default()
-
                        .hide_footer(BrowsePageProps::from(state).show_search)
+
                        .hide_footer(BrowserProps::from(state).show_search)
                        .to_boxed()
                })
                .to_boxed(),
            search: Search::new(state, action_tx.clone()).to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone())
-
                .on_update(|state| {
-
                    ShortcutsProps::default()
-
                        .shortcuts(&BrowsePageProps::from(state).shortcuts)
-
                        .to_boxed()
-
                })
-
                .to_boxed(),
        }
    }

@@ -201,12 +194,6 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
            self.search.handle_event(key);
        } else {
            match key {
-
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.base.action_tx.send(Action::Exit { selection: None });
-
                }
-
                Key::Char('?') => {
-
                    let _ = self.base.action_tx.send(Action::OpenHelp);
-
                }
                Key::Char('/') => {
                    let _ = self.base.action_tx.send(Action::OpenSearch);
                }
@@ -247,31 +234,23 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                                .ok()
                        });
                }
-
                _ => {
-
                    self.notifications.handle_event(key);
-
                }
+
                _ => self.notifications.handle_event(key),
            }
        }
    }

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

        self.notifications.update(state);
        self.search.update(state);
-
        self.shortcuts.update(state);
    }

    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let page_size = props.area.height.saturating_sub(6) as usize;
-

-
        let [content_area, shortcuts_area] =
-
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(props.area);
-

        if self.props.show_search {
            let [table_area, search_area] =
-
                Layout::vertical([Constraint::Min(1), Constraint::Length(2)]).areas(content_area);
+
                Layout::vertical([Constraint::Min(1), Constraint::Length(2)]).areas(props.area);

            self.notifications
                .render(frame, RenderProps::from(table_area));
@@ -279,9 +258,117 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
                .render(frame, RenderProps::from(search_area).focus(true));
        } else {
            self.notifications
-
                .render(frame, RenderProps::from(content_area).focus(true));
+
                .render(frame, RenderProps::from(props.area));
        }
+
    }
+

+
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
        &mut self.base
+
    }
+
}

+
#[derive(Clone)]
+
struct BrowserPageProps<'a> {
+
    /// Current page size (height of table content).
+
    page_size: usize,
+
    /// If this pages' keys should be handled (`false` if search is shown).
+
    global_keys: bool,
+
    /// This pages' shortcuts.
+
    shortcuts: Vec<(&'a str, &'a str)>,
+
}
+

+
impl<'a> From<&State> for BrowserPageProps<'a> {
+
    fn from(state: &State) -> Self {
+
        Self {
+
            page_size: state.browser.page_size,
+
            global_keys: !state.browser.show_search,
+
            shortcuts: if state.browser.show_search {
+
                vec![("esc", "cancel"), ("enter", "apply")]
+
            } else {
+
                match state.mode.selection() {
+
                    SelectionMode::Id => vec![("enter", "select"), ("/", "search")],
+
                    SelectionMode::Operation => vec![
+
                        ("enter", "show"),
+
                        ("c", "clear"),
+
                        ("/", "search"),
+
                        ("?", "help"),
+
                    ],
+
                }
+
            },
+
        }
+
    }
+
}
+

+
impl<'a> Properties for BrowserPageProps<'a> {}
+

+
pub struct BrowserPage<'a> {
+
    /// Internal base
+
    base: BaseView<State, Action>,
+
    /// Internal props
+
    props: BrowserPageProps<'a>,
+
    /// Notifications widget
+
    browser: BoxedWidget,
+
    /// Shortcut widget
+
    shortcuts: BoxedWidget,
+
}
+

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

+
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
+
        let props = BrowserPageProps::from(state);
+

+
        Self {
+
            base: BaseView {
+
                action_tx: action_tx.clone(),
+
                on_update: None,
+
                on_event: None,
+
            },
+
            props: props.clone(),
+
            browser: Browser::new(state, action_tx.clone()).to_boxed(),
+
            shortcuts: Shortcuts::new(state, action_tx.clone())
+
                .on_update(|state| {
+
                    ShortcutsProps::default()
+
                        .shortcuts(&BrowserPageProps::from(state).shortcuts)
+
                        .to_boxed()
+
                })
+
                .to_boxed(),
+
        }
+
    }
+

+
    fn handle_event(&mut self, key: Key) {
+
        self.browser.handle_event(key);
+

+
        if self.props.global_keys {
+
            match key {
+
                Key::Esc | Key::Ctrl('c') => {
+
                    let _ = self.base.action_tx.send(Action::Exit { selection: None });
+
                }
+
                Key::Char('?') => {
+
                    let _ = self.base.action_tx.send(Action::OpenHelp);
+
                }
+
                _ => {}
+
            }
+
        }
+
    }
+

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

+
        self.browser.update(state);
+
        self.shortcuts.update(state);
+
    }
+

+
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
+
        let page_size = props.area.height.saturating_sub(6) as usize;
+

+
        let [content_area, shortcuts_area] =
+
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(props.area);
+

+
        self.browser
+
            .render(frame, RenderProps::from(content_area).focus(true));
        self.shortcuts
            .render(frame, RenderProps::from(shortcuts_area));

@@ -378,8 +465,11 @@ impl Widget for Search {

#[derive(Clone)]
struct HelpPageProps<'a> {
+
    /// Current page size (height of table content).
    page_size: usize,
+
    /// Scroll progress of help paragraph.
    help_progress: usize,
+
    /// This pages' shortcuts.
    shortcuts: Vec<(&'a str, &'a str)>,
}

@@ -527,7 +617,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
    }
}

-
fn browse_footer<'a>(props: &BrowsePageProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
+
fn browse_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
    let search = Line::from(vec![
        span::default(" Search ").cyan().dim().reversed(),
        span::default(" "),