radicle-heartwood-lfs/radicle-tui/src/app/page.rs

405 lines
13 KiB
Rust

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<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>, 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<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>,
_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<Cid, Message, NoUserEvent>, 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<Cid, Message, NoUserEvent>) -> Result<()> {
app.subscribe(
&Cid::Home(HomeCid::Header),
Sub::new(subscription::navigation_clause(), SubClause::Always),
)?;
Ok(())
}
fn unsubscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>, 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<Cid, Message, NoUserEvent>) -> Result<()> {
Ok(())
}
fn unsubscribe(&self, _app: &mut Application<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>,
_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<Cid, Message, NoUserEvent>, 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<Cid, Message, NoUserEvent>) -> Result<()> {
app.subscribe(
&Cid::Patch(PatchCid::Header),
Sub::new(subscription::navigation_clause(), SubClause::Always),
)?;
Ok(())
}
fn unsubscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> 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<Box<dyn ViewPage>>,
}
impl PageStack {
pub fn push(
&mut self,
page: Box<dyn ViewPage>,
app: &mut Application<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>) -> 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<dyn ViewPage>> {
match self.pages.last_mut() {
Some(page) => Ok(page),
None => Err(anyhow::anyhow!(
"Could not peek active page. Page stack is empty."
)),
}
}
}
impl From<usize> for HomeCid {
fn from(index: usize) -> Self {
match index {
0 => HomeCid::Dashboard,
1 => HomeCid::IssueBrowser,
2 => HomeCid::PatchBrowser,
_ => HomeCid::Dashboard,
}
}
}
impl From<usize> for PatchCid {
fn from(index: usize) -> Self {
match index {
0 => PatchCid::Activity,
1 => PatchCid::Files,
_ => PatchCid::Activity,
}
}
}