Skip to content

Commit

Permalink
patch: Use section group in selection
Browse files Browse the repository at this point in the history
  • Loading branch information
erak committed May 14, 2024
1 parent 899e59d commit e57a249
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 53 deletions.
4 changes: 2 additions & 2 deletions bin/commands/patch/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use tui::Exit;

use tui::PageStack;

use self::ui::BrowsePage;
use self::ui::BrowserPage;
use self::ui::HelpPage;

use super::common::Mode;
Expand Down Expand Up @@ -205,7 +205,7 @@ impl App {
let window: Window<State, Action, Page> = Window::new(&state, action_tx.clone())
.page(
Page::Browse,
BrowsePage::new(&state, action_tx.clone()).to_boxed(),
BrowserPage::new(&state, action_tx.clone()).to_boxed(),
)
.page(
Page::Help,
Expand Down
222 changes: 171 additions & 51 deletions bin/commands/patch/select/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ use tui::ui::items::{PatchItem, PatchItemFilter};
use tui::ui::span;
use tui::ui::widget;
use tui::ui::widget::container::{
Column, Container, ContainerProps, Footer, FooterProps, Header, HeaderProps,
Column, Container, ContainerProps, Footer, FooterProps, Header, HeaderProps, SectionGroup,
SectionGroupProps,
};
use tui::ui::widget::input::{TextField, TextFieldProps, TextFieldState};
use tui::ui::widget::list::{Table, TableProps, TableUtils};
Expand All @@ -37,21 +38,32 @@ use super::{Action, State};
type BoxedWidget = widget::BoxedWidget<State, Action>;

#[derive(Clone)]
pub struct BrowsePageProps<'a> {
pub struct BrowserProps<'a> {
/// Application mode: openation and id or id only.
mode: Mode,
/// Filtered patches.
patches: Vec<PatchItem>,
/// Current (selected) table index
selected: Option<usize>,
search: String,
/// Patch statistics.
stats: HashMap<String, usize>,
/// Header columns
header: Vec<Column<'a>>,
/// Table columns
columns: Vec<Column<'a>>,
/// Max. width, before columns are cut-off.
cutoff: usize,
/// Column index that marks where to cut.
cutoff_after: usize,
/// Current page size (height of table content).
page_size: usize,
/// If search widget should be shown.
show_search: bool,
shortcuts: Vec<(&'a str, &'a str)>,
/// Current search string.
search: String,
}

impl<'a> From<&State> for BrowsePageProps<'a> {
impl<'a> From<&State> for BrowserProps<'a> {
fn from(state: &State) -> Self {
let mut draft = 0;
let mut open = 0;
Expand Down Expand Up @@ -82,7 +94,20 @@ impl<'a> From<&State> for BrowsePageProps<'a> {
Self {
mode: state.mode.clone(),
patches,
search: state.browser.search.read(),
selected: state.browser.selected,
stats,
header: [
Column::new(" ● ", Constraint::Length(3)),
Column::new("ID", Constraint::Length(8)),
Column::new("Title", Constraint::Fill(1)),
Column::new("Author", Constraint::Length(16)),
Column::new("", Constraint::Length(16)),
Column::new("Head", Constraint::Length(8)),
Column::new("+", Constraint::Length(6)),
Column::new("-", Constraint::Length(6)),
Column::new("Updated", Constraint::Length(16)),
]
.to_vec(),
columns: [
Column::new(" ● ", Constraint::Length(3)),
Column::new("ID", Constraint::Length(8)),
Expand All @@ -97,46 +122,33 @@ impl<'a> From<&State> for BrowsePageProps<'a> {
.to_vec(),
cutoff: 150,
cutoff_after: 5,
stats,
page_size: state.browser.page_size,
show_search: state.browser.show_search,
selected: state.browser.selected,
shortcuts: match state.mode {
Mode::Id => vec![("enter", "select"), ("/", "search")],
Mode::Operation => vec![
("enter", "show"),
("c", "checkout"),
("d", "diff"),
("/", "search"),
("?", "help"),
],
},
search: state.browser.search.read(),
}
}
}

impl<'a: 'static> Properties for BrowsePageProps<'a> {}
impl<'a: 'static> BoxedAny for BrowsePageProps<'a> {}
impl<'a: 'static> Properties for BrowserProps<'a> {}
impl<'a: 'static> BoxedAny for BrowserProps<'a> {}

pub struct BrowsePage<'a> {
pub struct Browser<'a> {
/// Internal base
base: BaseView<State, Action>,
/// Internal props
props: BrowsePageProps<'a>,
/// Notifications widget
props: BrowserProps<'a>,
/// Patches widget
patches: BoxedWidget,
/// Search widget
search: BoxedWidget,
/// Shortcut widget
shortcuts: BoxedWidget,
}

impl<'a: 'static> Widget for BrowsePage<'a> {
impl<'a: 'static> Widget for Browser<'a> {
type Action = Action;
type State = State;

fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
let props = BrowsePageProps::from(state);
let props = BrowserProps::from(state);

Self {
base: BaseView {
Expand All @@ -148,7 +160,7 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
patches: Container::new(state, action_tx.clone())
.header(
Header::new(state, action_tx.clone())
.columns(props.columns.clone())
.columns(props.header.clone())
.cutoff(props.cutoff, props.cutoff_after)
.to_boxed(),
)
Expand All @@ -164,7 +176,7 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
});
})
.on_update(|state| {
let props = BrowsePageProps::from(state);
let props = BrowserProps::from(state);

TableProps::default()
.columns(props.columns)
Expand All @@ -178,7 +190,7 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
.footer(
Footer::new(state, action_tx.clone())
.on_update(|state| {
let props = BrowsePageProps::from(state);
let props = BrowserProps::from(state);

FooterProps::default()
.columns(browse_footer(&props, props.selected))
Expand All @@ -188,18 +200,11 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
)
.on_update(|state| {
ContainerProps::default()
.hide_footer(BrowsePageProps::from(state).show_search)
.hide_footer(BrowserProps::from(state).show_search)
.to_boxed()
})
.to_boxed(),
search: Search::new(state, action_tx.clone()).to_boxed(),
shortcuts: Shortcuts::new(state, action_tx.clone())
.on_update(|state| {
ShortcutsProps::default()
.shortcuts(&BrowsePageProps::from(state).shortcuts)
.to_boxed()
})
.to_boxed(),
}
}

Expand Down Expand Up @@ -281,32 +286,147 @@ impl<'a: 'static> Widget for BrowsePage<'a> {
}

fn update(&mut self, state: &State) {
self.props = BrowsePageProps::from_callback(self.base.on_update, state)
.unwrap_or(BrowsePageProps::from(state));
self.props = BrowserProps::from_callback(self.base.on_update, state)
.unwrap_or(BrowserProps::from(state));

self.patches.update(state);
self.search.update(state);
self.shortcuts.update(state);
}

fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
let page_size = props.area.height.saturating_sub(6) as usize;

let [content_area, shortcuts_area] =
Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(props.area);

if self.props.show_search {
let [table_area, search_area] =
Layout::vertical([Constraint::Min(1), Constraint::Length(2)]).areas(content_area);
Layout::vertical([Constraint::Min(1), Constraint::Length(2)]).areas(props.area);

self.patches.render(frame, RenderProps::from(table_area));
self.search
.render(frame, RenderProps::from(search_area).focus(true));
.render(frame, RenderProps::from(search_area).focus(props.focus));
} else {
self.patches
.render(frame, RenderProps::from(content_area).focus(true));
self.patches.render(frame, props);
}
}

fn base_mut(&mut self) -> &mut BaseView<State, Action> {
&mut self.base
}
}

#[derive(Clone)]
struct BrowserPageProps<'a> {
/// Current page size (height of table content).
page_size: usize,
/// If this pages' keys should be handled (`false` if search is shown).
handle_keys: bool,
/// This pages' shortcuts.
shortcuts: Vec<(&'a str, &'a str)>,
}

impl<'a> From<&State> for BrowserPageProps<'a> {
fn from(state: &State) -> Self {
Self {
page_size: state.browser.page_size,
handle_keys: !state.browser.show_search,
shortcuts: if state.browser.show_search {
vec![("esc", "cancel"), ("enter", "apply")]
} else {
match state.mode {
Mode::Id => vec![("enter", "select"), ("/", "search")],
Mode::Operation => vec![
("enter", "show"),
("c", "checkout"),
("d", "diff"),
("/", "search"),
("?", "help"),
],
}
},
}
}
}

impl<'a> Properties for BrowserPageProps<'a> {}
impl<'a> BoxedAny for BrowserPageProps<'a> {}

pub struct BrowserPage<'a> {
/// Internal base
base: BaseView<State, Action>,
/// Internal props
props: BrowserPageProps<'a>,
/// Sections widget
sections: BoxedWidget,
/// Shortcut widget
shortcuts: BoxedWidget,
}

impl<'a: 'static> Widget for BrowserPage<'a> {
type Action = Action;
type State = State;

fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
let props = BrowserPageProps::from(state);

Self {
base: BaseView {
action_tx: action_tx.clone(),
on_update: None,
on_event: None,
},
props: props.clone(),
sections: SectionGroup::new(state, action_tx.clone())
.section(Browser::new(state, action_tx.clone()).to_boxed())
.on_update(|state| {
let props = BrowserPageProps::from(state);
SectionGroupProps::default()
.handle_keys(props.handle_keys)
.to_boxed()
})
.to_boxed(),
shortcuts: Shortcuts::new(state, action_tx.clone())
.on_update(|state| {
ShortcutsProps::default()
.shortcuts(&BrowserPageProps::from(state).shortcuts)
.to_boxed()
})
.to_boxed(),
}
}

fn handle_event(&mut self, key: Key) {
self.sections.handle_event(key);

if self.props.handle_keys {
match key {
Key::Esc | Key::Ctrl('c') => {
let _ = self.base.action_tx.send(Action::Exit { selection: None });
}
Key::Char('?') => {
let _ = self.base.action_tx.send(Action::OpenHelp);
}
_ => {}
}
}
}

fn update(&mut self, state: &State) {
self.props = BrowserPageProps::from_callback(self.base.on_update, state)
.unwrap_or(BrowserPageProps::from(state));

self.sections.update(state);
self.shortcuts.update(state);
}

fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
let page_size = props.area.height.saturating_sub(6) as usize;

let [content_area, shortcuts_area] =
Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(props.area);

self.sections.render(
frame,
RenderProps::from(content_area)
.layout(Layout::horizontal([Constraint::Min(1)]))
.focus(true),
);
self.shortcuts
.render(frame, RenderProps::from(shortcuts_area));

Expand Down Expand Up @@ -552,7 +672,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
}
}

fn browse_footer<'a>(props: &BrowsePageProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
fn browse_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
let filter = PatchItemFilter::from_str(&props.search).unwrap_or_default();

let search = Line::from(vec![
Expand Down

0 comments on commit e57a249

Please sign in to comment.