Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
tui: Fire proper list selection messages
Erik Kundt committed 2 years ago
commit 8e7d53aa9a0c047860a686760916042ff00fd134
parent a3077d5b1562c26a7f4510e74b19bb485df18a58
8 files changed +168 -189
modified radicle-tui/src/app/event.rs
@@ -60,9 +60,16 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<PatchBrowser> {
            }
            Event::Keyboard(KeyEvent {
                code: Key::Enter, ..
-
            }) => self
-
                .selected_item()
-
                .map(|item| Message::Patch(PatchMessage::Show(item.id().to_owned()))),
+
            }) => {
+
                let result = self.perform(Cmd::Submit);
+
                match result {
+
                    CmdResult::Submit(State::One(StateValue::Usize(selected))) => {
+
                        let item = self.items().get(selected)?;
+
                        Some(Message::Patch(PatchMessage::Show(item.id().to_owned())))
+
                    }
+
                    _ => None,
+
                }
+
            }
            _ => None,
        }
    }
@@ -83,9 +90,16 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<IssueBrowser> {
            }
            Event::Keyboard(KeyEvent {
                code: Key::Enter, ..
-
            }) => self
-
                .selected_item()
-
                .map(|item| Message::Issue(IssueMessage::Show(item.id().to_owned()))),
+
            }) => {
+
                let result = self.perform(Cmd::Submit);
+
                match result {
+
                    CmdResult::Submit(State::One(StateValue::Usize(selected))) => {
+
                        let item = self.items().get(selected)?;
+
                        Some(Message::Issue(IssueMessage::Show(item.id().to_owned())))
+
                    }
+
                    _ => None,
+
                }
+
            }
            _ => None,
        }
    }
modified radicle-tui/src/ui/cob.rs
@@ -256,12 +256,6 @@ impl TableItem<7> for IssueItem {
    }
}

-
impl TableItem<1> for () {
-
    fn row(&self, _theme: &Theme) -> [Cell; 1] {
-
        [Cell::default()]
-
    }
-
}
-

pub fn format_patch_state(state: &PatchState) -> (String, Color) {
    match state {
        PatchState::Open { conflicts: _ } => (" ● ".into(), Color::Green),
modified radicle-tui/src/ui/widget.rs
@@ -2,6 +2,7 @@ pub mod common;
pub mod home;
pub mod issue;
pub mod patch;
+
mod utils;

use std::ops::Deref;

modified radicle-tui/src/ui/widget/common.rs
@@ -11,7 +11,7 @@ use context::{Shortcut, Shortcuts};
use label::Label;
use list::{Property, PropertyList};

-
use self::list::{ColumnWidth, TableModel};
+
use self::list::ColumnWidth;

use super::Widget;

@@ -37,10 +37,10 @@ pub fn reversable_label(content: &str) -> Widget<Label> {
    label(content)
}

-
pub fn container_header(theme: &Theme, label: Widget<Label>) -> Widget<Header<(), 1>> {
-
    let model = TableModel::new([label], [ColumnWidth::Fixed(100)]);
+
pub fn container_header(theme: &Theme, label: Widget<Label>) -> Widget<Header<1>> {
+
    let header = Header::new([label], [ColumnWidth::Fixed(100)], theme.clone());

-
    Widget::new(Header::new(model, theme.clone()))
+
    Widget::new(header)
}

pub fn labeled_container(
modified radicle-tui/src/ui/widget/common/container.rs
@@ -3,17 +3,17 @@ use tuirealm::props::{
    AttrValue, Attribute, BorderSides, BorderType, Color, Props, Style, TextModifiers,
};
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
-
use tuirealm::tui::widgets::{Block, Row};
+
use tuirealm::tui::widgets::{Block, Cell, Row};
use tuirealm::{Frame, MockComponent, State, StateValue};

use crate::ui::ext::HeaderBlock;
use crate::ui::layout;
use crate::ui::state::TabState;
use crate::ui::theme::Theme;
-
use crate::ui::widget::{Widget, WidgetComponent};
+
use crate::ui::widget::{utils, Widget, WidgetComponent};

use super::label::Label;
-
use super::list::{Table, TableItem, TableModel};
+
use super::list::ColumnWidth;

/// Some user events need to be handled globally (e.g. user presses key `q` to quit
/// the application). This component can be used in conjunction with SubEventClause
@@ -128,27 +128,23 @@ impl WidgetComponent for Tabs {
}

/// A labeled container header.
-
pub struct Header<V, const W: usize>
-
where
-
    V: TableItem<W>,
-
{
-
    model: TableModel<V, W>,
+
pub struct Header<const W: usize> {
+
    header: [Widget<Label>; W],
+
    widths: [ColumnWidth; W],
    theme: Theme,
}

-
impl<V, const W: usize> Header<V, W>
-
where
-
    V: TableItem<W>,
-
{
-
    pub fn new(model: TableModel<V, W>, theme: Theme) -> Self {
-
        Self { model, theme }
+
impl<const W: usize> Header<W> {
+
    pub fn new(header: [Widget<Label>; W], widths: [ColumnWidth; W], theme: Theme) -> Self {
+
        Self {
+
            header,
+
            widths,
+
            theme,
+
        }
    }
}

-
impl<V, const W: usize> WidgetComponent for Header<V, W>
-
where
-
    V: TableItem<W> + Clone,
-
{
+
impl<const W: usize> WidgetComponent for Header<W> {
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
        let display = properties
            .get_or(Attribute::Display, AttrValue::Flag(true))
@@ -167,9 +163,18 @@ where
                .horizontal_margin(1)
                .split(area);

-
            let widths =
-
                Table::<V, W>::widths(area, self.model.widths(), self.theme.tables.spacing);
-
            let header: Row<'_> = Row::new(self.model.header(&self.theme));
+
            let widths = utils::column_widths(area, &self.widths, self.theme.tables.spacing);
+
            let header: [Cell; W] = self
+
                .header
+
                .iter()
+
                .map(|label| {
+
                    let cell: Cell = label.into();
+
                    cell.style(Style::default().fg(self.theme.colors.default_fg))
+
                })
+
                .collect::<Vec<_>>()
+
                .try_into()
+
                .unwrap();
+
            let header: Row<'_> = Row::new(header);

            let table = tuirealm::tui::widgets::Table::new(vec![])
                .column_spacing(self.theme.tables.spacing)
@@ -189,12 +194,12 @@ where
}

pub struct LabeledContainer {
-
    header: Widget<Header<(), 1>>,
+
    header: Widget<Header<1>>,
    component: Box<dyn MockComponent>,
}

impl LabeledContainer {
-
    pub fn new(header: Widget<Header<(), 1>>, component: Box<dyn MockComponent>) -> Self {
+
    pub fn new(header: Widget<Header<1>>, component: Box<dyn MockComponent>) -> Self {
        Self { header, component }
    }
}
modified radicle-tui/src/ui/widget/common/list.rs
@@ -2,11 +2,11 @@ use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{AttrValue, Attribute, BorderSides, BorderType, Color, Props, Style};
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
use tuirealm::tui::widgets::{Block, Cell, Row, TableState};
-
use tuirealm::{Frame, MockComponent, State};
+
use tuirealm::{Frame, MockComponent, State, StateValue};

use crate::ui::layout;
use crate::ui::theme::Theme;
-
use crate::ui::widget::{Widget, WidgetComponent};
+
use crate::ui::widget::{utils, Widget, WidgetComponent};

use super::container::Header;
use super::label::Label;
@@ -30,69 +30,6 @@ pub enum ColumnWidth {
    Grow,
}

-
/// A generic table model with [`W`] columns.
-
///
-
/// [`V`] needs to implement `TableItem` in order to be displayed by the
-
/// table this model is used in.
-
#[derive(Clone)]
-
pub struct TableModel<V, const W: usize>
-
where
-
    V: TableItem<W>,
-
{
-
    /// The table header.
-
    header: [Widget<Label>; W],
-
    /// Grow behavior of table columns.
-
    widths: [ColumnWidth; W],
-
    /// Items hold by this model.
-
    items: Vec<V>,
-
}
-

-
impl<V, const W: usize> TableModel<V, W>
-
where
-
    V: TableItem<W>,
-
{
-
    pub fn new(header: [Widget<Label>; W], widths: [ColumnWidth; W]) -> Self {
-
        Self {
-
            header,
-
            widths,
-
            items: vec![],
-
        }
-
    }
-

-
    /// Pushes a new row to this model.
-
    pub fn push_item(&mut self, item: V) {
-
        self.items.push(item);
-
    }
-

-
    /// Get all column widhts defined by this model.
-
    pub fn widths(&self) -> &[ColumnWidth; W] {
-
        &self.widths
-
    }
-

-
    /// Get the item count.
-
    pub fn count(&self) -> u16 {
-
        self.items.len() as u16
-
    }
-

-
    /// Get this model's table header.
-
    pub fn header(&self, theme: &Theme) -> [Cell; W] {
-
        self.header
-
            .iter()
-
            .map(|label| {
-
                let cell: Cell = label.into();
-
                cell.style(Style::default().fg(theme.colors.default_fg))
-
            })
-
            .collect::<Vec<_>>()
-
            .try_into()
-
            .unwrap()
-
    }
-

-
    /// Get this model's table rows.
-
    pub fn rows(&self, theme: &Theme) -> Vec<[Cell; W]> {
-
        self.items.iter().map(|item| item.row(theme)).collect()
-
    }
-
}
-

/// A component that displays a labeled property.
#[derive(Clone)]
pub struct Property {
@@ -186,7 +123,12 @@ pub struct Table<V, const W: usize>
where
    V: TableItem<W> + Clone,
{
-
    model: TableModel<V, W>,
+
    /// Items hold by this model.
+
    items: Vec<V>,
+
    /// The table header.
+
    header: [Widget<Label>; W],
+
    /// Grow behavior of table columns.
+
    widths: [ColumnWidth; W],
    state: TableState,
    theme: Theme,
}
@@ -201,83 +143,48 @@ where
        widths: [ColumnWidth; W],
        theme: Theme,
    ) -> Self {
-
        let mut model = TableModel::new(header, widths);
-
        for item in items {
-
            model.push_item(item.clone());
-
        }
-

        let mut state = TableState::default();
        state.select(Some(0));

        Self {
-
            model,
+
            items: items.to_vec(),
+
            header,
+
            widths,
            state,
            theme,
        }
    }

-
    fn select_previous(&mut self) {
-
        let index = match self.state.selected() {
-
            Some(selected) if selected == 0 => 0,
-
            Some(selected) => selected.saturating_sub(1),
-
            None => 0,
+
    fn select_previous(&mut self) -> Option<usize> {
+
        let old_index = self.state.selected();
+
        let new_index = match old_index {
+
            Some(selected) if selected == 0 => Some(0),
+
            Some(selected) => Some(selected.saturating_sub(1)),
+
            None => Some(0),
        };
-
        self.state.select(Some(index));
-
    }

-
    fn select_next(&mut self, len: usize) {
-
        let index = match self.state.selected() {
-
            Some(selected) if selected >= len.saturating_sub(1) => len.saturating_sub(1),
-
            Some(selected) => selected.saturating_add(1),
-
            None => 0,
-
        };
-
        self.state.select(Some(index));
+
        if old_index != new_index {
+
            self.state.select(new_index);
+
            self.state.selected()
+
        } else {
+
            None
+
        }
    }

-
    pub fn selection(&self) -> Option<&V> {
-
        self.state
-
            .selected()
-
            .and_then(|selected| self.model.items.get(selected))
-
    }
+
    fn select_next(&mut self, len: usize) -> Option<usize> {
+
        let old_index = self.state.selected();
+
        let new_index = match old_index {
+
            Some(selected) if selected >= len.saturating_sub(1) => Some(len.saturating_sub(1)),
+
            Some(selected) => Some(selected.saturating_add(1)),
+
            None => Some(0),
+
        };

-
    /// Calculates `Constraint::Percentage` for each fixed column width in `widths`,
-
    /// taking into account the available width in `area` and the column spacing given by `spacing`.
-
    pub fn widths(area: Rect, widths: &[ColumnWidth], spacing: u16) -> Vec<Constraint> {
-
        let total_spacing = spacing.saturating_mul(widths.len() as u16);
-
        let fixed_width = widths
-
            .iter()
-
            .fold(0u16, |total, &width| match width {
-
                ColumnWidth::Fixed(w) => total + w,
-
                ColumnWidth::Grow => total,
-
            })
-
            .saturating_add(total_spacing);
-

-
        let grow_count = widths.iter().fold(0u16, |count, &w| {
-
            if w == ColumnWidth::Grow {
-
                count + 1
-
            } else {
-
                count
-
            }
-
        });
-
        let grow_width = area
-
            .width
-
            .saturating_sub(fixed_width)
-
            .checked_div(grow_count)
-
            .unwrap_or(0);
-

-
        widths
-
            .iter()
-
            .map(|width| match width {
-
                ColumnWidth::Fixed(w) => {
-
                    let p: f64 = *w as f64 / area.width as f64 * 100_f64;
-
                    Constraint::Percentage(p.ceil() as u16)
-
                }
-
                ColumnWidth::Grow => {
-
                    let p: f64 = grow_width as f64 / area.width as f64 * 100_f64;
-
                    Constraint::Percentage(p.floor() as u16)
-
                }
-
            })
-
            .collect()
+
        if old_index != new_index {
+
            self.state.select(new_index);
+
            self.state.selected()
+
        } else {
+
            None
+
        }
    }
}

@@ -295,12 +202,11 @@ where
            .constraints(vec![Constraint::Length(3), Constraint::Min(1)])
            .split(area);

-
        let widths = Self::widths(area, self.model.widths(), self.theme.tables.spacing);
+
        let widths = utils::column_widths(area, &self.widths, self.theme.tables.spacing);
        let rows: Vec<Row<'_>> = self
-
            .model
-
            .rows(&self.theme)
+
            .items
            .iter()
-
            .map(|cells| Row::new(cells.clone()))
+
            .map(|item| Row::new(item.row(&self.theme)))
            .collect();

        let table = tuirealm::tui::widgets::Table::new(rows)
@@ -314,7 +220,11 @@ where
            .column_spacing(self.theme.tables.spacing)
            .widths(&widths);

-
        let mut header = Widget::new(Header::new(self.model.clone(), self.theme.clone()));
+
        let mut header = Widget::new(Header::new(
+
            self.header.clone(),
+
            self.widths,
+
            self.theme.clone(),
+
        ));
        header.view(frame, layout[0]);
        frame.render_stateful_widget(table, layout[1], &mut self.state);
    }
@@ -325,17 +235,19 @@ where

    fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
        use tuirealm::command::Direction;
-

-
        let len = self.model.count() as usize;
        match cmd {
-
            Cmd::Move(Direction::Up) => {
-
                self.select_previous();
-
                CmdResult::None
-
            }
-
            Cmd::Move(Direction::Down) => {
-
                self.select_next(len);
-
                CmdResult::None
-
            }
+
            Cmd::Move(Direction::Up) => match self.select_previous() {
+
                Some(selected) => CmdResult::Changed(State::One(StateValue::Usize(selected))),
+
                None => CmdResult::None,
+
            },
+
            Cmd::Move(Direction::Down) => match self.select_next(self.items.len()) {
+
                Some(selected) => CmdResult::Changed(State::One(StateValue::Usize(selected))),
+
                None => CmdResult::None,
+
            },
+
            Cmd::Submit => match self.state.selected() {
+
                Some(selected) => CmdResult::Submit(State::One(StateValue::Usize(selected))),
+
                None => CmdResult::None,
+
            },
            _ => CmdResult::None,
        }
    }
modified radicle-tui/src/ui/widget/home.rs
@@ -49,6 +49,7 @@ impl WidgetComponent for Dashboard {
}

pub struct IssueBrowser {
+
    items: Vec<IssueItem>,
    table: Widget<Table<IssueItem, 7>>,
    shortcuts: Widget<Shortcuts>,
}
@@ -92,11 +93,15 @@ impl IssueBrowser {
        let table = Widget::new(Table::new(&items, header, widths, theme.clone()))
            .highlight(theme.colors.item_list_highlighted_bg);

-
        Self { table, shortcuts }
+
        Self {
+
            items,
+
            table,
+
            shortcuts,
+
        }
    }

-
    pub fn selected_item(&self) -> Option<&IssueItem> {
-
        self.table.selection()
+
    pub fn items(&self) -> &Vec<IssueItem> {
+
        &self.items
    }
}

@@ -123,6 +128,7 @@ impl WidgetComponent for IssueBrowser {
}

pub struct PatchBrowser {
+
    items: Vec<PatchItem>,
    table: Widget<Table<PatchItem, 8>>,
    shortcuts: Widget<Shortcuts>,
}
@@ -168,11 +174,15 @@ impl PatchBrowser {
        let table = Widget::new(Table::new(&items, header, widths, theme.clone()))
            .highlight(theme.colors.item_list_highlighted_bg);

-
        Self { table, shortcuts }
+
        Self {
+
            items,
+
            table,
+
            shortcuts,
+
        }
    }

-
    pub fn selected_item(&self) -> Option<&PatchItem> {
-
        self.table.selection()
+
    pub fn items(&self) -> &Vec<PatchItem> {
+
        &self.items
    }
}

added radicle-tui/src/ui/widget/utils.rs
@@ -0,0 +1,43 @@
+
use tuirealm::tui::layout::{Constraint, Rect};
+

+
use super::common::list::ColumnWidth;
+

+
/// Calculates `Constraint::Percentage` for each fixed column width in `widths`,
+
/// taking into account the available width in `area` and the column spacing given by `spacing`.
+
pub fn column_widths(area: Rect, widths: &[ColumnWidth], spacing: u16) -> Vec<Constraint> {
+
    let total_spacing = spacing.saturating_mul(widths.len() as u16);
+
    let fixed_width = widths
+
        .iter()
+
        .fold(0u16, |total, &width| match width {
+
            ColumnWidth::Fixed(w) => total + w,
+
            ColumnWidth::Grow => total,
+
        })
+
        .saturating_add(total_spacing);
+

+
    let grow_count = widths.iter().fold(0u16, |count, &w| {
+
        if w == ColumnWidth::Grow {
+
            count + 1
+
        } else {
+
            count
+
        }
+
    });
+
    let grow_width = area
+
        .width
+
        .saturating_sub(fixed_width)
+
        .checked_div(grow_count)
+
        .unwrap_or(0);
+

+
    widths
+
        .iter()
+
        .map(|width| match width {
+
            ColumnWidth::Fixed(w) => {
+
                let p: f64 = *w as f64 / area.width as f64 * 100_f64;
+
                Constraint::Percentage(p.ceil() as u16)
+
            }
+
            ColumnWidth::Grow => {
+
                let p: f64 = grow_width as f64 / area.width as f64 * 100_f64;
+
                Constraint::Percentage(p.floor() as u16)
+
            }
+
        })
+
        .collect()
+
}