Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
tui: Split widgets into separate modules
Erik Kundt committed 3 years ago
commit 969219c51d2462d2d37b449ef728693525e86e02
parent 6346931ce2bd8fd3ec87c2b4e83efd02da401589
7 files changed +321 -298
modified radicle-tui/src/app.rs
@@ -18,7 +18,7 @@ use radicle_tui::ui::components::workspace::{
};
use radicle_tui::ui::layout;
use radicle_tui::ui::theme::{self, Theme};
-
use radicle_tui::ui::widget::Widget;
+
use radicle_tui::ui::widget::{self, Widget};

use radicle_tui::Tui;

@@ -135,7 +135,7 @@ impl Tui<Cid, Message> for App {
        self.mount_home_view(app, &self.theme.clone())?;

        // Add global key listener and subscribe to key events
-
        let global = ui::global_listener().to_boxed();
+
        let global = ui::widget::common::global_listener().to_boxed();
        app.mount(Cid::GlobalListener, global, subs::global())?;

        Ok(())
@@ -223,11 +223,12 @@ impl ViewPage for Home {
        context: &Context,
        theme: &Theme,
    ) -> Result<()> {
-
        let navigation = ui::home_navigation(theme).to_boxed();
+
        let navigation = widget::home::navigation(theme).to_boxed();

-
        let dashboard = ui::dashboard(theme, &context.id, &context.project).to_boxed();
-
        let issue_browser = ui::issue_browser(theme).to_boxed();
-
        let patch_browser = ui::patch_browser(theme, &context.patches, &context.profile).to_boxed();
+
        let dashboard = widget::home::dashboard(theme, &context.id, &context.project).to_boxed();
+
        let issue_browser = widget::home::issues(theme).to_boxed();
+
        let patch_browser =
+
            widget::home::patches(theme, &context.patches, &context.profile).to_boxed();

        app.remount(
            Cid::Home(HomeCid::Navigation),
@@ -291,9 +292,10 @@ impl ViewPage for PatchView {
        theme: &Theme,
    ) -> Result<()> {
        if let Some((id, patch)) = context.patches.get(context.selected_patch) {
-
            let navigation = ui::patch_navigation(theme).to_boxed();
-
            let activity = ui::patch_activity(theme, (*id, patch), &context.profile).to_boxed();
-
            let files = ui::patch_files(theme, (*id, patch), &context.profile).to_boxed();
+
            let navigation = widget::patch::navigation(theme).to_boxed();
+
            let activity =
+
                widget::patch::activity(theme, (*id, patch), &context.profile).to_boxed();
+
            let files = widget::patch::files(theme, (*id, patch), &context.profile).to_boxed();

            app.remount(
                Cid::Patch(PatchCid::Navigation),
@@ -338,11 +340,11 @@ impl ViewPage for PatchView {
impl tuirealm::Component<Message, NoUserEvent> for Widget<GlobalListener> {
    fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
        match event {
+
            Event::WindowResize(_, _) => Some(Message::Tick),
            Event::Keyboard(KeyEvent {
                code: Key::Char('q'),
                ..
            }) => Some(Message::Quit),
-
            Event::WindowResize(_, _) => Some(Message::Tick),
            _ => None,
        }
    }
modified radicle-tui/src/subs.rs
@@ -22,11 +22,14 @@ where
    Id: Clone + Hash + Eq + PartialEq,
    UserEvent: Clone + Eq + PartialEq + PartialOrd,
{
-
    vec![Sub::new(
-
        SubEventClause::Keyboard(KeyEvent {
-
            code: Key::Char('q'),
-
            modifiers: KeyModifiers::NONE,
-
        }),
-
        SubClause::Always,
-
    )]
+
    vec![
+
        Sub::new(
+
            SubEventClause::Keyboard(KeyEvent {
+
                code: Key::Char('q'),
+
                modifiers: KeyModifiers::NONE,
+
            }),
+
            SubClause::Always,
+
        ),
+
        Sub::new(SubEventClause::WindowResize, SubClause::Always),
+
    ]
}
modified radicle-tui/src/ui.rs
@@ -4,284 +4,3 @@ pub mod layout;
pub mod state;
pub mod theme;
pub mod widget;
-

-
use radicle::prelude::{Id, Project};
-
use radicle::Profile;
-
use tuirealm::props::{AttrValue, Attribute, Color, PropPayload, PropValue, TextSpan};
-
use tuirealm::MockComponent;
-

-
use radicle::cob::patch::{Patch, PatchId};
-

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

-
use widget::Widget;
-

-
use self::cob::patch;
-
use self::components::context::ContextBar;
-
use self::components::list::{List, Table};
-
use self::components::workspace::{Browser, Dashboard, IssueBrowser, PatchActivity, PatchFiles};
-
use self::theme::Theme;
-

-
pub fn global_listener() -> Widget<GlobalListener> {
-
    Widget::new(GlobalListener::default())
-
}
-

-
pub fn label(content: &str) -> Widget<Label> {
-
    // TODO: Remove when size constraints are implemented
-
    let width = content.chars().count() as u16;
-

-
    Widget::new(Label::default())
-
        .content(AttrValue::String(content.to_string()))
-
        .height(1)
-
        .width(width)
-
}
-

-
pub fn labeled_container(
-
    theme: &theme::Theme,
-
    title: &str,
-
    component: Box<dyn MockComponent>,
-
) -> Widget<LabeledContainer> {
-
    let title = label(&format!(" {title} "))
-
        .foreground(theme.colors.default_fg)
-
        .background(theme.colors.labeled_container_bg);
-
    let container = LabeledContainer::new(title, component);
-

-
    Widget::new(container).background(theme.colors.labeled_container_bg)
-
}
-

-
pub fn shortcut(theme: &theme::Theme, short: &str, long: &str) -> Widget<Shortcut> {
-
    let short = label(short).foreground(theme.colors.shortcut_short_fg);
-
    let divider = label(&theme.icons.whitespace.to_string());
-
    let long = label(long).foreground(theme.colors.shortcut_long_fg);
-

-
    // TODO: Remove when size constraints are implemented
-
    let short_w = short.query(Attribute::Width).unwrap().unwrap_size();
-
    let divider_w = divider.query(Attribute::Width).unwrap().unwrap_size();
-
    let long_w = long.query(Attribute::Width).unwrap().unwrap_size();
-
    let width = short_w.saturating_add(divider_w).saturating_add(long_w);
-

-
    let shortcut = Shortcut::new(short, divider, long);
-

-
    Widget::new(shortcut).height(1).width(width)
-
}
-

-
pub fn shortcuts(theme: &theme::Theme, shortcuts: Vec<Widget<Shortcut>>) -> Widget<Shortcuts> {
-
    let divider = label(&format!(" {} ", theme.icons.shortcutbar_divider))
-
        .foreground(theme.colors.shortcutbar_divider_fg);
-
    let shortcut_bar = Shortcuts::new(shortcuts, divider);
-

-
    Widget::new(shortcut_bar).height(1)
-
}
-

-
pub fn property(theme: &theme::Theme, name: &str, value: &str) -> Widget<Property> {
-
    let name = label(name).foreground(theme.colors.property_name_fg);
-
    let divider = label(&format!(" {} ", theme.icons.property_divider));
-
    let value = label(value).foreground(theme.colors.default_fg);
-

-
    // TODO: Remove when size constraints are implemented
-
    let name_w = name.query(Attribute::Width).unwrap().unwrap_size();
-
    let divider_w = divider.query(Attribute::Width).unwrap().unwrap_size();
-
    let value_w = value.query(Attribute::Width).unwrap().unwrap_size();
-
    let width = name_w.saturating_add(divider_w).saturating_add(value_w);
-

-
    let property = Property::new(name, divider, value);
-

-
    Widget::new(property).height(1).width(width)
-
}
-

-
pub fn property_list(
-
    _theme: &theme::Theme,
-
    properties: Vec<Widget<Property>>,
-
) -> Widget<PropertyList> {
-
    let property_list = PropertyList::new(properties);
-

-
    Widget::new(property_list)
-
}
-

-
pub fn tabs(theme: &theme::Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {
-
    let divider = label(&theme.icons.tab_divider.to_string());
-
    let tabs = Tabs::new(tabs, divider);
-

-
    Widget::new(tabs)
-
        .height(1)
-
        .foreground(theme.colors.tabs_fg)
-
        .highlight(theme.colors.tabs_highlighted_fg)
-
}
-

-
pub fn table(theme: &theme::Theme, items: &[impl List], profile: &Profile) -> Widget<Table> {
-
    let table = Table::default();
-
    let items = items.iter().map(|item| item.row(theme, profile)).collect();
-

-
    Widget::new(table)
-
        .content(AttrValue::Table(items))
-
        .background(theme.colors.labeled_container_bg)
-
        .highlight(theme.colors.item_list_highlighted_bg)
-
}
-

-
pub fn issue_browser(theme: &theme::Theme) -> Widget<IssueBrowser> {
-
    let shortcuts = shortcuts(
-
        theme,
-
        vec![
-
            shortcut(theme, "tab", "section"),
-
            shortcut(theme, "q", "quit"),
-
        ],
-
    );
-

-
    let not_implemented = label("not implemented").foreground(theme.colors.default_fg);
-
    let browser = IssueBrowser::new(not_implemented, shortcuts);
-

-
    Widget::new(browser)
-
}
-

-
pub fn patch_browser(
-
    theme: &theme::Theme,
-
    items: &[(PatchId, Patch)],
-
    profile: &Profile,
-
) -> Widget<Browser<(PatchId, Patch)>> {
-
    let shortcuts = shortcuts(
-
        theme,
-
        vec![
-
            shortcut(theme, "tab", "section"),
-
            shortcut(theme, "↑/↓", "navigate"),
-
            shortcut(theme, "enter", "show"),
-
            shortcut(theme, "q", "quit"),
-
        ],
-
    );
-

-
    let widths = AttrValue::Payload(PropPayload::Vec(vec![
-
        PropValue::U16(2),
-
        PropValue::U16(43),
-
        PropValue::U16(15),
-
        PropValue::U16(15),
-
        PropValue::U16(5),
-
        PropValue::U16(20),
-
    ]));
-
    let header = AttrValue::Payload(PropPayload::Vec(vec![
-
        PropValue::TextSpan(TextSpan::from("")),
-
        PropValue::TextSpan(TextSpan::from("title")),
-
        PropValue::TextSpan(TextSpan::from("author")),
-
        PropValue::TextSpan(TextSpan::from("tags")),
-
        PropValue::TextSpan(TextSpan::from("comments")),
-
        PropValue::TextSpan(TextSpan::from("date")),
-
    ]));
-

-
    let table = table(theme, items, profile)
-
        .custom("widths", widths)
-
        .custom("header", header);
-
    let browser: Browser<(PatchId, Patch)> = Browser::new(table, shortcuts);
-

-
    Widget::new(browser)
-
}
-

-
pub fn patch_activity(
-
    theme: &theme::Theme,
-
    patch: (PatchId, &Patch),
-
    profile: &Profile,
-
) -> Widget<PatchActivity> {
-
    let (id, patch) = patch;
-
    let shortcuts = shortcuts(
-
        theme,
-
        vec![
-
            shortcut(theme, "esc", "back"),
-
            shortcut(theme, "tab", "section"),
-
            shortcut(theme, "q", "quit"),
-
        ],
-
    );
-
    let context = patch_context(theme, (id, patch), profile);
-

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

-
    Widget::new(activity)
-
}
-

-
pub fn patch_files(
-
    theme: &theme::Theme,
-
    patch: (PatchId, &Patch),
-
    profile: &Profile,
-
) -> Widget<PatchFiles> {
-
    let (id, patch) = patch;
-
    let shortcuts = shortcuts(
-
        theme,
-
        vec![
-
            shortcut(theme, "esc", "back"),
-
            shortcut(theme, "tab", "section"),
-
            shortcut(theme, "q", "quit"),
-
        ],
-
    );
-
    let context = patch_context(theme, (id, patch), profile);
-

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

-
    Widget::new(files)
-
}
-

-
pub fn patch_context(
-
    _theme: &Theme,
-
    patch: (PatchId, &Patch),
-
    profile: &Profile,
-
) -> Widget<ContextBar> {
-
    let (id, patch) = patch;
-
    let id = patch::format_id(id);
-
    let title = patch.title();
-
    let author = patch::format_author(patch, profile);
-
    let comments = patch::format_comments(patch);
-

-
    let context = label(" patch ").background(Color::Rgb(238, 111, 248));
-
    let id = label(&format!(" {id} "))
-
        .foreground(Color::Rgb(117, 113, 249))
-
        .background(Color::Rgb(40, 40, 40));
-
    let title = label(&format!(" {title} "))
-
        .foreground(Color::Rgb(70, 70, 70))
-
        .background(Color::Rgb(40, 40, 40));
-
    let author = label(&format!(" {author} "))
-
        .foreground(Color::Rgb(117, 113, 249))
-
        .background(Color::Rgb(40, 40, 40));
-
    let comments = label(&format!(" {comments} "))
-
        .foreground(Color::Rgb(70, 70, 70))
-
        .background(Color::Rgb(50, 50, 50));
-

-
    let context_bar = ContextBar::new(context, id, author, title, comments);
-
    Widget::new(context_bar).height(1)
-
}
-

-
pub fn home_navigation(theme: &theme::Theme) -> Widget<Tabs> {
-
    tabs(
-
        theme,
-
        vec![label("dashboard"), label("issues"), label("patches")],
-
    )
-
}
-

-
pub fn patch_navigation(theme: &theme::Theme) -> Widget<Tabs> {
-
    tabs(theme, vec![label("activity"), label("files")])
-
}
-

-
pub fn dashboard(theme: &theme::Theme, id: &Id, project: &Project) -> Widget<Dashboard> {
-
    let about = labeled_container(
-
        theme,
-
        "about",
-
        property_list(
-
            theme,
-
            vec![
-
                property(theme, "id", &id.to_string()),
-
                property(theme, "name", project.name()),
-
                property(theme, "description", project.description()),
-
            ],
-
        )
-
        .to_boxed(),
-
    );
-
    let shortcuts = shortcuts(
-
        theme,
-
        vec![
-
            shortcut(theme, "tab", "section"),
-
            shortcut(theme, "q", "quit"),
-
        ],
-
    );
-
    let dashboard = Dashboard::new(about, shortcuts);
-

-
    Widget::new(dashboard)
-
}
modified radicle-tui/src/ui/widget.rs
@@ -1,3 +1,7 @@
+
pub mod common;
+
pub mod home;
+
pub mod patch;
+

use std::ops::Deref;

use tuirealm::command::{Cmd, CmdResult};
added radicle-tui/src/ui/widget/common.rs
@@ -0,0 +1,106 @@
+
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 super::Widget;
+

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

+
pub fn global_listener() -> Widget<GlobalListener> {
+
    Widget::new(GlobalListener::default())
+
}
+

+
pub fn label(content: &str) -> Widget<Label> {
+
    // TODO: Remove when size constraints are implemented
+
    let width = content.chars().count() as u16;
+

+
    Widget::new(Label::default())
+
        .content(AttrValue::String(content.to_string()))
+
        .height(1)
+
        .width(width)
+
}
+

+
pub fn labeled_container(
+
    theme: &Theme,
+
    title: &str,
+
    component: Box<dyn MockComponent>,
+
) -> Widget<LabeledContainer> {
+
    let title = label(&format!(" {title} "))
+
        .foreground(theme.colors.default_fg)
+
        .background(theme.colors.labeled_container_bg);
+
    let container = LabeledContainer::new(title, component);
+

+
    Widget::new(container).background(theme.colors.labeled_container_bg)
+
}
+

+
pub fn shortcut(theme: &Theme, short: &str, long: &str) -> Widget<Shortcut> {
+
    let short = label(short).foreground(theme.colors.shortcut_short_fg);
+
    let divider = label(&theme.icons.whitespace.to_string());
+
    let long = label(long).foreground(theme.colors.shortcut_long_fg);
+

+
    // TODO: Remove when size constraints are implemented
+
    let short_w = short.query(Attribute::Width).unwrap().unwrap_size();
+
    let divider_w = divider.query(Attribute::Width).unwrap().unwrap_size();
+
    let long_w = long.query(Attribute::Width).unwrap().unwrap_size();
+
    let width = short_w.saturating_add(divider_w).saturating_add(long_w);
+

+
    let shortcut = Shortcut::new(short, divider, long);
+

+
    Widget::new(shortcut).height(1).width(width)
+
}
+

+
pub fn shortcuts(theme: &Theme, shortcuts: Vec<Widget<Shortcut>>) -> Widget<Shortcuts> {
+
    let divider = label(&format!(" {} ", theme.icons.shortcutbar_divider))
+
        .foreground(theme.colors.shortcutbar_divider_fg);
+
    let shortcut_bar = Shortcuts::new(shortcuts, divider);
+

+
    Widget::new(shortcut_bar).height(1)
+
}
+

+
pub fn property(theme: &Theme, name: &str, value: &str) -> Widget<Property> {
+
    let name = label(name).foreground(theme.colors.property_name_fg);
+
    let divider = label(&format!(" {} ", theme.icons.property_divider));
+
    let value = label(value).foreground(theme.colors.default_fg);
+

+
    // TODO: Remove when size constraints are implemented
+
    let name_w = name.query(Attribute::Width).unwrap().unwrap_size();
+
    let divider_w = divider.query(Attribute::Width).unwrap().unwrap_size();
+
    let value_w = value.query(Attribute::Width).unwrap().unwrap_size();
+
    let width = name_w.saturating_add(divider_w).saturating_add(value_w);
+

+
    let property = Property::new(name, divider, value);
+

+
    Widget::new(property).height(1).width(width)
+
}
+

+
pub fn property_list(_theme: &Theme, properties: Vec<Widget<Property>>) -> Widget<PropertyList> {
+
    let property_list = PropertyList::new(properties);
+

+
    Widget::new(property_list)
+
}
+

+
pub fn tabs(theme: &Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {
+
    let divider = label(&theme.icons.tab_divider.to_string());
+
    let tabs = Tabs::new(tabs, divider);
+

+
    Widget::new(tabs)
+
        .height(1)
+
        .foreground(theme.colors.tabs_fg)
+
        .highlight(theme.colors.tabs_highlighted_fg)
+
}
+

+
pub fn table(theme: &Theme, items: &[impl List], profile: &Profile) -> Widget<Table> {
+
    let table = Table::default();
+
    let items = items.iter().map(|item| item.row(theme, profile)).collect();
+

+
    Widget::new(table)
+
        .content(AttrValue::Table(items))
+
        .background(theme.colors.labeled_container_bg)
+
        .highlight(theme.colors.item_list_highlighted_bg)
+
}
added radicle-tui/src/ui/widget/home.rs
@@ -0,0 +1,103 @@
+
use radicle::cob::patch::{Patch, PatchId};
+
use radicle::identity::{Id, Project};
+
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::theme::Theme;
+

+
use super::{common, Widget};
+

+
pub fn navigation(theme: &Theme) -> Widget<Tabs> {
+
    common::tabs(
+
        theme,
+
        vec![
+
            common::label("dashboard"),
+
            common::label("issues"),
+
            common::label("patches"),
+
        ],
+
    )
+
}
+

+
pub fn dashboard(theme: &Theme, id: &Id, project: &Project) -> Widget<Dashboard> {
+
    let about = common::labeled_container(
+
        theme,
+
        "about",
+
        common::property_list(
+
            theme,
+
            vec![
+
                common::property(theme, "id", &id.to_string()),
+
                common::property(theme, "name", project.name()),
+
                common::property(theme, "description", project.description()),
+
            ],
+
        )
+
        .to_boxed(),
+
    );
+
    let shortcuts = common::shortcuts(
+
        theme,
+
        vec![
+
            common::shortcut(theme, "tab", "section"),
+
            common::shortcut(theme, "q", "quit"),
+
        ],
+
    );
+
    let dashboard = Dashboard::new(about, shortcuts);
+

+
    Widget::new(dashboard)
+
}
+

+
pub fn patches(
+
    theme: &Theme,
+
    items: &[(PatchId, Patch)],
+
    profile: &Profile,
+
) -> Widget<Browser<(PatchId, Patch)>> {
+
    let shortcuts = common::shortcuts(
+
        theme,
+
        vec![
+
            common::shortcut(theme, "tab", "section"),
+
            common::shortcut(theme, "↑/↓", "navigate"),
+
            common::shortcut(theme, "enter", "show"),
+
            common::shortcut(theme, "q", "quit"),
+
        ],
+
    );
+

+
    let widths = AttrValue::Payload(PropPayload::Vec(vec![
+
        PropValue::U16(2),
+
        PropValue::U16(43),
+
        PropValue::U16(15),
+
        PropValue::U16(15),
+
        PropValue::U16(5),
+
        PropValue::U16(20),
+
    ]));
+
    let header = AttrValue::Payload(PropPayload::Vec(vec![
+
        PropValue::TextSpan(TextSpan::from("")),
+
        PropValue::TextSpan(TextSpan::from("title")),
+
        PropValue::TextSpan(TextSpan::from("author")),
+
        PropValue::TextSpan(TextSpan::from("tags")),
+
        PropValue::TextSpan(TextSpan::from("comments")),
+
        PropValue::TextSpan(TextSpan::from("date")),
+
    ]));
+

+
    let table = common::table(theme, items, profile)
+
        .custom("widths", widths)
+
        .custom("header", header);
+
    let browser: Browser<(PatchId, Patch)> = Browser::new(table, shortcuts);
+

+
    Widget::new(browser)
+
}
+

+
pub fn issues(theme: &Theme) -> Widget<IssueBrowser> {
+
    let shortcuts = common::shortcuts(
+
        theme,
+
        vec![
+
            common::shortcut(theme, "tab", "section"),
+
            common::shortcut(theme, "q", "quit"),
+
        ],
+
    );
+

+
    let not_implemented = common::label("not implemented").foreground(theme.colors.default_fg);
+
    let browser = IssueBrowser::new(not_implemented, shortcuts);
+

+
    Widget::new(browser)
+
}
added radicle-tui/src/ui/widget/patch.rs
@@ -0,0 +1,86 @@
+
use radicle::Profile;
+
use tuirealm::props::Color;
+

+
use radicle::cob::patch::{Patch, PatchId};
+

+
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::theme::Theme;
+

+
pub fn navigation(theme: &Theme) -> Widget<Tabs> {
+
    common::tabs(
+
        theme,
+
        vec![common::label("activity"), common::label("files")],
+
    )
+
}
+

+
pub fn activity(
+
    theme: &Theme,
+
    patch: (PatchId, &Patch),
+
    profile: &Profile,
+
) -> Widget<PatchActivity> {
+
    let (id, patch) = patch;
+
    let shortcuts = common::shortcuts(
+
        theme,
+
        vec![
+
            common::shortcut(theme, "esc", "back"),
+
            common::shortcut(theme, "tab", "section"),
+
            common::shortcut(theme, "q", "quit"),
+
        ],
+
    );
+
    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);
+

+
    Widget::new(activity)
+
}
+

+
pub fn files(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widget<PatchFiles> {
+
    let (id, patch) = patch;
+
    let shortcuts = common::shortcuts(
+
        theme,
+
        vec![
+
            common::shortcut(theme, "esc", "back"),
+
            common::shortcut(theme, "tab", "section"),
+
            common::shortcut(theme, "q", "quit"),
+
        ],
+
    );
+
    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);
+

+
    Widget::new(files)
+
}
+

+
pub fn context(_theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widget<ContextBar> {
+
    let (id, patch) = patch;
+
    let id = patch::format_id(id);
+
    let title = patch.title();
+
    let author = patch::format_author(patch, profile);
+
    let comments = patch::format_comments(patch);
+

+
    let context = common::label(" patch ").background(Color::Rgb(238, 111, 248));
+
    let id = common::label(&format!(" {} ", id))
+
        .foreground(Color::Rgb(117, 113, 249))
+
        .background(Color::Rgb(40, 40, 40));
+
    let title = common::label(&format!(" {} ", title))
+
        .foreground(Color::Rgb(70, 70, 70))
+
        .background(Color::Rgb(40, 40, 40));
+
    let author = common::label(&format!(" {} ", author))
+
        .foreground(Color::Rgb(117, 113, 249))
+
        .background(Color::Rgb(40, 40, 40));
+
    let comments = common::label(&format!(" {} ", comments))
+
        .foreground(Color::Rgb(70, 70, 70))
+
        .background(Color::Rgb(50, 50, 50));
+

+
    let context_bar = ContextBar::new(context, id, author, title, comments);
+

+
    Widget::new(context_bar).height(1)
+
}