Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Show / hide open issue form
Erik Kundt committed 2 years ago
commit d50f02f46bf9da530f01a92fd086add3e7511dfb
parent cb2490745e594d8125b70da15b0b3ee5c3dadc26
7 files changed +178 -30
modified 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,
}

modified src/app/event.rs
@@ -78,6 +78,11 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<issue::LargeList> {
            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<Message, NoUserEvent> for Widget<issue::IssueDetails> {
    }
}

+
impl tuirealm::Component<Message, NoUserEvent> for Widget<issue::NewForm> {
+
    fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
+
        match event {
+
            Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
+
                Some(Message::Issue(IssueMessage::ClosePopup(IssueCid::NewForm)))
+
            }
+
            _ => None,
+
        }
+
    }
+
}
+

impl tuirealm::Component<Message, NoUserEvent> for Widget<PatchBrowser> {
    fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
        match event {
modified 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);
    }
modified src/ui/state.rs
@@ -83,3 +83,51 @@ impl From<&ItemState> for ListState {
        state
    }
}
+

+
#[derive(Clone)]
+
pub struct FormState {
+
    focus: Option<usize>,
+
    len: usize,
+
}
+

+
impl FormState {
+
    pub fn new(focus: Option<usize>, len: usize) -> Self {
+
        Self { focus, len }
+
    }
+

+
    pub fn focus(&self) -> Option<usize> {
+
        self.focus
+
    }
+

+
    pub fn focus_previous(&mut self) -> Option<usize> {
+
        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<usize> {
+
        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
+
        }
+
    }
+
}
modified src/ui/widget/common.rs
@@ -1,6 +1,5 @@
pub mod container;
pub mod context;
-
pub mod form;
pub mod label;
pub mod list;

deleted src/ui/widget/common/form.rs
@@ -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<String, Box<dyn MockComponent>>,
-
    
-
}
-

-
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
modified 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<Input>,
+
    /// 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<LargeList> {
    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<NewForm> {
+
    let form = NewForm::new(context, theme);
+
    Widget::new(form)
+
}
+

pub fn details(
    context: &Context,
    theme: &Theme,