diff --git a/radicle-tui/src/app.rs b/radicle-tui/src/app.rs index a4578085..be229bde 100644 --- a/radicle-tui/src/app.rs +++ b/radicle-tui/src/app.rs @@ -23,7 +23,7 @@ use self::page::{IssuePage, PageStack}; #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum HomeCid { - Navigation, + Header, Dashboard, IssueBrowser, PatchBrowser, @@ -31,7 +31,7 @@ pub enum HomeCid { #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum PatchCid { - Navigation, + Header, Activity, Files, } diff --git a/radicle-tui/src/app/event.rs b/radicle-tui/src/app/event.rs index fd1b1f71..cd352735 100644 --- a/radicle-tui/src/app/event.rs +++ b/radicle-tui/src/app/event.rs @@ -2,7 +2,7 @@ use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection}; use tuirealm::event::{Event, Key, KeyEvent}; use tuirealm::{MockComponent, NoUserEvent, State, StateValue}; -use radicle_tui::ui::widget::common::container::{GlobalListener, LabeledContainer, Tabs}; +use radicle_tui::ui::widget::common::container::{AppHeader, GlobalListener, LabeledContainer}; use radicle_tui::ui::widget::common::context::{ContextBar, Shortcuts}; use radicle_tui::ui::widget::common::list::PropertyList; use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser, PatchBrowser}; @@ -29,7 +29,7 @@ impl tuirealm::Component for Widget { } } -impl tuirealm::Component for Widget { +impl tuirealm::Component for Widget { fn on(&mut self, event: Event) -> Option { match event { Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { diff --git a/radicle-tui/src/app/page.rs b/radicle-tui/src/app/page.rs index c080eca3..fad63e22 100644 --- a/radicle-tui/src/app/page.rs +++ b/radicle-tui/src/app/page.rs @@ -76,13 +76,14 @@ impl ViewPage for HomeView { context: &Context, theme: &Theme, ) -> Result<()> { - let navigation = widget::home::navigation(theme).to_boxed(); + let navigation = widget::home::navigation(theme); + let header = widget::common::app_header(context, theme, Some(navigation)).to_boxed(); let dashboard = widget::home::dashboard(context, theme).to_boxed(); let issue_browser = widget::home::issues(context, theme).to_boxed(); let patch_browser = widget::home::patches(context, theme).to_boxed(); - app.remount(Cid::Home(HomeCid::Navigation), navigation, vec![])?; + app.remount(Cid::Home(HomeCid::Header), header, vec![])?; app.remount(Cid::Home(HomeCid::Dashboard), dashboard, vec![])?; app.remount(Cid::Home(HomeCid::IssueBrowser), issue_browser, vec![])?; @@ -94,7 +95,7 @@ impl ViewPage for HomeView { } fn unmount(&self, app: &mut Application) -> Result<()> { - app.umount(&Cid::Home(HomeCid::Navigation))?; + app.umount(&Cid::Home(HomeCid::Header))?; app.umount(&Cid::Home(HomeCid::Dashboard))?; app.umount(&Cid::Home(HomeCid::IssueBrowser))?; app.umount(&Cid::Home(HomeCid::PatchBrowser))?; @@ -120,13 +121,13 @@ impl ViewPage for HomeView { let area = frame.size(); let layout = layout::default_page(area); - app.view(&Cid::Home(HomeCid::Navigation), frame, layout[0]); + app.view(&Cid::Home(HomeCid::Header), frame, layout[0]); app.view(&self.active_component, frame, layout[1]); } fn subscribe(&self, app: &mut Application) -> Result<()> { app.subscribe( - &Cid::Home(HomeCid::Navigation), + &Cid::Home(HomeCid::Header), Sub::new(subscription::navigation_clause(), SubClause::Always), )?; @@ -135,7 +136,7 @@ impl ViewPage for HomeView { fn unsubscribe(&self, app: &mut Application) -> Result<()> { app.unsubscribe( - &Cid::Home(HomeCid::Navigation), + &Cid::Home(HomeCid::Header), subscription::navigation_clause(), )?; @@ -258,11 +259,13 @@ impl ViewPage for PatchView { theme: &Theme, ) -> Result<()> { let (id, patch) = &self.patch; - let navigation = widget::patch::navigation(theme).to_boxed(); + let navigation = widget::patch::navigation(theme); + let header = widget::common::app_header(context, theme, Some(navigation)).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), navigation, vec![])?; + app.remount(Cid::Patch(PatchCid::Header), header, vec![])?; app.remount(Cid::Patch(PatchCid::Activity), activity, vec![])?; app.remount(Cid::Patch(PatchCid::Files), files, vec![])?; @@ -272,7 +275,7 @@ impl ViewPage for PatchView { } fn unmount(&self, app: &mut Application) -> Result<()> { - app.umount(&Cid::Patch(PatchCid::Navigation))?; + app.umount(&Cid::Patch(PatchCid::Header))?; app.umount(&Cid::Patch(PatchCid::Activity))?; app.umount(&Cid::Patch(PatchCid::Files))?; Ok(()) @@ -297,13 +300,13 @@ impl ViewPage for PatchView { let area = frame.size(); let layout = layout::default_page(area); - app.view(&Cid::Patch(PatchCid::Navigation), frame, layout[0]); + app.view(&Cid::Patch(PatchCid::Header), frame, layout[0]); app.view(&self.active_component, frame, layout[1]); } fn subscribe(&self, app: &mut Application) -> Result<()> { app.subscribe( - &Cid::Patch(PatchCid::Navigation), + &Cid::Patch(PatchCid::Header), Sub::new(subscription::navigation_clause(), SubClause::Always), )?; @@ -312,7 +315,7 @@ impl ViewPage for PatchView { fn unsubscribe(&self, app: &mut Application) -> Result<()> { app.unsubscribe( - &Cid::Patch(PatchCid::Navigation), + &Cid::Patch(PatchCid::Header), subscription::navigation_clause(), )?; diff --git a/radicle-tui/src/ui/context.rs b/radicle-tui/src/ui/context.rs index fbd01ede..5743e11e 100644 --- a/radicle-tui/src/ui/context.rs +++ b/radicle-tui/src/ui/context.rs @@ -3,7 +3,6 @@ use radicle::Profile; use radicle::storage::git::Repository; use radicle::storage::ReadStorage; - pub struct Context { profile: Profile, id: Id, @@ -14,6 +13,7 @@ pub struct Context { impl Context { pub fn new(profile: Profile, id: Id, project: Project) -> Self { let repository = profile.storage.repository(id).unwrap(); + Self { id, profile, diff --git a/radicle-tui/src/ui/layout.rs b/radicle-tui/src/ui/layout.rs index f259a0f3..b0ae5627 100644 --- a/radicle-tui/src/ui/layout.rs +++ b/radicle-tui/src/ui/layout.rs @@ -9,6 +9,12 @@ pub struct IssuePreview { pub shortcuts: Rect, } +pub struct AppHeader { + pub nav: Rect, + pub info: Rect, + pub line: Rect, +} + pub fn v_stack( widgets: Vec>, area: Rect, @@ -53,6 +59,30 @@ pub fn h_stack( widgets.into_iter().zip(layout.into_iter()).collect() } +pub fn app_header(area: Rect, info_w: u16) -> AppHeader { + let nav_w = area.width.saturating_sub(info_w); + + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + ]) + .split(area); + + let top = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(nav_w), Constraint::Length(info_w)].as_ref()) + .split(layout[1]); + + AppHeader { + nav: top[0], + info: top[1], + line: layout[2], + } +} + pub fn default_page(area: Rect) -> Vec { let nav_h = 3u16; let margin_h = 1u16; diff --git a/radicle-tui/src/ui/theme.rs b/radicle-tui/src/ui/theme.rs index 3b348e89..d4f84072 100644 --- a/radicle-tui/src/ui/theme.rs +++ b/radicle-tui/src/ui/theme.rs @@ -11,7 +11,8 @@ const COLOR_DEFAULT_FAINT: Color = Color::Rgb(20, 20, 20); pub struct Colors { pub default_fg: Color, pub tabs_highlighted_fg: Color, - pub workspaces_info_fg: Color, + pub app_header_project_fg: Color, + pub app_header_rid_fg: Color, pub labeled_container_bg: Color, pub item_list_highlighted_bg: Color, pub property_name_fg: Color, @@ -79,7 +80,8 @@ pub fn default_dark() -> Theme { colors: Colors { default_fg: COLOR_DEFAULT_FG, tabs_highlighted_fg: Color::Magenta, - workspaces_info_fg: Color::Yellow, + app_header_project_fg: Color::Cyan, + app_header_rid_fg: Color::Yellow, labeled_container_bg: COLOR_DEFAULT_FAINT, item_list_highlighted_bg: COLOR_DEFAULT_DARKER, property_name_fg: Color::Cyan, diff --git a/radicle-tui/src/ui/widget/common.rs b/radicle-tui/src/ui/widget/common.rs index a1648b0b..d5b5eaf0 100644 --- a/radicle-tui/src/ui/widget/common.rs +++ b/radicle-tui/src/ui/widget/common.rs @@ -11,11 +11,12 @@ use context::{Shortcut, Shortcuts}; use label::Label; use list::{Property, PropertyList}; -use self::container::Container; +use self::container::{AppHeader, AppInfo, Container, VerticalLine}; use self::list::{ColumnWidth, PropertyTable}; use super::Widget; +use crate::ui::context::Context; use crate::ui::theme::Theme; pub fn global_listener() -> Widget { @@ -115,10 +116,39 @@ pub fn property_table(_theme: &Theme, properties: Vec>) -> Widg Widget::new(table) } -pub fn tabs(theme: &Theme, tabs: Vec>) -> Widget { - let line = - label(&theme.icons.tab_overline.to_string()).foreground(theme.colors.tabs_highlighted_fg); - let tabs = Tabs::new(tabs, line); +pub fn tabs(_theme: &Theme, tabs: Vec>) -> Widget { + let tabs = Tabs::new(tabs); Widget::new(tabs).height(2) } + +pub fn app_info(context: &Context, theme: &Theme) -> Widget { + let project = label(context.project().name()).foreground(theme.colors.app_header_project_fg); + let rid = label(&format!(" ({})", context.id())).foreground(theme.colors.app_header_rid_fg); + + let project_w = project + .query(Attribute::Width) + .unwrap_or(AttrValue::Size(0)) + .unwrap_size(); + let rid_w = rid + .query(Attribute::Width) + .unwrap_or(AttrValue::Size(0)) + .unwrap_size(); + + let info = AppInfo::new(project, rid); + Widget::new(info).width(project_w.saturating_add(rid_w)) +} + +pub fn app_header( + context: &Context, + theme: &Theme, + nav: Option>, +) -> Widget { + let line = + label(&theme.icons.tab_overline.to_string()).foreground(theme.colors.tabs_highlighted_fg); + let line = Widget::new(VerticalLine::new(line)); + let info = app_info(context, theme); + let header = AppHeader::new(nav, info, line); + + Widget::new(header) +} diff --git a/radicle-tui/src/ui/widget/common/container.rs b/radicle-tui/src/ui/widget/common/container.rs index cafa2e9f..5c330660 100644 --- a/radicle-tui/src/ui/widget/common/container.rs +++ b/radicle-tui/src/ui/widget/common/container.rs @@ -33,21 +33,62 @@ impl WidgetComponent for GlobalListener { } } +/// A vertical separator. +#[derive(Clone)] +pub struct VerticalLine { + line: Widget