diff --git a/radicle-tui/src/app.rs b/radicle-tui/src/app.rs index 3e390503..daa8a189 100644 --- a/radicle-tui/src/app.rs +++ b/radicle-tui/src/app.rs @@ -1,31 +1,22 @@ +pub mod event; +pub mod page; +pub mod subscription; + use anyhow::Result; -use tui_realm_stdlib::Phantom; -use tuirealm::application::PollStrategy; -use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection}; -use tuirealm::event::{Event, Key, KeyEvent}; -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::{ContextBar, Shortcuts}; -use radicle_tui::ui::components::list::PropertyList; -use radicle_tui::ui::components::workspace::{ - Browser, Dashboard, IssueBrowser, PatchActivity, PatchFiles, -}; -use radicle_tui::ui::layout; -use radicle_tui::ui::theme::{self, Theme}; -use radicle_tui::ui::widget::{self, Widget}; - -use radicle_tui::Tui; - use radicle::cob::patch::{Patch, PatchId}; use radicle::identity::{Id, Project}; use radicle::profile::Profile; +use tuirealm::application::PollStrategy; +use tuirealm::{Application, Frame, NoUserEvent}; + +use radicle_tui::cob::patch::{self}; + +use radicle_tui::ui; +use radicle_tui::ui::theme::{self, Theme}; +use radicle_tui::Tui; + #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum HomeCid { Navigation, @@ -81,7 +72,7 @@ pub struct Context { #[allow(dead_code)] pub struct App { context: Context, - active_page: Box, + active_page: Box, theme: Theme, quit: bool, } @@ -100,7 +91,7 @@ impl App { patches, }, theme: theme::default_dark(), - active_page: Box::::default(), + active_page: Box::::default(), quit: false, } } @@ -110,7 +101,7 @@ impl App { app: &mut Application, theme: &Theme, ) -> Result<()> { - self.active_page = Box::::default(); + self.active_page = Box::::default(); self.active_page.mount(app, &self.context, theme)?; self.active_page.activate(app)?; @@ -122,7 +113,7 @@ impl App { app: &mut Application, theme: &Theme, ) -> Result<()> { - self.active_page = Box::::default(); + self.active_page = Box::::default(); self.active_page.mount(app, &self.context, theme)?; self.active_page.activate(app)?; @@ -136,7 +127,7 @@ impl Tui for App { // Add global key listener and subscribe to key events let global = ui::widget::common::global_listener().to_boxed(); - app.mount(Cid::GlobalListener, global, subs::global())?; + app.mount(Cid::GlobalListener, global, subscription::global())?; Ok(()) } @@ -178,280 +169,3 @@ impl Tui for App { 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<()>; -} - -/// -/// Home -/// -pub struct Home { - active_component: Cid, -} - -impl Default for Home { - fn default() -> Self { - Home { - active_component: Cid::Home(HomeCid::Dashboard), - } - } -} - -impl ViewPage for Home { - fn mount( - &self, - app: &mut Application, - context: &Context, - theme: &Theme, - ) -> Result<()> { - let navigation = widget::home::navigation(theme).to_boxed(); - - let dashboard = widget::home::dashboard(theme, &context.id, &context.project).to_boxed(); - let issue_browser = widget::home::issues(theme).to_boxed(); - let patch_browser = - widget::home::patches(theme, &context.patches, &context.profile).to_boxed(); - - app.remount( - Cid::Home(HomeCid::Navigation), - navigation, - subs::navigation(), - )?; - - 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![])?; - - Ok(()) - } - - fn update(&mut self, message: Message) { - if let Message::NavigationChanged(index) = message { - self.active_component = match index { - 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) { - let area = frame.size(); - let navigation_h = 2u16; - let layout = layout::default_page(area, navigation_h); - - app.view(&Cid::Home(HomeCid::Navigation), frame, layout[0]); - app.view(&self.active_component, frame, layout[1]); - } - - 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((id, patch)) = context.patches.get(context.selected_patch) { - let navigation = widget::patch::navigation(theme).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, - subs::navigation(), - )?; - app.remount(Cid::Patch(PatchCid::Activity), activity, vec![])?; - app.remount(Cid::Patch(PatchCid::Files), files, 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 layout = layout::default_page(area, navigation_h); - - app.view(&Cid::Patch(PatchCid::Navigation), frame, layout[0]); - app.view(&self.active_component, frame, layout[1]); - } - - fn activate(&self, app: &mut Application) -> Result<()> { - app.active(&self.active_component)?; - Ok(()) - } -} - -/// 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 tuirealm::Component for Widget { - fn on(&mut self, event: Event) -> Option { - match event { - Event::WindowResize(_, _) => Some(Message::Tick), - Event::Keyboard(KeyEvent { - code: Key::Char('q'), - .. - }) => Some(Message::Quit), - _ => None, - } - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, event: Event) -> Option { - match event { - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - match self.perform(Cmd::Move(MoveDirection::Right)) { - CmdResult::Changed(State::One(StateValue::U16(index))) => { - Some(Message::NavigationChanged(index)) - } - _ => None, - } - } - _ => None, - } - } -} - -impl tuirealm::Component for Widget> { - fn on(&mut self, event: Event) -> Option { - match event { - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { - self.perform(Cmd::Move(MoveDirection::Up)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => { - self.perform(Cmd::Move(MoveDirection::Down)); - Some(Message::Tick) - } - 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 tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - 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 { - 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 tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} - -impl tuirealm::Component for Phantom { - fn on(&mut self, _event: Event) -> Option { - None - } -} diff --git a/radicle-tui/src/app/event.rs b/radicle-tui/src/app/event.rs new file mode 100644 index 00000000..3fcf3bb7 --- /dev/null +++ b/radicle-tui/src/app/event.rs @@ -0,0 +1,139 @@ +use radicle::cob::patch::{Patch, PatchId}; + +use tui_realm_stdlib::Phantom; +use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection}; +use tuirealm::event::{Event, Key, KeyEvent}; +use tuirealm::{MockComponent, NoUserEvent, State, StateValue}; + +use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer, Tabs}; +use radicle_tui::ui::components::context::{ContextBar, Shortcuts}; +use radicle_tui::ui::components::list::PropertyList; +use radicle_tui::ui::components::workspace::{ + Browser, Dashboard, IssueBrowser, PatchActivity, PatchFiles, +}; + +use radicle_tui::ui::widget::Widget; + +use super::{Message, PatchMessage}; + +/// 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 tuirealm::Component for Widget { + fn on(&mut self, event: Event) -> Option { + match event { + Event::WindowResize(_, _) => Some(Message::Tick), + Event::Keyboard(KeyEvent { + code: Key::Char('q'), + .. + }) => Some(Message::Quit), + _ => None, + } + } +} + +impl tuirealm::Component for Widget { + fn on(&mut self, event: Event) -> Option { + match event { + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + match self.perform(Cmd::Move(MoveDirection::Right)) { + CmdResult::Changed(State::One(StateValue::U16(index))) => { + Some(Message::NavigationChanged(index)) + } + _ => None, + } + } + _ => None, + } + } +} + +impl tuirealm::Component for Widget> { + fn on(&mut self, event: Event) -> Option { + match event { + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(MoveDirection::Up)); + Some(Message::Tick) + } + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(MoveDirection::Down)); + Some(Message::Tick) + } + 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 tuirealm::Component for Widget { + fn on(&mut self, _event: Event) -> Option { + None + } +} + +impl tuirealm::Component for Widget { + fn on(&mut self, _event: Event) -> Option { + 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 { + 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 tuirealm::Component for Widget { + fn on(&mut self, _event: Event) -> Option { + None + } +} + +impl tuirealm::Component for Widget { + fn on(&mut self, _event: Event) -> Option { + None + } +} + +impl tuirealm::Component for Widget { + fn on(&mut self, _event: Event) -> Option { + None + } +} + +impl tuirealm::Component for Phantom { + fn on(&mut self, _event: Event) -> Option { + None + } +} diff --git a/radicle-tui/src/app/page.rs b/radicle-tui/src/app/page.rs new file mode 100644 index 00000000..cd130ded --- /dev/null +++ b/radicle-tui/src/app/page.rs @@ -0,0 +1,160 @@ +use anyhow::Result; +use radicle_tui::ui::{layout, theme::Theme, widget}; +use tuirealm::{Frame, NoUserEvent}; + +use super::{subscription, Application, Cid, Context, HomeCid, 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 { + 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<()>; +} + +/// +/// Home +/// +pub struct Home { + active_component: Cid, +} + +impl Default for Home { + fn default() -> Self { + Home { + active_component: Cid::Home(HomeCid::Dashboard), + } + } +} + +impl ViewPage for Home { + fn mount( + &self, + app: &mut Application, + context: &Context, + theme: &Theme, + ) -> Result<()> { + let navigation = widget::home::navigation(theme).to_boxed(); + + let dashboard = widget::home::dashboard(theme, &context.id, &context.project).to_boxed(); + let issue_browser = widget::home::issues(theme).to_boxed(); + let patch_browser = + widget::home::patches(theme, &context.patches, &context.profile).to_boxed(); + + app.remount( + Cid::Home(HomeCid::Navigation), + navigation, + subscription::navigation(), + )?; + + 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![])?; + + Ok(()) + } + + fn update(&mut self, message: Message) { + if let Message::NavigationChanged(index) = message { + self.active_component = match index { + 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) { + let area = frame.size(); + let navigation_h = 2u16; + let layout = layout::default_page(area, navigation_h); + + app.view(&Cid::Home(HomeCid::Navigation), frame, layout[0]); + app.view(&self.active_component, frame, layout[1]); + } + + 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((id, patch)) = context.patches.get(context.selected_patch) { + let navigation = widget::patch::navigation(theme).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, + subscription::navigation(), + )?; + app.remount(Cid::Patch(PatchCid::Activity), activity, vec![])?; + app.remount(Cid::Patch(PatchCid::Files), files, 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 layout = layout::default_page(area, navigation_h); + + app.view(&Cid::Patch(PatchCid::Navigation), frame, layout[0]); + app.view(&self.active_component, frame, layout[1]); + } + + fn activate(&self, app: &mut Application) -> Result<()> { + app.active(&self.active_component)?; + Ok(()) + } +} diff --git a/radicle-tui/src/subs.rs b/radicle-tui/src/app/subscription.rs similarity index 100% rename from radicle-tui/src/subs.rs rename to radicle-tui/src/app/subscription.rs diff --git a/radicle-tui/src/lib.rs b/radicle-tui/src/lib.rs index 71913b11..cf419cba 100644 --- a/radicle-tui/src/lib.rs +++ b/radicle-tui/src/lib.rs @@ -8,7 +8,6 @@ use tuirealm::Frame; use tuirealm::{Application, EventListenerCfg, NoUserEvent}; pub mod cob; -pub mod subs; pub mod ui; /// Trait that must be implemented by client applications in order to be run