tui: Implement page stack and page state
This commit is contained in:
parent
e7cc32a278
commit
4142d85178
|
|
@ -4,7 +4,6 @@ pub mod subscription;
|
|||
|
||||
use anyhow::Result;
|
||||
|
||||
use radicle::cob::patch::{Patch, PatchId};
|
||||
use radicle::identity::{Id, Project};
|
||||
use radicle::profile::Profile;
|
||||
|
||||
|
|
@ -17,6 +16,10 @@ use radicle_tui::ui;
|
|||
use radicle_tui::ui::theme::{self, Theme};
|
||||
use radicle_tui::Tui;
|
||||
|
||||
use page::{HomeView, PatchView};
|
||||
|
||||
use self::page::PageStack;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum HomeCid {
|
||||
Navigation,
|
||||
|
|
@ -43,12 +46,12 @@ pub enum Cid {
|
|||
/// Messages handled by this application.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum HomeMessage {
|
||||
Show,
|
||||
PatchChanged(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum PatchMessage {
|
||||
Show(usize),
|
||||
Show,
|
||||
Leave,
|
||||
}
|
||||
|
||||
|
|
@ -65,14 +68,12 @@ pub struct Context {
|
|||
profile: Profile,
|
||||
id: Id,
|
||||
project: Project,
|
||||
selected_patch: usize,
|
||||
patches: Vec<(PatchId, Patch)>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct App {
|
||||
context: Context,
|
||||
active_page: Box<dyn page::ViewPage>,
|
||||
pages: PageStack,
|
||||
theme: Theme,
|
||||
quit: bool,
|
||||
}
|
||||
|
|
@ -81,49 +82,63 @@ pub struct App {
|
|||
/// components and sets focus to a default one.
|
||||
impl App {
|
||||
pub fn new(profile: Profile, id: Id, project: Project) -> Self {
|
||||
let patches = patch::load_all(&profile, id);
|
||||
Self {
|
||||
context: Context {
|
||||
id,
|
||||
profile,
|
||||
project,
|
||||
selected_patch: 0,
|
||||
patches,
|
||||
},
|
||||
pages: PageStack::default(),
|
||||
theme: theme::default_dark(),
|
||||
active_page: Box::<page::Home>::default(),
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn mount_home_view(
|
||||
fn view_home(
|
||||
&mut self,
|
||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||
theme: &Theme,
|
||||
) -> Result<()> {
|
||||
self.active_page = Box::<page::Home>::default();
|
||||
self.active_page.mount(app, &self.context, theme)?;
|
||||
self.active_page.activate(app)?;
|
||||
let patches = patch::load_all(&self.context.profile, self.context.id);
|
||||
let home = Box::new(HomeView::new(patches));
|
||||
self.pages.push(home, app, &self.context, theme)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_patch_view(
|
||||
fn view_patch(
|
||||
&mut self,
|
||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||
theme: &Theme,
|
||||
) -> Result<()> {
|
||||
self.active_page = Box::<page::PatchView>::default();
|
||||
self.active_page.mount(app, &self.context, theme)?;
|
||||
self.active_page.activate(app)?;
|
||||
let page = self.pages.peek_mut()?;
|
||||
let state = page.state().unwrap_map();
|
||||
let patches = state
|
||||
.and_then(|mut values| values.remove("patches"))
|
||||
.and_then(|value| value.unwrap_patches());
|
||||
|
||||
Ok(())
|
||||
match patches {
|
||||
Some((patches, selection)) => match patches.get(selection) {
|
||||
Some((id, patch)) => {
|
||||
let view = Box::new(PatchView::new((*id, patch.clone())));
|
||||
self.pages.push(view, app, &self.context, theme)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
None => Err(anyhow::anyhow!(
|
||||
"Could not mount 'page::PatchView'. Patch not found."
|
||||
)),
|
||||
},
|
||||
None => Err(anyhow::anyhow!(
|
||||
"Could not mount 'page::PatchView'. No state value for 'patches' found."
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tui<Cid, Message> for App {
|
||||
fn init(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||
self.mount_home_view(app, &self.theme.clone())?;
|
||||
self.view_home(app, &self.theme.clone())?;
|
||||
|
||||
// Add global key listener and subscribe to key events
|
||||
let global = ui::widget::common::global_listener().to_boxed();
|
||||
|
|
@ -133,7 +148,9 @@ impl Tui<Cid, Message> for App {
|
|||
}
|
||||
|
||||
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame) {
|
||||
self.active_page.as_mut().view(app, frame);
|
||||
if let Ok(page) = self.pages.peek_mut() {
|
||||
page.view(app, frame);
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<bool> {
|
||||
|
|
@ -142,20 +159,15 @@ impl Tui<Cid, Message> for App {
|
|||
let theme = theme::default_dark();
|
||||
for message in messages {
|
||||
match message {
|
||||
Message::Home(HomeMessage::Show) => {
|
||||
self.mount_home_view(app, &theme)?;
|
||||
}
|
||||
Message::Patch(PatchMessage::Show(index)) => {
|
||||
self.context.selected_patch = index;
|
||||
self.mount_patch_view(app, &theme)?;
|
||||
Message::Patch(PatchMessage::Show) => {
|
||||
self.view_patch(app, &theme)?;
|
||||
}
|
||||
Message::Patch(PatchMessage::Leave) => {
|
||||
self.mount_home_view(app, &theme)?;
|
||||
self.pages.pop(app)?;
|
||||
}
|
||||
Message::Quit => self.quit = true,
|
||||
_ => {
|
||||
self.active_page.update(message);
|
||||
self.active_page.activate(app)?;
|
||||
self.pages.peek_mut()?.update(app, message)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use radicle_tui::ui::components::patch;
|
|||
|
||||
use radicle_tui::ui::widget::Widget;
|
||||
|
||||
use super::{Message, PatchMessage};
|
||||
use super::{HomeMessage, 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
|
||||
|
|
@ -51,23 +51,24 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<Browser<(PatchId, Patc
|
|||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||
match event {
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(MoveDirection::Up));
|
||||
Some(Message::Tick)
|
||||
match self.perform(Cmd::Move(MoveDirection::Up)) {
|
||||
CmdResult::Changed(State::One(StateValue::Usize(index))) => {
|
||||
Some(Message::Home(HomeMessage::PatchChanged(index)))
|
||||
}
|
||||
_ => Some(Message::Tick),
|
||||
}
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(MoveDirection::Down));
|
||||
Some(Message::Tick)
|
||||
}
|
||||
}) => match self.perform(Cmd::Move(MoveDirection::Down)) {
|
||||
CmdResult::Changed(State::One(StateValue::Usize(index))) => {
|
||||
Some(Message::Home(HomeMessage::PatchChanged(index)))
|
||||
}
|
||||
_ => 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,
|
||||
},
|
||||
}) => Some(Message::Patch(PatchMessage::Show)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use radicle_tui::ui::{layout, theme::Theme, widget};
|
||||
|
||||
use radicle::cob::patch::{Patch, PatchId};
|
||||
use tuirealm::{Frame, NoUserEvent};
|
||||
|
||||
use super::{subscription, Application, Cid, Context, HomeCid, Message, PatchCid};
|
||||
use radicle_tui::ui::layout;
|
||||
use radicle_tui::ui::theme::Theme;
|
||||
use radicle_tui::ui::widget;
|
||||
|
||||
use super::{subscription, Application, Cid, Context, HomeCid, HomeMessage, 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
|
||||
|
|
@ -12,6 +19,7 @@ use super::{subscription, Application, Cid, Context, HomeCid, Message, PatchCid}
|
|||
/// 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>,
|
||||
|
|
@ -19,41 +27,54 @@ pub trait ViewPage {
|
|||
theme: &Theme,
|
||||
) -> Result<()>;
|
||||
|
||||
fn update(&mut self, message: Message);
|
||||
/// 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);
|
||||
|
||||
fn activate(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()>;
|
||||
/// Can be used to retrieve a view page's internal state in a unified form.
|
||||
fn state(&self) -> PageState;
|
||||
}
|
||||
|
||||
///
|
||||
/// Home
|
||||
///
|
||||
pub struct Home {
|
||||
pub struct HomeView {
|
||||
active_component: Cid,
|
||||
patches: (Vec<(PatchId, Patch)>, usize),
|
||||
}
|
||||
|
||||
impl Default for Home {
|
||||
fn default() -> Self {
|
||||
Home {
|
||||
impl HomeView {
|
||||
pub fn new(patches: Vec<(PatchId, Patch)>) -> Self {
|
||||
HomeView {
|
||||
active_component: Cid::Home(HomeCid::Dashboard),
|
||||
patches: (patches, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewPage for Home {
|
||||
impl ViewPage for HomeView {
|
||||
fn mount(
|
||||
&self,
|
||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||
context: &Context,
|
||||
theme: &Theme,
|
||||
) -> Result<()> {
|
||||
let (patches, _) = &self.patches;
|
||||
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();
|
||||
let patch_browser = widget::home::patches(theme, patches, &context.profile).to_boxed();
|
||||
|
||||
app.remount(
|
||||
Cid::Home(HomeCid::Navigation),
|
||||
|
|
@ -65,18 +86,36 @@ impl ViewPage for Home {
|
|||
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 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 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<()> {
|
||||
match message {
|
||||
Message::NavigationChanged(index) => {
|
||||
self.active_component = Cid::Home(HomeCid::from(index as usize));
|
||||
}
|
||||
Message::Home(HomeMessage::PatchChanged(index)) => {
|
||||
self.patches.1 = index;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
app.active(&self.active_component)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame) {
|
||||
|
|
@ -87,9 +126,17 @@ impl ViewPage for Home {
|
|||
app.view(&self.active_component, frame, layout[1]);
|
||||
}
|
||||
|
||||
fn activate(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||
app.active(&self.active_component)?;
|
||||
Ok(())
|
||||
fn state(&self) -> PageState {
|
||||
let (patches, selected) = &self.patches;
|
||||
PageState::Map(
|
||||
[(
|
||||
"patches".to_string(),
|
||||
PageStateValue::Patches(patches.clone(), *selected),
|
||||
)]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,12 +145,14 @@ impl ViewPage for Home {
|
|||
///
|
||||
pub struct PatchView {
|
||||
active_component: Cid,
|
||||
patch: (PatchId, Patch),
|
||||
}
|
||||
|
||||
impl Default for PatchView {
|
||||
fn default() -> Self {
|
||||
impl PatchView {
|
||||
pub fn new(patch: (PatchId, Patch)) -> Self {
|
||||
PatchView {
|
||||
active_component: Cid::Patch(PatchCid::Activity),
|
||||
patch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -115,31 +164,42 @@ impl ViewPage for PatchView {
|
|||
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();
|
||||
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)?;
|
||||
|
||||
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) {
|
||||
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 = match index {
|
||||
0 => Cid::Patch(PatchCid::Activity),
|
||||
1 => Cid::Patch(PatchCid::Files),
|
||||
_ => Cid::Patch(PatchCid::Activity),
|
||||
};
|
||||
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) {
|
||||
|
|
@ -150,8 +210,104 @@ impl ViewPage for PatchView {
|
|||
app.view(&self.active_component, frame, layout[1]);
|
||||
}
|
||||
|
||||
fn activate(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||
app.active(&self.active_component)?;
|
||||
Ok(())
|
||||
fn state(&self) -> PageState {
|
||||
PageState::None
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a state value that can be retrieved from a view page.
|
||||
#[derive(Clone)]
|
||||
pub enum PageStateValue {
|
||||
/// List of patches and its selected patch
|
||||
Patches(Vec<(PatchId, Patch)>, usize),
|
||||
}
|
||||
|
||||
impl PageStateValue {
|
||||
pub fn unwrap_patches(self) -> Option<(Vec<(PatchId, Patch)>, usize)> {
|
||||
match self {
|
||||
PageStateValue::Patches(patches, selection) => Some((patches, selection)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// View pages provide a way to retrieve their state in a unified manner
|
||||
/// in case that state needs to be passed to other pages.
|
||||
#[derive(Clone)]
|
||||
pub enum PageState {
|
||||
None,
|
||||
Map(HashMap<String, PageStateValue>),
|
||||
}
|
||||
|
||||
impl PageState {
|
||||
pub fn unwrap_map(self) -> Option<HashMap<String, PageStateValue>> {
|
||||
match self {
|
||||
PageState::Map(map) => Some(map),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,9 +80,9 @@ impl Window {
|
|||
|
||||
while !tui.quit() {
|
||||
if update {
|
||||
self.terminal.raw_mut().draw(|frame| {
|
||||
tui.view(&mut app, frame);
|
||||
})?;
|
||||
self.terminal
|
||||
.raw_mut()
|
||||
.draw(|frame| tui.view(&mut app, frame))?;
|
||||
}
|
||||
update = tui.update(&mut app)?;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue