tui: Implement view pages

Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
Erik Kundt 2023-04-04 17:20:09 +02:00
parent 52222939ae
commit f0ef4efe6c
3 changed files with 152 additions and 81 deletions

View File

@ -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::context::Shortcuts;
use radicle_tui::ui::components::list::PropertyList; use radicle_tui::ui::components::list::PropertyList;
use radicle_tui::ui::components::workspace::Browser; 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::ui::widget::Widget;
use radicle_tui::subs; use radicle_tui::subs;
@ -28,13 +28,6 @@ use radicle::cob::patch::{Patch, PatchId};
use radicle::identity::{Id, Project}; use radicle::identity::{Id, Project};
use radicle::profile::Profile; 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. /// All components known to this application.
#[derive(Debug, Eq, PartialEq, Clone, Hash)] #[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum ComponentId { pub enum ComponentId {
@ -46,13 +39,25 @@ pub enum ComponentId {
GlobalListener, GlobalListener,
} }
#[allow(dead_code)] /// Messages handled by this application.
pub struct App { #[derive(Debug, Eq, PartialEq)]
pub enum Message {
NavigationChanged(u16),
Quit,
}
pub struct Context {
profile: Profile, profile: Profile,
id: Id, id: Id,
project: Project, project: Project,
patches: Vec<(PatchId, Patch)>, patches: Vec<(PatchId, Patch)>,
active: ComponentId, }
#[allow(dead_code)]
pub struct App {
context: Context,
active_page: Box<dyn ViewPage>,
theme: Theme,
quit: bool, quit: bool,
} }
@ -62,62 +67,152 @@ impl App {
pub fn new(profile: Profile, id: Id, project: Project) -> Self { pub fn new(profile: Profile, id: Id, project: Project) -> Self {
let patches = patch::load_all(&profile, id); let patches = patch::load_all(&profile, id);
Self { Self {
id, context: Context {
profile, id,
project, profile,
patches, project,
active: ComponentId::Dashboard, patches,
},
theme: theme::default_dark(),
active_page: Box::<Home>::default(),
quit: false, quit: false,
} }
} }
fn mount_home(
&mut self,
app: &mut Application<ComponentId, Message, NoUserEvent>,
theme: &Theme,
) -> Result<()> {
self.active_page = Box::<Home>::default();
self.active_page.mount(app, &self.context, theme)?;
self.active_page.activate(app)?;
Ok(())
}
} }
impl Tui<ComponentId, Message> for App { impl Tui<ComponentId, Message> for App {
fn init(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> Result<()> { fn init(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> 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<ComponentId, Message, NoUserEvent>,
frame: &mut Frame,
) {
self.active_page.as_mut().view(app, frame);
}
fn update(
&mut self,
app: &mut Application<ComponentId, Message, NoUserEvent>,
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<ComponentId, Message, NoUserEvent>,
context: &Context,
theme: &Theme,
) -> Result<()>;
fn update(&mut self, message: Message);
fn view(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>, frame: &mut Frame);
fn activate(&self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> 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<ComponentId, Message, NoUserEvent>,
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::<Phantom>::default(); let issue_browser = Box::<Phantom>::default();
let patch_browser = ui::patch_browser(&theme, &self.patches, &self.profile).to_boxed(); let patch_browser = ui::patch_browser(theme, &context.patches, &context.profile).to_boxed();
let global_listener = ui::global_listener().to_boxed();
let shortcuts = ui::shortcuts( let shortcuts = ui::shortcuts(
&theme, theme,
vec![ vec![
ui::shortcut(&theme, "tab", "section"), ui::shortcut(theme, "tab", "section"),
ui::shortcut(&theme, "q", "quit"), ui::shortcut(theme, "q", "quit"),
], ],
) )
.to_boxed(); .to_boxed();
app.mount( app.remount(ComponentId::Navigation, navigation, subs::navigation())?;
ComponentId::Navigation,
navigation,
vec![subs::navigation()],
)?;
app.mount(ComponentId::Dashboard, dashboard, vec![])?; app.remount(ComponentId::Dashboard, dashboard, vec![])?;
app.mount(ComponentId::IssueBrowser, issue_browser, vec![])?; app.remount(ComponentId::IssueBrowser, issue_browser, vec![])?;
app.mount(ComponentId::PatchBrowser, patch_browser, vec![])?; app.remount(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::Shortcuts, shortcuts, vec![])?;
Ok(()) 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( fn view(
&mut self, &mut self,
app: &mut Application<ComponentId, Message, NoUserEvent>, app: &mut Application<ComponentId, Message, NoUserEvent>,
@ -152,38 +247,14 @@ impl Tui<ComponentId, Message> for App {
.split(area); .split(area);
app.view(&ComponentId::Navigation, frame, layout[0]); 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]); app.view(&ComponentId::Shortcuts, frame, layout[2]);
} }
fn update( fn activate(&self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> Result<()> {
&mut self, app.active(&self.active_component)?;
app: &mut Application<ComponentId, Message, NoUserEvent>,
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,
}
}
}
Ok(()) Ok(())
} }
fn quit(&self) -> bool {
self.quit
}
} }
/// Since the framework does not know the type of messages that are being /// Since the framework does not know the type of messages that are being

View File

@ -3,30 +3,30 @@ use std::hash::Hash;
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::{Sub, SubClause, SubEventClause}; use tuirealm::{Sub, SubClause, SubEventClause};
pub fn navigation<Id, UserEvent>() -> Sub<Id, UserEvent> pub fn navigation<Id, UserEvent>() -> Vec<Sub<Id, UserEvent>>
where where
Id: Clone + Hash + Eq + PartialEq, Id: Clone + Hash + Eq + PartialEq,
UserEvent: Clone + Eq + PartialEq + PartialOrd, UserEvent: Clone + Eq + PartialEq + PartialOrd,
{ {
Sub::new( vec![Sub::new(
SubEventClause::Keyboard(KeyEvent { SubEventClause::Keyboard(KeyEvent {
code: Key::Tab, code: Key::Tab,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
SubClause::Always, SubClause::Always,
) )]
} }
pub fn global<Id, UserEvent>() -> Sub<Id, UserEvent> pub fn global<Id, UserEvent>() -> Vec<Sub<Id, UserEvent>>
where where
Id: Clone + Hash + Eq + PartialEq, Id: Clone + Hash + Eq + PartialEq,
UserEvent: Clone + Eq + PartialEq + PartialOrd, UserEvent: Clone + Eq + PartialEq + PartialOrd,
{ {
Sub::new( vec![Sub::new(
SubEventClause::Keyboard(KeyEvent { SubEventClause::Keyboard(KeyEvent {
code: Key::Char('q'), code: Key::Char('q'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
SubClause::Always, SubClause::Always,
) )]
} }

View File

@ -1,6 +1,6 @@
use tuirealm::props::Color; use tuirealm::props::Color;
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Colors { pub struct Colors {
pub default_fg: Color, pub default_fg: Color,
pub tabs_fg: Color, pub tabs_fg: Color,
@ -20,7 +20,7 @@ pub struct Colors {
pub browser_patch_list_timestamp: Color, pub browser_patch_list_timestamp: Color,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Icons { pub struct Icons {
pub property_divider: char, pub property_divider: char,
pub shortcutbar_divider: char, pub shortcutbar_divider: char,
@ -42,7 +42,7 @@ pub struct Icons {
/// "shortcuts.divider: "∙", /// "shortcuts.divider: "∙",
/// } /// }
/// } /// }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Theme { pub struct Theme {
pub name: String, pub name: String,
pub colors: Colors, pub colors: Colors,