tui: Use table model in patch browser

This commit is contained in:
Erik Kundt 2023-05-17 12:28:42 +02:00
parent 0a673bc552
commit 0361ca3818
5 changed files with 140 additions and 166 deletions

View File

@ -4,14 +4,14 @@ pub mod subscription;
use anyhow::Result;
use radicle::cob::patch::{PatchId, Patches};
use radicle::identity::{Id, Project};
use radicle::profile::Profile;
use radicle::storage::ReadStorage;
use tuirealm::application::PollStrategy;
use tuirealm::{Application, Frame, NoUserEvent};
use radicle_tui::cob::patch::{self};
use radicle_tui::ui;
use radicle_tui::ui::theme::{self, Theme};
use radicle_tui::Tui;
@ -45,13 +45,11 @@ pub enum Cid {
/// Messages handled by this application.
#[derive(Debug, Eq, PartialEq)]
pub enum HomeMessage {
PatchChanged(usize),
}
pub enum HomeMessage {}
#[derive(Debug, Eq, PartialEq)]
pub enum PatchMessage {
Show,
Show(PatchId),
Leave,
}
@ -99,8 +97,7 @@ impl App {
app: &mut Application<Cid, Message, NoUserEvent>,
theme: &Theme,
) -> Result<()> {
let patches = patch::load_all(&self.context.profile, self.context.id);
let home = Box::new(HomeView::new(patches));
let home = Box::<HomeView>::default();
self.pages.push(home, app, &self.context, theme)?;
Ok(())
@ -109,29 +106,26 @@ impl App {
fn view_patch(
&mut self,
app: &mut Application<Cid, Message, NoUserEvent>,
id: PatchId,
theme: &Theme,
) -> Result<()> {
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());
let repo = self
.context
.profile
.storage
.repository(self.context.id)
.unwrap();
let patches = Patches::open(&repo).unwrap();
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)?;
if let Some(patch) = patches.get(&id)? {
let view = Box::new(PatchView::new((id, patch)));
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."
)),
Ok(())
} else {
Err(anyhow::anyhow!(
"Could not mount 'page::PatchView'. Patch not found."
))
}
}
}
@ -159,8 +153,8 @@ impl Tui<Cid, Message> for App {
let theme = theme::default_dark();
for message in messages {
match message {
Message::Patch(PatchMessage::Show) => {
self.view_patch(app, &theme)?;
Message::Patch(PatchMessage::Show(id)) => {
self.view_patch(app, id, &theme)?;
}
Message::Patch(PatchMessage::Leave) => {
self.pages.pop(app)?;

View File

@ -1,21 +1,16 @@
use radicle::cob::patch::{Patch, PatchId};
use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection};
use tuirealm::event::{Event, Key, KeyEvent};
use tuirealm::{MockComponent, NoUserEvent, State, StateValue};
use radicle_tui::ui::widget::common::container::{GlobalListener, LabeledContainer, Tabs};
use radicle_tui::ui::widget::common::context::{ContextBar, Shortcuts};
use radicle_tui::ui::widget::common::list::PropertyList;
use radicle_tui::ui::widget::common::Browser;
use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser};
use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser, PatchBrowser};
use radicle_tui::ui::widget::patch;
use radicle_tui::ui::widget::Widget;
use super::{HomeMessage, Message, PatchMessage};
use super::{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
@ -50,28 +45,24 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<Tabs> {
}
}
impl tuirealm::Component<Message, NoUserEvent> for Widget<Browser<(PatchId, Patch)>> {
impl tuirealm::Component<Message, NoUserEvent> for Widget<PatchBrowser> {
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
match event {
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
match self.perform(Cmd::Move(MoveDirection::Up)) {
CmdResult::Changed(State::One(StateValue::Usize(index))) => {
Some(Message::Home(HomeMessage::PatchChanged(index)))
}
_ => Some(Message::Tick),
}
self.perform(Cmd::Move(MoveDirection::Up));
Some(Message::Tick)
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => match self.perform(Cmd::Move(MoveDirection::Down)) {
CmdResult::Changed(State::One(StateValue::Usize(index))) => {
Some(Message::Home(HomeMessage::PatchChanged(index)))
}
_ => Some(Message::Tick),
},
}) => {
self.perform(Cmd::Move(MoveDirection::Down));
Some(Message::Tick)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => Some(Message::Patch(PatchMessage::Show)),
}) => self
.selected_item()
.map(|item| Message::Patch(PatchMessage::Show(item.id()))),
_ => None,
}
}

View File

@ -3,63 +3,20 @@ pub mod context;
pub mod label;
pub mod list;
use std::marker::PhantomData;
use radicle::Profile;
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{AttrValue, Attribute, PropPayload, PropValue, TextSpan};
use tuirealm::tui::layout::Rect;
use tuirealm::{Frame, MockComponent, Props, State};
use tuirealm::props::{AttrValue, Attribute};
use tuirealm::MockComponent;
use container::{GlobalListener, Header, LabeledContainer, Tabs};
use context::{Shortcut, Shortcuts};
use label::Label;
use list::{List, Property, PropertyList, Table};
use list::{Property, PropertyList};
use super::{Widget, WidgetComponent};
use self::list::{ColumnWidth, TableModel};
use super::Widget;
use crate::ui::layout;
use crate::ui::theme::Theme;
pub struct Browser<T> {
list: Widget<Table>,
shortcuts: Widget<Shortcuts>,
phantom: PhantomData<T>,
}
impl<T: List> Browser<T> {
pub fn new(list: Widget<Table>, shortcuts: Widget<Shortcuts>) -> Self {
Self {
list,
shortcuts,
phantom: PhantomData,
}
}
}
impl<T: List> WidgetComponent for Browser<T> {
fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
let shortcuts_h = self
.shortcuts
.query(Attribute::Height)
.unwrap_or(AttrValue::Size(0))
.unwrap_size();
let layout = layout::root_component(area, shortcuts_h);
self.list.view(frame, layout[0]);
self.shortcuts.view(frame, layout[1]);
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
self.list.perform(cmd)
}
}
pub fn global_listener() -> Widget<GlobalListener> {
Widget::new(GlobalListener::default())
}
@ -76,34 +33,14 @@ pub fn label(content: &str) -> Widget<Label> {
pub fn reversable_label(content: &str) -> Widget<Label> {
let content = &format!(" {content} ");
label(content)
}
pub fn container_header(theme: &Theme, label: &str) -> Widget<Header> {
let content = AttrValue::Payload(PropPayload::Vec(vec![PropValue::TextSpan(
TextSpan::from(&format!(" {label} ")).fg(theme.colors.default_fg),
)]));
let widths = AttrValue::Payload(PropPayload::Vec(vec![PropValue::U16(100)]));
pub fn container_header(theme: &Theme, label: Widget<Label>) -> Widget<Header<(), 1>> {
let model = TableModel::new([label], [ColumnWidth::Fixed(100)]);
Widget::new(Header::default())
.content(content)
.custom("widths", widths)
}
pub fn table_header(theme: &Theme, labels: &[&str], widths: &[u16]) -> Widget<Header> {
let content = labels
.iter()
.map(|label| {
PropValue::TextSpan(TextSpan::from(label.to_string()).fg(theme.colors.default_fg))
})
.collect::<Vec<_>>();
let widths = AttrValue::Payload(PropPayload::Vec(
widths.iter().map(|w| PropValue::U16(*w)).collect(),
));
Widget::new(Header::default())
.content(AttrValue::Payload(PropPayload::Vec(content)))
.custom("widths", widths)
Widget::new(Header::new(model, theme.clone()))
}
pub fn labeled_container(
@ -111,7 +48,10 @@ pub fn labeled_container(
title: &str,
component: Box<dyn MockComponent>,
) -> Widget<LabeledContainer> {
let header = container_header(theme, title);
let header = container_header(
theme,
label(&format!(" {title} ")).foreground(theme.colors.default_fg),
);
let container = LabeledContainer::new(header, component);
Widget::new(container)
@ -170,26 +110,3 @@ pub fn tabs(theme: &Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {
Widget::new(tabs).height(2)
}
pub fn table(
theme: &Theme,
labels: &[&str],
widths: &[u16],
items: &[impl List],
profile: &Profile,
) -> Widget<Table> {
let items = items.iter().map(|item| item.row(theme, profile)).collect();
let header = table_header(theme, labels, widths);
let table = Table::new(header);
let widths = AttrValue::Payload(PropPayload::Vec(
widths.iter().map(|w| PropValue::U16(*w)).collect(),
));
Widget::new(table)
.content(AttrValue::Table(items))
.custom("widths", widths)
.background(theme.colors.labeled_container_bg)
.highlight(theme.colors.item_list_highlighted_bg)
}

View File

@ -1,18 +1,22 @@
use radicle::cob::patch::{Patch, PatchId};
use radicle::prelude::{Id, Project};
use radicle::storage::ReadStorage;
use radicle::Profile;
use radicle::prelude::{Id, Project};
use radicle::cob::patch::Patches;
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::tui::layout::Rect;
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State};
use super::{Widget, WidgetComponent};
use super::common;
use super::common::container::{LabeledContainer, Tabs};
use super::common::context::Shortcuts;
use super::common::label::Label;
use super::common::{self, Browser};
use super::common::list::{ColumnWidth, Table};
use super::{Widget, WidgetComponent};
use crate::ui::cob::PatchItem;
use crate::ui::layout;
use crate::ui::theme::Theme;
@ -88,6 +92,84 @@ impl WidgetComponent for IssueBrowser {
}
}
pub struct PatchBrowser {
table: Widget<Table<PatchItem, 8>>,
shortcuts: Widget<Shortcuts>,
}
impl PatchBrowser {
pub fn new(profile: &Profile, id: &Id, shortcuts: Widget<Shortcuts>, theme: Theme) -> Self {
let repo = profile.storage.repository(*id).unwrap();
let patches = Patches::open(&repo)
.and_then(|patches| patches.all().map(|iter| iter.flatten().collect::<Vec<_>>()));
let header = [
common::label(""),
common::label("ID"),
common::label("Title"),
common::label("Author"),
common::label("Head"),
common::label("+"),
common::label("-"),
common::label("Updated"),
];
let widths = [
ColumnWidth::Fixed(3),
ColumnWidth::Fixed(7),
ColumnWidth::Grow,
ColumnWidth::Fixed(21),
ColumnWidth::Fixed(7),
ColumnWidth::Fixed(4),
ColumnWidth::Fixed(4),
ColumnWidth::Fixed(18),
];
let mut items = vec![];
if let Ok(mut patches) = patches {
patches.sort_by(|(_, a, _), (_, b, _)| b.timestamp().cmp(&a.timestamp()));
patches.sort_by(|(_, a, _), (_, b, _)| a.state().cmp(b.state()));
for (id, patch, _) in patches {
if let Ok(item) = PatchItem::try_from((profile, &repo, id, patch)) {
items.push(item);
}
}
}
let table = Widget::new(Table::new(&items, header, widths, theme.clone()))
.highlight(theme.colors.item_list_highlighted_bg);
Self { table, shortcuts }
}
pub fn selected_item(&self) -> Option<&PatchItem> {
self.table.selection()
}
}
impl WidgetComponent for PatchBrowser {
fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
let shortcuts_h = self
.shortcuts
.query(Attribute::Height)
.unwrap_or(AttrValue::Size(0))
.unwrap_size();
let layout = layout::root_component(area, shortcuts_h);
self.table.view(frame, layout[0]);
self.shortcuts.view(frame, layout[1]);
}
fn state(&self) -> State {
self.table.state()
}
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
self.table.perform(cmd)
}
}
pub fn navigation(theme: &Theme) -> Widget<Tabs> {
common::tabs(
theme,
@ -125,11 +207,7 @@ pub fn dashboard(theme: &Theme, id: &Id, project: &Project) -> Widget<Dashboard>
Widget::new(dashboard)
}
pub fn patches(
theme: &Theme,
items: &[(PatchId, Patch)],
profile: &Profile,
) -> Widget<Browser<(PatchId, Patch)>> {
pub fn patches(theme: &Theme, id: &Id, profile: &Profile) -> Widget<PatchBrowser> {
let shortcuts = common::shortcuts(
theme,
vec![
@ -140,13 +218,7 @@ pub fn patches(
],
);
let labels = vec!["", "title", "author", "time", "comments", "tags"];
let widths = vec![3u16, 42, 15, 10, 5, 25];
let table = common::table(theme, &labels, &widths, items, profile);
let browser: Browser<(PatchId, Patch)> = Browser::new(table, shortcuts);
Widget::new(browser)
Widget::new(PatchBrowser::new(profile, id, shortcuts, theme.clone()))
}
pub fn issues(theme: &Theme) -> Widget<IssueBrowser> {

View File

@ -179,8 +179,8 @@ pub fn context(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Wi
let id = format::cob(&id);
let title = patch.title();
let author = cob::format_author(patch, profile);
let comments = rev.discussion().comments().count();
let author = cob::format_author(patch.author().id(), is_you);
let comments = rev.discussion().len();
let context = common::label(" patch ").background(theme.colors.context_badge_bg);
let id = common::label(&format!(" {id} "))