From 0f52af8b2e0ec208944f6623eeff0c6a7b014163 Mon Sep 17 00:00:00 2001 From: Erik Kundt Date: Tue, 4 Apr 2023 18:55:47 +0200 Subject: [PATCH] tui: Implement patch view page Signed-off-by: Erik Kundt --- radicle-tui/src/app.rs | 305 ++++++++++++++++----- radicle-tui/src/ui.rs | 22 +- radicle-tui/src/ui/components/container.rs | 3 +- radicle-tui/src/ui/components/workspace.rs | 66 ++++- radicle-tui/src/ui/layout.rs | 50 ++++ 5 files changed, 368 insertions(+), 78 deletions(-) diff --git a/radicle-tui/src/app.rs b/radicle-tui/src/app.rs index cdefa97f..2cca21f8 100644 --- a/radicle-tui/src/app.rs +++ b/radicle-tui/src/app.rs @@ -7,41 +7,66 @@ use tuirealm::application::PollStrategy; use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection}; use tuirealm::event::{Event, Key, KeyEvent}; use tuirealm::props::{AttrValue, Attribute}; -use tuirealm::tui::layout::{Constraint, Direction, Layout}; -use tuirealm::{Application, Component, Frame, MockComponent, NoUserEvent, State, StateValue}; +use tuirealm::{Application, Frame, MockComponent, NoUserEvent, State, StateValue}; use radicle_tui::cob::patch::{self}; +use radicle_tui::subs; use radicle_tui::ui; use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer, Tabs}; use radicle_tui::ui::components::context::Shortcuts; use radicle_tui::ui::components::list::PropertyList; -use radicle_tui::ui::components::workspace::Browser; +use radicle_tui::ui::components::workspace::{Browser, PatchActivity, PatchFiles}; +use radicle_tui::ui::layout; use radicle_tui::ui::theme::{self, Theme}; use radicle_tui::ui::widget::Widget; -use radicle_tui::subs; - use radicle_tui::Tui; use radicle::cob::patch::{Patch, PatchId}; use radicle::identity::{Id, Project}; use radicle::profile::Profile; -/// All components known to this application. #[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub enum ComponentId { +pub enum HomeCid { Navigation, Dashboard, IssueBrowser, PatchBrowser, +} + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum PatchCid { + Navigation, + Activity, + Files, +} + +/// All component ids known to this application. +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum Cid { + Home(HomeCid), + Patch(PatchCid), Shortcuts, GlobalListener, } /// Messages handled by this application. +#[derive(Debug, Eq, PartialEq)] +pub enum HomeMessage { + Show, +} + +#[derive(Debug, Eq, PartialEq)] +pub enum PatchMessage { + Show(usize), + Leave, +} + #[derive(Debug, Eq, PartialEq)] pub enum Message { + Home(HomeMessage), + Patch(PatchMessage), NavigationChanged(u16), Quit, } @@ -50,6 +75,7 @@ pub struct Context { profile: Profile, id: Id, project: Project, + selected_patch: usize, patches: Vec<(PatchId, Patch)>, } @@ -71,6 +97,7 @@ impl App { id, profile, project, + selected_patch: 0, patches, }, theme: theme::default_dark(), @@ -79,9 +106,9 @@ impl App { } } - fn mount_home( + fn mount_home_view( &mut self, - app: &mut Application, + app: &mut Application, theme: &Theme, ) -> Result<()> { self.active_page = Box::::default(); @@ -90,35 +117,54 @@ impl App { Ok(()) } + + fn mount_patch_view( + &mut self, + app: &mut Application, + theme: &Theme, + ) -> Result<()> { + self.active_page = Box::::default(); + self.active_page.mount(app, &self.context, theme)?; + self.active_page.activate(app)?; + + Ok(()) + } } -impl Tui for App { - fn init(&mut self, app: &mut Application) -> Result<()> { - self.mount_home(app, &self.theme.clone())?; +impl Tui for App { + fn init(&mut self, app: &mut Application) -> Result<()> { + self.mount_home_view(app, &self.theme.clone())?; // Add global key listener and subscribe to key events let global = ui::global_listener().to_boxed(); - app.mount(ComponentId::GlobalListener, global, subs::global())?; + app.mount(Cid::GlobalListener, global, subs::global())?; Ok(()) } - fn view( - &mut self, - app: &mut Application, - frame: &mut Frame, - ) { + fn view(&mut self, app: &mut Application, frame: &mut Frame) { self.active_page.as_mut().view(app, frame); } fn update( &mut self, - app: &mut Application, + app: &mut Application, interval: u64, ) -> Result<()> { if let Ok(messages) = app.tick(PollStrategy::TryFor(Duration::from_millis(interval))) { + let theme = theme::default_dark(); for message in messages { match message { + Message::Home(HomeMessage::Show) => { + self.mount_home_view(app, &theme)?; + } + Message::Patch(PatchMessage::Show(index)) => { + self.context.selected_patch = index; + self.mount_patch_view(app, &theme)?; + } + Message::Patch(PatchMessage::Leave) => { + self.mount_home_view(app, &theme)?; + } Message::Quit => self.quit = true, _ => { self.active_page.update(message); @@ -137,35 +183,38 @@ impl Tui for App { } /// `tuirealm`'s event and prop system is designed to work with flat component hierarchies. -/// Building deep nested component hierarchies would need a lot more additional effort to -/// properly pass events and props down these hierarchies. This makes it hard to implement +/// Building deep nested component hierarchies would need a lot more additional effort to +/// properly pass events and props down these hierarchies. This makes it hard to implement /// full app views (home, patch details etc) as components. -/// +/// /// View pages take into account these flat component hierarchies, and provide -/// switchable sets of components. +/// switchable sets of components. pub trait ViewPage { fn mount( &self, - app: &mut Application, + app: &mut Application, context: &Context, theme: &Theme, ) -> Result<()>; fn update(&mut self, message: Message); - fn view(&mut self, app: &mut Application, frame: &mut Frame); + fn view(&mut self, app: &mut Application, frame: &mut Frame); - fn activate(&self, app: &mut Application) -> Result<()>; + fn activate(&self, app: &mut Application) -> Result<()>; } +/// +/// Home +/// pub struct Home { - active_component: ComponentId, + active_component: Cid, } impl Default for Home { fn default() -> Self { Home { - active_component: ComponentId::Dashboard, + active_component: Cid::Home(HomeCid::Dashboard), } } } @@ -173,11 +222,11 @@ impl Default for Home { impl ViewPage for Home { fn mount( &self, - app: &mut Application, + app: &mut Application, context: &Context, theme: &Theme, ) -> Result<()> { - let navigation = ui::navigation(theme).to_boxed(); + let navigation = ui::home_navigation(theme).to_boxed(); let dashboard = ui::dashboard(theme, &context.id, &context.project).to_boxed(); let issue_browser = Box::::default(); @@ -192,66 +241,144 @@ impl ViewPage for Home { ) .to_boxed(); - app.remount(ComponentId::Navigation, navigation, subs::navigation())?; + app.remount( + Cid::Home(HomeCid::Navigation), + navigation, + subs::navigation(), + )?; - app.remount(ComponentId::Dashboard, dashboard, vec![])?; - app.remount(ComponentId::IssueBrowser, issue_browser, vec![])?; - app.remount(ComponentId::PatchBrowser, patch_browser, vec![])?; + app.remount(Cid::Home(HomeCid::Dashboard), dashboard, vec![])?; + app.remount( + Cid::Home(HomeCid::IssueBrowser), + issue_browser, + vec![], + )?; + app.remount( + Cid::Home(HomeCid::PatchBrowser), + patch_browser, + vec![], + )?; - app.remount(ComponentId::Shortcuts, shortcuts, vec![])?; + app.remount(Cid::Shortcuts, shortcuts, vec![])?; Ok(()) } fn update(&mut self, message: Message) { if let Message::NavigationChanged(index) = message { self.active_component = match index { - 0 => ComponentId::Dashboard, - 1 => ComponentId::IssueBrowser, - 2 => ComponentId::PatchBrowser, - _ => ComponentId::Dashboard, + 0 => Cid::Home(HomeCid::Dashboard), + 1 => Cid::Home(HomeCid::IssueBrowser), + 2 => Cid::Home(HomeCid::PatchBrowser), + _ => Cid::Home(HomeCid::Dashboard), }; } } - fn view( - &mut self, - app: &mut Application, - frame: &mut Frame, - ) { + fn view(&mut self, app: &mut Application, frame: &mut Frame) { let area = frame.size(); - let margin_h = 1u16; let navigation_h = 2u16; let shortcuts_h = app - .query(&ComponentId::Shortcuts, Attribute::Height) + .query(&Cid::Shortcuts, Attribute::Height) .ok() .flatten() .unwrap_or(AttrValue::Size(0)) .unwrap_size(); - let workspaces_h = area.height.saturating_sub( - shortcuts_h - .saturating_add(navigation_h) - .saturating_add(margin_h), + let layout = layout::default_page(area, navigation_h, shortcuts_h); + + app.view( + &Cid::Home(HomeCid::Navigation), + frame, + layout[0], ); - - let layout = Layout::default() - .direction(Direction::Vertical) - .horizontal_margin(margin_h) - .constraints( - [ - Constraint::Length(navigation_h), - Constraint::Length(workspaces_h), - Constraint::Length(shortcuts_h), - ] - .as_ref(), - ) - .split(area); - - app.view(&ComponentId::Navigation, frame, layout[0]); app.view(&self.active_component, frame, layout[1]); - app.view(&ComponentId::Shortcuts, frame, layout[2]); + app.view(&Cid::Shortcuts, frame, layout[2]); } - fn activate(&self, app: &mut Application) -> Result<()> { + fn activate(&self, app: &mut Application) -> Result<()> { + app.active(&self.active_component)?; + Ok(()) + } +} + +/// +/// Patch detail page +/// +pub struct PatchView { + active_component: Cid, +} + +impl Default for PatchView { + fn default() -> Self { + PatchView { + active_component: Cid::Patch(PatchCid::Activity), + } + } +} + +impl ViewPage for PatchView { + fn mount( + &self, + app: &mut Application, + context: &Context, + theme: &Theme, + ) -> Result<()> { + if let Some((_, _)) = context.patches.get(context.selected_patch) { + let navigation = ui::patch_navigation(theme).to_boxed(); + let activity = ui::patch_activity(theme).to_boxed(); + let files = ui::patch_files(theme).to_boxed(); + let shortcuts = ui::shortcuts( + theme, + vec![ + ui::shortcut(theme, "esc", "back"), + ui::shortcut(theme, "tab", "section"), + ui::shortcut(theme, "q", "quit"), + ], + ) + .to_boxed(); + + app.remount( + Cid::Patch(PatchCid::Navigation), + navigation, + subs::navigation(), + )?; + app.remount(Cid::Patch(PatchCid::Activity), activity, vec![])?; + app.remount(Cid::Patch(PatchCid::Files), files, vec![])?; + app.remount(Cid::Shortcuts, shortcuts, vec![])?; + } + Ok(()) + } + + fn update(&mut self, message: Message) { + if let Message::NavigationChanged(index) = message { + self.active_component = match index { + 0 => Cid::Patch(PatchCid::Activity), + 1 => Cid::Patch(PatchCid::Files), + _ => Cid::Patch(PatchCid::Activity), + }; + } + } + + fn view(&mut self, app: &mut Application, frame: &mut Frame) { + let area = frame.size(); + let navigation_h = 2u16; + let shortcuts_h = app + .query(&Cid::Shortcuts, Attribute::Height) + .ok() + .flatten() + .unwrap_or(AttrValue::Size(0)) + .unwrap_size(); + let layout = layout::default_page(area, navigation_h, shortcuts_h); + + app.view( + &Cid::Patch(PatchCid::Navigation), + frame, + layout[0], + ); + app.view(&self.active_component, frame, layout[1]); + app.view(&Cid::Shortcuts, frame, layout[2]); + } + + fn activate(&self, app: &mut Application) -> Result<()> { app.active(&self.active_component)?; Ok(()) } @@ -260,7 +387,7 @@ impl ViewPage for Home { /// Since the framework does not know the type of messages that are being /// passed around in the app, the following handlers need to be implemented for /// each component used. -impl Component for Widget { +impl tuirealm::Component for Widget { fn on(&mut self, event: Event) -> Option { match event { Event::Keyboard(KeyEvent { @@ -272,7 +399,7 @@ impl Component for Widget { } } -impl Component for Widget { +impl tuirealm::Component for Widget { fn on(&mut self, event: Event) -> Option { match event { Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { @@ -288,7 +415,7 @@ impl Component for Widget { } } -impl Component for Widget> { +impl tuirealm::Component for Widget> { fn on(&mut self, event: Event) -> Option { match event { Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { @@ -301,30 +428,60 @@ impl Component for Widget> { self.perform(Cmd::Move(MoveDirection::Down)); None } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.perform(Cmd::Submit) { + CmdResult::Submit(State::One(StateValue::Usize(index))) => { + Some(Message::Patch(PatchMessage::Show(index))) + } + _ => None, + }, _ => None, } } } -impl Component for Widget { +impl tuirealm::Component for Widget { + fn on(&mut self, event: Event) -> Option { + match event { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Message::Patch(PatchMessage::Leave)) + } + _ => None, + } + } +} + +impl tuirealm::Component for Widget { + fn on(&mut self, event: Event) -> Option { + match event { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Message::Patch(PatchMessage::Leave)) + } + _ => None, + } + } +} + +impl tuirealm::Component for Widget { fn on(&mut self, _event: Event) -> Option { None } } -impl Component for Widget { +impl tuirealm::Component for Widget { fn on(&mut self, _event: Event) -> Option { None } } -impl Component for Widget { +impl tuirealm::Component for Widget { fn on(&mut self, _event: Event) -> Option { None } } -impl Component for Phantom { +impl tuirealm::Component for Phantom { fn on(&mut self, _event: Event) -> Option { None } diff --git a/radicle-tui/src/ui.rs b/radicle-tui/src/ui.rs index b2d17c68..165ab265 100644 --- a/radicle-tui/src/ui.rs +++ b/radicle-tui/src/ui.rs @@ -20,7 +20,7 @@ use components::list::{Property, PropertyList}; use widget::Widget; use self::components::list::{List, Table}; -use self::components::workspace::Browser; +use self::components::workspace::{Browser, PatchActivity, PatchFiles}; pub fn global_listener() -> Widget { Widget::new(GlobalListener::default()) @@ -148,13 +148,31 @@ pub fn patch_browser( Widget::new(browser) } -pub fn navigation(theme: &theme::Theme) -> Widget { +pub fn patch_activity(theme: &theme::Theme) -> Widget { + let not_implemented = label("not implemented").foreground(theme.colors.default_fg); + let activity = PatchActivity::new(not_implemented); + + Widget::new(activity) +} + +pub fn patch_files(theme: &theme::Theme) -> Widget { + let not_implemented = label("not implemented").foreground(theme.colors.default_fg); + let files = PatchFiles::new(not_implemented); + + Widget::new(files) +} + +pub fn home_navigation(theme: &theme::Theme) -> Widget { tabs( theme, vec![label("dashboard"), label("issues"), label("patches")], ) } +pub fn patch_navigation(theme: &theme::Theme) -> Widget { + tabs(theme, vec![label("activity"), label("files")]) +} + pub fn dashboard(theme: &theme::Theme, id: &Id, project: &Project) -> Widget { labeled_container( theme, diff --git a/radicle-tui/src/ui/components/container.rs b/radicle-tui/src/ui/components/container.rs index 495b6077..5cdec926 100644 --- a/radicle-tui/src/ui/components/container.rs +++ b/radicle-tui/src/ui/components/container.rs @@ -49,12 +49,13 @@ pub struct Tabs { impl Tabs { pub fn new(tabs: Vec>, divider: Widget