From 9f8c6882931d3d587ab463a005268988e9191d4b Mon Sep 17 00:00:00 2001 From: Johann Woelper Date: Sat, 9 Dec 2023 17:37:42 +0100 Subject: [PATCH] feat: New toast system to improve UI messages and notifications --- Cargo.lock | 10 +++++ Cargo.toml | 1 + src/appstate.rs | 15 +++++--- src/main.rs | 98 +++++++++++++++---------------------------------- src/ui.rs | 18 +++++---- 5 files changed, 61 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccb8aea5..1eef56bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1323,6 +1323,15 @@ dependencies = [ "nohash-hasher", ] +[[package]] +name = "egui-notify" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3b10f21c7bb2afaecb8d98677cdb233a8b1f336b9b95abd610a33aeced04fc" +dependencies = [ + "egui", +] + [[package]] name = "egui-phosphor" version = "0.3.0" @@ -3829,6 +3838,7 @@ dependencies = [ "dark-light", "dds-rs", "dirs 5.0.1", + "egui-notify", "egui-phosphor", "egui_plot", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index 3dc40b32..a9cbeeb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ trash = "3.1" lutgen = {version ="0.9.0", features = ["lutgen-palettes"]} libheif-rs = { version = "0.22.0", default-features = false, optional = true} egui-phosphor = "=0.3.0" +egui-notify = "0.10.0" [features] heif = ["libheif-rs"] diff --git a/src/appstate.rs b/src/appstate.rs index a9cfd671..6c7ae100 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -4,6 +4,7 @@ use crate::{ settings::PersistentSettings, utils::{ExtendedImageInfo, Frame, Player}, }; +use egui_notify::Toasts; use image::RgbaImage; use nalgebra::Vector2; use notan::{egui::epaint::ahash::HashMap, prelude::Texture, AppState}; @@ -42,13 +43,12 @@ impl Message { } /// The state of the application -#[derive(Debug, AppState)] +#[derive(AppState)] pub struct OculanteState { pub image_geometry: ImageGeometry, pub compare_list: HashMap, pub drag_enabled: bool, pub reset_image: bool, - pub message: Option, /// Is the image fully loaded? pub is_loaded: bool, pub window_size: Vector2, @@ -80,7 +80,6 @@ pub struct OculanteState { pub always_on_top: bool, pub network_mode: bool, /// how long the toast message appears - pub toast_cooldown: f32, /// data to transform image once fullscreen is entered/left pub fullscreen_offset: Option<(i32, i32)>, /// List of images to cycle through. Usually the current dir or dropped files @@ -88,16 +87,21 @@ pub struct OculanteState { pub checker_texture: Option, pub redraw: bool, pub first_start: bool, + pub toasts: Toasts, } impl OculanteState { - pub fn send_message(&self, msg: &str) { + pub fn send_message_info(&self, msg: &str) { _ = self.message_channel.0.send(Message::info(msg)); } pub fn send_message_err(&self, msg: &str) { _ = self.message_channel.0.send(Message::err(msg)); } + + pub fn send_message_warn(&self, msg: &str) { + _ = self.message_channel.0.send(Message::warn(msg)); + } } impl Default for OculanteState { @@ -111,7 +115,6 @@ impl Default for OculanteState { compare_list: Default::default(), drag_enabled: Default::default(), reset_image: Default::default(), - message: Default::default(), is_loaded: Default::default(), cursor: Default::default(), cursor_relative: Default::default(), @@ -138,12 +141,12 @@ impl Default for OculanteState { always_on_top: Default::default(), network_mode: Default::default(), window_size: Default::default(), - toast_cooldown: Default::default(), fullscreen_offset: Default::default(), scrubber: Default::default(), checker_texture: Default::default(), redraw: Default::default(), first_start: true, + toasts: Toasts::default(), } } } diff --git a/src/main.rs b/src/main.rs index c4280c06..83157f5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -219,11 +219,11 @@ fn init(gfx: &mut Graphics, plugins: &mut Plugins) -> OculanteState { start_img_location = Some(location.clone()); } else { // Unsupported extension - state.send_message(&format!("ERROR: Unsupported file: {} - Open Github issue if you think this should not happen.", location.display())); + state.send_message_err(&format!("ERROR: Unsupported file: {} - Open Github issue if you think this should not happen.", location.display())); } } else { // Not a valid path, or user doesn't have permission to access? - state.send_message(&format!("ERROR: Can't open file: {}", location.display())); + state.send_message_err(&format!("ERROR: Can't open file: {}", location.display())); } // Assign image path if we have a valid one here @@ -239,7 +239,7 @@ fn init(gfx: &mut Graphics, plugins: &mut Plugins) -> OculanteState { if let Some(port) = matches.value_of("l") { match port.parse::() { Ok(p) => { - state.message = Some(Message::info(&format!("Listening on {p}"))); + state.send_message_info(&format!("Listening on {p}")); recv(p, state.texture_channel.0.clone()); state.current_path = Some(PathBuf::from(&format!("network port {p}"))); state.network_mode = true; @@ -456,7 +456,7 @@ fn event(app: &mut App, state: &mut OculanteState, evt: Event) { if key_pressed(app, state, DeleteFile) { if let Some(p) = &state.current_path { _ = trash::delete(p); - state.send_message("Deleted image"); + state.send_message_info("Deleted image"); } } if key_pressed(app, state, ZoomIn) { @@ -569,7 +569,7 @@ fn event(app: &mut App, state: &mut OculanteState, evt: Event) { state.player.load(&p, state.message_channel.0.clone()); state.current_path = Some(p); } else { - state.message = Some(Message::warn("Unsupported file!")); + state.send_message_warn("Unsupported file!"); } } } @@ -674,25 +674,28 @@ fn update(app: &mut App, state: &mut OculanteState) { app.window().request_frame(); } - // Only receive messages if current one is cleared - // debug!("cooldown {}", state.toast_cooldown); - - if state.message.is_none() { - state.toast_cooldown = 0.; - - // check if a new message has been sent - if let Ok(msg) = state.message_channel.1.try_recv() { - debug!("Received message: {:?}", msg); - match msg { - Message::LoadError(_) => { - state.current_image = None; - state.is_loaded = true; - state.current_texture = None; - } - _ => (), + // check if a new message has been sent + if let Ok(msg) = state.message_channel.1.try_recv() { + debug!("Received message: {:?}", msg); + match msg { + Message::LoadError(e) => { + state.toasts.error(e); + state.current_image = None; + state.is_loaded = true; + state.current_texture = None; + } + Message::Info(m) => { + state.toasts.info(m).set_duration(Some(Duration::from_secs(1))); + } + Message::Warning(m) => { + state.toasts.warning(m); + } + Message::Error(m) => { + state.toasts.error(m); + } + Message::Saved(_) => { + state.toasts.info("Saved"); } - - state.message = Some(msg); } } state.first_start = false; @@ -767,7 +770,7 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O if p.with_extension("oculante").is_file() { if let Ok(f) = std::fs::File::open(p.with_extension("oculante")) { if let Ok(edit_state) = serde_json::from_reader::<_, EditState>(f) { - state.send_message("Edits have been loaded for this image."); + state.send_message_info("Edits have been loaded for this image."); state.edit_state = edit_state; state.persistent_settings.edit_enabled = true; state.reset_image = true; @@ -780,7 +783,7 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O if let Ok(f) = std::fs::File::open(parent.join(".oculante")) { if let Ok(edit_state) = serde_json::from_reader::<_, EditState>(f) { - state.send_message( + state.send_message_info( "Directory edits have been loaded for this image.", ); state.edit_state = edit_state; @@ -968,7 +971,8 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O let egui_output = plugins.egui(|ctx| { // the top menu bar - ctx.request_repaint_after(Duration::from_secs(1)); + // ctx.request_repaint_after(Duration::from_secs(1)); + state.toasts.show(ctx); if !state.persistent_settings.zen_mode { egui::TopBottomPanel::top("menu") @@ -988,48 +992,6 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O }); } - if let Some(message) = &state.message.clone() { - // debug!("Message is set, showing"); - egui::TopBottomPanel::bottom("message").show_animated( - ctx, - state.message.is_some(), - |ui| { - ui.horizontal(|ui| { - match message { - Message::Info(txt) => { - ui.label(format!("💬 {txt}")); - } - Message::Warning(txt) => { - ui.colored_label(Color32::GOLD, format!("⚠ {txt}")); - } - Message::Error(txt) | Message::LoadError(txt) => { - ui.colored_label(Color32::RED, format!("🕱 {txt}")); - } - Message::Saved(path) => { - ui.colored_label(Color32::RED, format!("Saved!")); - state.current_path = Some(path.clone()); - set_title(app, state); - } - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { - if ui.small_button("🗙").clicked() { - state.message = None - } - }); - }); - - ui.ctx().request_repaint(); - }, - ); - let max_anim_len = 2.5; - - state.toast_cooldown += app.timer.delta_f32(); - - if state.toast_cooldown > max_anim_len { - debug!("Setting message to none, timer reached."); - state.message = None; - } - } if state.persistent_settings.info_enabled && !state.settings_enabled diff --git a/src/ui.rs b/src/ui.rs index 4b354fcd..32ef7888 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -409,7 +409,7 @@ pub fn settings_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: #[cfg(debug_assertions)] if ui.button("send test msg").clicked() { - state.send_message("Test"); + state.send_message_info("Test"); } egui::ComboBox::from_label("Color theme") @@ -572,7 +572,7 @@ pub fn settings_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: #[cfg(feature = "update")] if ui.button("Check for updates").on_hover_text("Check and install update if available. You will need to restart the app to use the new version.").clicked() { - state.send_message("Checking for updates..."); + state.send_message_info("Checking for updates..."); crate::update::update(Some(state.message_channel.0.clone())); state.settings_enabled = false; } @@ -1232,8 +1232,7 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu .save(p) { Ok(_) => { debug!("Saved to {}", p.display()); - - state.send_message("Saved"); + state.send_message_info("Saved"); //Re-apply exif if let Some(info) = &state.image_info { // before doing anything, make sure we have raw exif data @@ -1247,7 +1246,7 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu } } Err(e) => { - state.send_message(&format!("Could not save: {e}")); + state.send_message_err(&format!("Could not save: {e}")); } } } @@ -1939,7 +1938,12 @@ pub fn main_menu(ui: &mut Ui, state: &mut OculanteState, app: &mut App, gfx: &mu .clicked() { _ = trash::delete(p); - state.send_message("Deleted image"); + state.send_message_info(&format!( + "Deleted {}", + p.file_name() + .map(|f| f.to_string_lossy().to_string()) + .unwrap_or_default() + )); } } @@ -2007,7 +2011,7 @@ pub fn main_menu(ui: &mut Ui, state: &mut OculanteState, app: &mut App, gfx: &mu .image_sender .send(crate::utils::Frame::new_still(image)); // Since pasted data has no path, make sure it's not set - state.send_message("Image pasted"); + state.send_message_info("Image pasted"); } } else { state.send_message_err("Clipboard did not contain image")