From db99b30bc2b6be3ecd98077017f689661f0651a2 Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Sat, 13 Jan 2024 06:14:04 +0200 Subject: [PATCH] wip - notification panel --- core/src/core.rs | 18 ++- core/src/egui/mnemonic.rs | 2 +- core/src/egui/popup.rs | 12 +- core/src/egui/theme/color.rs | 3 + core/src/egui/theme/mod.rs | 10 ++ core/src/imports.rs | 2 +- core/src/menu.rs | 5 + core/src/modules/account_manager/address.rs | 2 +- core/src/modules/donations.rs | 2 +- core/src/modules/logs.rs | 2 +- core/src/modules/request.rs | 4 +- core/src/modules/testing.rs | 36 ++++- core/src/notifications.rs | 160 +++++++++++++++++++- core/src/runtime/mod.rs | 23 +++ core/src/status.rs | 2 +- resources/i18n/i18n.json | 20 ++- 16 files changed, 279 insertions(+), 24 deletions(-) diff --git a/core/src/core.rs b/core/src/core.rs index c33b66a..203fcba 100644 --- a/core/src/core.rs +++ b/core/src/core.rs @@ -55,6 +55,7 @@ pub struct Core { pub window_frame: bool, callback_map: CallbackMap, network_load_samples: VecDeque, + notifications: Notifications, } impl Core { @@ -192,6 +193,7 @@ impl Core { window_frame, callback_map: CallbackMap::default(), network_load_samples: VecDeque::default(), + notifications: Notifications::default(), }; modules.values().for_each(|module| { @@ -310,6 +312,10 @@ impl Core { &mut self.device } + pub fn notifications(&mut self) -> &mut Notifications { + &mut self.notifications + } + pub fn module(&self) -> &Module { &self.module } @@ -572,7 +578,8 @@ impl Core { user_notification: UserNotification::success(format!( "Capture saved to\n{}", path.to_string_lossy() - )), + )) + .as_toast(), }) .unwrap() }) @@ -680,7 +687,11 @@ impl Core { Events::Notify { user_notification: notification, } => { - notification.render(&mut self.toasts); + if notification.is_toast() { + notification.toast(&mut self.toasts); + } else { + self.notifications.push(notification); + } } Events::Close { .. } => {} Events::UnlockSuccess => {} @@ -693,6 +704,7 @@ impl Core { Events::Wallet { event } => { match *event { CoreWallet::Error { message } => { + // runtime().notify(UserNotification::error(message.as_str())); println!("{message}"); } CoreWallet::UtxoProcStart => { @@ -700,6 +712,8 @@ impl Core { } CoreWallet::UtxoProcStop => {} CoreWallet::UtxoProcError { message } => { + runtime().notify(UserNotification::error(message.as_str())); + if message.contains("network type") { self.state.error = Some(message); } diff --git a/core/src/egui/mnemonic.rs b/core/src/egui/mnemonic.rs index e4fa991..31a8663 100644 --- a/core/src/egui/mnemonic.rs +++ b/core/src/egui/mnemonic.rs @@ -124,7 +124,7 @@ impl<'render> MnemonicPresenter<'render> { if ui.medium_button(format!("{CLIPBOARD_TEXT} Copy to clipboard")).clicked() { ui.output_mut(|o| o.copied_text = self.phrase.to_string()); - runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}", i18n("Copied to clipboard"))).short()); + runtime().notify_clipboard(i18n("Copied to clipboard")); } } }); diff --git a/core/src/egui/popup.rs b/core/src/egui/popup.rs index 59a2e19..0b825a3 100644 --- a/core/src/egui/popup.rs +++ b/core/src/egui/popup.rs @@ -12,11 +12,16 @@ pub struct PopupPanel<'panel> { caption: Option, with_close_button: bool, close_on_interaction: bool, + close_on_escape: bool, above_or_below: AboveOrBelow, with_padding: bool, } impl<'panel> PopupPanel<'panel> { + pub fn id(ui: &mut Ui, id: impl Into) -> Id { + ui.make_persistent_id(id.into()) + } + pub fn new( ui: &mut Ui, id: impl Into, @@ -34,6 +39,7 @@ impl<'panel> PopupPanel<'panel> { caption: None, with_close_button: false, close_on_interaction: false, + close_on_escape: true, above_or_below: AboveOrBelow::Below, with_padding: true, } @@ -90,6 +96,7 @@ impl<'panel> PopupPanel<'panel> { &response, self.above_or_below, self.close_on_interaction, + self.close_on_escape, |ui| { if let Some(width) = self.min_width { ui.set_min_width(width); @@ -152,6 +159,7 @@ pub fn popup_above_or_below_widget_local( widget_response: &Response, above_or_below: AboveOrBelow, close_on_interaction: bool, + close_on_escape: bool, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { if ui.memory(|mem| mem.is_popup_open(popup_id)) { @@ -186,7 +194,9 @@ pub fn popup_above_or_below_widget_local( if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() { close_popup = true; } - } else if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() { + } else if close_on_escape + && (ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere()) + { let response = inner.response; ui.ctx().input(|i| { let pointer = &i.pointer; diff --git a/core/src/egui/theme/color.rs b/core/src/egui/theme/color.rs index 1f9c683..689aa0a 100644 --- a/core/src/egui/theme/color.rs +++ b/core/src/egui/theme/color.rs @@ -14,6 +14,7 @@ pub struct ThemeColor { pub error_color: Color32, pub alert_color: Color32, pub warning_color: Color32, + pub info_color: Color32, pub icon_syncing_color: Color32, pub icon_connected_color: Color32, pub icon_color_default: Color32, @@ -84,6 +85,7 @@ impl ThemeColor { error_color: Color32::from_rgb(255, 136, 136), alert_color: Color32::from_rgb(255, 136, 136), warning_color: egui::Color32::from_rgb(255, 255, 136), + info_color: egui::Color32::from_rgb(66, 178, 252), icon_syncing_color: egui::Color32::from_rgb(255, 255, 136), icon_connected_color: egui::Color32::from_rgb(85, 233, 136), icon_color_default: Color32::from_rgb(240, 240, 240), @@ -152,6 +154,7 @@ impl ThemeColor { error_color: Color32::from_rgb(77, 41, 41), alert_color: Color32::from_rgb(77, 41, 41), warning_color: egui::Color32::from_rgb(77, 77, 41), + info_color: egui::Color32::from_rgb(41, 56, 77), icon_syncing_color: egui::Color32::from_rgb(117, 117, 4), icon_connected_color: egui::Color32::from_rgb(8, 110, 65), icon_color_default: Color32::from_rgb(32, 32, 32), diff --git a/core/src/egui/theme/mod.rs b/core/src/egui/theme/mod.rs index 28fbd48..a56fe7f 100644 --- a/core/src/egui/theme/mod.rs +++ b/core/src/egui/theme/mod.rs @@ -159,6 +159,16 @@ pub fn warning_color() -> Color32 { theme_color().warning_color } +#[inline(always)] +pub fn info_color() -> Color32 { + theme_color().info_color +} + +#[inline(always)] +pub fn strong_color() -> Color32 { + theme_color().strong_color +} + // ~ pub trait MetricGroupExtension { diff --git a/core/src/imports.rs b/core/src/imports.rs index ec0b685..e291820 100644 --- a/core/src/imports.rs +++ b/core/src/imports.rs @@ -71,7 +71,7 @@ pub use crate::menu::Menu; pub use crate::modules; pub use crate::modules::{Module, ModuleCaps, ModuleStyle, ModuleT}; pub use crate::network::Network; -pub use crate::notifications::{UserNotification, UserNotifyKind}; +pub use crate::notifications::{Notifications, UserNotification, UserNotifyKind}; pub use crate::primitives::{ Account, AccountCollection, AccountSelectorButtonExtension, BlockDagGraphSettings, DaaBucket, DagBlock, Transaction, TransactionCollection, diff --git a/core/src/menu.rs b/core/src/menu.rs index 7b39c92..dc1fb6c 100644 --- a/core/src/menu.rs +++ b/core/src/menu.rs @@ -151,6 +151,11 @@ impl<'core> Menu<'core> { ) .with_min_width(64.) .build(ui); + + if self.core.notifications().has_some() { + ui.separator(); + self.core.notifications().render(ui); + } }); }); }); diff --git a/core/src/modules/account_manager/address.rs b/core/src/modules/account_manager/address.rs index 089fc63..7fb315e 100644 --- a/core/src/modules/account_manager/address.rs +++ b/core/src/modules/account_manager/address.rs @@ -24,7 +24,7 @@ impl<'context> AddressPane<'context> { // }) .clicked() { ui.output_mut(|o| o.copied_text = rc.context.address().to_string()); - runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}", i18n("Copied to clipboard"))).short()) + runtime().notify_clipboard(i18n("Copied to clipboard")); } } } \ No newline at end of file diff --git a/core/src/modules/donations.rs b/core/src/modules/donations.rs index 4011e9a..eaf71c5 100644 --- a/core/src/modules/donations.rs +++ b/core/src/modules/donations.rs @@ -48,7 +48,7 @@ impl Donations { if response.clicked() { ui.output_mut(|o| o.copied_text = Self::ADDRESS_KASPA_NG_FUND.to_owned()); - runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}", i18n("Copied to clipboard"))).short()); + runtime().notify_clipboard(i18n("Copied to clipboard")); } ui.label(" "); diff --git a/core/src/modules/logs.rs b/core/src/modules/logs.rs index 6202408..674e4f9 100644 --- a/core/src/modules/logs.rs +++ b/core/src/modules/logs.rs @@ -50,7 +50,7 @@ impl ModuleT for Logs { .clicked() { let logs = self.runtime.kaspa_service().logs().iter().map(|log| log.to_string()).collect::>().join("\n"); ui.output_mut(|o| o.copied_text = logs); - runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}",i18n("Copied to clipboard"))).short()) + runtime().notify_clipboard(i18n("Copied to clipboard")); } } } diff --git a/core/src/modules/request.rs b/core/src/modules/request.rs index 794fc86..affc27d 100644 --- a/core/src/modules/request.rs +++ b/core/src/modules/request.rs @@ -88,7 +88,7 @@ impl Request { if response.clicked() { ui.output_mut(|o| o.copied_text = address.to_owned()); - runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}", i18n("Address copied to clipboard"))).short()); + runtime().notify_clipboard(i18n("Address copied to clipboard")); } ui.label(" "); @@ -104,7 +104,7 @@ impl Request { if response.clicked() { ui.output_mut(|o| o.copied_text = request_uri.to_owned()); - runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}", i18n("URI copied to clipboard"))).short()); + runtime().notify_clipboard(i18n("URI copied to clipboard")); } ui.label(" "); diff --git a/core/src/modules/testing.rs b/core/src/modules/testing.rs index caed893..4501b48 100644 --- a/core/src/modules/testing.rs +++ b/core/src/modules/testing.rs @@ -21,6 +21,7 @@ pub struct Testing { // text : String, // graph_data: Vec, + #[allow(dead_code)] mnemonic_presenter_context : MnemonicPresenterContext, } @@ -74,6 +75,39 @@ impl ModuleT for Testing { _frame: &mut eframe::Frame, ui: &mut egui::Ui, ) { + + if ui.large_button("notify regular").clicked() { + runtime().notify(UserNotification::info("This is a regular notification").short()); + } + + if ui.large_button("notify error").clicked() { + runtime().notify(UserNotification::error("This is an error notification").short()); + } + + if ui.large_button("notify warning").clicked() { + runtime().notify(UserNotification::warning("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.").short()); + } + + if ui.large_button("notify success").clicked() { + runtime().notify(UserNotification::success("This is a success notification").short()); + } + + if ui.large_button("notify info").clicked() { + runtime().notify(UserNotification::info("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, ").short()); + } + + } +} + +impl Testing { + + fn _render_mnemonic_presenter( + &mut self, + _core: &mut Core, + _ctx: &egui::Context, + _frame: &mut eframe::Frame, + ui: &mut egui::Ui, + ) { egui::ScrollArea::vertical() .id_source("test_mnemonic_size_scroll") .auto_shrink([true; 2]) @@ -96,8 +130,6 @@ impl ModuleT for Testing { }); // self.mnemonic_presenter_context.render(ui); } -} -impl Testing { fn _render_v1( &mut self, _core: &mut Core, diff --git a/core/src/notifications.rs b/core/src/notifications.rs index ae1c266..0290202 100644 --- a/core/src/notifications.rs +++ b/core/src/notifications.rs @@ -1,7 +1,7 @@ +use crate::imports::*; use egui_notify::Toasts; -use std::time::Duration; -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] pub enum UserNotifyKind { Info, Success, @@ -17,6 +17,7 @@ pub struct UserNotification { pub duration: Option, pub progress: bool, pub closable: bool, + pub toast: bool, } impl Default for UserNotification { @@ -27,6 +28,7 @@ impl Default for UserNotification { duration: Some(Duration::from_millis(3500)), progress: true, closable: false, + toast: false, } } } @@ -40,6 +42,15 @@ impl UserNotification { } } + pub fn as_toast(mut self) -> Self { + self.toast = true; + self + } + + pub fn is_toast(&self) -> bool { + self.toast + } + pub fn info(text: impl Into) -> Self { Self::new(UserNotifyKind::Info, text) } @@ -72,7 +83,7 @@ impl UserNotification { self } - pub fn render(self, toasts: &mut Toasts) { + pub fn toast(self, toasts: &mut Toasts) { match self.kind { UserNotifyKind::Info => { toasts @@ -111,4 +122,147 @@ impl UserNotification { } } } + + pub fn icon(&self) -> RichText { + use egui_phosphor::thin::*; + + match self.kind { + UserNotifyKind::Info => RichText::new(INFO).color(info_color()), + UserNotifyKind::Success => RichText::new(INFO).color(strong_color()), + UserNotifyKind::Warning => RichText::new(WARNING).color(warning_color()), + UserNotifyKind::Error => RichText::new(SEAL_WARNING).color(error_color()), + UserNotifyKind::Basic => RichText::new(INFO).color(info_color()), + } + } + + pub fn text(&self) -> RichText { + match self.kind { + UserNotifyKind::Info => RichText::new(&self.message), + UserNotifyKind::Success => RichText::new(&self.message), + UserNotifyKind::Warning => RichText::new(&self.message).color(warning_color()), + UserNotifyKind::Error => RichText::new(&self.message).color(error_color()), + UserNotifyKind::Basic => RichText::new(&self.message), + } + } +} + +#[derive(Default)] +pub struct Notifications { + pub notifications: Vec, + pub last_notification: usize, + pub errors: bool, + pub warnings: bool, + pub infos: bool, +} + +impl Notifications { + pub fn new() -> Self { + Self::default() + } + + pub fn clear(&mut self) { + self.notifications.clear(); + self.errors = false; + self.warnings = false; + self.infos = false; + } + + pub fn has_some(&self) -> bool { + !self.notifications.is_empty() + } + + pub fn push(&mut self, notification: UserNotification) { + if notification.kind == UserNotifyKind::Error { + self.errors = true; + } else if notification.kind == UserNotifyKind::Warning { + self.warnings = true; + } else if notification.kind == UserNotifyKind::Info { + self.infos = true; + } + + self.notifications.push(notification); + } + + pub fn render(&mut self, ui: &mut Ui) { + use egui_phosphor::light::*; + + if self.notifications.len() != self.last_notification { + let id = PopupPanel::id(ui, "notification_popup"); + ui.memory_mut(|mem| mem.open_popup(id)); + self.last_notification = self.notifications.len(); + } + + let icon = if self.errors { + RichText::new(SEAL_WARNING).color(error_color()) + } else if self.warnings { + RichText::new(WARNING).color(warning_color()) + } else if self.infos { + RichText::new(INFO).color(info_color()) + } else { + RichText::new(INFO) + }; + + let screen_rect = ui.ctx().screen_rect(); + let width = (screen_rect.width() / 4. * 3.).min(500.); + let height = (screen_rect.height() / 4.).min(240.); + + PopupPanel::new( + ui, + "notification_popup", + |ui| ui.add(Label::new(icon.size(16.)).sense(Sense::click())), + |ui, close| { + egui::ScrollArea::vertical() + .id_source("notification_popup_scroll") + .auto_shrink([false; 2]) + .stick_to_bottom(true) + .show(ui, |ui| { + Grid::new("notification_popup_grid") + .num_columns(2) + // .spacing([2.0,2.0]) + .show(ui, |ui| { + for notification in self.notifications.iter() { + // ui.label(notification.icon().size(24.)); + // ui.label(notification.text().size(16.)); + ui.label(notification.icon().size(20.)); + ui.horizontal_wrapped(|ui| { + ui.label(notification.text().size(14.)); + }); + ui.end_row(); + } + }); + }); + + ui.separator(); + ui.add_space(4.); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.medium_button(i18n("Close")).clicked() { + *close = true; + } + if ui.medium_button(i18n("Clear")).clicked() { + self.clear(); + *close = true; + } + if ui + .medium_button(format!("{CLIPBOARD} {}", i18n("Copy"))) + .clicked() + { + let notifications = self + .notifications + .iter() + .map(|notification| notification.message.to_string()) + .collect::>() + .join("\n"); + ui.output_mut(|o| o.copied_text = notifications); + runtime().notify_clipboard(i18n("Copied to clipboard")); + *close = true; + } + }); + }, + ) + .with_min_width(width) + .with_max_height(height) + .with_caption(i18n("Notifications")) + .with_close_on_interaction(false) + .build(ui); + } } diff --git a/core/src/runtime/mod.rs b/core/src/runtime/mod.rs index 8f87ba4..23bc932 100644 --- a/core/src/runtime/mod.rs +++ b/core/src/runtime/mod.rs @@ -222,6 +222,29 @@ impl Runtime { .ok(); } + pub fn toast(&self, user_notification: UserNotification) { + self.inner + .application_events + .sender + .try_send(Events::Notify { + user_notification: user_notification.as_toast(), + }) + .ok(); + } + + pub fn notify_clipboard(&self, text: impl Into) { + use egui_phosphor::light::CLIPBOARD_TEXT; + let user_notification = UserNotification::info(format!("{CLIPBOARD_TEXT} {}", text.into())) + .short() + .as_toast(); + + self.inner + .application_events + .sender + .try_send(Events::Notify { user_notification }) + .ok(); + } + pub fn spawn_task(&self, task: F) where F: Future> + Send + 'static, diff --git a/core/src/status.rs b/core/src/status.rs index 42bbbd3..af47355 100644 --- a/core/src/status.rs +++ b/core/src/status.rs @@ -114,7 +114,7 @@ impl<'core> Status<'core> { } } else { ui.label(RichText::new(egui_phosphor::light::CLOUD_ARROW_DOWN).size(status_icon_size)); - ui.label(RichText::new(i18n("..."))); + ui.label(RichText::new("...")); } } diff --git a/resources/i18n/i18n.json b/resources/i18n/i18n.json index c38802b..486b3ef 100644 --- a/resources/i18n/i18n.json +++ b/resources/i18n/i18n.json @@ -13,14 +13,14 @@ "hi": "Hindi", "fi": "Finnish", "vi": "Vietnamese", + "fil": "Filipino", "fa": "Farsi", "lt": "Lithuanian", "pa": "Panjabi", "nl": "Dutch", "es": "EspaƱol", - "sv": "Swedish", - "fil": "Filipino", "uk": "Ukrainian", + "sv": "Swedish", "af": "Afrikaans", "et": "Esti", "en": "English", @@ -61,14 +61,14 @@ "hi": {}, "fi": {}, "vi": {}, + "fil": {}, "fa": {}, "lt": {}, + "sv": {}, "pa": {}, "nl": {}, "es": {}, - "sv": {}, "uk": {}, - "fil": {}, "af": {}, "et": {}, "en": { @@ -111,20 +111,21 @@ "Payment & Recovery Password": "Payment & Recovery Password", "All": "All", "Connect to a local node (localhost)": "Connect to a local node (localhost)", + "peers": "peers", "Processed Headers": "Processed Headers", "Creating Wallet": "Creating Wallet", "Medium Wide": "Medium Wide", - "Apply": "Apply", "Select Private Key Type": "Select Private Key Type", - "Because of its focus on security and performance, this software is entirely developed in Rust, demanding significantly more time and effort compared to other traditional modern web-driven software.": "Because of its focus on security and performance, this software is entirely developed in Rust, demanding significantly more time and effort compared to other traditional modern web-driven software.", - "Settings": "Settings", "wRPC URL:": "wRPC URL:", + "Because of its focus on security and performance, this software is entirely developed in Rust, demanding significantly more time and effort compared to other traditional modern web-driven software.": "Because of its focus on security and performance, this software is entirely developed in Rust, demanding significantly more time and effort compared to other traditional modern web-driven software.", + "Apply": "Apply", "Optional": "Optional", + "Settings": "Settings", "Check for Updates": "Check for Updates", "wRPC Connection Settings": "wRPC Connection Settings", - "p2p Rx/s": "p2p Rx/s", "Theme Color": "Theme Color", "Custom Public Node": "Custom Public Node", + "p2p Rx/s": "p2p Rx/s", "Remote Connection:": "Remote Connection:", "Difficulty": "Difficulty", "Uptime:": "Uptime:", @@ -154,6 +155,7 @@ "Payment Request": "Payment Request", "The balance may be out of date during node sync": "The balance may be out of date during node sync", "Testnet-10": "Testnet-10", + "Clear": "Clear", "p2p Tx/s": "p2p Tx/s", "wRPC Encoding:": "wRPC Encoding:", "License Information": "License Information", @@ -262,6 +264,7 @@ "Wallet Name": "Wallet Name", "Mass Processed": "Mass Processed", "Please specify the private key type for the new wallet": "Please specify the private key type for the new wallet", + "Notifications": "Notifications", "Public p2p Nodes for": "Public p2p Nodes for", "Disables node connectivity (Offline Mode).": "Disables node connectivity (Offline Mode).", "Total Tx/s": "Total Tx/s", @@ -323,6 +326,7 @@ "Total Rx/s": "Total Rx/s", "MT": "MT", "Allow custom arguments for the Rusty Kaspa daemon": "Allow custom arguments for the Rusty Kaspa daemon", + "Copy": "Copy", "Range:": "Range:", "Custom arguments:": "Custom arguments:", "wRPC JSON Rx": "wRPC JSON Rx",