Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
tui: Reorganize component modules
Erik Kundt committed 3 years ago
commit fed4e84dd7d7b3d1cca268bf53e801409a06a4a1
parent a2da673c3132801795a0adf3af7ab01575a0ad21
18 files changed +999 -979
modified radicle-tui/src/app/event.rs
@@ -5,12 +5,12 @@ use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection};
use tuirealm::event::{Event, Key, KeyEvent};
use tuirealm::{MockComponent, NoUserEvent, State, StateValue};

-
use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer, Tabs};
-
use radicle_tui::ui::components::context::{ContextBar, Shortcuts};
-
use radicle_tui::ui::components::list::PropertyList;
-
use radicle_tui::ui::components::workspace::{
-
    Browser, Dashboard, IssueBrowser, PatchActivity, PatchFiles,
-
};
+
use radicle_tui::ui::components::common::container::{GlobalListener, LabeledContainer, Tabs};
+
use radicle_tui::ui::components::common::context::{ContextBar, Shortcuts};
+
use radicle_tui::ui::components::common::list::PropertyList;
+
use radicle_tui::ui::components::common::Browser;
+
use radicle_tui::ui::components::home::{Dashboard, IssueBrowser};
+
use radicle_tui::ui::components::patch;

use radicle_tui::ui::widget::Widget;

@@ -86,7 +86,7 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<IssueBrowser> {
    }
}

-
impl tuirealm::Component<Message, NoUserEvent> for Widget<PatchActivity> {
+
impl tuirealm::Component<Message, NoUserEvent> for Widget<patch::Activity> {
    fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
        match event {
            Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
@@ -97,7 +97,7 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<PatchActivity> {
    }
}

-
impl tuirealm::Component<Message, NoUserEvent> for Widget<PatchFiles> {
+
impl tuirealm::Component<Message, NoUserEvent> for Widget<patch::Files> {
    fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
        match event {
            Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
modified radicle-tui/src/ui/cob/patch.rs
@@ -7,7 +7,7 @@ use radicle::cob::patch::{Patch, PatchId};

use tuirealm::props::{Color, TextSpan};

-
use crate::ui::components::list::List;
+
use crate::ui::components::common::list::List;
use crate::ui::theme::Theme;

pub fn format_status(_patch: &Patch) -> String {
modified radicle-tui/src/ui/components.rs
@@ -1,5 +1,3 @@
-
pub mod container;
-
pub mod context;
-
pub mod label;
-
pub mod list;
-
pub mod workspace;
+
pub mod common;
+
pub mod home;
+
pub mod patch;
added radicle-tui/src/ui/components/common.rs
@@ -0,0 +1,55 @@
+
pub mod container;
+
pub mod context;
+
pub mod label;
+
pub mod list;
+

+
use std::marker::PhantomData;
+

+
use tuirealm::command::{Cmd, CmdResult};
+
use tuirealm::props::Props;
+
use tuirealm::tui::layout::Rect;
+
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, State};
+

+
use crate::ui::layout;
+
use crate::ui::widget::{Widget, WidgetComponent};
+

+
use context::Shortcuts;
+
use list::{List, Table};
+

+
pub struct Browser<T> {
+
    list: Widget<Table>,
+
    shortcuts: Widget<Shortcuts>,
+
    phantom: PhantomData<T>,
+
}
+

+
impl<T: List> Browser<T> {
+
    pub fn new(list: Widget<Table>, shortcuts: Widget<Shortcuts>) -> Self {
+
        Self {
+
            list,
+
            shortcuts,
+
            phantom: PhantomData,
+
        }
+
    }
+
}
+

+
impl<T: List> WidgetComponent for Browser<T> {
+
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
+
        let shortcuts_h = self
+
            .shortcuts
+
            .query(Attribute::Height)
+
            .unwrap_or(AttrValue::Size(0))
+
            .unwrap_size();
+
        let layout = layout::root_component(area, shortcuts_h);
+

+
        self.list.view(frame, layout[0]);
+
        self.shortcuts.view(frame, layout[1]);
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
+
        self.list.perform(cmd)
+
    }
+
}
added radicle-tui/src/ui/components/common/container.rs
@@ -0,0 +1,212 @@
+
use tui_realm_stdlib::Phantom;
+

+
use tuirealm::command::{Cmd, CmdResult};
+
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style};
+
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
+
use tuirealm::tui::text::{Span, Spans};
+
use tuirealm::tui::widgets::Block;
+
use tuirealm::{Frame, MockComponent, State, StateValue};
+

+
use crate::ui::components::common::label::Label;
+
use crate::ui::layout;
+
use crate::ui::state::TabState;
+
use crate::ui::widget::{Widget, WidgetComponent};
+

+
/// 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
+
/// to handle those events.
+
#[derive(Default)]
+
pub struct GlobalListener {}
+

+
impl WidgetComponent for GlobalListener {
+
    fn view(&mut self, _properties: &Props, _frame: &mut Frame, _area: Rect) {}
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
+

+
/// 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
+
/// to handle those events.
+
#[derive(Default, MockComponent)]
+
pub struct GlobalPhantom {
+
    component: Phantom,
+
}
+

+
/// A tab header that displays all labels horizontally aligned and separated
+
/// by a divider. Highlights the label defined by the current tab index.
+
#[derive(Clone)]
+
pub struct Tabs {
+
    tabs: Vec<Widget<Label>>,
+
    divider: Widget<Label>,
+
    state: TabState,
+
}
+

+
impl Tabs {
+
    pub fn new(tabs: Vec<Widget<Label>>, divider: Widget<Label>) -> Self {
+
        let count = &tabs.len();
+
        Self {
+
            tabs,
+
            divider,
+
            state: TabState {
+
                selected: 0,
+
                len: *count as u16,
+
            },
+
        }
+
    }
+
}
+

+
impl WidgetComponent for Tabs {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let selected = self.state().unwrap_one().unwrap_u16();
+
        let display = properties
+
            .get_or(Attribute::Display, AttrValue::Flag(true))
+
            .unwrap_flag();
+
        let foreground = properties
+
            .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
+
            .unwrap_color();
+
        let highlight = properties
+
            .get_or(Attribute::HighlightedColor, AttrValue::Color(Color::Reset))
+
            .unwrap_color();
+

+
        if display {
+
            let spans = self
+
                .tabs
+
                .iter()
+
                .map(|tab| Spans::from(vec![Span::from(tab)]))
+
                .collect::<Vec<_>>();
+

+
            let tabs = tuirealm::tui::widgets::Tabs::new(spans)
+
                .style(Style::default().fg(foreground))
+
                .highlight_style(Style::default().fg(highlight))
+
                .divider(Span::from(&self.divider))
+
                .select(selected as usize);
+

+
            frame.render_widget(tabs, area);
+
        }
+
    }
+

+
    fn state(&self) -> State {
+
        State::One(StateValue::U16(self.state.selected))
+
    }
+

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

+
        match cmd {
+
            Cmd::Move(Direction::Right) => {
+
                let prev = self.state.selected;
+
                self.state.incr_tab_index(true);
+
                if prev != self.state.selected {
+
                    CmdResult::Changed(self.state())
+
                } else {
+
                    CmdResult::None
+
                }
+
            }
+
            _ => CmdResult::None,
+
        }
+
    }
+
}
+

+
/// A labeled container header.
+
#[derive(Clone)]
+
struct Header {
+
    content: Widget<Label>,
+
}
+

+
impl Header {
+
    pub fn new(content: Widget<Label>) -> Self {
+
        Self { content }
+
    }
+
}
+

+
impl WidgetComponent for Header {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let display = properties
+
            .get_or(Attribute::Display, AttrValue::Flag(true))
+
            .unwrap_flag();
+
        let spacer = Widget::new(Label::default())
+
            .content(AttrValue::String(String::default()))
+
            .to_boxed();
+

+
        if display {
+
            let labels: Vec<Box<dyn MockComponent>> = vec![self.content.clone().to_boxed(), spacer];
+

+
            let layout = layout::h_stack(labels, area);
+
            for (mut shortcut, area) in layout {
+
                shortcut.view(frame, area);
+
            }
+
        }
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
+

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

+
impl LabeledContainer {
+
    pub fn new(content: Widget<Label>, component: Box<dyn MockComponent>) -> Self {
+
        Self {
+
            header: Widget::new(Header::new(content)),
+
            component,
+
        }
+
    }
+
}
+

+
impl WidgetComponent for LabeledContainer {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let display = properties
+
            .get_or(Attribute::Display, AttrValue::Flag(true))
+
            .unwrap_flag();
+
        let background = properties
+
            .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
+
            .unwrap_color();
+
        let header_height = self
+
            .header
+
            .query(Attribute::Height)
+
            .unwrap_or(AttrValue::Size(1))
+
            .unwrap_size();
+

+
        if display {
+
            let layout = Layout::default()
+
                .direction(Direction::Vertical)
+
                .constraints([Constraint::Length(header_height), Constraint::Length(0)].as_ref())
+
                .split(area);
+

+
            self.header.view(frame, layout[0]);
+

+
            // Make some space on the left
+
            let inner_layout = Layout::default()
+
                .direction(Direction::Horizontal)
+
                .constraints(vec![Constraint::Length(1), Constraint::Min(0)].as_ref())
+
                .split(layout[1]);
+
            // reverse draw order: child needs to be drawn first?
+
            self.component.view(frame, inner_layout[1]);
+

+
            let block = Block::default().style(Style::default().bg(background));
+
            frame.render_widget(block, layout[1]);
+
        }
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
+
        self.component.perform(cmd)
+
    }
+
}
added radicle-tui/src/ui/components/common/context.rs
@@ -0,0 +1,174 @@
+
use tuirealm::command::{Cmd, CmdResult};
+
use tuirealm::props::{AttrValue, Attribute, Props};
+
use tuirealm::tui::layout::Rect;
+
use tuirealm::{Frame, MockComponent, State};
+

+
use crate::ui::components::common::label::Label;
+
use crate::ui::layout;
+
use crate::ui::widget::{Widget, WidgetComponent};
+

+
/// A shortcut that consists of a label displaying the "hotkey", a label that displays
+
/// the action and a spacer between them.
+
#[derive(Clone)]
+
pub struct Shortcut {
+
    short: Widget<Label>,
+
    divider: Widget<Label>,
+
    long: Widget<Label>,
+
}
+

+
impl Shortcut {
+
    pub fn new(short: Widget<Label>, divider: Widget<Label>, long: Widget<Label>) -> Self {
+
        Self {
+
            short,
+
            divider,
+
            long,
+
        }
+
    }
+
}
+

+
impl WidgetComponent for Shortcut {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let display = properties
+
            .get_or(Attribute::Display, AttrValue::Flag(true))
+
            .unwrap_flag();
+

+
        if display {
+
            let labels: Vec<Box<dyn MockComponent>> = vec![
+
                self.short.clone().to_boxed(),
+
                self.divider.clone().to_boxed(),
+
                self.long.clone().to_boxed(),
+
            ];
+

+
            let layout = layout::h_stack(labels, area);
+
            for (mut shortcut, area) in layout {
+
                shortcut.view(frame, area);
+
            }
+
        }
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
+

+
/// A shortcut bar that displays multiple shortcuts and separates them with a
+
/// divider.
+
pub struct Shortcuts {
+
    shortcuts: Vec<Widget<Shortcut>>,
+
    divider: Widget<Label>,
+
}
+

+
impl Shortcuts {
+
    pub fn new(shortcuts: Vec<Widget<Shortcut>>, divider: Widget<Label>) -> Self {
+
        Self { shortcuts, divider }
+
    }
+
}
+

+
impl WidgetComponent for Shortcuts {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let display = properties
+
            .get_or(Attribute::Display, AttrValue::Flag(true))
+
            .unwrap_flag();
+

+
        if display {
+
            let mut widgets: Vec<Box<dyn MockComponent>> = vec![];
+
            let mut shortcuts = self.shortcuts.iter_mut().peekable();
+

+
            while let Some(shortcut) = shortcuts.next() {
+
                if shortcuts.peek().is_some() {
+
                    widgets.push(shortcut.clone().to_boxed());
+
                    widgets.push(self.divider.clone().to_boxed())
+
                } else {
+
                    widgets.push(shortcut.clone().to_boxed());
+
                }
+
            }
+

+
            let layout = layout::h_stack(widgets, area);
+
            for (mut widget, area) in layout {
+
                widget.view(frame, area);
+
            }
+
        }
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
+

+
pub struct ContextBar {
+
    context: Widget<Label>,
+
    id: Widget<Label>,
+
    author: Widget<Label>,
+
    title: Widget<Label>,
+
    comments: Widget<Label>,
+
}
+

+
impl ContextBar {
+
    pub fn new(
+
        context: Widget<Label>,
+
        id: Widget<Label>,
+
        author: Widget<Label>,
+
        title: Widget<Label>,
+
        comments: Widget<Label>,
+
    ) -> Self {
+
        Self {
+
            context,
+
            id,
+
            author,
+
            title,
+
            comments,
+
        }
+
    }
+
}
+

+
impl WidgetComponent for ContextBar {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let display = properties
+
            .get_or(Attribute::Display, AttrValue::Flag(true))
+
            .unwrap_flag();
+

+
        let context_w = self.context.query(Attribute::Width).unwrap().unwrap_size();
+
        let id_w = self.id.query(Attribute::Width).unwrap().unwrap_size();
+
        let author_w = self.author.query(Attribute::Width).unwrap().unwrap_size();
+
        let count_w = self.comments.query(Attribute::Width).unwrap().unwrap_size();
+

+
        if display {
+
            let layout = layout::h_stack(
+
                vec![
+
                    self.context.clone().to_boxed(),
+
                    self.id.clone().to_boxed(),
+
                    self.title
+
                        .clone()
+
                        .width(
+
                            area.width
+
                                .saturating_sub(context_w + id_w + author_w + count_w),
+
                        )
+
                        .to_boxed(),
+
                    self.author.clone().to_boxed(),
+
                    self.comments.clone().to_boxed(),
+
                ],
+
                area,
+
            );
+

+
            for (mut component, area) in layout {
+
                component.view(frame, area);
+
            }
+
        }
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
added radicle-tui/src/ui/components/common/label.rs
@@ -0,0 +1,81 @@
+
use tuirealm::command::{Cmd, CmdResult};
+
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style};
+
use tuirealm::tui::layout::Rect;
+
use tuirealm::tui::text::{Span, Text};
+
use tuirealm::{Frame, MockComponent, State};
+

+
use crate::ui::widget::{Widget, WidgetComponent};
+

+
/// A label that can be styled using a foreground color and text modifiers.
+
/// Its height is fixed, its width depends on the length of the text it displays.
+
#[derive(Clone, Default)]
+
pub struct Label;
+

+
impl WidgetComponent for Label {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        use tui_realm_stdlib::Label;
+

+
        let content = properties
+
            .get_or(Attribute::Content, AttrValue::String(String::default()))
+
            .unwrap_string();
+
        let display = properties
+
            .get_or(Attribute::Display, AttrValue::Flag(true))
+
            .unwrap_flag();
+
        let foreground = properties
+
            .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
+
            .unwrap_color();
+
        let background = properties
+
            .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
+
            .unwrap_color();
+

+
        if display {
+
            let mut label = match properties.get(Attribute::TextProps) {
+
                Some(modifiers) => Label::default()
+
                    .foreground(foreground)
+
                    .background(background)
+
                    .modifiers(modifiers.unwrap_text_modifiers())
+
                    .text(content),
+
                None => Label::default()
+
                    .foreground(foreground)
+
                    .background(background)
+
                    .text(content),
+
            };
+

+
            label.view(frame, area);
+
        }
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
+

+
impl From<&Widget<Label>> for Span<'_> {
+
    fn from(label: &Widget<Label>) -> Self {
+
        let content = label
+
            .query(Attribute::Content)
+
            .unwrap_or(AttrValue::String(String::default()))
+
            .unwrap_string();
+

+
        Span::styled(content, Style::default())
+
    }
+
}
+

+
impl From<&Widget<Label>> for Text<'_> {
+
    fn from(label: &Widget<Label>) -> Self {
+
        let content = label
+
            .query(Attribute::Content)
+
            .unwrap_or(AttrValue::String(String::default()))
+
            .unwrap_string();
+
        let foreground = label
+
            .query(Attribute::Foreground)
+
            .unwrap_or(AttrValue::Color(Color::Reset))
+
            .unwrap_color();
+

+
        Text::styled(content, Style::default().fg(foreground))
+
    }
+
}
added radicle-tui/src/ui/components/common/list.rs
@@ -0,0 +1,247 @@
+
use radicle::Profile;
+
use tuirealm::command::{Cmd, CmdResult, Direction};
+
use tuirealm::props::{
+
    AttrValue, Attribute, Color, PropPayload, PropValue, Props, Style, TextModifiers, TextSpan,
+
};
+
use tuirealm::tui::layout::{Constraint, Rect};
+
use tuirealm::tui::widgets::{Cell, Row, TableState};
+
use tuirealm::{Frame, MockComponent, State, StateValue};
+

+
use crate::ui::components::common::label::Label;
+
use crate::ui::layout;
+
use crate::ui::theme::Theme;
+
use crate::ui::widget::{Widget, WidgetComponent};
+

+
pub trait List {
+
    fn row(&self, theme: &Theme, profile: &Profile) -> Vec<TextSpan>;
+
}
+

+
/// A component that displays a labeled property.
+
#[derive(Clone)]
+
pub struct Property {
+
    label: Widget<Label>,
+
    divider: Widget<Label>,
+
    property: Widget<Label>,
+
}
+

+
impl Property {
+
    pub fn new(label: Widget<Label>, divider: Widget<Label>, property: Widget<Label>) -> Self {
+
        Self {
+
            label,
+
            divider,
+
            property,
+
        }
+
    }
+
}
+

+
impl WidgetComponent for Property {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let display = properties
+
            .get_or(Attribute::Display, AttrValue::Flag(true))
+
            .unwrap_flag();
+

+
        if display {
+
            let labels: Vec<Box<dyn MockComponent>> = vec![
+
                self.label.clone().to_boxed(),
+
                self.divider.clone().to_boxed(),
+
                self.property.clone().to_boxed(),
+
            ];
+

+
            let layout = layout::h_stack(labels, area);
+
            for (mut label, area) in layout {
+
                label.view(frame, area);
+
            }
+
        }
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
+

+
/// A component that can display lists of labeled properties
+
#[derive(Default)]
+
pub struct PropertyList {
+
    properties: Vec<Widget<Property>>,
+
}
+

+
impl PropertyList {
+
    pub fn new(properties: Vec<Widget<Property>>) -> Self {
+
        Self { properties }
+
    }
+
}
+

+
impl WidgetComponent for PropertyList {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let display = properties
+
            .get_or(Attribute::Display, AttrValue::Flag(true))
+
            .unwrap_flag();
+

+
        if display {
+
            let properties = self
+
                .properties
+
                .iter()
+
                .map(|property| property.clone().to_boxed() as Box<dyn MockComponent>)
+
                .collect();
+

+
            let layout = layout::v_stack(properties, area);
+
            for (mut property, area) in layout {
+
                property.view(frame, area);
+
            }
+
        }
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
+

+
pub struct Table {
+
    state: TableState,
+
}
+

+
impl Default for Table {
+
    fn default() -> Self {
+
        let mut state = TableState::default();
+
        state.select(Some(0));
+
        Self { state }
+
    }
+
}
+

+
impl Table {
+
    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,
+
        };
+
        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));
+
    }
+

+
    fn header<'a>(spans: Vec<PropValue>) -> Row<'a> {
+
        Row::new(
+
            spans
+
                .iter()
+
                .map(|span| {
+
                    Cell::from(span.clone().unwrap_text_span().content)
+
                        .style(Style::default().add_modifier(TextModifiers::BOLD))
+
                })
+
                .collect::<Vec<_>>(),
+
        )
+
    }
+

+
    fn rows<'a>(spans: Vec<Vec<TextSpan>>) -> Vec<Row<'a>> {
+
        spans
+
            .iter()
+
            .map(|spans| {
+
                let cells = spans.iter().map(|span| {
+
                    let style = Style::default().fg(span.fg);
+
                    Cell::from(span.content.clone()).style(style)
+
                });
+
                Row::new(cells).height(1)
+
            })
+
            .collect::<Vec<Row>>()
+
    }
+

+
    fn widths(widths: Vec<PropValue>) -> Vec<Constraint> {
+
        widths
+
            .iter()
+
            .map(|prop| Constraint::Percentage(prop.clone().unwrap_u16()))
+
            .collect()
+
    }
+
}
+

+
impl WidgetComponent for Table {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let content = properties
+
            .get_or(Attribute::Content, AttrValue::Table(vec![]))
+
            .unwrap_table();
+
        let background = properties
+
            .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
+
            .unwrap_color();
+
        let highlight = properties
+
            .get_or(Attribute::HighlightedColor, AttrValue::Color(Color::Reset))
+
            .unwrap_color();
+
        let header = properties
+
            .get_or(
+
                Attribute::Custom("header"),
+
                AttrValue::Payload(PropPayload::Vec(vec![])),
+
            )
+
            .unwrap_payload()
+
            .unwrap_vec();
+
        let widths = properties
+
            .get_or(
+
                Attribute::Custom("widths"),
+
                AttrValue::Payload(PropPayload::Vec(vec![])),
+
            )
+
            .unwrap_payload()
+
            .unwrap_vec();
+

+
        let header = Self::header(header);
+
        let rows = Self::rows(content);
+
        let widths = Self::widths(widths);
+

+
        let table = tuirealm::tui::widgets::Table::new(rows)
+
            .highlight_style(Style::default().bg(highlight))
+
            .style(Style::default().bg(background))
+
            .column_spacing(3u16)
+
            .header(header)
+
            .widths(&widths);
+

+
        frame.render_stateful_widget(table, area, &mut self.state);
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, properties: &Props, cmd: Cmd) -> CmdResult {
+
        let content = properties
+
            .get_or(Attribute::Content, AttrValue::Table(vec![]))
+
            .unwrap_table();
+

+
        match cmd {
+
            Cmd::Move(Direction::Up) => {
+
                self.select_previous();
+
                if let Some(selected) = self.state.selected() {
+
                    CmdResult::Changed(State::One(StateValue::Usize(selected)))
+
                } else {
+
                    CmdResult::None
+
                }
+
            }
+
            Cmd::Move(Direction::Down) => {
+
                self.select_next(content.len());
+
                if let Some(selected) = self.state.selected() {
+
                    CmdResult::Changed(State::One(StateValue::Usize(selected)))
+
                } else {
+
                    CmdResult::None
+
                }
+
            }
+
            Cmd::Submit => {
+
                if let Some(selected) = self.state.selected() {
+
                    CmdResult::Submit(State::One(StateValue::Usize(selected)))
+
                } else {
+
                    CmdResult::None
+
                }
+
            }
+
            _ => CmdResult::None,
+
        }
+
    }
+
}
deleted radicle-tui/src/ui/components/container.rs
@@ -1,212 +0,0 @@
-
use tui_realm_stdlib::Phantom;
-

-
use tuirealm::command::{Cmd, CmdResult};
-
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style};
-
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
-
use tuirealm::tui::text::{Span, Spans};
-
use tuirealm::tui::widgets::Block;
-
use tuirealm::{Frame, MockComponent, State, StateValue};
-

-
use crate::ui::components::label::Label;
-
use crate::ui::layout;
-
use crate::ui::state::TabState;
-
use crate::ui::widget::{Widget, WidgetComponent};
-

-
/// 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
-
/// to handle those events.
-
#[derive(Default)]
-
pub struct GlobalListener {}
-

-
impl WidgetComponent for GlobalListener {
-
    fn view(&mut self, _properties: &Props, _frame: &mut Frame, _area: Rect) {}
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
-

-
/// 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
-
/// to handle those events.
-
#[derive(Default, MockComponent)]
-
pub struct GlobalPhantom {
-
    component: Phantom,
-
}
-

-
/// A tab header that displays all labels horizontally aligned and separated
-
/// by a divider. Highlights the label defined by the current tab index.
-
#[derive(Clone)]
-
pub struct Tabs {
-
    tabs: Vec<Widget<Label>>,
-
    divider: Widget<Label>,
-
    state: TabState,
-
}
-

-
impl Tabs {
-
    pub fn new(tabs: Vec<Widget<Label>>, divider: Widget<Label>) -> Self {
-
        let count = &tabs.len();
-
        Self {
-
            tabs,
-
            divider,
-
            state: TabState {
-
                selected: 0,
-
                len: *count as u16,
-
            },
-
        }
-
    }
-
}
-

-
impl WidgetComponent for Tabs {
-
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
-
        let selected = self.state().unwrap_one().unwrap_u16();
-
        let display = properties
-
            .get_or(Attribute::Display, AttrValue::Flag(true))
-
            .unwrap_flag();
-
        let foreground = properties
-
            .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
-
            .unwrap_color();
-
        let highlight = properties
-
            .get_or(Attribute::HighlightedColor, AttrValue::Color(Color::Reset))
-
            .unwrap_color();
-

-
        if display {
-
            let spans = self
-
                .tabs
-
                .iter()
-
                .map(|tab| Spans::from(vec![Span::from(tab)]))
-
                .collect::<Vec<_>>();
-

-
            let tabs = tuirealm::tui::widgets::Tabs::new(spans)
-
                .style(Style::default().fg(foreground))
-
                .highlight_style(Style::default().fg(highlight))
-
                .divider(Span::from(&self.divider))
-
                .select(selected as usize);
-

-
            frame.render_widget(tabs, area);
-
        }
-
    }
-

-
    fn state(&self) -> State {
-
        State::One(StateValue::U16(self.state.selected))
-
    }
-

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

-
        match cmd {
-
            Cmd::Move(Direction::Right) => {
-
                let prev = self.state.selected;
-
                self.state.incr_tab_index(true);
-
                if prev != self.state.selected {
-
                    CmdResult::Changed(self.state())
-
                } else {
-
                    CmdResult::None
-
                }
-
            }
-
            _ => CmdResult::None,
-
        }
-
    }
-
}
-

-
/// A labeled container header.
-
#[derive(Clone)]
-
struct Header {
-
    content: Widget<Label>,
-
}
-

-
impl Header {
-
    pub fn new(content: Widget<Label>) -> Self {
-
        Self { content }
-
    }
-
}
-

-
impl WidgetComponent for Header {
-
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
-
        let display = properties
-
            .get_or(Attribute::Display, AttrValue::Flag(true))
-
            .unwrap_flag();
-
        let spacer = Widget::new(Label::default())
-
            .content(AttrValue::String(String::default()))
-
            .to_boxed();
-

-
        if display {
-
            let labels: Vec<Box<dyn MockComponent>> = vec![self.content.clone().to_boxed(), spacer];
-

-
            let layout = layout::h_stack(labels, area);
-
            for (mut shortcut, area) in layout {
-
                shortcut.view(frame, area);
-
            }
-
        }
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
-

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

-
impl LabeledContainer {
-
    pub fn new(content: Widget<Label>, component: Box<dyn MockComponent>) -> Self {
-
        Self {
-
            header: Widget::new(Header::new(content)),
-
            component,
-
        }
-
    }
-
}
-

-
impl WidgetComponent for LabeledContainer {
-
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
-
        let display = properties
-
            .get_or(Attribute::Display, AttrValue::Flag(true))
-
            .unwrap_flag();
-
        let background = properties
-
            .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
-
            .unwrap_color();
-
        let header_height = self
-
            .header
-
            .query(Attribute::Height)
-
            .unwrap_or(AttrValue::Size(1))
-
            .unwrap_size();
-

-
        if display {
-
            let layout = Layout::default()
-
                .direction(Direction::Vertical)
-
                .constraints([Constraint::Length(header_height), Constraint::Length(0)].as_ref())
-
                .split(area);
-

-
            self.header.view(frame, layout[0]);
-

-
            // Make some space on the left
-
            let inner_layout = Layout::default()
-
                .direction(Direction::Horizontal)
-
                .constraints(vec![Constraint::Length(1), Constraint::Min(0)].as_ref())
-
                .split(layout[1]);
-
            // reverse draw order: child needs to be drawn first?
-
            self.component.view(frame, inner_layout[1]);
-

-
            let block = Block::default().style(Style::default().bg(background));
-
            frame.render_widget(block, layout[1]);
-
        }
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
-
        self.component.perform(cmd)
-
    }
-
}
deleted radicle-tui/src/ui/components/context.rs
@@ -1,174 +0,0 @@
-
use tuirealm::command::{Cmd, CmdResult};
-
use tuirealm::props::{AttrValue, Attribute, Props};
-
use tuirealm::tui::layout::Rect;
-
use tuirealm::{Frame, MockComponent, State};
-

-
use crate::ui::components::label::Label;
-
use crate::ui::layout;
-
use crate::ui::widget::{Widget, WidgetComponent};
-

-
/// A shortcut that consists of a label displaying the "hotkey", a label that displays
-
/// the action and a spacer between them.
-
#[derive(Clone)]
-
pub struct Shortcut {
-
    short: Widget<Label>,
-
    divider: Widget<Label>,
-
    long: Widget<Label>,
-
}
-

-
impl Shortcut {
-
    pub fn new(short: Widget<Label>, divider: Widget<Label>, long: Widget<Label>) -> Self {
-
        Self {
-
            short,
-
            divider,
-
            long,
-
        }
-
    }
-
}
-

-
impl WidgetComponent for Shortcut {
-
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
-
        let display = properties
-
            .get_or(Attribute::Display, AttrValue::Flag(true))
-
            .unwrap_flag();
-

-
        if display {
-
            let labels: Vec<Box<dyn MockComponent>> = vec![
-
                self.short.clone().to_boxed(),
-
                self.divider.clone().to_boxed(),
-
                self.long.clone().to_boxed(),
-
            ];
-

-
            let layout = layout::h_stack(labels, area);
-
            for (mut shortcut, area) in layout {
-
                shortcut.view(frame, area);
-
            }
-
        }
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
-

-
/// A shortcut bar that displays multiple shortcuts and separates them with a
-
/// divider.
-
pub struct Shortcuts {
-
    shortcuts: Vec<Widget<Shortcut>>,
-
    divider: Widget<Label>,
-
}
-

-
impl Shortcuts {
-
    pub fn new(shortcuts: Vec<Widget<Shortcut>>, divider: Widget<Label>) -> Self {
-
        Self { shortcuts, divider }
-
    }
-
}
-

-
impl WidgetComponent for Shortcuts {
-
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
-
        let display = properties
-
            .get_or(Attribute::Display, AttrValue::Flag(true))
-
            .unwrap_flag();
-

-
        if display {
-
            let mut widgets: Vec<Box<dyn MockComponent>> = vec![];
-
            let mut shortcuts = self.shortcuts.iter_mut().peekable();
-

-
            while let Some(shortcut) = shortcuts.next() {
-
                if shortcuts.peek().is_some() {
-
                    widgets.push(shortcut.clone().to_boxed());
-
                    widgets.push(self.divider.clone().to_boxed())
-
                } else {
-
                    widgets.push(shortcut.clone().to_boxed());
-
                }
-
            }
-

-
            let layout = layout::h_stack(widgets, area);
-
            for (mut widget, area) in layout {
-
                widget.view(frame, area);
-
            }
-
        }
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
-

-
pub struct ContextBar {
-
    context: Widget<Label>,
-
    id: Widget<Label>,
-
    author: Widget<Label>,
-
    title: Widget<Label>,
-
    comments: Widget<Label>,
-
}
-

-
impl ContextBar {
-
    pub fn new(
-
        context: Widget<Label>,
-
        id: Widget<Label>,
-
        author: Widget<Label>,
-
        title: Widget<Label>,
-
        comments: Widget<Label>,
-
    ) -> Self {
-
        Self {
-
            context,
-
            id,
-
            author,
-
            title,
-
            comments,
-
        }
-
    }
-
}
-

-
impl WidgetComponent for ContextBar {
-
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
-
        let display = properties
-
            .get_or(Attribute::Display, AttrValue::Flag(true))
-
            .unwrap_flag();
-

-
        let context_w = self.context.query(Attribute::Width).unwrap().unwrap_size();
-
        let id_w = self.id.query(Attribute::Width).unwrap().unwrap_size();
-
        let author_w = self.author.query(Attribute::Width).unwrap().unwrap_size();
-
        let count_w = self.comments.query(Attribute::Width).unwrap().unwrap_size();
-

-
        if display {
-
            let layout = layout::h_stack(
-
                vec![
-
                    self.context.clone().to_boxed(),
-
                    self.id.clone().to_boxed(),
-
                    self.title
-
                        .clone()
-
                        .width(
-
                            area.width
-
                                .saturating_sub(context_w + id_w + author_w + count_w),
-
                        )
-
                        .to_boxed(),
-
                    self.author.clone().to_boxed(),
-
                    self.comments.clone().to_boxed(),
-
                ],
-
                area,
-
            );
-

-
            for (mut component, area) in layout {
-
                component.view(frame, area);
-
            }
-
        }
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
added radicle-tui/src/ui/components/home.rs
@@ -0,0 +1,83 @@
+
use tuirealm::command::{Cmd, CmdResult};
+
use tuirealm::props::Props;
+
use tuirealm::tui::layout::Rect;
+
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, State};
+

+
use crate::ui::layout;
+
use crate::ui::widget::{Widget, WidgetComponent};
+

+
use super::common::container::LabeledContainer;
+
use super::common::context::Shortcuts;
+
use super::common::label::Label;
+

+
pub struct Dashboard {
+
    about: Widget<LabeledContainer>,
+
    shortcuts: Widget<Shortcuts>,
+
}
+

+
impl Dashboard {
+
    pub fn new(about: Widget<LabeledContainer>, shortcuts: Widget<Shortcuts>) -> Self {
+
        Self { about, shortcuts }
+
    }
+
}
+

+
impl WidgetComponent for Dashboard {
+
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
+
        let shortcuts_h = self
+
            .shortcuts
+
            .query(Attribute::Height)
+
            .unwrap_or(AttrValue::Size(0))
+
            .unwrap_size();
+
        let layout = layout::root_component(area, shortcuts_h);
+

+
        self.about.view(frame, layout[0]);
+
        self.shortcuts.view(frame, layout[1]);
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
+

+
pub struct IssueBrowser {
+
    label: Widget<Label>,
+
    shortcuts: Widget<Shortcuts>,
+
}
+

+
impl IssueBrowser {
+
    pub fn new(label: Widget<Label>, shortcuts: Widget<Shortcuts>) -> Self {
+
        Self { label, shortcuts }
+
    }
+
}
+

+
impl WidgetComponent for IssueBrowser {
+
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
+
        let label_w = self
+
            .label
+
            .query(Attribute::Width)
+
            .unwrap_or(AttrValue::Size(1))
+
            .unwrap_size();
+
        let shortcuts_h = self
+
            .shortcuts
+
            .query(Attribute::Height)
+
            .unwrap_or(AttrValue::Size(0))
+
            .unwrap_size();
+
        let layout = layout::root_component(area, shortcuts_h);
+

+
        self.label
+
            .view(frame, layout::centered_label(label_w, layout[0]));
+
        self.shortcuts.view(frame, layout[1])
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
deleted radicle-tui/src/ui/components/label.rs
@@ -1,81 +0,0 @@
-
use tuirealm::command::{Cmd, CmdResult};
-
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style};
-
use tuirealm::tui::layout::Rect;
-
use tuirealm::tui::text::{Span, Text};
-
use tuirealm::{Frame, MockComponent, State};
-

-
use crate::ui::widget::{Widget, WidgetComponent};
-

-
/// A label that can be styled using a foreground color and text modifiers.
-
/// Its height is fixed, its width depends on the length of the text it displays.
-
#[derive(Clone, Default)]
-
pub struct Label;
-

-
impl WidgetComponent for Label {
-
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
-
        use tui_realm_stdlib::Label;
-

-
        let content = properties
-
            .get_or(Attribute::Content, AttrValue::String(String::default()))
-
            .unwrap_string();
-
        let display = properties
-
            .get_or(Attribute::Display, AttrValue::Flag(true))
-
            .unwrap_flag();
-
        let foreground = properties
-
            .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
-
            .unwrap_color();
-
        let background = properties
-
            .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
-
            .unwrap_color();
-

-
        if display {
-
            let mut label = match properties.get(Attribute::TextProps) {
-
                Some(modifiers) => Label::default()
-
                    .foreground(foreground)
-
                    .background(background)
-
                    .modifiers(modifiers.unwrap_text_modifiers())
-
                    .text(content),
-
                None => Label::default()
-
                    .foreground(foreground)
-
                    .background(background)
-
                    .text(content),
-
            };
-

-
            label.view(frame, area);
-
        }
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
-

-
impl From<&Widget<Label>> for Span<'_> {
-
    fn from(label: &Widget<Label>) -> Self {
-
        let content = label
-
            .query(Attribute::Content)
-
            .unwrap_or(AttrValue::String(String::default()))
-
            .unwrap_string();
-

-
        Span::styled(content, Style::default())
-
    }
-
}
-

-
impl From<&Widget<Label>> for Text<'_> {
-
    fn from(label: &Widget<Label>) -> Self {
-
        let content = label
-
            .query(Attribute::Content)
-
            .unwrap_or(AttrValue::String(String::default()))
-
            .unwrap_string();
-
        let foreground = label
-
            .query(Attribute::Foreground)
-
            .unwrap_or(AttrValue::Color(Color::Reset))
-
            .unwrap_color();
-

-
        Text::styled(content, Style::default().fg(foreground))
-
    }
-
}
deleted radicle-tui/src/ui/components/list.rs
@@ -1,247 +0,0 @@
-
use radicle::Profile;
-
use tuirealm::command::{Cmd, CmdResult, Direction};
-
use tuirealm::props::{
-
    AttrValue, Attribute, Color, PropPayload, PropValue, Props, Style, TextModifiers, TextSpan,
-
};
-
use tuirealm::tui::layout::{Constraint, Rect};
-
use tuirealm::tui::widgets::{Cell, Row, TableState};
-
use tuirealm::{Frame, MockComponent, State, StateValue};
-

-
use crate::ui::components::label::Label;
-
use crate::ui::layout;
-
use crate::ui::theme::Theme;
-
use crate::ui::widget::{Widget, WidgetComponent};
-

-
pub trait List {
-
    fn row(&self, theme: &Theme, profile: &Profile) -> Vec<TextSpan>;
-
}
-

-
/// A component that displays a labeled property.
-
#[derive(Clone)]
-
pub struct Property {
-
    label: Widget<Label>,
-
    divider: Widget<Label>,
-
    property: Widget<Label>,
-
}
-

-
impl Property {
-
    pub fn new(label: Widget<Label>, divider: Widget<Label>, property: Widget<Label>) -> Self {
-
        Self {
-
            label,
-
            divider,
-
            property,
-
        }
-
    }
-
}
-

-
impl WidgetComponent for Property {
-
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
-
        let display = properties
-
            .get_or(Attribute::Display, AttrValue::Flag(true))
-
            .unwrap_flag();
-

-
        if display {
-
            let labels: Vec<Box<dyn MockComponent>> = vec![
-
                self.label.clone().to_boxed(),
-
                self.divider.clone().to_boxed(),
-
                self.property.clone().to_boxed(),
-
            ];
-

-
            let layout = layout::h_stack(labels, area);
-
            for (mut label, area) in layout {
-
                label.view(frame, area);
-
            }
-
        }
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
-

-
/// A component that can display lists of labeled properties
-
#[derive(Default)]
-
pub struct PropertyList {
-
    properties: Vec<Widget<Property>>,
-
}
-

-
impl PropertyList {
-
    pub fn new(properties: Vec<Widget<Property>>) -> Self {
-
        Self { properties }
-
    }
-
}
-

-
impl WidgetComponent for PropertyList {
-
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
-
        let display = properties
-
            .get_or(Attribute::Display, AttrValue::Flag(true))
-
            .unwrap_flag();
-

-
        if display {
-
            let properties = self
-
                .properties
-
                .iter()
-
                .map(|property| property.clone().to_boxed() as Box<dyn MockComponent>)
-
                .collect();
-

-
            let layout = layout::v_stack(properties, area);
-
            for (mut property, area) in layout {
-
                property.view(frame, area);
-
            }
-
        }
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
-

-
pub struct Table {
-
    state: TableState,
-
}
-

-
impl Default for Table {
-
    fn default() -> Self {
-
        let mut state = TableState::default();
-
        state.select(Some(0));
-
        Self { state }
-
    }
-
}
-

-
impl Table {
-
    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,
-
        };
-
        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));
-
    }
-

-
    fn header<'a>(spans: Vec<PropValue>) -> Row<'a> {
-
        Row::new(
-
            spans
-
                .iter()
-
                .map(|span| {
-
                    Cell::from(span.clone().unwrap_text_span().content)
-
                        .style(Style::default().add_modifier(TextModifiers::BOLD))
-
                })
-
                .collect::<Vec<_>>(),
-
        )
-
    }
-

-
    fn rows<'a>(spans: Vec<Vec<TextSpan>>) -> Vec<Row<'a>> {
-
        spans
-
            .iter()
-
            .map(|spans| {
-
                let cells = spans.iter().map(|span| {
-
                    let style = Style::default().fg(span.fg);
-
                    Cell::from(span.content.clone()).style(style)
-
                });
-
                Row::new(cells).height(1)
-
            })
-
            .collect::<Vec<Row>>()
-
    }
-

-
    fn widths(widths: Vec<PropValue>) -> Vec<Constraint> {
-
        widths
-
            .iter()
-
            .map(|prop| Constraint::Percentage(prop.clone().unwrap_u16()))
-
            .collect()
-
    }
-
}
-

-
impl WidgetComponent for Table {
-
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
-
        let content = properties
-
            .get_or(Attribute::Content, AttrValue::Table(vec![]))
-
            .unwrap_table();
-
        let background = properties
-
            .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
-
            .unwrap_color();
-
        let highlight = properties
-
            .get_or(Attribute::HighlightedColor, AttrValue::Color(Color::Reset))
-
            .unwrap_color();
-
        let header = properties
-
            .get_or(
-
                Attribute::Custom("header"),
-
                AttrValue::Payload(PropPayload::Vec(vec![])),
-
            )
-
            .unwrap_payload()
-
            .unwrap_vec();
-
        let widths = properties
-
            .get_or(
-
                Attribute::Custom("widths"),
-
                AttrValue::Payload(PropPayload::Vec(vec![])),
-
            )
-
            .unwrap_payload()
-
            .unwrap_vec();
-

-
        let header = Self::header(header);
-
        let rows = Self::rows(content);
-
        let widths = Self::widths(widths);
-

-
        let table = tuirealm::tui::widgets::Table::new(rows)
-
            .highlight_style(Style::default().bg(highlight))
-
            .style(Style::default().bg(background))
-
            .column_spacing(3u16)
-
            .header(header)
-
            .widths(&widths);
-

-
        frame.render_stateful_widget(table, area, &mut self.state);
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, properties: &Props, cmd: Cmd) -> CmdResult {
-
        let content = properties
-
            .get_or(Attribute::Content, AttrValue::Table(vec![]))
-
            .unwrap_table();
-

-
        match cmd {
-
            Cmd::Move(Direction::Up) => {
-
                self.select_previous();
-
                if let Some(selected) = self.state.selected() {
-
                    CmdResult::Changed(State::One(StateValue::Usize(selected)))
-
                } else {
-
                    CmdResult::None
-
                }
-
            }
-
            Cmd::Move(Direction::Down) => {
-
                self.select_next(content.len());
-
                if let Some(selected) = self.state.selected() {
-
                    CmdResult::Changed(State::One(StateValue::Usize(selected)))
-
                } else {
-
                    CmdResult::None
-
                }
-
            }
-
            Cmd::Submit => {
-
                if let Some(selected) = self.state.selected() {
-
                    CmdResult::Submit(State::One(StateValue::Usize(selected)))
-
                } else {
-
                    CmdResult::None
-
                }
-
            }
-
            _ => CmdResult::None,
-
        }
-
    }
-
}
added radicle-tui/src/ui/components/patch.rs
@@ -0,0 +1,118 @@
+
use tuirealm::command::{Cmd, CmdResult};
+
use tuirealm::props::Props;
+
use tuirealm::tui::layout::Rect;
+
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, State};
+

+
use crate::ui::layout;
+
use crate::ui::widget::{Widget, WidgetComponent};
+

+
use super::common::context::{ContextBar, Shortcuts};
+
use super::common::label::Label;
+

+
pub struct Activity {
+
    label: Widget<Label>,
+
    context: Widget<ContextBar>,
+
    shortcuts: Widget<Shortcuts>,
+
}
+

+
impl Activity {
+
    pub fn new(
+
        label: Widget<Label>,
+
        context: Widget<ContextBar>,
+
        shortcuts: Widget<Shortcuts>,
+
    ) -> Self {
+
        Self {
+
            label,
+
            context,
+
            shortcuts,
+
        }
+
    }
+
}
+

+
impl WidgetComponent for Activity {
+
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
+
        let label_w = self
+
            .label
+
            .query(Attribute::Width)
+
            .unwrap_or(AttrValue::Size(1))
+
            .unwrap_size();
+
        let context_h = self
+
            .context
+
            .query(Attribute::Height)
+
            .unwrap_or(AttrValue::Size(0))
+
            .unwrap_size();
+
        let shortcuts_h = self
+
            .shortcuts
+
            .query(Attribute::Height)
+
            .unwrap_or(AttrValue::Size(0))
+
            .unwrap_size();
+
        let layout = layout::root_component_with_context(area, context_h, shortcuts_h);
+

+
        self.label
+
            .view(frame, layout::centered_label(label_w, layout[0]));
+
        self.context.view(frame, layout[1]);
+
        self.shortcuts.view(frame, layout[2]);
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
+

+
pub struct Files {
+
    label: Widget<Label>,
+
    context: Widget<ContextBar>,
+
    shortcuts: Widget<Shortcuts>,
+
}
+

+
impl Files {
+
    pub fn new(
+
        label: Widget<Label>,
+
        context: Widget<ContextBar>,
+
        shortcuts: Widget<Shortcuts>,
+
    ) -> Self {
+
        Self {
+
            label,
+
            context,
+
            shortcuts,
+
        }
+
    }
+
}
+

+
impl WidgetComponent for Files {
+
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
+
        let label_w = self
+
            .label
+
            .query(Attribute::Width)
+
            .unwrap_or(AttrValue::Size(1))
+
            .unwrap_size();
+
        let context_h = self
+
            .context
+
            .query(Attribute::Height)
+
            .unwrap_or(AttrValue::Size(0))
+
            .unwrap_size();
+
        let shortcuts_h = self
+
            .shortcuts
+
            .query(Attribute::Height)
+
            .unwrap_or(AttrValue::Size(0))
+
            .unwrap_size();
+
        let layout = layout::root_component_with_context(area, context_h, shortcuts_h);
+

+
        self.label
+
            .view(frame, layout::centered_label(label_w, layout[0]));
+
        self.context.view(frame, layout[1]);
+
        self.shortcuts.view(frame, layout[2]);
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
+
        CmdResult::None
+
    }
+
}
deleted radicle-tui/src/ui/components/workspace.rs
@@ -1,231 +0,0 @@
-
use std::marker::PhantomData;
-

-
use tuirealm::command::{Cmd, CmdResult};
-
use tuirealm::props::Props;
-
use tuirealm::tui::layout::Rect;
-
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, State};
-

-
use crate::ui::layout;
-
use crate::ui::widget::{Widget, WidgetComponent};
-

-
use super::container::LabeledContainer;
-
use super::context::{ContextBar, Shortcuts};
-
use super::label::Label;
-
use super::list::{List, Table};
-

-
pub struct Browser<T> {
-
    list: Widget<Table>,
-
    shortcuts: Widget<Shortcuts>,
-
    phantom: PhantomData<T>,
-
}
-

-
impl<T: List> Browser<T> {
-
    pub fn new(list: Widget<Table>, shortcuts: Widget<Shortcuts>) -> Self {
-
        Self {
-
            list,
-
            shortcuts,
-
            phantom: PhantomData,
-
        }
-
    }
-
}
-

-
impl<T: List> WidgetComponent for Browser<T> {
-
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
-
        let shortcuts_h = self
-
            .shortcuts
-
            .query(Attribute::Height)
-
            .unwrap_or(AttrValue::Size(0))
-
            .unwrap_size();
-
        let layout = layout::root_component(area, shortcuts_h);
-

-
        self.list.view(frame, layout[0]);
-
        self.shortcuts.view(frame, layout[1]);
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
-
        self.list.perform(cmd)
-
    }
-
}
-

-
pub struct Dashboard {
-
    about: Widget<LabeledContainer>,
-
    shortcuts: Widget<Shortcuts>,
-
}
-
impl Dashboard {
-
    pub fn new(about: Widget<LabeledContainer>, shortcuts: Widget<Shortcuts>) -> Self {
-
        Self { about, shortcuts }
-
    }
-
}
-

-
impl WidgetComponent for Dashboard {
-
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
-
        let shortcuts_h = self
-
            .shortcuts
-
            .query(Attribute::Height)
-
            .unwrap_or(AttrValue::Size(0))
-
            .unwrap_size();
-
        let layout = layout::root_component(area, shortcuts_h);
-

-
        self.about.view(frame, layout[0]);
-
        self.shortcuts.view(frame, layout[1]);
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
-

-
pub struct IssueBrowser {
-
    label: Widget<Label>,
-
    shortcuts: Widget<Shortcuts>,
-
}
-

-
impl IssueBrowser {
-
    pub fn new(label: Widget<Label>, shortcuts: Widget<Shortcuts>) -> Self {
-
        Self { label, shortcuts }
-
    }
-
}
-

-
impl WidgetComponent for IssueBrowser {
-
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
-
        let label_w = self
-
            .label
-
            .query(Attribute::Width)
-
            .unwrap_or(AttrValue::Size(1))
-
            .unwrap_size();
-
        let shortcuts_h = self
-
            .shortcuts
-
            .query(Attribute::Height)
-
            .unwrap_or(AttrValue::Size(0))
-
            .unwrap_size();
-
        let layout = layout::root_component(area, shortcuts_h);
-

-
        self.label
-
            .view(frame, layout::centered_label(label_w, layout[0]));
-
        self.shortcuts.view(frame, layout[1])
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
-

-
pub struct PatchActivity {
-
    label: Widget<Label>,
-
    context: Widget<ContextBar>,
-
    shortcuts: Widget<Shortcuts>,
-
}
-

-
impl PatchActivity {
-
    pub fn new(
-
        label: Widget<Label>,
-
        context: Widget<ContextBar>,
-
        shortcuts: Widget<Shortcuts>,
-
    ) -> Self {
-
        Self {
-
            label,
-
            context,
-
            shortcuts,
-
        }
-
    }
-
}
-

-
impl WidgetComponent for PatchActivity {
-
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
-
        let label_w = self
-
            .label
-
            .query(Attribute::Width)
-
            .unwrap_or(AttrValue::Size(1))
-
            .unwrap_size();
-
        let context_h = self
-
            .context
-
            .query(Attribute::Height)
-
            .unwrap_or(AttrValue::Size(0))
-
            .unwrap_size();
-
        let shortcuts_h = self
-
            .shortcuts
-
            .query(Attribute::Height)
-
            .unwrap_or(AttrValue::Size(0))
-
            .unwrap_size();
-
        let layout = layout::root_component_with_context(area, context_h, shortcuts_h);
-

-
        self.label
-
            .view(frame, layout::centered_label(label_w, layout[0]));
-
        self.context.view(frame, layout[1]);
-
        self.shortcuts.view(frame, layout[2]);
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
-

-
pub struct PatchFiles {
-
    label: Widget<Label>,
-
    context: Widget<ContextBar>,
-
    shortcuts: Widget<Shortcuts>,
-
}
-

-
impl PatchFiles {
-
    pub fn new(
-
        label: Widget<Label>,
-
        context: Widget<ContextBar>,
-
        shortcuts: Widget<Shortcuts>,
-
    ) -> Self {
-
        Self {
-
            label,
-
            context,
-
            shortcuts,
-
        }
-
    }
-
}
-

-
impl WidgetComponent for PatchFiles {
-
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
-
        let label_w = self
-
            .label
-
            .query(Attribute::Width)
-
            .unwrap_or(AttrValue::Size(1))
-
            .unwrap_size();
-
        let context_h = self
-
            .context
-
            .query(Attribute::Height)
-
            .unwrap_or(AttrValue::Size(0))
-
            .unwrap_size();
-
        let shortcuts_h = self
-
            .shortcuts
-
            .query(Attribute::Height)
-
            .unwrap_or(AttrValue::Size(0))
-
            .unwrap_size();
-
        let layout = layout::root_component_with_context(area, context_h, shortcuts_h);
-

-
        self.label
-
            .view(frame, layout::centered_label(label_w, layout[0]));
-
        self.context.view(frame, layout[1]);
-
        self.shortcuts.view(frame, layout[2]);
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
-
        CmdResult::None
-
    }
-
}
modified radicle-tui/src/ui/widget/common.rs
@@ -2,15 +2,15 @@ use radicle::Profile;
use tuirealm::props::{AttrValue, Attribute};
use tuirealm::MockComponent;

-
use crate::ui::components::container::{GlobalListener, LabeledContainer, Tabs};
-
use crate::ui::components::context::{Shortcut, Shortcuts};
-
use crate::ui::components::label::Label;
-
use crate::ui::components::list::{Property, PropertyList};
+
use crate::ui;

-
use super::Widget;
+
use ui::components::common::container::{GlobalListener, LabeledContainer, Tabs};
+
use ui::components::common::context::{Shortcut, Shortcuts};
+
use ui::components::common::label::Label;
+
use ui::components::common::list::{List, Property, PropertyList, Table};
+
use ui::theme::Theme;

-
use crate::ui::components::list::{List, Table};
-
use crate::ui::theme::Theme;
+
use super::Widget;

pub fn global_listener() -> Widget<GlobalListener> {
    Widget::new(GlobalListener::default())
modified radicle-tui/src/ui/widget/home.rs
@@ -4,8 +4,9 @@ use radicle::Profile;
use tuirealm::props::{PropPayload, PropValue, TextSpan};
use tuirealm::AttrValue;

-
use crate::ui::components::container::Tabs;
-
use crate::ui::components::workspace::{Browser, Dashboard, IssueBrowser};
+
use crate::ui::components::common::container::Tabs;
+
use crate::ui::components::common::Browser;
+
use crate::ui::components::home::{Dashboard, IssueBrowser};
use crate::ui::theme::Theme;

use super::{common, Widget};
modified radicle-tui/src/ui/widget/patch.rs
@@ -7,9 +7,9 @@ use super::common;
use super::Widget;

use crate::ui::cob::patch;
-
use crate::ui::components::container::Tabs;
-
use crate::ui::components::context::ContextBar;
-
use crate::ui::components::workspace::{PatchActivity, PatchFiles};
+
use crate::ui::components::common::container::Tabs;
+
use crate::ui::components::common::context::ContextBar;
+
use crate::ui::components::patch::Activity;
use crate::ui::theme::Theme;

pub fn navigation(theme: &Theme) -> Widget<Tabs> {
@@ -19,11 +19,7 @@ pub fn navigation(theme: &Theme) -> Widget<Tabs> {
    )
}

-
pub fn activity(
-
    theme: &Theme,
-
    patch: (PatchId, &Patch),
-
    profile: &Profile,
-
) -> Widget<PatchActivity> {
+
pub fn activity(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widget<Activity> {
    let (id, patch) = patch;
    let shortcuts = common::shortcuts(
        theme,
@@ -36,12 +32,12 @@ pub fn activity(
    let context = context(theme, (id, patch), profile);

    let not_implemented = common::label("not implemented").foreground(theme.colors.default_fg);
-
    let activity = PatchActivity::new(not_implemented, context, shortcuts);
+
    let activity = Activity::new(not_implemented, context, shortcuts);

    Widget::new(activity)
}

-
pub fn files(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widget<PatchFiles> {
+
pub fn files(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widget<Activity> {
    let (id, patch) = patch;
    let shortcuts = common::shortcuts(
        theme,
@@ -54,7 +50,7 @@ pub fn files(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widg
    let context = context(theme, (id, patch), profile);

    let not_implemented = common::label("not implemented").foreground(theme.colors.default_fg);
-
    let files = PatchFiles::new(not_implemented, context, shortcuts);
+
    let files = Activity::new(not_implemented, context, shortcuts);

    Widget::new(files)
}