use anyhow::Result; use radicle::cob::issue::{Issue, IssueId}; use radicle::cob::patch::{Patch, PatchId}; use radicle_tui::cob; use tuirealm::{Frame, NoUserEvent, Sub, SubClause}; use radicle_tui::ui::context::Context; use radicle_tui::ui::layout; use radicle_tui::ui::theme::Theme; use radicle_tui::ui::widget; use super::{subscription, Application, Cid, HomeCid, IssueCid, IssueMessage, Message, PatchCid}; /// `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 /// full app views (home, patch details etc) as components. /// /// View pages take into account these flat component hierarchies, and provide /// switchable sets of components. pub trait ViewPage { /// Will be called whenever a view page is pushed onto the page stack. Should create and mount all widgets. fn mount( &self, app: &mut Application, context: &Context, theme: &Theme, ) -> Result<()>; /// Will be called whenever a view page is popped from the page stack. Should unmount all widgets. fn unmount(&self, app: &mut Application) -> Result<()>; /// Will be called whenever a view page is on top of the stack and can be used to update its internal /// state depending on the message passed. fn update( &mut self, app: &mut Application, context: &Context, theme: &Theme, message: Message, ) -> Result<()>; /// Will be called whenever a view page is on top of the page stack and needs to be rendered. fn view(&mut self, app: &mut Application, frame: &mut Frame); /// Will be called whenever this view page is pushed to the stack, or it is on top of the stack again /// after another view page was popped from the stack. fn subscribe(&self, app: &mut Application) -> Result<()>; /// Will be called whenever this view page is on top of the stack and another view page is pushed /// to the stack, or if this is popped from the stack. fn unsubscribe(&self, app: &mut Application) -> Result<()>; } /// /// Home /// pub struct HomeView { active_component: Cid, } impl Default for HomeView { fn default() -> Self { HomeView { active_component: Cid::Home(HomeCid::Dashboard), } } } impl ViewPage for HomeView { fn mount( &self, app: &mut Application, context: &Context, theme: &Theme, ) -> Result<()> { 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::Header), header, 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.active(&self.active_component)?; Ok(()) } fn unmount(&self, app: &mut Application) -> Result<()> { app.umount(&Cid::Home(HomeCid::Header))?; app.umount(&Cid::Home(HomeCid::Dashboard))?; app.umount(&Cid::Home(HomeCid::IssueBrowser))?; app.umount(&Cid::Home(HomeCid::PatchBrowser))?; Ok(()) } fn update( &mut self, app: &mut Application, _context: &Context, _theme: &Theme, message: Message, ) -> Result<()> { if let Message::NavigationChanged(index) = message { self.active_component = Cid::Home(HomeCid::from(index as usize)); app.active(&self.active_component)?; } Ok(()) } fn view(&mut self, app: &mut Application, frame: &mut Frame) { let area = frame.size(); let layout = layout::default_page(area); 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::Header), Sub::new(subscription::navigation_clause(), SubClause::Always), )?; Ok(()) } fn unsubscribe(&self, app: &mut Application) -> Result<()> { app.unsubscribe( &Cid::Home(HomeCid::Header), subscription::navigation_clause(), )?; Ok(()) } } /// /// Issue detail page /// pub struct IssuePage { issue: (IssueId, Issue), } impl IssuePage { pub fn new(issue: (IssueId, Issue)) -> Self { IssuePage { issue } } } impl ViewPage for IssuePage { fn mount( &self, app: &mut Application, context: &Context, theme: &Theme, ) -> Result<()> { let (id, issue) = &self.issue; let header = widget::common::app_header(context, theme, None).to_boxed(); let list = widget::issue::list(context, theme, (*id, issue.clone())).to_boxed(); let details = widget::issue::details(context, theme, (*id, issue.clone())).to_boxed(); let shortcuts = widget::common::shortcuts( theme, vec![ widget::common::shortcut(theme, "esc", "back"), widget::common::shortcut(theme, "q", "quit"), ], ) .to_boxed(); app.remount(Cid::Issue(IssueCid::Header), header, vec![])?; app.remount(Cid::Issue(IssueCid::List), list, vec![])?; app.remount(Cid::Issue(IssueCid::Details), details, vec![])?; app.remount(Cid::Issue(IssueCid::Shortcuts), shortcuts, vec![])?; app.active(&Cid::Issue(IssueCid::List))?; Ok(()) } fn unmount(&self, app: &mut Application) -> Result<()> { app.umount(&Cid::Issue(IssueCid::Header))?; app.umount(&Cid::Issue(IssueCid::List))?; app.umount(&Cid::Issue(IssueCid::Details))?; app.umount(&Cid::Issue(IssueCid::Shortcuts))?; Ok(()) } fn update( &mut self, app: &mut Application, context: &Context, theme: &Theme, message: Message, ) -> Result<()> { if let Message::Issue(IssueMessage::Changed(id)) = message { let repo = context.repository(); if let Some(issue) = cob::issue::find(repo, &id)? { let details = widget::issue::details(context, theme, (id, issue)).to_boxed(); app.remount(Cid::Issue(IssueCid::Details), details, vec![])?; } Message::Issue(IssueMessage::FocusList) => { app.active(&Cid::Issue(IssueCid::List))?; } Message::Issue(IssueMessage::FocusDiscussion) => { app.active(&Cid::Issue(IssueCid::Discussion))?; } _ => {} } Ok(()) } fn view(&mut self, app: &mut Application, frame: &mut Frame) { let area = frame.size(); let shortcuts_h = 1u16; let layout = layout::issue_preview(area, shortcuts_h); app.view(&Cid::Issue(IssueCid::Header), frame, layout.header); app.view(&Cid::Issue(IssueCid::List), frame, layout.list); app.view(&Cid::Issue(IssueCid::Details), frame, layout.details); app.view(&Cid::Issue(IssueCid::Shortcuts), frame, layout.shortcuts); } fn subscribe(&self, _app: &mut Application) -> Result<()> { Ok(()) } fn unsubscribe(&self, _app: &mut Application) -> Result<()> { Ok(()) } } /// /// Patch detail page /// pub struct PatchView { active_component: Cid, patch: (PatchId, Patch), } impl PatchView { pub fn new(patch: (PatchId, Patch)) -> Self { PatchView { active_component: Cid::Patch(PatchCid::Activity), patch, } } } impl ViewPage for PatchView { fn mount( &self, app: &mut Application, context: &Context, theme: &Theme, ) -> Result<()> { let (id, patch) = &self.patch; 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::Header), header, vec![])?; app.remount(Cid::Patch(PatchCid::Activity), activity, vec![])?; app.remount(Cid::Patch(PatchCid::Files), files, vec![])?; app.active(&self.active_component)?; Ok(()) } fn unmount(&self, app: &mut Application) -> Result<()> { app.umount(&Cid::Patch(PatchCid::Header))?; app.umount(&Cid::Patch(PatchCid::Activity))?; app.umount(&Cid::Patch(PatchCid::Files))?; Ok(()) } fn update( &mut self, app: &mut Application, _context: &Context, _theme: &Theme, message: Message, ) -> Result<()> { if let Message::NavigationChanged(index) = message { self.active_component = Cid::Patch(PatchCid::from(index as usize)); } app.active(&self.active_component)?; Ok(()) } fn view(&mut self, app: &mut Application, frame: &mut Frame) { let area = frame.size(); let layout = layout::default_page(area); 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::Header), Sub::new(subscription::navigation_clause(), SubClause::Always), )?; Ok(()) } fn unsubscribe(&self, app: &mut Application) -> Result<()> { app.unsubscribe( &Cid::Patch(PatchCid::Header), subscription::navigation_clause(), )?; Ok(()) } } /// View pages need to preserve their state (e.g. selected navigation tab, contents /// and the selected row of a table). Therefor they should not be (re-)created /// each time they are displayed. /// Instead the application can push a new page onto the page stack if it needs to /// be displayed. Its components are then created using the internal state. If a /// new page needs to be displayed, it will also be pushed onto the stack. Leaving /// that page again will pop it from the stack. The application can then return to /// the previously displayed page in the state it was left. #[derive(Default)] pub struct PageStack { pages: Vec>, } impl PageStack { pub fn push( &mut self, page: Box, app: &mut Application, context: &Context, theme: &Theme, ) -> Result<()> { if let Some(page) = self.pages.last() { page.unsubscribe(app)?; } page.mount(app, context, theme)?; page.subscribe(app)?; self.pages.push(page); Ok(()) } pub fn pop(&mut self, app: &mut Application) -> Result<()> { self.peek_mut()?.unsubscribe(app)?; self.peek_mut()?.unmount(app)?; self.pages.pop(); self.peek_mut()?.subscribe(app)?; Ok(()) } pub fn peek_mut(&mut self) -> Result<&mut Box> { match self.pages.last_mut() { Some(page) => Ok(page), None => Err(anyhow::anyhow!( "Could not peek active page. Page stack is empty." )), } } } impl From for HomeCid { fn from(index: usize) -> Self { match index { 0 => HomeCid::Dashboard, 1 => HomeCid::IssueBrowser, 2 => HomeCid::PatchBrowser, _ => HomeCid::Dashboard, } } } impl From for PatchCid { fn from(index: usize) -> Self { match index { 0 => PatchCid::Activity, 1 => PatchCid::Files, _ => PatchCid::Activity, } } }