tui: Implement patches workspace
Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
parent
80c1e45a99
commit
8325eb69d7
|
|
@ -9,27 +9,25 @@ use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers};
|
|||
use tuirealm::props::{AttrValue, Attribute};
|
||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::{
|
||||
Application, Component, Frame, MockComponent, NoUserEvent, Sub, SubClause, SubEventClause,
|
||||
Application, Component, Frame, MockComponent, NoUserEvent, StateValue, Sub, SubClause,
|
||||
SubEventClause,
|
||||
};
|
||||
|
||||
use radicle_tui::cob::patch::{self};
|
||||
|
||||
use radicle_tui::ui;
|
||||
use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer};
|
||||
use radicle_tui::ui::components::container::{GlobalListener, LabeledContainer, Tabs};
|
||||
use radicle_tui::ui::components::context::Shortcuts;
|
||||
use radicle_tui::ui::components::list::PropertyList;
|
||||
use radicle_tui::ui::components::workspace::Workspaces;
|
||||
use radicle_tui::ui::components::workspace::Browser;
|
||||
use radicle_tui::ui::theme;
|
||||
use radicle_tui::ui::widget::Widget;
|
||||
|
||||
use radicle_tui::Tui;
|
||||
|
||||
use radicle::cob::patch::{Patch, PatchId};
|
||||
use radicle::identity::{Id, Project};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct App {
|
||||
id: Id,
|
||||
project: Project,
|
||||
quit: bool,
|
||||
}
|
||||
use radicle::profile::Profile;
|
||||
|
||||
/// Messages handled by this application.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
|
|
@ -40,18 +38,33 @@ pub enum Message {
|
|||
/// All components known to this application.
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum ComponentId {
|
||||
Workspaces,
|
||||
Navigation,
|
||||
Dashboard,
|
||||
IssueBrowser,
|
||||
PatchBrowser,
|
||||
Shortcuts,
|
||||
GlobalListener,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct App {
|
||||
profile: Profile,
|
||||
id: Id,
|
||||
project: Project,
|
||||
patches: Vec<(PatchId, Patch)>,
|
||||
quit: bool,
|
||||
}
|
||||
|
||||
/// Creates a new application using a tui-realm-application, mounts all
|
||||
/// components and sets focus to a default one.
|
||||
impl App {
|
||||
pub fn new(id: Id, project: Project) -> Self {
|
||||
pub fn new(profile: Profile, id: Id, project: Project) -> Self {
|
||||
let patches = patch::load_all(&profile, id);
|
||||
Self {
|
||||
id,
|
||||
profile,
|
||||
project,
|
||||
patches,
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
|
|
@ -61,56 +74,47 @@ impl Tui<ComponentId, Message> for App {
|
|||
fn init(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> Result<()> {
|
||||
let theme = theme::default_dark();
|
||||
|
||||
let dashboard = ui::labeled_container(
|
||||
let navigation = ui::navigation(&theme).to_boxed();
|
||||
|
||||
let dashboard = ui::dashboard(&theme, &self.id, &self.project).to_boxed();
|
||||
let issue_browser = Box::<Phantom>::default();
|
||||
let patch_browser = ui::patch_browser(&theme, &self.patches, &self.profile).to_boxed();
|
||||
|
||||
let shortcuts = ui::shortcuts(
|
||||
&theme,
|
||||
"about",
|
||||
ui::property_list(
|
||||
&theme,
|
||||
vec![
|
||||
ui::property(&theme, "id", &self.id.to_string()),
|
||||
ui::property(&theme, "name", self.project.name()),
|
||||
ui::property(&theme, "description", self.project.description()),
|
||||
],
|
||||
)
|
||||
.to_boxed(),
|
||||
vec![
|
||||
ui::shortcut(&theme, "tab", "section"),
|
||||
ui::shortcut(&theme, "q", "quit"),
|
||||
],
|
||||
)
|
||||
.to_boxed();
|
||||
|
||||
app.mount(ComponentId::Navigation, navigation, vec![])?;
|
||||
|
||||
app.mount(ComponentId::Dashboard, dashboard, vec![])?;
|
||||
app.mount(ComponentId::IssueBrowser, issue_browser, vec![])?;
|
||||
app.mount(
|
||||
ComponentId::Workspaces,
|
||||
ui::workspaces(
|
||||
&theme,
|
||||
self.project.name(),
|
||||
ui::tabs(
|
||||
&theme,
|
||||
vec![
|
||||
ui::label("dashboard"),
|
||||
ui::label("issues"),
|
||||
ui::label("patches"),
|
||||
],
|
||||
ComponentId::PatchBrowser,
|
||||
patch_browser,
|
||||
vec![
|
||||
Sub::new(
|
||||
SubEventClause::Keyboard(KeyEvent {
|
||||
code: Key::Up,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}),
|
||||
SubClause::Always,
|
||||
),
|
||||
vec![
|
||||
dashboard,
|
||||
Box::<Phantom>::default(),
|
||||
Box::<Phantom>::default(),
|
||||
],
|
||||
)
|
||||
.to_boxed(),
|
||||
vec![],
|
||||
Sub::new(
|
||||
SubEventClause::Keyboard(KeyEvent {
|
||||
code: Key::Down,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}),
|
||||
SubClause::Always,
|
||||
),
|
||||
],
|
||||
)?;
|
||||
|
||||
app.mount(
|
||||
ComponentId::Shortcuts,
|
||||
ui::shortcuts(
|
||||
&theme,
|
||||
vec![
|
||||
ui::shortcut(&theme, "tab", "section"),
|
||||
ui::shortcut(&theme, "q", "quit"),
|
||||
],
|
||||
)
|
||||
.to_boxed(),
|
||||
vec![],
|
||||
)?;
|
||||
app.mount(ComponentId::Shortcuts, shortcuts, vec![])?;
|
||||
|
||||
// Add global key listener and subscribe to key events
|
||||
app.mount(
|
||||
|
|
@ -126,7 +130,7 @@ impl Tui<ComponentId, Message> for App {
|
|||
)?;
|
||||
|
||||
// We need to give focus to a component then
|
||||
app.active(&ComponentId::Workspaces)?;
|
||||
app.active(&ComponentId::Navigation)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -138,21 +142,25 @@ impl Tui<ComponentId, Message> for App {
|
|||
) {
|
||||
let area = frame.size();
|
||||
let margin_h = 1u16;
|
||||
let navigation_h = 2u16;
|
||||
let shortcuts_h = app
|
||||
.query(&ComponentId::Shortcuts, Attribute::Height)
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(AttrValue::Size(0))
|
||||
.unwrap_size();
|
||||
let workspaces_h = area
|
||||
.height
|
||||
.saturating_sub(shortcuts_h.saturating_add(margin_h));
|
||||
let workspaces_h = area.height.saturating_sub(
|
||||
shortcuts_h
|
||||
.saturating_add(navigation_h)
|
||||
.saturating_add(margin_h),
|
||||
);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.horizontal_margin(margin_h)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(navigation_h),
|
||||
Constraint::Length(workspaces_h),
|
||||
Constraint::Length(shortcuts_h),
|
||||
]
|
||||
|
|
@ -160,8 +168,21 @@ impl Tui<ComponentId, Message> for App {
|
|||
)
|
||||
.split(area);
|
||||
|
||||
app.view(&ComponentId::Workspaces, frame, layout[0]);
|
||||
app.view(&ComponentId::Shortcuts, frame, layout[1]);
|
||||
let workspace = app
|
||||
.state(&ComponentId::Navigation)
|
||||
.unwrap_or(tuirealm::State::One(StateValue::U16(0)))
|
||||
.unwrap_one();
|
||||
|
||||
let component = match workspace {
|
||||
StateValue::U16(0) => ComponentId::Dashboard,
|
||||
StateValue::U16(1) => ComponentId::IssueBrowser,
|
||||
StateValue::U16(2) => ComponentId::PatchBrowser,
|
||||
_ => ComponentId::Dashboard,
|
||||
};
|
||||
|
||||
app.view(&ComponentId::Navigation, frame, layout[0]);
|
||||
app.view(&component, frame, layout[1]);
|
||||
app.view(&ComponentId::Shortcuts, frame, layout[2]);
|
||||
}
|
||||
|
||||
fn update(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>, interval: u64) {
|
||||
|
|
@ -194,7 +215,7 @@ impl Component<Message, NoUserEvent> for Widget<GlobalListener> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Component<Message, NoUserEvent> for Widget<Workspaces> {
|
||||
impl Component<Message, NoUserEvent> for Widget<Tabs> {
|
||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||
match event {
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
|
|
@ -206,6 +227,24 @@ impl Component<Message, NoUserEvent> for Widget<Workspaces> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Component<Message, NoUserEvent> for Widget<Browser<(PatchId, Patch)>> {
|
||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||
match event {
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(MoveDirection::Up));
|
||||
None
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(MoveDirection::Down));
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Message, NoUserEvent> for Widget<LabeledContainer> {
|
||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
||||
None
|
||||
|
|
@ -223,3 +262,9 @@ impl Component<Message, NoUserEvent> for Widget<Shortcuts> {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Message, NoUserEvent> for Phantom {
|
||||
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use tuirealm::terminal::TerminalBridge;
|
|||
use tuirealm::Frame;
|
||||
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
||||
|
||||
pub mod cob;
|
||||
pub mod ui;
|
||||
|
||||
/// Trait that must be implemented by client applications in order to be run
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ fn execute() -> anyhow::Result<()> {
|
|||
let project = payload.project()?;
|
||||
|
||||
let mut window = Window::default();
|
||||
window.run(&mut app::App::new(id, project), 1000 / FPS)?;
|
||||
window.run(&mut app::App::new(profile, id, project), 1000 / FPS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,26 @@
|
|||
pub mod cob;
|
||||
pub mod components;
|
||||
pub mod layout;
|
||||
pub mod state;
|
||||
pub mod theme;
|
||||
pub mod widget;
|
||||
|
||||
use radicle::prelude::{Id, Project};
|
||||
use radicle::Profile;
|
||||
use tuirealm::props::{AttrValue, Attribute, PropPayload, PropValue, TextSpan};
|
||||
use tuirealm::MockComponent;
|
||||
|
||||
use radicle::cob::patch::{Patch, PatchId};
|
||||
|
||||
use components::container::{GlobalListener, LabeledContainer, Tabs};
|
||||
use components::context::{Shortcut, Shortcuts};
|
||||
use components::label::Label;
|
||||
use components::list::{Property, PropertyList};
|
||||
use components::workspace::Workspaces;
|
||||
|
||||
use widget::Widget;
|
||||
|
||||
use tuirealm::props::{AttrValue, Attribute};
|
||||
use tuirealm::MockComponent;
|
||||
use self::components::list::{List, Table};
|
||||
use self::components::workspace::Browser;
|
||||
|
||||
pub fn global_listener() -> Widget<GlobalListener> {
|
||||
Widget::new(GlobalListener::default())
|
||||
|
|
@ -100,14 +108,65 @@ pub fn tabs(theme: &theme::Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {
|
|||
.highlight(theme.colors.tabs_highlighted_fg)
|
||||
}
|
||||
|
||||
pub fn workspaces(
|
||||
theme: &theme::Theme,
|
||||
info: &str,
|
||||
tabs: Widget<Tabs>,
|
||||
children: Vec<Box<dyn MockComponent>>,
|
||||
) -> Widget<Workspaces> {
|
||||
let info = label(info).foreground(theme.colors.workspaces_info_fg);
|
||||
let workspaces = Workspaces::new(tabs, info, children);
|
||||
pub fn table(theme: &theme::Theme, items: &[impl List], profile: &Profile) -> Widget<Table> {
|
||||
let table = Table::default();
|
||||
let items = items.iter().map(|item| item.row(theme, profile)).collect();
|
||||
|
||||
Widget::new(workspaces)
|
||||
Widget::new(table)
|
||||
.content(AttrValue::Table(items))
|
||||
.background(theme.colors.labeled_container_bg)
|
||||
.highlight(theme.colors.item_list_highlighted_bg)
|
||||
}
|
||||
|
||||
pub fn patch_browser(
|
||||
theme: &theme::Theme,
|
||||
items: &[(PatchId, Patch)],
|
||||
profile: &Profile,
|
||||
) -> Widget<Browser<(PatchId, Patch)>> {
|
||||
let widths = AttrValue::Payload(PropPayload::Vec(vec![
|
||||
PropValue::U16(2),
|
||||
PropValue::U16(43),
|
||||
PropValue::U16(15),
|
||||
PropValue::U16(15),
|
||||
PropValue::U16(5),
|
||||
PropValue::U16(20),
|
||||
]));
|
||||
let header = AttrValue::Payload(PropPayload::Vec(vec![
|
||||
PropValue::TextSpan(TextSpan::from("")),
|
||||
PropValue::TextSpan(TextSpan::from("title")),
|
||||
PropValue::TextSpan(TextSpan::from("author")),
|
||||
PropValue::TextSpan(TextSpan::from("tags")),
|
||||
PropValue::TextSpan(TextSpan::from("comments")),
|
||||
PropValue::TextSpan(TextSpan::from("date")),
|
||||
]));
|
||||
|
||||
let table = table(theme, items, profile)
|
||||
.custom("widths", widths)
|
||||
.custom("header", header);
|
||||
let browser: Browser<(PatchId, Patch)> = Browser::new(table);
|
||||
|
||||
Widget::new(browser)
|
||||
}
|
||||
|
||||
pub fn navigation(theme: &theme::Theme) -> Widget<Tabs> {
|
||||
tabs(
|
||||
theme,
|
||||
vec![label("dashboard"), label("issues"), label("patches")],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn dashboard(theme: &theme::Theme, id: &Id, project: &Project) -> Widget<LabeledContainer> {
|
||||
labeled_container(
|
||||
theme,
|
||||
"about",
|
||||
property_list(
|
||||
theme,
|
||||
vec![
|
||||
property(theme, "id", &id.to_string()),
|
||||
property(theme, "name", project.name()),
|
||||
property(theme, "description", project.description()),
|
||||
],
|
||||
)
|
||||
.to_boxed(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,132 +1,43 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use tuirealm::command::{Cmd, CmdResult};
|
||||
use tuirealm::props::{AttrValue, Attribute, Props};
|
||||
use tuirealm::props::Props;
|
||||
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tuirealm::{Frame, MockComponent, State};
|
||||
|
||||
use crate::ui::components::label::Label;
|
||||
use crate::ui::widget::{Widget, WidgetComponent};
|
||||
|
||||
use super::container::Tabs;
|
||||
use super::list::{List, Table};
|
||||
|
||||
/// Workspace header that displays all labels horizontally aligned and separated
|
||||
/// by a divider. Highlights the label defined by the current tab index.
|
||||
#[derive(Clone)]
|
||||
struct Header {
|
||||
tabs: Widget<Tabs>,
|
||||
info: Widget<Label>,
|
||||
pub struct Browser<T> {
|
||||
list: Widget<Table>,
|
||||
phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub fn new(tabs: Widget<Tabs>, info: Widget<Label>) -> Self {
|
||||
Self { tabs, info }
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetComponent for Header {
|
||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
||||
let display = properties
|
||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
||||
.unwrap_flag();
|
||||
let info_width = self
|
||||
.info
|
||||
.query(Attribute::Width)
|
||||
.unwrap_or(AttrValue::Size(1))
|
||||
.unwrap_size();
|
||||
let tabs_width = area.width.saturating_sub(info_width);
|
||||
|
||||
if display {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(tabs_width),
|
||||
Constraint::Length(info_width),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
|
||||
self.tabs.view(frame, layout[0]);
|
||||
self.info.view(frame, layout[1]);
|
||||
}
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::None
|
||||
}
|
||||
|
||||
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||
self.tabs.perform(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
/// A container with a tab header. Displays the component selected by the index
|
||||
/// held in the header state.
|
||||
pub struct Workspaces {
|
||||
header: Widget<Header>,
|
||||
children: Vec<Box<dyn MockComponent>>,
|
||||
}
|
||||
|
||||
impl Workspaces {
|
||||
pub fn new(
|
||||
tabs: Widget<Tabs>,
|
||||
info: Widget<Label>,
|
||||
children: Vec<Box<dyn MockComponent>>,
|
||||
) -> Self {
|
||||
impl<T: List> Browser<T> {
|
||||
pub fn new(list: Widget<Table>) -> Self {
|
||||
Self {
|
||||
header: Widget::new(Header::new(tabs, info)),
|
||||
children,
|
||||
list,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetComponent for Workspaces {
|
||||
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
|
||||
let display = properties
|
||||
.get_or(Attribute::Display, AttrValue::Flag(true))
|
||||
.unwrap_flag();
|
||||
let header_height = self
|
||||
.header
|
||||
.query(Attribute::Height)
|
||||
.unwrap_or(AttrValue::Size(1))
|
||||
.unwrap_size();
|
||||
let selected = self.header.tabs.state().unwrap_one().unwrap_u16();
|
||||
impl<T: List> WidgetComponent for Browser<T> {
|
||||
fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Min(1)].as_ref())
|
||||
.split(area);
|
||||
|
||||
if display {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(header_height),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(0),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
|
||||
self.header.view(frame, layout[0]);
|
||||
|
||||
if let Some(child) = self.children.get_mut(selected as usize) {
|
||||
child.view(frame, layout[2]);
|
||||
}
|
||||
}
|
||||
self.list.view(frame, layout[0]);
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::None
|
||||
}
|
||||
|
||||
fn perform(&mut self, cmd: Cmd) -> CmdResult {
|
||||
CmdResult::Batch(
|
||||
[
|
||||
self.children
|
||||
.iter_mut()
|
||||
.map(|child| child.perform(cmd))
|
||||
.collect(),
|
||||
vec![self.header.perform(cmd)],
|
||||
]
|
||||
.concat(),
|
||||
)
|
||||
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
|
||||
self.list.perform(cmd)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue