diff --git a/radicle-tui/src/app.rs b/radicle-tui/src/app.rs index d7ead958..cdefa97f 100644 --- a/radicle-tui/src/app.rs +++ b/radicle-tui/src/app.rs @@ -17,7 +17,7 @@ use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer, T 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::theme; +use radicle_tui::ui::theme::{self, Theme}; use radicle_tui::ui::widget::Widget; use radicle_tui::subs; @@ -28,13 +28,6 @@ use radicle::cob::patch::{Patch, PatchId}; use radicle::identity::{Id, Project}; use radicle::profile::Profile; -/// Messages handled by this application. -#[derive(Debug, Eq, PartialEq)] -pub enum Message { - NavigationChanged(u16), - Quit, -} - /// All components known to this application. #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum ComponentId { @@ -46,13 +39,25 @@ pub enum ComponentId { GlobalListener, } -#[allow(dead_code)] -pub struct App { +/// Messages handled by this application. +#[derive(Debug, Eq, PartialEq)] +pub enum Message { + NavigationChanged(u16), + Quit, +} + +pub struct Context { profile: Profile, id: Id, project: Project, patches: Vec<(PatchId, Patch)>, - active: ComponentId, +} + +#[allow(dead_code)] +pub struct App { + context: Context, + active_page: Box, + theme: Theme, quit: bool, } @@ -62,62 +67,152 @@ impl App { pub fn new(profile: Profile, id: Id, project: Project) -> Self { let patches = patch::load_all(&profile, id); Self { - id, - profile, - project, - patches, - active: ComponentId::Dashboard, + context: Context { + id, + profile, + project, + patches, + }, + theme: theme::default_dark(), + active_page: Box::::default(), quit: false, } } + + fn mount_home( + &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<()> { - let theme = theme::default_dark(); + self.mount_home(app, &self.theme.clone())?; - let navigation = ui::navigation(&theme).to_boxed(); + // Add global key listener and subscribe to key events + let global = ui::global_listener().to_boxed(); + app.mount(ComponentId::GlobalListener, global, subs::global())?; - let dashboard = ui::dashboard(&theme, &self.id, &self.project).to_boxed(); + Ok(()) + } + + 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, + interval: u64, + ) -> Result<()> { + if let Ok(messages) = app.tick(PollStrategy::TryFor(Duration::from_millis(interval))) { + for message in messages { + match message { + Message::Quit => self.quit = true, + _ => { + self.active_page.update(message); + self.active_page.activate(app)?; + } + } + } + } + + Ok(()) + } + + fn quit(&self) -> bool { + self.quit + } +} + +/// `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 { + fn mount( + &self, + 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 activate(&self, app: &mut Application) -> Result<()>; +} + +pub struct Home { + active_component: ComponentId, +} + +impl Default for Home { + fn default() -> Self { + Home { + active_component: ComponentId::Dashboard, + } + } +} + +impl ViewPage for Home { + fn mount( + &self, + app: &mut Application, + context: &Context, + theme: &Theme, + ) -> Result<()> { + let navigation = ui::navigation(theme).to_boxed(); + + let dashboard = ui::dashboard(theme, &context.id, &context.project).to_boxed(); let issue_browser = Box::::default(); - let patch_browser = ui::patch_browser(&theme, &self.patches, &self.profile).to_boxed(); - - let global_listener = ui::global_listener().to_boxed(); + let patch_browser = ui::patch_browser(theme, &context.patches, &context.profile).to_boxed(); let shortcuts = ui::shortcuts( - &theme, + theme, vec![ - ui::shortcut(&theme, "tab", "section"), - ui::shortcut(&theme, "q", "quit"), + ui::shortcut(theme, "tab", "section"), + ui::shortcut(theme, "q", "quit"), ], ) .to_boxed(); - app.mount( - ComponentId::Navigation, - navigation, - vec![subs::navigation()], - )?; + app.remount(ComponentId::Navigation, navigation, subs::navigation())?; - app.mount(ComponentId::Dashboard, dashboard, vec![])?; - app.mount(ComponentId::IssueBrowser, issue_browser, vec![])?; - app.mount(ComponentId::PatchBrowser, patch_browser, vec![])?; - - app.mount(ComponentId::Shortcuts, shortcuts, vec![])?; - - // Add global key listener and subscribe to key events - app.mount( - ComponentId::GlobalListener, - global_listener, - vec![subs::global()], - )?; - - // We need to give focus to a component then - app.active(&ComponentId::Dashboard)?; + app.remount(ComponentId::Dashboard, dashboard, vec![])?; + app.remount(ComponentId::IssueBrowser, issue_browser, vec![])?; + app.remount(ComponentId::PatchBrowser, patch_browser, vec![])?; + app.remount(ComponentId::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, + }; + } + } + fn view( &mut self, app: &mut Application, @@ -152,38 +247,14 @@ impl Tui for App { .split(area); app.view(&ComponentId::Navigation, frame, layout[0]); - app.view(&self.active, frame, layout[1]); + app.view(&self.active_component, frame, layout[1]); app.view(&ComponentId::Shortcuts, frame, layout[2]); } - fn update( - &mut self, - app: &mut Application, - interval: u64, - ) -> Result<()> { - if let Ok(messages) = app.tick(PollStrategy::TryFor(Duration::from_millis(interval))) { - for message in messages { - match message { - Message::NavigationChanged(index) => { - self.active = match index { - 0 => ComponentId::Dashboard, - 1 => ComponentId::IssueBrowser, - 2 => ComponentId::PatchBrowser, - _ => ComponentId::Dashboard, - }; - app.active(&self.active)?; - } - Message::Quit => self.quit = true, - } - } - } - + fn activate(&self, app: &mut Application) -> Result<()> { + app.active(&self.active_component)?; Ok(()) } - - fn quit(&self) -> bool { - self.quit - } } /// Since the framework does not know the type of messages that are being diff --git a/radicle-tui/src/subs.rs b/radicle-tui/src/subs.rs index bdc6e65a..17823d92 100644 --- a/radicle-tui/src/subs.rs +++ b/radicle-tui/src/subs.rs @@ -3,30 +3,30 @@ use std::hash::Hash; use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::{Sub, SubClause, SubEventClause}; -pub fn navigation() -> Sub +pub fn navigation() -> Vec> where Id: Clone + Hash + Eq + PartialEq, UserEvent: Clone + Eq + PartialEq + PartialOrd, { - Sub::new( + vec![Sub::new( SubEventClause::Keyboard(KeyEvent { code: Key::Tab, modifiers: KeyModifiers::NONE, }), SubClause::Always, - ) + )] } -pub fn global() -> Sub +pub fn global() -> Vec> where Id: Clone + Hash + Eq + PartialEq, UserEvent: Clone + Eq + PartialEq + PartialOrd, { - Sub::new( + vec![Sub::new( SubEventClause::Keyboard(KeyEvent { code: Key::Char('q'), modifiers: KeyModifiers::NONE, }), SubClause::Always, - ) + )] } diff --git a/radicle-tui/src/ui/theme.rs b/radicle-tui/src/ui/theme.rs index 9e266917..f2af194b 100644 --- a/radicle-tui/src/ui/theme.rs +++ b/radicle-tui/src/ui/theme.rs @@ -1,6 +1,6 @@ use tuirealm::props::Color; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Colors { pub default_fg: Color, pub tabs_fg: Color, @@ -20,7 +20,7 @@ pub struct Colors { pub browser_patch_list_timestamp: Color, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Icons { pub property_divider: char, pub shortcutbar_divider: char, @@ -42,7 +42,7 @@ pub struct Icons { /// "shortcuts.divider: "∙", /// } /// } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Theme { pub name: String, pub colors: Colors,