tui: Use table model in patch browser
This commit is contained in:
parent
0a673bc552
commit
0361ca3818
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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} "))
|
||||
|
|
|
|||
Loading…
Reference in New Issue