Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Table scrollbar
Merged did:key:z6MkswQE...2C1V opened 1 year ago
  • Tables can render a scrollbar
  • Selection interfaces don’t show their browser scroll progress anymore
8 files changed +58 -63 5138f419 415699fa
modified CHANGELOG.md
@@ -9,6 +9,7 @@
- Widgets can be mutated in their render function
- Scrollable widgets calculate their state by using a stored render height
- Per-column visibility for tables depending on their render width
+
- Tables can render a scrollbar
- Predefined layouts for section groups
- New widgets:
- `SplitContainer`: Vertically split container
@@ -22,6 +23,10 @@
- Use container focus for table highlighting
- Default keybindings for switching sections

+
**Binary features**
+

+
- Selection interfaces don't show their browser scroll progress anymore
+

### Removed

**Library features:**
@@ -30,7 +35,7 @@
- Ability to send messages through widgets
- All Radicle-dependent code (moved to `bin/`)
- Page size attribute from scrollable widgets
-
- Cutoff attributes from table properties
+
- Cutoff and footer attributes from table properties

## [0.3.1] - 2024-06-11

modified bin/commands/inbox/select.rs
@@ -65,7 +65,6 @@ pub enum AppPage {
pub struct BrowserState {
    items: Vec<NotificationItem>,
    selected: Option<usize>,
-
    scroll: usize,
    filter: NotificationItemFilter,
    search: store::StateValue<String>,
    show_search: bool,
@@ -193,7 +192,6 @@ impl TryFrom<&Context> for State {
            browser: BrowserState {
                items: notifications,
                selected: Some(0),
-
                scroll: 0,
                filter,
                search,
                show_search: false,
@@ -212,7 +210,6 @@ pub enum Message {
    },
    Select {
        selected: Option<usize>,
-
        scroll: usize,
    },
    OpenSearch,
    UpdateSearch {
@@ -234,9 +231,8 @@ impl store::State<Selection> for State {
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
        match message {
            Message::Exit { selection } => Some(Exit { value: selection }),
-
            Message::Select { selected, scroll } => {
+
            Message::Select { selected } => {
                self.browser.selected = selected;
-
                self.browser.scroll = scroll;
                None
            }
            Message::OpenSearch => {
@@ -254,13 +250,11 @@ impl store::State<Selection> for State {
                    }
                }

-
                self.browser.scroll = 0;
                None
            }
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
-
                self.browser.scroll = 0;
                None
            }
            Message::CloseSearch => {
modified bin/commands/inbox/select/ui.rs
@@ -40,8 +40,6 @@ pub struct BrowserProps<'a> {
    notifications: Vec<NotificationItem>,
    /// Current (selected) table index
    selected: Option<usize>,
-
    /// Current scroll progress
-
    progress: usize,
    /// Notification statistics.
    stats: HashMap<String, usize>,
    /// Table columns
@@ -79,7 +77,6 @@ impl<'a> From<&State> for BrowserProps<'a> {
            header,
            notifications,
            selected: state.browser.selected,
-
            progress: state.browser.scroll,
            stats,
            columns: [
                Column::new("", Constraint::Length(5)),
@@ -129,11 +126,10 @@ impl Browser {
                    Table::<State, Message, NotificationItem, 9>::default()
                        .to_widget(tx.clone())
                        .on_event(|_, s, _| {
-
                            let (selected, scroll) =
+
                            let (selected, _) =
                                s.and_then(|s| s.unwrap_table()).unwrap_or_default();
                            Some(Message::Select {
                                selected: Some(selected),
-
                                scroll,
                            })
                        })
                        .on_update(|state| {
@@ -143,7 +139,6 @@ impl Browser {
                                .columns(props.columns)
                                .items(state.browser.notifications())
                                .selected(state.browser.selected)
-
                                .footer(!state.browser.show_search)
                                .to_boxed_any()
                                .into()
                        }),
@@ -288,8 +283,10 @@ fn browse_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
            .dim(),
        span::default(" Unseen").dim(),
    ]);
-

-
    let progress = span::default(&format!("{}%", props.progress)).dim();
+
    let sum = Line::from(vec![
+
        span::default("Σ ").dim(),
+
        span::default(&props.notifications.len().to_string()).dim(),
+
    ]);

    match NotificationItemFilter::from_str(&props.search)
        .unwrap_or_default()
@@ -307,7 +304,7 @@ fn browse_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
                    Text::from(block.clone()),
                    Constraint::Min(block.width() as u16),
                ),
-
                Column::new(Text::from(progress), Constraint::Min(4)),
+
                Column::new(Text::from(sum.clone()), Constraint::Min(sum.width() as u16)),
            ]
            .to_vec()
        }
@@ -321,7 +318,7 @@ fn browse_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
                Text::from(unseen.clone()),
                Constraint::Min(unseen.width() as u16),
            ),
-
            Column::new(Text::from(progress), Constraint::Min(4)),
+
            Column::new(Text::from(sum.clone()), Constraint::Min(sum.width() as u16)),
        ]
        .to_vec(),
    }
modified bin/commands/issue/select.rs
@@ -56,11 +56,9 @@ pub enum AppPage {
#[derive(Clone, Debug)]
pub struct BrowserState {
    items: Vec<IssueItem>,
-
    scroll: usize,
    selected: Option<usize>,
    filter: IssueItemFilter,
    search: store::StateValue<String>,
-

    show_search: bool,
}

@@ -111,7 +109,6 @@ impl TryFrom<&Context> for State {
            browser: BrowserState {
                items,
                selected: Some(0),
-
                scroll: 0,
                filter,
                search,
                show_search: false,
@@ -130,7 +127,6 @@ pub enum Message {
    },
    Select {
        selected: Option<usize>,
-
        scroll: usize,
    },
    OpenSearch,
    UpdateSearch {
@@ -152,9 +148,8 @@ impl store::State<Selection> for State {
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
        match message {
            Message::Exit { selection } => Some(Exit { value: selection }),
-
            Message::Select { selected, scroll } => {
+
            Message::Select { selected } => {
                self.browser.selected = selected;
-
                self.browser.scroll = scroll;
                None
            }
            Message::OpenSearch => {
@@ -172,13 +167,11 @@ impl store::State<Selection> for State {
                    }
                }

-
                self.browser.scroll = 0;
                None
            }
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
-
                self.browser.scroll = 0;
                None
            }
            Message::CloseSearch => {
modified bin/commands/issue/select/ui.rs
@@ -42,8 +42,6 @@ pub struct BrowserProps<'a> {
    issues: Vec<IssueItem>,
    /// Current (selected) table index
    selected: Option<usize>,
-
    /// Current scroll progress
-
    progress: usize,
    /// Issue statistics.
    stats: HashMap<String, usize>,
    /// Header columns
@@ -114,7 +112,6 @@ impl<'a> From<&State> for BrowserProps<'a> {
                Column::new("Opened", Constraint::Length(16)).hide_small(),
            ]
            .to_vec(),
-
            progress: state.browser.scroll,
            search: state.browser.search.read(),
            show_search: state.browser.show_search,
        }
@@ -144,11 +141,10 @@ impl Browser {
                    Table::<State, Message, IssueItem, 8>::default()
                        .to_widget(tx.clone())
                        .on_event(|_, s, _| {
-
                            let (selected, scroll) =
+
                            let (selected, _) =
                                s.and_then(|s| s.unwrap_table()).unwrap_or_default();
                            Some(Message::Select {
                                selected: Some(selected),
-
                                scroll,
                            })
                        })
                        .on_update(|state| {
@@ -158,7 +154,6 @@ impl Browser {
                                .columns(props.columns)
                                .items(state.browser.issues())
                                .selected(state.browser.selected)
-
                                .footer(!state.browser.show_search)
                                .to_boxed_any()
                                .into()
                        }),
@@ -315,8 +310,6 @@ fn browse_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
        span::default(&props.issues.len().to_string()).dim(),
    ]);

-
    let progress = span::default(&format!("{}%", props.progress)).dim();
-

    match IssueItemFilter::from_str(&props.search)
        .unwrap_or_default()
        .state()
@@ -338,7 +331,7 @@ fn browse_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
                    Text::from(block.clone()),
                    Constraint::Min(block.width() as u16),
                ),
-
                Column::new(Text::from(progress), Constraint::Min(4)),
+
                Column::new(Text::from(sum.clone()), Constraint::Min(sum.width() as u16)),
            ]
            .to_vec()
        }
@@ -353,7 +346,6 @@ fn browse_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
                Constraint::Min(closed.width() as u16),
            ),
            Column::new(Text::from(sum.clone()), Constraint::Min(sum.width() as u16)),
-
            Column::new(Text::from(progress), Constraint::Min(4)),
        ]
        .to_vec(),
    }
modified bin/commands/patch/select.rs
@@ -55,7 +55,6 @@ pub enum AppPage {
pub struct BrowserState {
    items: Vec<PatchItem>,
    selected: Option<usize>,
-
    scroll: usize,
    filter: PatchItemFilter,
    search: store::StateValue<String>,
    show_search: bool,
@@ -108,7 +107,6 @@ impl TryFrom<&Context> for State {
            browser: BrowserState {
                items,
                selected: Some(0),
-
                scroll: 0,
                filter,
                search,
                show_search: false,
@@ -127,7 +125,6 @@ pub enum Message {
    },
    Select {
        selected: Option<usize>,
-
        scroll: usize,
    },
    OpenSearch,
    UpdateSearch {
@@ -149,9 +146,8 @@ impl store::State<Selection> for State {
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
        match message {
            Message::Exit { selection } => Some(Exit { value: selection }),
-
            Message::Select { selected, scroll } => {
+
            Message::Select { selected } => {
                self.browser.selected = selected;
-
                self.browser.scroll = scroll;
                None
            }
            Message::OpenSearch => {
@@ -169,13 +165,11 @@ impl store::State<Selection> for State {
                    }
                }

-
                self.browser.scroll = 0;
                None
            }
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
-
                self.browser.scroll = 0;
                None
            }
            Message::CloseSearch => {
modified bin/commands/patch/select/ui.rs
@@ -44,8 +44,6 @@ pub struct BrowserProps<'a> {
    patches: Vec<PatchItem>,
    /// Current (selected) table index
    selected: Option<usize>,
-
    /// Current scroll progress
-
    progress: usize,
    /// Patch statistics.
    stats: HashMap<String, usize>,
    /// Header columns
@@ -90,7 +88,6 @@ impl<'a> From<&State> for BrowserProps<'a> {
            mode: state.mode.clone(),
            patches,
            selected: state.browser.selected,
-
            progress: state.browser.scroll,
            stats,
            header: [
                Column::new(" ● ", Constraint::Length(3)),
@@ -145,11 +142,10 @@ impl Browser {
                    Table::<State, Message, PatchItem, 9>::default()
                        .to_widget(tx.clone())
                        .on_event(|_, s, _| {
-
                            let (selected, scroll) =
+
                            let (selected, _) =
                                s.and_then(|s| s.unwrap_table()).unwrap_or_default();
                            Some(Message::Select {
                                selected: Some(selected),
-
                                scroll,
                            })
                        })
                        .on_update(|state| {
@@ -159,7 +155,6 @@ impl Browser {
                                .columns(props.columns)
                                .items(state.browser.patches())
                                .selected(state.browser.selected)
-
                                .footer(!state.browser.show_search)
                                .to_boxed_any()
                                .into()
                        }),
@@ -339,8 +334,6 @@ fn browser_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
        span::default(&props.patches.len().to_string()).dim(),
    ]);

-
    let progress = span::default(&format!("{}%", props.progress)).dim();
-

    match filter.status() {
        Some(state) => {
            let block = match state {
@@ -356,7 +349,7 @@ fn browser_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
                    Text::from(block.clone()),
                    Constraint::Min(block.width() as u16),
                ),
-
                Column::new(Text::from(progress), Constraint::Min(4)),
+
                Column::new(Text::from(sum.clone()), Constraint::Min(sum.width() as u16)),
            ]
        }
        None => vec![
@@ -378,7 +371,6 @@ fn browser_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
                Constraint::Min(archived.width() as u16),
            ),
            Column::new(Text::from(sum.clone()), Constraint::Min(sum.width() as u16)),
-
            Column::new(Text::from(progress), Constraint::Min(4)),
        ],
    }
}
modified src/ui/widget/list.rs
@@ -1,12 +1,12 @@
use std::cmp;
use std::marker::PhantomData;

-
use ratatui::widgets::{Cell, Row};
+
use ratatui::widgets::{Cell, Row, Scrollbar, ScrollbarState};
use ratatui::Frame;
use termion::event::Key;

-
use ratatui::layout::Constraint;
-
use ratatui::style::Stylize;
+
use ratatui::layout::{Constraint, Layout};
+
use ratatui::style::{Style, Stylize};
use ratatui::text::Text;
use ratatui::widgets::TableState;

@@ -29,7 +29,7 @@ where
    pub items: Vec<R>,
    pub selected: Option<usize>,
    pub columns: Vec<Column<'a>>,
-
    pub has_footer: bool,
+
    pub show_scrollbar: bool,
}

impl<'a, R, const W: usize> Default for TableProps<'a, R, W>
@@ -40,7 +40,7 @@ where
        Self {
            items: vec![],
            columns: vec![],
-
            has_footer: false,
+
            show_scrollbar: true,
            selected: Some(0),
        }
    }
@@ -65,8 +65,8 @@ where
        self
    }

-
    pub fn footer(mut self, has_footer: bool) -> Self {
-
        self.has_footer = has_footer;
+
    pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
+
        self.show_scrollbar = show_scrollbar;
        self
    }
}
@@ -213,6 +213,9 @@ where
            .and_then(|props| props.inner_ref::<TableProps<R, W>>())
            .unwrap_or(&default);

+
        let show_scrollbar = props.show_scrollbar && props.items.len() >= self.height.into();
+
        let has_items = !props.items.is_empty();
+

        let widths: Vec<Constraint> = props
            .columns
            .iter()
@@ -225,7 +228,17 @@ where
            })
            .collect();

-
        if !props.items.is_empty() {
+
        if has_items {
+
            let [table_area, scroller_area] = Layout::horizontal([
+
                Constraint::Min(1),
+
                if show_scrollbar {
+
                    Constraint::Length(1)
+
                } else {
+
                    Constraint::Length(0)
+
                },
+
            ])
+
            .areas(render.area);
+

            let rows = props
                .items
                .iter()
@@ -246,13 +259,28 @@ where
                    Row::new(cells)
                })
                .collect::<Vec<_>>();
+

            let rows = ratatui::widgets::Table::default()
                .rows(rows)
                .widths(widths)
                .column_spacing(1)
                .highlight_style(style::highlight(render.focus));
-

-
            frame.render_stateful_widget(rows, render.area, &mut self.state.0);
+
            frame.render_stateful_widget(rows, table_area, &mut self.state.0);
+

+
            let scroller = Scrollbar::default()
+
                .begin_symbol(None)
+
                .track_symbol(None)
+
                .end_symbol(None)
+
                .thumb_symbol("┃")
+
                .style(if render.focus {
+
                    Style::default()
+
                } else {
+
                    Style::default().dim()
+
                });
+
            let mut scroller_state = ScrollbarState::default()
+
                .content_length(props.items.len().saturating_sub(self.height.into()))
+
                .position(self.state.0.offset());
+
            frame.render_stateful_widget(scroller, scroller_area, &mut scroller_state);
        } else {
            let center = layout::centered_rect(render.area, 50, 10);
            let hint = Text::from(span::default("Nothing to show"))