From dd16ed780e7a91305ffbc944aee348672f432cc6 Mon Sep 17 00:00:00 2001 From: Erik Kundt Date: Tue, 23 May 2023 18:27:03 +0200 Subject: [PATCH] tui: Implement larger list on issue page --- radicle-tui/src/app.rs | 4 + radicle-tui/src/app/event.rs | 23 +++- radicle-tui/src/app/page.rs | 34 +++--- radicle-tui/src/ui/cob.rs | 44 +++++++- radicle-tui/src/ui/layout.rs | 44 ++++++++ radicle-tui/src/ui/state.rs | 71 ++++++++++++ radicle-tui/src/ui/theme.rs | 4 +- radicle-tui/src/ui/widget/common.rs | 2 +- radicle-tui/src/ui/widget/common/list.rs | 134 ++++++++++++++++------- radicle-tui/src/ui/widget/issue.rs | 66 ++++++++--- 10 files changed, 343 insertions(+), 83 deletions(-) diff --git a/radicle-tui/src/app.rs b/radicle-tui/src/app.rs index 8875a72f..d0a6879d 100644 --- a/radicle-tui/src/app.rs +++ b/radicle-tui/src/app.rs @@ -39,6 +39,7 @@ pub enum PatchCid { #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum IssueCid { List, + Shortcuts, } /// All component ids known to this application. @@ -174,6 +175,9 @@ impl Tui for App { Message::Issue(IssueMessage::Show(id)) => { self.view_issue(app, id, &theme)?; } + Message::Issue(IssueMessage::Leave) => { + self.pages.pop(app)?; + } Message::Patch(PatchMessage::Show(id)) => { self.view_patch(app, id, &theme)?; } diff --git a/radicle-tui/src/app/event.rs b/radicle-tui/src/app/event.rs index ca323870..4b23481b 100644 --- a/radicle-tui/src/app/event.rs +++ b/radicle-tui/src/app/event.rs @@ -6,7 +6,7 @@ use radicle_tui::ui::widget::common::container::{GlobalListener, LabeledContaine use radicle_tui::ui::widget::common::context::{ContextBar, Shortcuts}; use radicle_tui::ui::widget::common::list::PropertyList; use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser, PatchBrowser}; -use radicle_tui::ui::widget::patch; +use radicle_tui::ui::widget::{issue, patch}; use radicle_tui::ui::widget::Widget; @@ -45,6 +45,27 @@ impl tuirealm::Component for Widget { } } +impl tuirealm::Component for Widget { + fn on(&mut self, event: Event) -> Option { + match event { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Message::Issue(IssueMessage::Leave)) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(MoveDirection::Up)); + Some(Message::Tick) + } + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(MoveDirection::Down)); + Some(Message::Tick) + } + _ => None, + } + } +} + impl tuirealm::Component for Widget { fn on(&mut self, event: Event) -> Option { match event { diff --git a/radicle-tui/src/app/page.rs b/radicle-tui/src/app/page.rs index f5adbace..0ae6e719 100644 --- a/radicle-tui/src/app/page.rs +++ b/radicle-tui/src/app/page.rs @@ -163,9 +163,19 @@ impl ViewPage for IssuePage { theme: &Theme, ) -> Result<()> { let (id, issue) = &self.issue; - let list = widget::issue::list(theme, (*id, issue), context.profile()).to_boxed(); + let list = widget::issue::list(context, theme, (*id, issue)).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::List), list, vec![])?; + app.remount(Cid::Issue(IssueCid::Shortcuts), shortcuts, vec![])?; + app.active(&self.active_component)?; Ok(()) @@ -173,6 +183,7 @@ impl ViewPage for IssuePage { fn unmount(&self, app: &mut Application) -> Result<()> { app.umount(&Cid::Issue(IssueCid::List))?; + app.umount(&Cid::Issue(IssueCid::Shortcuts))?; Ok(()) } @@ -188,27 +199,18 @@ impl ViewPage for IssuePage { fn view(&mut self, app: &mut Application, frame: &mut Frame) { let area = frame.size(); - let layout = layout::default_page(area); + let shortcuts_h = 1u16; + let layout = layout::issue_preview(area, shortcuts_h); - app.view(&Cid::Patch(PatchCid::Navigation), frame, layout[0]); - app.view(&self.active_component, frame, layout[1]); + app.view(&Cid::Issue(IssueCid::List), frame, layout.left); + app.view(&Cid::Issue(IssueCid::Shortcuts), frame, layout.shortcuts); } - fn subscribe(&self, app: &mut Application) -> Result<()> { - app.subscribe( - &Cid::Home(HomeCid::Navigation), - Sub::new(subscription::navigation_clause(), SubClause::Always), - )?; - + fn subscribe(&self, _app: &mut Application) -> Result<()> { Ok(()) } - fn unsubscribe(&self, app: &mut Application) -> Result<()> { - app.unsubscribe( - &Cid::Home(HomeCid::Navigation), - subscription::navigation_clause(), - )?; - + fn unsubscribe(&self, _app: &mut Application) -> Result<()> { Ok(()) } } diff --git a/radicle-tui/src/ui/cob.rs b/radicle-tui/src/ui/cob.rs index a1275acb..f7222062 100644 --- a/radicle-tui/src/ui/cob.rs +++ b/radicle-tui/src/ui/cob.rs @@ -13,11 +13,14 @@ use radicle::cob::patch::{Patch, PatchId, State as PatchState}; use radicle::cob::{Tag, Timestamp}; use tuirealm::props::{Color, Style}; +use tuirealm::tui::text::{Span, Spans}; use tuirealm::tui::widgets::Cell; use crate::ui::theme::Theme; use crate::ui::widget::common::list::TableItem; +use super::widget::common::list::ListItem; + /// An author item that can be used in tables, list or trees. /// /// Breaks up dependencies to [`Profile`] and [`Repository`] that @@ -197,13 +200,11 @@ impl IssueItem { } } -impl TryFrom<(&Profile, &Repository, IssueId, Issue)> for IssueItem { - type Error = anyhow::Error; - - fn try_from(value: (&Profile, &Repository, IssueId, Issue)) -> Result { +impl From<(&Profile, &Repository, IssueId, Issue)> for IssueItem { + fn from(value: (&Profile, &Repository, IssueId, Issue)) -> Self { let (profile, _, id, issue) = value; - Ok(IssueItem { + IssueItem { id, state: *issue.state(), title: issue.title().into(), @@ -220,7 +221,7 @@ impl TryFrom<(&Profile, &Repository, IssueId, Issue)> for IssueItem { }) .collect::>(), timestamp: issue.timestamp(), - }) + } } } @@ -256,6 +257,37 @@ impl TableItem<7> for IssueItem { } } +impl ListItem for IssueItem { + fn row(&self, theme: &Theme) -> tuirealm::tui::widgets::ListItem { + let (state, state_color) = format_issue_state(&self.state); + let lines = vec![ + Spans::from(vec![ + Span::styled(state, Style::default().fg(state_color)), + Span::styled( + self.title.clone(), + Style::default().fg(theme.colors.browser_list_title), + ), + ]), + Spans::from(vec![ + Span::raw(String::from(" ")), + Span::styled( + format_author(&self.author.did, self.author.is_you), + Style::default().fg(theme.colors.browser_list_author), + ), + Span::styled( + format!(" {} ", theme.icons.property_divider), + Style::default().fg(theme.colors.property_divider_fg), + ), + Span::styled( + format::timestamp(&self.timestamp).to_string(), + Style::default().fg(theme.colors.browser_list_timestamp), + ), + ]), + ]; + tuirealm::tui::widgets::ListItem::new(lines) + } +} + pub fn format_patch_state(state: &PatchState) -> (String, Color) { match state { PatchState::Open { conflicts: _ } => (" ● ".into(), Color::Green), diff --git a/radicle-tui/src/ui/layout.rs b/radicle-tui/src/ui/layout.rs index 8c2a7400..71dc7232 100644 --- a/radicle-tui/src/ui/layout.rs +++ b/radicle-tui/src/ui/layout.rs @@ -2,6 +2,12 @@ use tuirealm::props::{AttrValue, Attribute}; use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect}; use tuirealm::MockComponent; +pub struct IssuePreview { + pub left: Rect, + pub right: Rect, + pub shortcuts: Rect, +} + pub fn v_stack( widgets: Vec>, area: Rect, @@ -58,6 +64,17 @@ pub fn default_page(area: Rect) -> Vec { .split(area) } +pub fn headerless_page(area: Rect) -> Vec { + let margin_h = 1u16; + let content_h = area.height.saturating_sub(margin_h); + + Layout::default() + .direction(Direction::Vertical) + .horizontal_margin(margin_h) + .constraints([Constraint::Length(content_h)].as_ref()) + .split(area) +} + pub fn root_component(area: Rect, shortcuts_h: u16) -> Vec { let content_h = area.height.saturating_sub(shortcuts_h); @@ -120,3 +137,30 @@ pub fn centered_label(label_w: u16, area: Rect) -> Rect { ) .split(layout[1])[1] } + +pub fn issue_preview(area: Rect, shortcuts_h: u16) -> IssuePreview { + let content_h = area.height.saturating_sub(shortcuts_h); + + let root = Layout::default() + .direction(Direction::Vertical) + .horizontal_margin(1) + .constraints( + [ + Constraint::Length(content_h), + Constraint::Length(shortcuts_h), + ] + .as_ref(), + ) + .split(area); + + let split = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(root[0]); + + IssuePreview { + left: split[0], + right: split[1], + shortcuts: root[1], + } +} diff --git a/radicle-tui/src/ui/state.rs b/radicle-tui/src/ui/state.rs index 3193b7a9..f7218913 100644 --- a/radicle-tui/src/ui/state.rs +++ b/radicle-tui/src/ui/state.rs @@ -1,3 +1,5 @@ +use tuirealm::tui::widgets::{ListState, TableState}; + /// State that holds the index of a selected tab item and the count of all tab items. /// The index can be increased and will start at 0, if length was reached. #[derive(Clone, Default)] @@ -15,3 +17,72 @@ impl TabState { } } } + +#[derive(Clone)] +pub struct ItemState { + selected: Option, + len: usize, +} + +impl ItemState { + pub fn new(len: usize) -> Self { + Self { + selected: Some(0), + len, + } + } + + pub fn selected(&self) -> Option { + self.selected + } + + pub fn select_previous(&mut self) -> Option { + let old_index = self.selected(); + let new_index = match old_index { + Some(selected) if selected == 0 => Some(0), + Some(selected) => Some(selected.saturating_sub(1)), + None => Some(0), + }; + + if old_index != new_index { + self.selected = new_index; + self.selected() + } else { + None + } + } + + pub fn select_next(&mut self) -> Option { + let old_index = self.selected(); + let new_index = match old_index { + Some(selected) if selected >= self.len.saturating_sub(1) => { + Some(self.len.saturating_sub(1)) + } + Some(selected) => Some(selected.saturating_add(1)), + None => Some(0), + }; + + if old_index != new_index { + self.selected = new_index; + self.selected() + } else { + None + } + } +} + +impl From<&ItemState> for TableState { + fn from(value: &ItemState) -> Self { + let mut state = TableState::default(); + state.select(value.selected); + state + } +} + +impl From<&ItemState> for ListState { + fn from(value: &ItemState) -> Self { + let mut state = ListState::default(); + state.select(value.selected); + state + } +} diff --git a/radicle-tui/src/ui/theme.rs b/radicle-tui/src/ui/theme.rs index 7e8a31c1..3b348e89 100644 --- a/radicle-tui/src/ui/theme.rs +++ b/radicle-tui/src/ui/theme.rs @@ -21,6 +21,7 @@ pub struct Colors { pub shortcutbar_divider_fg: Color, pub browser_list_id: Color, pub browser_list_title: Color, + pub browser_list_description: Color, pub browser_list_author: Color, pub browser_list_tags: Color, pub browser_list_comments: Color, @@ -82,12 +83,13 @@ pub fn default_dark() -> Theme { labeled_container_bg: COLOR_DEFAULT_FAINT, item_list_highlighted_bg: COLOR_DEFAULT_DARKER, property_name_fg: Color::Cyan, - property_divider_fg: COLOR_DEFAULT_FG, + property_divider_fg: COLOR_DEFAULT_DARK, shortcut_short_fg: COLOR_DEFAULT_DARK, shortcut_long_fg: COLOR_DEFAULT_DARKER, shortcutbar_divider_fg: COLOR_DEFAULT_DARKER, browser_list_id: Color::Cyan, browser_list_title: COLOR_DEFAULT_FG, + browser_list_description: COLOR_DEFAULT_DARK, browser_list_author: Color::Gray, browser_list_tags: Color::LightBlue, browser_list_comments: COLOR_DEFAULT_DARK_FG, diff --git a/radicle-tui/src/ui/widget/common.rs b/radicle-tui/src/ui/widget/common.rs index faebab51..71ddf51c 100644 --- a/radicle-tui/src/ui/widget/common.rs +++ b/radicle-tui/src/ui/widget/common.rs @@ -38,7 +38,7 @@ pub fn reversable_label(content: &str) -> Widget