Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add message, warning and error popup
Erik Kundt committed 2 years ago
commit 6d14f62eb64b88d7c8f309cd958a7712b739f68c
parent 92e74a66809266cb783be29b3d390966e122c2f0
7 files changed +213 -27
modified src/app.rs
@@ -9,6 +9,7 @@ use radicle::cob::patch::PatchId;
use radicle::identity::{Id, Project};
use radicle::profile::Profile;

+
use radicle_tui::ui::widget;
use tuirealm::application::PollStrategy;
use tuirealm::{Application, Frame, NoUserEvent};

@@ -51,6 +52,7 @@ pub enum Cid {
    Issue(IssueCid),
    Patch(PatchCid),
    GlobalListener,
+
    Popup,
}

/// Messages handled by this application.
@@ -72,11 +74,20 @@ pub enum PatchMessage {
}

#[derive(Clone, Debug, Eq, PartialEq)]
+
pub enum PopupMessage {
+
    Info(String),
+
    Warning(String),
+
    Error(String),
+
    Hide,
+
}
+

+
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Message {
    Home(HomeMessage),
    Issue(IssueMessage),
    Patch(PatchMessage),
    NavigationChanged(u16),
+
    Popup(PopupMessage),
    Tick,
    Quit,
    Batch(Vec<Message>),
@@ -189,6 +200,22 @@ impl App {
                self.pages.pop(app)?;
                Ok(None)
            }
+
            Message::Popup(PopupMessage::Info(info)) => {
+
                self.show_info_popup(app, &theme, &info)?;
+
                Ok(None)
+
            }
+
            Message::Popup(PopupMessage::Warning(warning)) => {
+
                self.show_warning_popup(app, &theme, &warning)?;
+
                Ok(None)
+
            }
+
            Message::Popup(PopupMessage::Error(error)) => {
+
                self.show_error_popup(app, &theme, &error)?;
+
                Ok(None)
+
            }
+
            Message::Popup(PopupMessage::Hide) => {
+
                self.hide_popup(app)?;
+
                Ok(None)
+
            }
            Message::Quit => {
                self.quit = true;
                Ok(None)
@@ -199,6 +226,52 @@ impl App {
                .update(app, &self.context, &theme, message),
        }
    }
+

+
    fn show_info_popup(
+
        &mut self,
+
        app: &mut Application<Cid, Message, NoUserEvent>,
+
        theme: &Theme,
+
        message: &str,
+
    ) -> Result<()> {
+
        let popup = widget::common::info(theme, message);
+
        app.remount(Cid::Popup, popup.to_boxed(), vec![])?;
+
        app.active(&Cid::Popup)?;
+

+
        Ok(())
+
    }
+

+
    fn show_warning_popup(
+
        &mut self,
+
        app: &mut Application<Cid, Message, NoUserEvent>,
+
        theme: &Theme,
+
        message: &str,
+
    ) -> Result<()> {
+
        let popup = widget::common::warning(theme, message);
+
        app.remount(Cid::Popup, popup.to_boxed(), vec![])?;
+
        app.active(&Cid::Popup)?;
+

+
        Ok(())
+
    }
+

+
    fn show_error_popup(
+
        &mut self,
+
        app: &mut Application<Cid, Message, NoUserEvent>,
+
        theme: &Theme,
+
        message: &str,
+
    ) -> Result<()> {
+
        let popup = widget::common::error(theme, message);
+
        app.remount(Cid::Popup, popup.to_boxed(), vec![])?;
+
        app.active(&Cid::Popup)?;
+

+
        Ok(())
+
    }
+

+
    fn hide_popup(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
+
        app.blur()?;
+
        app.umount(&Cid::Popup)?;
+

+
        Ok(())
+
    }
}

impl Tui<Cid, Message> for App {
@@ -216,6 +289,10 @@ impl Tui<Cid, Message> for App {
        if let Ok(page) = self.pages.peek_mut() {
            page.view(app, frame);
        }
+

+
        if app.mounted(&Cid::Popup) {
+
            app.view(&Cid::Popup, frame, frame.size());
+
        }
    }

    fn update(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<bool> {
modified src/app/event.rs
@@ -2,7 +2,7 @@ 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::{AppHeader, GlobalListener, LabeledContainer};
+
use radicle_tui::ui::widget::common::container::{AppHeader, GlobalListener, LabeledContainer, Popup};
use radicle_tui::ui::widget::common::context::{ContextBar, Shortcuts};
use radicle_tui::ui::widget::common::list::PropertyList;
use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser, PatchBrowser};
@@ -10,7 +10,7 @@ use radicle_tui::ui::widget::{issue, patch};

use radicle_tui::ui::widget::Widget;

-
use super::{IssueMessage, Message, PatchMessage};
+
use super::{IssueMessage, Message, PatchMessage, PopupMessage};

/// 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
@@ -172,6 +172,17 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<patch::Files> {
    }
}

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

impl tuirealm::Component<Message, NoUserEvent> for Widget<LabeledContainer> {
    fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
        None
modified src/ui/layout.rs
@@ -170,6 +170,32 @@ pub fn centered_label(label_w: u16, area: Rect) -> Rect {
        .split(layout[1])[1]
}

+
pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
+
    let popup_layout = Layout::default()
+
        .direction(Direction::Vertical)
+
        .constraints(
+
            [
+
                Constraint::Percentage((100 - percent_y) / 2),
+
                Constraint::Percentage(percent_y),
+
                Constraint::Percentage((100 - percent_y) / 2),
+
            ]
+
            .as_ref(),
+
        )
+
        .split(r);
+

+
    Layout::default()
+
        .direction(Direction::Horizontal)
+
        .constraints(
+
            [
+
                Constraint::Percentage((100 - percent_x) / 2),
+
                Constraint::Percentage(percent_x),
+
                Constraint::Percentage((100 - percent_x) / 2),
+
            ]
+
            .as_ref(),
+
        )
+
        .split(popup_layout[1])[1]
+
}
+

pub fn issue_preview(area: Rect, shortcuts_h: u16) -> IssuePreview {
    let header_h = 3u16;
    let content_h = area
modified src/ui/widget/common.rs
@@ -11,7 +11,8 @@ use context::{Shortcut, Shortcuts};
use label::Label;
use list::{Property, PropertyList};

-
use self::container::{AppHeader, AppInfo, Container, VerticalLine};
+
use self::container::{AppHeader, AppInfo, Container, VerticalLine, Popup};
+
use self::label::Textarea;
use self::list::{ColumnWidth, PropertyTable};

use super::Widget;
@@ -152,3 +153,33 @@ pub fn app_header(

    Widget::new(header)
}
+

+
pub fn info(theme: &Theme, message: &str) -> Widget<Popup> {
+
    let textarea =
+
        Widget::new(Textarea::new(theme.clone())).content(AttrValue::String(message.to_owned()));
+
    let container = labeled_container(theme, "Info", textarea.to_boxed());
+

+
    Widget::new(Popup::new(theme.clone(), container))
+
        .width(50)
+
        .height(20)
+
}
+

+
pub fn warning(theme: &Theme, message: &str) -> Widget<Popup> {
+
    let textarea =
+
        Widget::new(Textarea::new(theme.clone())).content(AttrValue::String(message.to_owned()));
+
    let container = labeled_container(theme, "Warning", textarea.to_boxed());
+

+
    Widget::new(Popup::new(theme.clone(), container))
+
        .width(50)
+
        .height(20)
+
}
+

+
pub fn error(theme: &Theme, message: &str) -> Widget<Popup> {
+
    let textarea =
+
        Widget::new(Textarea::new(theme.clone())).content(AttrValue::String(message.to_owned()));
+
    let container = labeled_container(theme, "Error", textarea.to_boxed());
+

+
    Widget::new(Popup::new(theme.clone(), container))
+
        .width(50)
+
        .height(20)
+
}
modified src/ui/widget/common/container.rs
@@ -1,7 +1,7 @@
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{AttrValue, Attribute, BorderSides, BorderType, Props, Style, TextModifiers};
-
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
-
use tuirealm::tui::widgets::{Block, Cell, Row};
+
use tuirealm::tui::layout::{Constraint, Direction, Layout, Margin, Rect};
+
use tuirealm::tui::widgets::{Block, Cell, Clear, Row};
use tuirealm::{Frame, MockComponent, State, StateValue};

use crate::ui::ext::HeaderBlock;
@@ -363,6 +363,8 @@ impl WidgetComponent for Container {
                .constraints(vec![Constraint::Length(1), Constraint::Min(0)].as_ref())
                .split(area);
            // reverse draw order: child needs to be drawn first?
+
            self.component
+
                .attr(Attribute::Focus, AttrValue::Flag(focus));
            self.component.view(frame, layout[1]);

            let block = Block::default()
@@ -422,29 +424,73 @@ impl WidgetComponent for LabeledContainer {
        if display {
            let layout = Layout::default()
                .direction(Direction::Vertical)
-
                .constraints([Constraint::Length(header_height), Constraint::Length(0)].as_ref())
+
                .constraints([Constraint::Length(header_height), Constraint::Min(1)].as_ref())
                .split(area);

-
            // Make some space on the left
-
            let inner_layout = Layout::default()
-
                .direction(Direction::Horizontal)
-
                .horizontal_margin(1)
-
                .constraints(vec![Constraint::Length(1), Constraint::Min(0)].as_ref())
-
                .split(layout[1]);
-
            // reverse draw order: child needs to be drawn first?
-

-
            self.component
-
                .attr(Attribute::Focus, AttrValue::Flag(focus));
-
            self.component.view(frame, inner_layout[1]);
+
            self.header.attr(Attribute::Focus, AttrValue::Flag(focus));
+
            self.header.view(frame, layout[0]);

            let block = Block::default()
                .borders(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT)
                .border_style(Style::default().fg(color))
                .border_type(BorderType::Rounded);
-
            frame.render_widget(block, layout[1]);
+
            frame.render_widget(block.clone(), layout[1]);

-
            self.header.attr(Attribute::Focus, AttrValue::Flag(focus));
-
            self.header.view(frame, layout[0]);
+
            self.component
+
                .attr(Attribute::Focus, AttrValue::Flag(focus));
+
            self.component.view(
+
                frame,
+
                block.inner(layout[1]).inner(&Margin {
+
                    vertical: 0,
+
                    horizontal: 1,
+
                }),
+
            );
+
        }
+
    }
+

+
    fn state(&self) -> State {
+
        State::None
+
    }
+

+
    fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
+
        self.component.perform(cmd)
+
    }
+
}
+

+
pub struct Popup {
+
    component: Widget<LabeledContainer>,
+
}
+

+
impl Popup {
+
    pub fn new(_theme: Theme, component: Widget<LabeledContainer>) -> Self {
+
        Self { component }
+
    }
+
}
+

+
impl WidgetComponent for Popup {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, _area: Rect) {
+
        let display = properties
+
            .get_or(Attribute::Display, AttrValue::Flag(true))
+
            .unwrap_flag();
+
        let focus = properties
+
            .get_or(Attribute::Focus, AttrValue::Flag(false))
+
            .unwrap_flag();
+
        let width = properties
+
            .get_or(Attribute::Width, AttrValue::Size(50))
+
            .unwrap_size();
+
        let height = properties
+
            .get_or(Attribute::Height, AttrValue::Size(50))
+
            .unwrap_size();
+

+
        if display {
+
            let size = frame.size();
+

+
            let area = layout::centered_rect(width, height, size);
+
            frame.render_widget(Clear, area);
+

+
            self.component
+
                .attr(Attribute::Focus, AttrValue::Flag(focus));
+
            self.component.view(frame, area);
        }
    }

modified src/ui/widget/common/label.rs
@@ -147,7 +147,7 @@ impl WidgetComponent for Textarea {
        // spans than plain text.
        let body = textwrap::wrap(&content, area.width.saturating_sub(2) as usize);
        let len = body.len();
-
        let height = layout[0].height - 1;
+
        let height = layout[0].height.saturating_sub(1);

        let body: String = body.iter().map(|line| format!("{}\n", line)).collect();

modified src/ui/widget/common/list.rs
@@ -336,11 +336,6 @@ where
            .get_or(Attribute::HighlightedColor, AttrValue::Color(Color::Reset))
            .unwrap_color();

-
        let layout = Layout::default()
-
            .direction(Direction::Vertical)
-
            .constraints(vec![Constraint::Min(1), Constraint::Length(1)])
-
            .split(area);
-

        let rows: Vec<ListItem> = self
            .items
            .iter()
@@ -348,7 +343,7 @@ where
            .collect();
        let list = List::new(rows).highlight_style(Style::default().bg(highlight));

-
        frame.render_stateful_widget(list, layout[0], &mut ListState::from(&self.state));
+
        frame.render_stateful_widget(list, area, &mut ListState::from(&self.state));
    }

    fn state(&self) -> State {