tui: Split widgets into separate modules
This commit is contained in:
parent
6346931ce2
commit
969219c51d
|
|
@ -18,7 +18,7 @@ use radicle_tui::ui::components::workspace::{
|
||||||
};
|
};
|
||||||
use radicle_tui::ui::layout;
|
use radicle_tui::ui::layout;
|
||||||
use radicle_tui::ui::theme::{self, Theme};
|
use radicle_tui::ui::theme::{self, Theme};
|
||||||
use radicle_tui::ui::widget::Widget;
|
use radicle_tui::ui::widget::{self, Widget};
|
||||||
|
|
||||||
use radicle_tui::Tui;
|
use radicle_tui::Tui;
|
||||||
|
|
||||||
|
|
@ -135,7 +135,7 @@ impl Tui<Cid, Message> for App {
|
||||||
self.mount_home_view(app, &self.theme.clone())?;
|
self.mount_home_view(app, &self.theme.clone())?;
|
||||||
|
|
||||||
// Add global key listener and subscribe to key events
|
// 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())?;
|
app.mount(Cid::GlobalListener, global, subs::global())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -223,11 +223,12 @@ impl ViewPage for Home {
|
||||||
context: &Context,
|
context: &Context,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
) -> Result<()> {
|
) -> 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 dashboard = widget::home::dashboard(theme, &context.id, &context.project).to_boxed();
|
||||||
let issue_browser = ui::issue_browser(theme).to_boxed();
|
let issue_browser = widget::home::issues(theme).to_boxed();
|
||||||
let patch_browser = ui::patch_browser(theme, &context.patches, &context.profile).to_boxed();
|
let patch_browser =
|
||||||
|
widget::home::patches(theme, &context.patches, &context.profile).to_boxed();
|
||||||
|
|
||||||
app.remount(
|
app.remount(
|
||||||
Cid::Home(HomeCid::Navigation),
|
Cid::Home(HomeCid::Navigation),
|
||||||
|
|
@ -291,9 +292,10 @@ impl ViewPage for PatchView {
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Some((id, patch)) = context.patches.get(context.selected_patch) {
|
if let Some((id, patch)) = context.patches.get(context.selected_patch) {
|
||||||
let navigation = ui::patch_navigation(theme).to_boxed();
|
let navigation = widget::patch::navigation(theme).to_boxed();
|
||||||
let activity = ui::patch_activity(theme, (*id, patch), &context.profile).to_boxed();
|
let activity =
|
||||||
let files = ui::patch_files(theme, (*id, patch), &context.profile).to_boxed();
|
widget::patch::activity(theme, (*id, patch), &context.profile).to_boxed();
|
||||||
|
let files = widget::patch::files(theme, (*id, patch), &context.profile).to_boxed();
|
||||||
|
|
||||||
app.remount(
|
app.remount(
|
||||||
Cid::Patch(PatchCid::Navigation),
|
Cid::Patch(PatchCid::Navigation),
|
||||||
|
|
@ -338,11 +340,11 @@ impl ViewPage for PatchView {
|
||||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<GlobalListener> {
|
impl tuirealm::Component<Message, NoUserEvent> for Widget<GlobalListener> {
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
match event {
|
match event {
|
||||||
|
Event::WindowResize(_, _) => Some(Message::Tick),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('q'),
|
code: Key::Char('q'),
|
||||||
..
|
..
|
||||||
}) => Some(Message::Quit),
|
}) => Some(Message::Quit),
|
||||||
Event::WindowResize(_, _) => Some(Message::Tick),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,14 @@ where
|
||||||
Id: Clone + Hash + Eq + PartialEq,
|
Id: Clone + Hash + Eq + PartialEq,
|
||||||
UserEvent: Clone + Eq + PartialEq + PartialOrd,
|
UserEvent: Clone + Eq + PartialEq + PartialOrd,
|
||||||
{
|
{
|
||||||
vec![Sub::new(
|
vec![
|
||||||
|
Sub::new(
|
||||||
SubEventClause::Keyboard(KeyEvent {
|
SubEventClause::Keyboard(KeyEvent {
|
||||||
code: Key::Char('q'),
|
code: Key::Char('q'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}),
|
}),
|
||||||
SubClause::Always,
|
SubClause::Always,
|
||||||
)]
|
),
|
||||||
|
Sub::new(SubEventClause::WindowResize, SubClause::Always),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,284 +4,3 @@ pub mod layout;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
pub mod widget;
|
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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
pub mod common;
|
||||||
|
pub mod home;
|
||||||
|
pub mod patch;
|
||||||
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
use tuirealm::command::{Cmd, CmdResult};
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue