diff --git a/src/app.rs b/src/app.rs index 786910e6..fc700da4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -47,6 +47,7 @@ pub enum IssueCid { List, Details, Context, + NewForm, Shortcuts, } @@ -69,6 +70,8 @@ pub enum IssueMessage { Show(IssueId), Changed(IssueId), Focus(IssueCid), + OpenPopup(IssueCid), + ClosePopup(IssueCid), Leave, } diff --git a/src/app/event.rs b/src/app/event.rs index a7c1a00c..7163a984 100644 --- a/src/app/event.rs +++ b/src/app/event.rs @@ -78,6 +78,11 @@ impl tuirealm::Component for Widget { Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => Some(Message::Issue(IssueMessage::Focus(IssueCid::Details))), + Event::Keyboard(KeyEvent { + code: Key::Char('o'), .. + }) => { + Some(Message::Issue(IssueMessage::OpenPopup(IssueCid::NewForm))) + } _ => None, } } @@ -104,6 +109,17 @@ 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::ClosePopup(IssueCid::NewForm))) + } + _ => None, + } + } +} + impl tuirealm::Component for Widget { fn on(&mut self, event: Event) -> Option { match event { diff --git a/src/app/page.rs b/src/app/page.rs index d042e584..2e19afc7 100644 --- a/src/app/page.rs +++ b/src/app/page.rs @@ -427,6 +427,7 @@ impl ViewPage for IssuePage { app.umount(&Cid::Issue(IssueCid::List))?; app.umount(&Cid::Issue(IssueCid::Details))?; app.umount(&Cid::Issue(IssueCid::Context))?; + app.umount(&Cid::Issue(IssueCid::NewForm))?; app.umount(&Cid::Issue(IssueCid::Shortcuts))?; Ok(()) } @@ -457,6 +458,17 @@ impl ViewPage for IssuePage { self.activate(app, cid)?; self.update_shortcuts(app, self.active_component.clone())?; } + Message::Issue(IssueMessage::OpenPopup(cid)) => { + if cid == IssueCid::NewForm { + let new_form = widget::issue::new_form(context, theme).to_boxed(); + app.remount(Cid::Issue(IssueCid::NewForm), new_form, vec![])?; + app.active(&Cid::Issue(IssueCid::NewForm))?; + } + } + Message::Issue(IssueMessage::ClosePopup(cid)) => { + app.blur()?; + app.umount(&Cid::Issue(cid))?; + } _ => {} } @@ -472,7 +484,13 @@ impl ViewPage for IssuePage { app.view(&Cid::Issue(IssueCid::Header), frame, layout.header); app.view(&Cid::Issue(IssueCid::List), frame, layout.left); - app.view(&Cid::Issue(IssueCid::Details), frame, layout.right); + + if app.mounted(&Cid::Issue(IssueCid::NewForm)) { + app.view(&Cid::Issue(IssueCid::NewForm), frame, layout.right); + } else { + app.view(&Cid::Issue(IssueCid::Details), frame, layout.right); + } + app.view(&Cid::Issue(IssueCid::Context), frame, layout.context); app.view(&Cid::Issue(IssueCid::Shortcuts), frame, layout.shortcuts); } diff --git a/src/ui/state.rs b/src/ui/state.rs index ddbf1387..e473841a 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -83,3 +83,51 @@ impl From<&ItemState> for ListState { state } } + +#[derive(Clone)] +pub struct FormState { + focus: Option, + len: usize, +} + +impl FormState { + pub fn new(focus: Option, len: usize) -> Self { + Self { focus, len } + } + + pub fn focus(&self) -> Option { + self.focus + } + + pub fn focus_previous(&mut self) -> Option { + let old_index = self.focus(); + let new_index = match old_index { + Some(focus) if focus == 0 => Some(0), + Some(focus) => Some(focus.saturating_sub(1)), + None => Some(0), + }; + + if old_index != new_index { + self.focus = new_index; + self.focus() + } else { + None + } + } + + pub fn focus_next(&mut self) -> Option { + let old_index = self.focus(); + let new_index = match old_index { + Some(focus) if focus >= self.len.saturating_sub(1) => Some(self.len.saturating_sub(1)), + Some(focus) => Some(focus.saturating_add(1)), + None => Some(0), + }; + + if old_index != new_index { + self.focus = new_index; + self.focus() + } else { + None + } + } +} diff --git a/src/ui/widget/common.rs b/src/ui/widget/common.rs index bfad2f5f..7947fd3c 100644 --- a/src/ui/widget/common.rs +++ b/src/ui/widget/common.rs @@ -1,6 +1,5 @@ pub mod container; pub mod context; -pub mod form; pub mod label; pub mod list; diff --git a/src/ui/widget/common/form.rs b/src/ui/widget/common/form.rs deleted file mode 100644 index 31f98dd3..00000000 --- a/src/ui/widget/common/form.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::collections::HashMap; - -use tuirealm::command::{CmdResult, Cmd}; -use tuirealm::tui::layout::Rect; -use tuirealm::{MockComponent, Props, Frame, State}; - -use crate::ui::widget::WidgetComponent; - -struct Form { - inputs: HashMap>, - -} - -impl Form { - -} - -impl WidgetComponent for Form { - fn view(&mut self, _properties: &Props, _frame: &mut Frame, _area: Rect) {} - - fn state(&self) -> State { - State::None - } - - fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult { - CmdResult::None - } -} \ No newline at end of file diff --git a/src/ui/widget/issue.rs b/src/ui/widget/issue.rs index 52a097d8..fb905605 100644 --- a/src/ui/widget/issue.rs +++ b/src/ui/widget/issue.rs @@ -3,6 +3,10 @@ use radicle::cob::thread::CommentId; use radicle::cob::issue::Issue; use radicle::cob::issue::IssueId; +use tui_realm_stdlib::Input; +use tuirealm::props::BorderType; +use tuirealm::props::Borders; +use tuirealm::props::Style; use tuirealm::tui::layout::Constraint; use tuirealm::tui::layout::Direction; use tuirealm::tui::layout::Layout; @@ -19,6 +23,7 @@ use super::Widget; use crate::ui::cob; use crate::ui::cob::IssueItem; use crate::ui::context::Context; +use crate::ui::state::FormState; use crate::ui::theme::Theme; use super::*; @@ -244,6 +249,88 @@ impl WidgetComponent for CommentBody { } } +pub struct NewForm { + /// + _issue: Issue, + // This form's fields: title, tags, assignees, description. + inputs: Vec, + /// State that holds the current focus etc. + state: FormState, +} + +impl NewForm { + pub fn new(_context: &Context, theme: &Theme) -> Self { + let foreground = theme.colors.default_fg; + let placeholder_style = Style::default().fg(theme.colors.container_border_fg); + let inactive_style = Style::default().fg(theme.colors.container_border_fg); + let borders = Borders::default() + .modifiers(BorderType::Rounded) + .color(theme.colors.container_border_focus_fg); + + let title = Input::default() + .foreground(foreground) + .borders(borders.clone()) + .inactive(inactive_style) + .placeholder("Title", placeholder_style); + let tags = Input::default() + .foreground(foreground) + .borders(borders.clone()) + .inactive(inactive_style) + .placeholder("Tags", placeholder_style); + let assignees = Input::default() + .foreground(foreground) + .borders(borders.clone()) + .inactive(inactive_style) + .placeholder("Assignees", placeholder_style); + let description = Input::default() + .foreground(foreground) + .borders(borders) + .inactive(inactive_style) + .placeholder("Description", placeholder_style); + + let state = FormState::new(Some(0), 4); + + Self { + _issue: Issue::default(), + inputs: vec![title, tags, assignees, description], + state, + } + } +} + +impl WidgetComponent for NewForm { + fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) { + let focus = self.state.focus().unwrap_or(0); + if let Some(input) = self.inputs.get_mut(focus) { + input.attr(Attribute::Focus, AttrValue::Flag(true)); + } + + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Min(3), + ]) + .split(area); + + for (index, area) in layout.iter().enumerate().take(self.inputs.len()) { + if let Some(input) = self.inputs.get_mut(index) { + input.view(frame, *area); + } + } + } + + fn state(&self) -> State { + State::None + } + + fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult { + CmdResult::None + } +} + pub fn list(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Widget { let list = LargeList::new(context, theme, Some(issue)); @@ -264,6 +351,11 @@ pub fn description( Widget::new(body) } +pub fn new_form(context: &Context, theme: &Theme) -> Widget { + let form = NewForm::new(context, theme); + Widget::new(form) +} + pub fn details( context: &Context, theme: &Theme,