From 88bfe974d0deea456c03c97cb17495d0b5d9109e Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 19 Aug 2022 09:31:47 +0200 Subject: [PATCH] Add dashboard view --- gui/src/app/menu.rs | 1 + gui/src/app/message.rs | 8 +- gui/src/app/mod.rs | 7 +- gui/src/app/{state.rs => state/mod.rs} | 8 +- gui/src/app/view/message.rs | 8 ++ gui/src/app/view/mod.rs | 108 +++++++++++++++++++++++++ gui/src/app/view/warning.rs | 97 ++++++++++++++++++++++ 7 files changed, 225 insertions(+), 12 deletions(-) rename gui/src/app/{state.rs => state/mod.rs} (71%) create mode 100644 gui/src/app/view/message.rs create mode 100644 gui/src/app/view/mod.rs create mode 100644 gui/src/app/view/warning.rs diff --git a/gui/src/app/menu.rs b/gui/src/app/menu.rs index 2ff7eb67..cca33768 100644 --- a/gui/src/app/menu.rs +++ b/gui/src/app/menu.rs @@ -1,4 +1,5 @@ #[derive(Debug, Clone, PartialEq, Eq)] pub enum Menu { Home, + Settings, } diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs index cffc5097..212ce892 100644 --- a/gui/src/app/message.rs +++ b/gui/src/app/message.rs @@ -1,14 +1,12 @@ use minisafe::config::Config as DaemonConfig; -use crate::app::{error::Error, menu::Menu}; +use crate::app::{error::Error, view}; -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum Message { - Reload, Tick, Event(iced_native::Event), - Clipboard(String), - Menu(Menu), + View(view::Message), LoadDaemonConfig(Box), DaemonConfigLoaded(Result<(), Error>), } diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 1bd655e2..3ebc038d 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -3,6 +3,7 @@ pub mod context; pub mod menu; pub mod message; pub mod state; +pub mod view; mod error; @@ -76,12 +77,12 @@ impl App { let res = self.context.load_daemon_config(*cfg); self.update(Message::DaemonConfigLoaded(res)) } - Message::Menu(menu) => { + Message::View(view::Message::Menu(menu)) => { self.context.menu = menu; self.state = new_state(&self.context); self.state.load(&self.context) } - Message::Clipboard(text) => clipboard::write(text), + Message::View(view::Message::Clipboard(text)) => clipboard::write(text), Message::Event(Event::Window(window::Event::CloseRequested)) => { self.stop(); Command::none() @@ -91,6 +92,6 @@ impl App { } pub fn view(&self) -> Element { - self.state.view(&self.context) + self.state.view(&self.context).map(Message::View) } } diff --git a/gui/src/app/state.rs b/gui/src/app/state/mod.rs similarity index 71% rename from gui/src/app/state.rs rename to gui/src/app/state/mod.rs index d2da92dd..91d02279 100644 --- a/gui/src/app/state.rs +++ b/gui/src/app/state/mod.rs @@ -1,10 +1,10 @@ use iced::pure::{column, Element}; use iced::{Command, Subscription}; -use super::{context::Context, message::Message}; +use super::{context::Context, message::Message, view}; pub trait State { - fn view(&self, ctx: &Context) -> Element; + fn view<'a>(&self, ctx: &'a Context) -> Element<'a, view::Message>; fn update(&mut self, ctx: &Context, message: Message) -> Command; fn subscription(&self) -> Subscription { Subscription::none() @@ -17,8 +17,8 @@ pub trait State { pub struct Home {} impl State for Home { - fn view(&self, _ctx: &Context) -> Element { - column().into() + fn view<'a>(&self, ctx: &'a Context) -> Element<'a, view::Message> { + view::dashboard(&ctx.menu, None, column()) } fn update(&mut self, _ctx: &Context, _message: Message) -> Command { Command::none() diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs new file mode 100644 index 00000000..9fd7bd83 --- /dev/null +++ b/gui/src/app/view/message.rs @@ -0,0 +1,8 @@ +use crate::app::menu::Menu; + +#[derive(Debug, Clone)] +pub enum Message { + Reload, + Clipboard(String), + Menu(Menu), +} diff --git a/gui/src/app/view/mod.rs b/gui/src/app/view/mod.rs new file mode 100644 index 00000000..b2e1c1f0 --- /dev/null +++ b/gui/src/app/view/mod.rs @@ -0,0 +1,108 @@ +mod message; +mod warning; + +pub use message::Message; +use warning::warn; + +use iced::{ + pure::{column, container, row, scrollable, widget, Element}, + Length, +}; + +use crate::ui::{ + color, + component::{button, separation, text::*}, + icon::{home_icon, settings_icon}, +}; + +use crate::app::{error::Error, menu::Menu}; + +pub fn sidebar(menu: &Menu) -> widget::Container { + let home_button = if *menu == Menu::Home { + button::primary(Some(home_icon()), "Home") + .on_press(Message::Reload) + .width(iced::Length::Units(200)) + } else { + button::transparent(Some(home_icon()), "Home") + .on_press(Message::Menu(Menu::Home)) + .width(iced::Length::Units(200)) + }; + let settings_button = if *menu == Menu::Settings { + button::primary(Some(settings_icon()), "Settings") + .on_press(Message::Menu(Menu::Settings)) + .width(iced::Length::Units(200)) + } else { + button::transparent(Some(settings_icon()), "Settings") + .on_press(Message::Menu(Menu::Settings)) + .width(iced::Length::Units(200)) + }; + + container( + column() + .padding(10) + .push( + column() + .push( + column() + .push(container(text("Minisafe").bold()).padding(10)) + .push(separation().width(Length::Units(200))) + .spacing(10), + ) + .push(home_button) + .spacing(20) + .height(Length::Fill), + ) + .push(container(settings_button).height(Length::Shrink)), + ) + .style(SidebarStyle) +} + +pub struct SidebarStyle; +impl widget::container::StyleSheet for SidebarStyle { + fn style(&self) -> widget::container::Style { + widget::container::Style { + background: color::FOREGROUND.into(), + border_width: 1.0, + border_color: color::SECONDARY, + ..widget::container::Style::default() + } + } +} + +pub fn dashboard<'a, T: Into>>( + menu: &'a Menu, + warning: Option<&Error>, + content: T, +) -> Element<'a, Message> { + row() + .push(sidebar(menu).width(Length::Shrink).height(Length::Fill)) + .push( + column().push(warn(warning)).push( + main_section(container(scrollable(content))) + .width(Length::Fill) + .height(Length::Fill), + ), + ) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .into() +} + +fn main_section<'a, T: 'a>(menu: widget::Container<'a, T>) -> widget::Container<'a, T> { + container(menu.max_width(1500)) + .padding(20) + .style(MainSectionStyle) + .center_x() + .width(Length::Fill) + .height(Length::Fill) +} + +pub struct MainSectionStyle; +impl widget::container::StyleSheet for MainSectionStyle { + fn style(&self) -> widget::container::Style { + widget::container::Style { + background: color::BACKGROUND.into(), + ..widget::container::Style::default() + } + } +} diff --git a/gui/src/app/view/warning.rs b/gui/src/app/view/warning.rs new file mode 100644 index 00000000..6cda233f --- /dev/null +++ b/gui/src/app/view/warning.rs @@ -0,0 +1,97 @@ +use std::convert::From; + +use iced::{ + pure::{column, container, row, widget}, + Length, +}; + +use crate::{ + app::error::Error, + daemon::{client::error::RpcErrorCode, DaemonError}, + ui::{color, component::text::*, icon}, +}; + +/// Simple warning message displayed to non technical user. +pub struct WarningMessage(String); + +impl From<&Error> for WarningMessage { + fn from(error: &Error) -> WarningMessage { + match error { + Error::Config(e) => WarningMessage(e.to_owned()), + Error::Daemon(e) => match e { + DaemonError::Rpc(code, _) => { + if *code == RpcErrorCode::JSONRPC2_INVALID_PARAMS as i32 { + WarningMessage("Some fields are invalid".to_string()) + } else { + WarningMessage("Internal error".to_string()) + } + } + DaemonError::Unexpected(_) => WarningMessage("Unknown error".to_string()), + DaemonError::Start(_) => WarningMessage("Daemon failed to start".to_string()), + DaemonError::NoAnswer | DaemonError::Transport(..) => { + WarningMessage("Communication with Daemon failed".to_string()) + } + }, + Error::Unexpected(_) => WarningMessage("Unknown error".to_string()), + } + } +} + +impl std::fmt::Display for WarningMessage { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +pub fn warn<'a, T: 'a>(error: Option<&Error>) -> widget::Container<'a, T> { + if let Some(w) = error { + let message: WarningMessage = w.into(); + warning(&message.to_string(), &w.to_string()).width(Length::Fill) + } else { + container(column()).width(Length::Fill) + } +} + +pub fn warning<'a, T: 'a>(message: &str, error: &str) -> widget::Container<'a, T> { + container( + widget::Tooltip::new( + row() + .push(icon::warning_icon()) + .push(text(message)) + .spacing(20), + error, + widget::tooltip::Position::Bottom, + ) + .style(TooltipWarningStyle), + ) + .padding(15) + .center_x() + .style(WarningStyle) + .width(Length::Fill) +} + +struct WarningStyle; +impl widget::container::StyleSheet for WarningStyle { + fn style(&self) -> widget::container::Style { + widget::container::Style { + border_radius: 0.0, + text_color: iced::Color::BLACK.into(), + background: color::WARNING.into(), + border_color: color::WARNING, + ..widget::container::Style::default() + } + } +} + +struct TooltipWarningStyle; +impl widget::container::StyleSheet for TooltipWarningStyle { + fn style(&self) -> widget::container::Style { + widget::container::Style { + border_radius: 0.0, + border_width: 1.0, + text_color: color::WARNING.into(), + background: color::FOREGROUND.into(), + border_color: color::WARNING, + } + } +}