tui: Implement patch view page
Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
parent
f0ef4efe6c
commit
0f52af8b2e
|
|
@ -7,41 +7,66 @@ use tuirealm::application::PollStrategy;
|
||||||
use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection};
|
use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection};
|
||||||
use tuirealm::event::{Event, Key, KeyEvent};
|
use tuirealm::event::{Event, Key, KeyEvent};
|
||||||
use tuirealm::props::{AttrValue, Attribute};
|
use tuirealm::props::{AttrValue, Attribute};
|
||||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
use tuirealm::{Application, Frame, MockComponent, NoUserEvent, State, StateValue};
|
||||||
use tuirealm::{Application, Component, Frame, MockComponent, NoUserEvent, State, StateValue};
|
|
||||||
|
|
||||||
use radicle_tui::cob::patch::{self};
|
use radicle_tui::cob::patch::{self};
|
||||||
|
|
||||||
|
use radicle_tui::subs;
|
||||||
use radicle_tui::ui;
|
use radicle_tui::ui;
|
||||||
use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer, Tabs};
|
use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer, Tabs};
|
||||||
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, PatchActivity, PatchFiles};
|
||||||
|
use radicle_tui::ui::layout;
|
||||||
use radicle_tui::ui::theme::{self, 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::Tui;
|
use radicle_tui::Tui;
|
||||||
|
|
||||||
use radicle::cob::patch::{Patch, PatchId};
|
use radicle::cob::patch::{Patch, PatchId};
|
||||||
use radicle::identity::{Id, Project};
|
use radicle::identity::{Id, Project};
|
||||||
use radicle::profile::Profile;
|
use radicle::profile::Profile;
|
||||||
|
|
||||||
/// All components known to this application.
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
pub enum ComponentId {
|
pub enum HomeCid {
|
||||||
Navigation,
|
Navigation,
|
||||||
Dashboard,
|
Dashboard,
|
||||||
IssueBrowser,
|
IssueBrowser,
|
||||||
PatchBrowser,
|
PatchBrowser,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
|
pub enum PatchCid {
|
||||||
|
Navigation,
|
||||||
|
Activity,
|
||||||
|
Files,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All component ids known to this application.
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
|
pub enum Cid {
|
||||||
|
Home(HomeCid),
|
||||||
|
Patch(PatchCid),
|
||||||
Shortcuts,
|
Shortcuts,
|
||||||
GlobalListener,
|
GlobalListener,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Messages handled by this application.
|
/// Messages handled by this application.
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum HomeMessage {
|
||||||
|
Show,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum PatchMessage {
|
||||||
|
Show(usize),
|
||||||
|
Leave,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
|
Home(HomeMessage),
|
||||||
|
Patch(PatchMessage),
|
||||||
NavigationChanged(u16),
|
NavigationChanged(u16),
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +75,7 @@ pub struct Context {
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
id: Id,
|
id: Id,
|
||||||
project: Project,
|
project: Project,
|
||||||
|
selected_patch: usize,
|
||||||
patches: Vec<(PatchId, Patch)>,
|
patches: Vec<(PatchId, Patch)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,6 +97,7 @@ impl App {
|
||||||
id,
|
id,
|
||||||
profile,
|
profile,
|
||||||
project,
|
project,
|
||||||
|
selected_patch: 0,
|
||||||
patches,
|
patches,
|
||||||
},
|
},
|
||||||
theme: theme::default_dark(),
|
theme: theme::default_dark(),
|
||||||
|
|
@ -79,9 +106,9 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mount_home(
|
fn mount_home_view(
|
||||||
&mut self,
|
&mut self,
|
||||||
app: &mut Application<ComponentId, Message, NoUserEvent>,
|
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.active_page = Box::<Home>::default();
|
self.active_page = Box::<Home>::default();
|
||||||
|
|
@ -90,35 +117,54 @@ impl App {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mount_patch_view(
|
||||||
|
&mut self,
|
||||||
|
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||||
|
theme: &Theme,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.active_page = Box::<PatchView>::default();
|
||||||
|
self.active_page.mount(app, &self.context, theme)?;
|
||||||
|
self.active_page.activate(app)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tui<ComponentId, Message> for App {
|
impl Tui<Cid, Message> for App {
|
||||||
fn init(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> Result<()> {
|
fn init(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||||
self.mount_home(app, &self.theme.clone())?;
|
self.mount_home_view(app, &self.theme.clone())?;
|
||||||
|
|
||||||
// Add global key listener and subscribe to key events
|
// Add global key listener and subscribe to key events
|
||||||
let global = ui::global_listener().to_boxed();
|
let global = ui::global_listener().to_boxed();
|
||||||
app.mount(ComponentId::GlobalListener, global, subs::global())?;
|
app.mount(Cid::GlobalListener, global, subs::global())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(
|
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame) {
|
||||||
&mut self,
|
|
||||||
app: &mut Application<ComponentId, Message, NoUserEvent>,
|
|
||||||
frame: &mut Frame,
|
|
||||||
) {
|
|
||||||
self.active_page.as_mut().view(app, frame);
|
self.active_page.as_mut().view(app, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
app: &mut Application<ComponentId, Message, NoUserEvent>,
|
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||||
interval: u64,
|
interval: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Ok(messages) = app.tick(PollStrategy::TryFor(Duration::from_millis(interval))) {
|
if let Ok(messages) = app.tick(PollStrategy::TryFor(Duration::from_millis(interval))) {
|
||||||
|
let theme = theme::default_dark();
|
||||||
for message in messages {
|
for message in messages {
|
||||||
match message {
|
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::Leave) => {
|
||||||
|
self.mount_home_view(app, &theme)?;
|
||||||
|
}
|
||||||
Message::Quit => self.quit = true,
|
Message::Quit => self.quit = true,
|
||||||
_ => {
|
_ => {
|
||||||
self.active_page.update(message);
|
self.active_page.update(message);
|
||||||
|
|
@ -137,35 +183,38 @@ impl Tui<ComponentId, Message> for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `tuirealm`'s event and prop system is designed to work with flat component hierarchies.
|
/// `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
|
/// 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
|
/// properly pass events and props down these hierarchies. This makes it hard to implement
|
||||||
/// full app views (home, patch details etc) as components.
|
/// full app views (home, patch details etc) as components.
|
||||||
///
|
///
|
||||||
/// View pages take into account these flat component hierarchies, and provide
|
/// View pages take into account these flat component hierarchies, and provide
|
||||||
/// switchable sets of components.
|
/// switchable sets of components.
|
||||||
pub trait ViewPage {
|
pub trait ViewPage {
|
||||||
fn mount(
|
fn mount(
|
||||||
&self,
|
&self,
|
||||||
app: &mut Application<ComponentId, Message, NoUserEvent>,
|
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
fn update(&mut self, message: Message);
|
fn update(&mut self, message: Message);
|
||||||
|
|
||||||
fn view(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>, frame: &mut Frame);
|
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame);
|
||||||
|
|
||||||
fn activate(&self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> Result<()>;
|
fn activate(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Home
|
||||||
|
///
|
||||||
pub struct Home {
|
pub struct Home {
|
||||||
active_component: ComponentId,
|
active_component: Cid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Home {
|
impl Default for Home {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Home {
|
Home {
|
||||||
active_component: ComponentId::Dashboard,
|
active_component: Cid::Home(HomeCid::Dashboard),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,11 +222,11 @@ impl Default for Home {
|
||||||
impl ViewPage for Home {
|
impl ViewPage for Home {
|
||||||
fn mount(
|
fn mount(
|
||||||
&self,
|
&self,
|
||||||
app: &mut Application<ComponentId, Message, NoUserEvent>,
|
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let navigation = ui::navigation(theme).to_boxed();
|
let navigation = ui::home_navigation(theme).to_boxed();
|
||||||
|
|
||||||
let dashboard = ui::dashboard(theme, &context.id, &context.project).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();
|
||||||
|
|
@ -192,66 +241,144 @@ impl ViewPage for Home {
|
||||||
)
|
)
|
||||||
.to_boxed();
|
.to_boxed();
|
||||||
|
|
||||||
app.remount(ComponentId::Navigation, navigation, subs::navigation())?;
|
app.remount(
|
||||||
|
Cid::Home(HomeCid::Navigation),
|
||||||
|
navigation,
|
||||||
|
subs::navigation(),
|
||||||
|
)?;
|
||||||
|
|
||||||
app.remount(ComponentId::Dashboard, dashboard, vec![])?;
|
app.remount(Cid::Home(HomeCid::Dashboard), dashboard, vec![])?;
|
||||||
app.remount(ComponentId::IssueBrowser, issue_browser, vec![])?;
|
app.remount(
|
||||||
app.remount(ComponentId::PatchBrowser, patch_browser, vec![])?;
|
Cid::Home(HomeCid::IssueBrowser),
|
||||||
|
issue_browser,
|
||||||
|
vec![],
|
||||||
|
)?;
|
||||||
|
app.remount(
|
||||||
|
Cid::Home(HomeCid::PatchBrowser),
|
||||||
|
patch_browser,
|
||||||
|
vec![],
|
||||||
|
)?;
|
||||||
|
|
||||||
app.remount(ComponentId::Shortcuts, shortcuts, vec![])?;
|
app.remount(Cid::Shortcuts, shortcuts, vec![])?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, message: Message) {
|
fn update(&mut self, message: Message) {
|
||||||
if let Message::NavigationChanged(index) = message {
|
if let Message::NavigationChanged(index) = message {
|
||||||
self.active_component = match index {
|
self.active_component = match index {
|
||||||
0 => ComponentId::Dashboard,
|
0 => Cid::Home(HomeCid::Dashboard),
|
||||||
1 => ComponentId::IssueBrowser,
|
1 => Cid::Home(HomeCid::IssueBrowser),
|
||||||
2 => ComponentId::PatchBrowser,
|
2 => Cid::Home(HomeCid::PatchBrowser),
|
||||||
_ => ComponentId::Dashboard,
|
_ => Cid::Home(HomeCid::Dashboard),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(
|
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame) {
|
||||||
&mut self,
|
|
||||||
app: &mut Application<ComponentId, Message, NoUserEvent>,
|
|
||||||
frame: &mut Frame,
|
|
||||||
) {
|
|
||||||
let area = frame.size();
|
let area = frame.size();
|
||||||
let margin_h = 1u16;
|
|
||||||
let navigation_h = 2u16;
|
let navigation_h = 2u16;
|
||||||
let shortcuts_h = app
|
let shortcuts_h = app
|
||||||
.query(&ComponentId::Shortcuts, Attribute::Height)
|
.query(&Cid::Shortcuts, Attribute::Height)
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.unwrap_or(AttrValue::Size(0))
|
.unwrap_or(AttrValue::Size(0))
|
||||||
.unwrap_size();
|
.unwrap_size();
|
||||||
let workspaces_h = area.height.saturating_sub(
|
let layout = layout::default_page(area, navigation_h, shortcuts_h);
|
||||||
shortcuts_h
|
|
||||||
.saturating_add(navigation_h)
|
app.view(
|
||||||
.saturating_add(margin_h),
|
&Cid::Home(HomeCid::Navigation),
|
||||||
|
frame,
|
||||||
|
layout[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.horizontal_margin(margin_h)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Length(navigation_h),
|
|
||||||
Constraint::Length(workspaces_h),
|
|
||||||
Constraint::Length(shortcuts_h),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
app.view(&ComponentId::Navigation, frame, layout[0]);
|
|
||||||
app.view(&self.active_component, frame, layout[1]);
|
app.view(&self.active_component, frame, layout[1]);
|
||||||
app.view(&ComponentId::Shortcuts, frame, layout[2]);
|
app.view(&Cid::Shortcuts, frame, layout[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn activate(&self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> Result<()> {
|
fn activate(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>,
|
||||||
|
context: &Context,
|
||||||
|
theme: &Theme,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some((_, _)) = context.patches.get(context.selected_patch) {
|
||||||
|
let navigation = ui::patch_navigation(theme).to_boxed();
|
||||||
|
let activity = ui::patch_activity(theme).to_boxed();
|
||||||
|
let files = ui::patch_files(theme).to_boxed();
|
||||||
|
let shortcuts = ui::shortcuts(
|
||||||
|
theme,
|
||||||
|
vec![
|
||||||
|
ui::shortcut(theme, "esc", "back"),
|
||||||
|
ui::shortcut(theme, "tab", "section"),
|
||||||
|
ui::shortcut(theme, "q", "quit"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.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![])?;
|
||||||
|
app.remount(Cid::Shortcuts, shortcuts, 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<Cid, Message, NoUserEvent>, frame: &mut Frame) {
|
||||||
|
let area = frame.size();
|
||||||
|
let navigation_h = 2u16;
|
||||||
|
let shortcuts_h = app
|
||||||
|
.query(&Cid::Shortcuts, Attribute::Height)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or(AttrValue::Size(0))
|
||||||
|
.unwrap_size();
|
||||||
|
let layout = layout::default_page(area, navigation_h, shortcuts_h);
|
||||||
|
|
||||||
|
app.view(
|
||||||
|
&Cid::Patch(PatchCid::Navigation),
|
||||||
|
frame,
|
||||||
|
layout[0],
|
||||||
|
);
|
||||||
|
app.view(&self.active_component, frame, layout[1]);
|
||||||
|
app.view(&Cid::Shortcuts, frame, layout[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||||
app.active(&self.active_component)?;
|
app.active(&self.active_component)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +387,7 @@ impl ViewPage for Home {
|
||||||
/// 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
|
||||||
/// passed around in the app, the following handlers need to be implemented for
|
/// passed around in the app, the following handlers need to be implemented for
|
||||||
/// each component used.
|
/// each component used.
|
||||||
impl Component<Message, NoUserEvent> for Widget<GlobalListener> {
|
impl tuirealm::Component<Message, NoUserEvent> for Widget<GlobalListener> {
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
match event {
|
match event {
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
|
|
@ -272,7 +399,7 @@ impl Component<Message, NoUserEvent> for Widget<GlobalListener> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Message, NoUserEvent> for Widget<Tabs> {
|
impl tuirealm::Component<Message, NoUserEvent> for Widget<Tabs> {
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
match event {
|
match event {
|
||||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||||
|
|
@ -288,7 +415,7 @@ impl Component<Message, NoUserEvent> for Widget<Tabs> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Message, NoUserEvent> for Widget<Browser<(PatchId, Patch)>> {
|
impl tuirealm::Component<Message, NoUserEvent> for Widget<Browser<(PatchId, Patch)>> {
|
||||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
match event {
|
match event {
|
||||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||||
|
|
@ -301,30 +428,60 @@ impl Component<Message, NoUserEvent> for Widget<Browser<(PatchId, Patch)>> {
|
||||||
self.perform(Cmd::Move(MoveDirection::Down));
|
self.perform(Cmd::Move(MoveDirection::Down));
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Message, NoUserEvent> for Widget<LabeledContainer> {
|
impl tuirealm::Component<Message, NoUserEvent> for Widget<PatchActivity> {
|
||||||
|
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
|
match event {
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Message::Patch(PatchMessage::Leave))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tuirealm::Component<Message, NoUserEvent> for Widget<PatchFiles> {
|
||||||
|
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
|
match event {
|
||||||
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
|
Some(Message::Patch(PatchMessage::Leave))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tuirealm::Component<Message, NoUserEvent> for Widget<LabeledContainer> {
|
||||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Message, NoUserEvent> for Widget<PropertyList> {
|
impl tuirealm::Component<Message, NoUserEvent> for Widget<PropertyList> {
|
||||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Message, NoUserEvent> for Widget<Shortcuts> {
|
impl tuirealm::Component<Message, NoUserEvent> for Widget<Shortcuts> {
|
||||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component<Message, NoUserEvent> for Phantom {
|
impl tuirealm::Component<Message, NoUserEvent> for Phantom {
|
||||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use components::list::{Property, PropertyList};
|
||||||
use widget::Widget;
|
use widget::Widget;
|
||||||
|
|
||||||
use self::components::list::{List, Table};
|
use self::components::list::{List, Table};
|
||||||
use self::components::workspace::Browser;
|
use self::components::workspace::{Browser, PatchActivity, PatchFiles};
|
||||||
|
|
||||||
pub fn global_listener() -> Widget<GlobalListener> {
|
pub fn global_listener() -> Widget<GlobalListener> {
|
||||||
Widget::new(GlobalListener::default())
|
Widget::new(GlobalListener::default())
|
||||||
|
|
@ -148,13 +148,31 @@ pub fn patch_browser(
|
||||||
Widget::new(browser)
|
Widget::new(browser)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn navigation(theme: &theme::Theme) -> Widget<Tabs> {
|
pub fn patch_activity(theme: &theme::Theme) -> Widget<PatchActivity> {
|
||||||
|
let not_implemented = label("not implemented").foreground(theme.colors.default_fg);
|
||||||
|
let activity = PatchActivity::new(not_implemented);
|
||||||
|
|
||||||
|
Widget::new(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn patch_files(theme: &theme::Theme) -> Widget<PatchFiles> {
|
||||||
|
let not_implemented = label("not implemented").foreground(theme.colors.default_fg);
|
||||||
|
let files = PatchFiles::new(not_implemented);
|
||||||
|
|
||||||
|
Widget::new(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn home_navigation(theme: &theme::Theme) -> Widget<Tabs> {
|
||||||
tabs(
|
tabs(
|
||||||
theme,
|
theme,
|
||||||
vec![label("dashboard"), label("issues"), label("patches")],
|
vec![label("dashboard"), label("issues"), label("patches")],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn patch_navigation(theme: &theme::Theme) -> Widget<Tabs> {
|
||||||
|
tabs(theme, vec![label("activity"), label("files")])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dashboard(theme: &theme::Theme, id: &Id, project: &Project) -> Widget<LabeledContainer> {
|
pub fn dashboard(theme: &theme::Theme, id: &Id, project: &Project) -> Widget<LabeledContainer> {
|
||||||
labeled_container(
|
labeled_container(
|
||||||
theme,
|
theme,
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,13 @@ pub struct Tabs {
|
||||||
|
|
||||||
impl Tabs {
|
impl Tabs {
|
||||||
pub fn new(tabs: Vec<Widget<Label>>, divider: Widget<Label>) -> Self {
|
pub fn new(tabs: Vec<Widget<Label>>, divider: Widget<Label>) -> Self {
|
||||||
|
let count = &tabs.len();
|
||||||
Self {
|
Self {
|
||||||
tabs,
|
tabs,
|
||||||
divider,
|
divider,
|
||||||
state: TabState {
|
state: TabState {
|
||||||
selected: 0,
|
selected: 0,
|
||||||
len: 3,
|
len: *count as u16,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@ use std::marker::PhantomData;
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
use tuirealm::command::{Cmd, CmdResult};
|
||||||
use tuirealm::props::Props;
|
use tuirealm::props::Props;
|
||||||
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
|
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
|
||||||
use tuirealm::{Frame, MockComponent, State};
|
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, State};
|
||||||
|
|
||||||
|
use crate::ui::layout;
|
||||||
use crate::ui::widget::{Widget, WidgetComponent};
|
use crate::ui::widget::{Widget, WidgetComponent};
|
||||||
|
|
||||||
|
use super::label::Label;
|
||||||
use super::list::{List, Table};
|
use super::list::{List, Table};
|
||||||
|
|
||||||
pub struct Browser<T> {
|
pub struct Browser<T> {
|
||||||
|
|
@ -41,3 +43,65 @@ impl<T: List> WidgetComponent for Browser<T> {
|
||||||
self.list.perform(cmd)
|
self.list.perform(cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PatchActivity {
|
||||||
|
label: Widget<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PatchActivity {
|
||||||
|
pub fn new(label: Widget<Label>) -> Self {
|
||||||
|
Self { label }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetComponent for PatchActivity {
|
||||||
|
fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
|
||||||
|
let label_w = self
|
||||||
|
.label
|
||||||
|
.query(Attribute::Width)
|
||||||
|
.unwrap_or(AttrValue::Size(1))
|
||||||
|
.unwrap_size();
|
||||||
|
let rect = layout::centered_label(label_w, area);
|
||||||
|
|
||||||
|
self.label.view(frame, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PatchFiles {
|
||||||
|
label: Widget<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PatchFiles {
|
||||||
|
pub fn new(label: Widget<Label>) -> Self {
|
||||||
|
Self { label }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetComponent for PatchFiles {
|
||||||
|
fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
|
||||||
|
let label_w = self
|
||||||
|
.label
|
||||||
|
.query(Attribute::Width)
|
||||||
|
.unwrap_or(AttrValue::Size(1))
|
||||||
|
.unwrap_size();
|
||||||
|
let rect = layout::centered_label(label_w, area);
|
||||||
|
|
||||||
|
self.label.view(frame, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult {
|
||||||
|
CmdResult::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,3 +45,53 @@ pub fn h_stack(
|
||||||
|
|
||||||
widgets.into_iter().zip(layout.into_iter()).collect()
|
widgets.into_iter().zip(layout.into_iter()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_page(area: Rect, nav_h: u16, shortcuts_h: u16) -> Vec<Rect> {
|
||||||
|
let margin_h = 1u16;
|
||||||
|
let content_h = area
|
||||||
|
.height
|
||||||
|
.saturating_sub(shortcuts_h.saturating_add(nav_h).saturating_add(margin_h));
|
||||||
|
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.horizontal_margin(margin_h)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(nav_h),
|
||||||
|
Constraint::Length(content_h),
|
||||||
|
Constraint::Length(shortcuts_h),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(area)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn centered_label(label_w: u16, area: Rect) -> Rect {
|
||||||
|
let label_h = 1u16;
|
||||||
|
let spacer_w = area.width.saturating_sub(label_w).saturating_div(2);
|
||||||
|
let spacer_h = area.height.saturating_sub(label_h).saturating_div(2);
|
||||||
|
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(spacer_h),
|
||||||
|
Constraint::Length(label_h),
|
||||||
|
Constraint::Length(spacer_h),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(spacer_w),
|
||||||
|
Constraint::Length(label_w),
|
||||||
|
Constraint::Length(spacer_w),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(layout[1])[1]
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue