251 lines
7.7 KiB
Rust
251 lines
7.7 KiB
Rust
use anyhow::Result;
|
|
|
|
use radicle::cob::patch::{Patch, PatchId};
|
|
use tuirealm::{Frame, NoUserEvent};
|
|
|
|
use radicle_tui::ui::layout;
|
|
use radicle_tui::ui::theme::Theme;
|
|
use radicle_tui::ui::widget;
|
|
|
|
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 {
|
|
/// 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>,
|
|
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);
|
|
}
|
|
|
|
///
|
|
/// 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).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.id, &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![])?;
|
|
|
|
app.active(&self.active_component)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn unmount(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
app.umount(&Cid::Home(HomeCid::Navigation))?;
|
|
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>,
|
|
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::Navigation), frame, layout[0]);
|
|
app.view(&self.active_component, frame, layout[1]);
|
|
}
|
|
}
|
|
|
|
///
|
|
/// 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).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![])?;
|
|
|
|
app.active(&self.active_component)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn unmount(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
app.umount(&Cid::Patch(PatchCid::Navigation))?;
|
|
app.umount(&Cid::Patch(PatchCid::Activity))?;
|
|
app.umount(&Cid::Patch(PatchCid::Files))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn update(
|
|
&mut self,
|
|
app: &mut Application<Cid, Message, NoUserEvent>,
|
|
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::Navigation), frame, layout[0]);
|
|
app.view(&self.active_component, frame, layout[1]);
|
|
}
|
|
}
|
|
|
|
/// 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<()> {
|
|
page.mount(app, context, theme)?;
|
|
self.pages.push(page);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn pop(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
|
self.peek_mut()?.unmount(app)?;
|
|
self.pages.pop();
|
|
|
|
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,
|
|
}
|
|
}
|
|
}
|