From aa8a898e03dc7f388f6c9996b55b1b26eb8b1da8 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:19:17 +0200 Subject: [PATCH 01/13] refactor: unified alert window --- src/ui/components/confirmation_dialog.rs | 282 +++++++++++++++++++++++ src/ui/components/mod.rs | 1 + src/ui/components/styled.rs | 3 + src/ui/identities/transfer_screen.rs | 194 +++++++++------- 4 files changed, 398 insertions(+), 82 deletions(-) create mode 100644 src/ui/components/confirmation_dialog.rs diff --git a/src/ui/components/confirmation_dialog.rs b/src/ui/components/confirmation_dialog.rs new file mode 100644 index 000000000..3db2e08da --- /dev/null +++ b/src/ui/components/confirmation_dialog.rs @@ -0,0 +1,282 @@ +use egui::{Color32, InnerResponse, RichText, Ui, Widget}; + +/// Response from showing a confirmation dialog +#[derive(Debug, Clone, PartialEq)] +pub enum ConfirmationDialogResponse { + /// Dialog is still open, no action taken + None, + /// User clicked confirm button + Confirmed, + /// User clicked cancel button or closed dialog + Canceled, +} + +/// A reusable confirmation dialog component that implements the Widget trait +/// +/// This component provides a consistent modal dialog for confirming user actions +/// across the application. It supports customizable titles, messages, button text, +/// styling options including a danger mode for destructive actions, and callback +/// functions for handling user responses. +/// +/// # Examples +/// +/// Basic usage: +/// ```rust +/// # use dash_evo_tool::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationDialogResponse}; +/// # use egui::Ui; +/// # fn example(ui: &mut Ui) { +/// let response = ConfirmationDialog::new("Confirm Action", "Are you sure?") +/// .show(ui); +/// +/// match response.inner { +/// ConfirmationDialogResponse::Confirmed => println!("User confirmed"), +/// ConfirmationDialogResponse::Canceled => println!("User canceled"), +/// ConfirmationDialogResponse::None => println!("Dialog still open"), +/// _ => {} +/// }; +/// # } +/// ``` +/// +/// With custom styling: +/// ```rust +/// # use dash_evo_tool::ui::components::confirmation_dialog::ConfirmationDialog; +/// # use egui::Ui; +/// # fn example(ui: &mut Ui) { +/// let response = ConfirmationDialog::new("Delete Item", "This action cannot be undone") +/// .confirm_text("Delete") +/// .cancel_text("Keep") +/// .danger_mode(true) +/// .show(ui); +/// # } +/// ``` +pub struct ConfirmationDialog { + title: String, + message: String, + confirm_text: String, + cancel_text: String, + danger_mode: bool, + is_open: bool, + on_confirm: Option, + on_cancel: Option, +} + +impl ConfirmationDialog { + /// Create a new confirmation dialog with the given title and message + pub fn new(title: impl Into, message: impl Into) -> Self { + Self { + title: title.into(), + message: message.into(), + confirm_text: "Confirm".to_string(), + cancel_text: "Cancel".to_string(), + danger_mode: false, + is_open: true, + on_confirm: None, + on_cancel: None, + } + } + + /// Set the text for the confirm button + pub fn confirm_text(mut self, text: impl Into) -> Self { + self.confirm_text = text.into(); + self + } + + /// Set the text for the cancel button + pub fn cancel_text(mut self, text: impl Into) -> Self { + self.cancel_text = text.into(); + self + } + + /// Enable danger mode (red confirm button) for destructive actions + pub fn danger_mode(mut self, enabled: bool) -> Self { + self.danger_mode = enabled; + self + } + + /// Set whether the dialog is open + pub fn open(mut self, open: bool) -> Self { + self.is_open = open; + self + } +} + +impl ConfirmationDialog +where + F: FnOnce(), + G: FnOnce(), +{ + /// Set a callback to execute when the user clicks the confirm button + pub fn on_confirm(self, callback: F2) -> ConfirmationDialog + where + F2: FnOnce(), + { + ConfirmationDialog { + title: self.title, + message: self.message, + confirm_text: self.confirm_text, + cancel_text: self.cancel_text, + danger_mode: self.danger_mode, + is_open: self.is_open, + on_confirm: Some(callback), + on_cancel: self.on_cancel, + } + } + + /// Set a callback to execute when the user clicks the cancel button or closes the dialog + pub fn on_cancel(self, callback: G2) -> ConfirmationDialog + where + G2: FnOnce(), + { + ConfirmationDialog { + title: self.title, + message: self.message, + confirm_text: self.confirm_text, + cancel_text: self.cancel_text, + danger_mode: self.danger_mode, + is_open: self.is_open, + on_confirm: self.on_confirm, + on_cancel: Some(callback), + } + } + + /// Show the dialog and return the user's response + pub fn show(self, ui: &mut Ui) -> InnerResponse { + let mut is_open = self.is_open; + let mut on_ok = self.on_confirm; + let mut on_cancel = self.on_cancel; + + if !is_open { + return InnerResponse::new( + ConfirmationDialogResponse::Canceled, + ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()), + ); + } + + let mut final_response = ConfirmationDialogResponse::None; + let window_response = egui::Window::new(&self.title) + .collapsible(false) + .resizable(false) + .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) + .open(&mut is_open) + .show(ui.ctx(), |ui| { + // Set minimum width for the dialog + ui.set_min_width(300.0); + + // Message content + ui.add_space(10.0); + ui.label(&self.message); + ui.add_space(20.0); + + // Buttons + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // Confirm button + let confirm_button = if self.danger_mode { + egui::Button::new( + RichText::new(&self.confirm_text).color(Color32::WHITE), + ) + .fill(Color32::from_rgb(220, 53, 69)) // Red for danger + } else { + egui::Button::new( + RichText::new(&self.confirm_text).color(Color32::WHITE), + ) + .fill(Color32::from_rgb(0, 128, 255)) // Blue for primary + }; + + if ui.add(confirm_button).clicked() { + final_response = ConfirmationDialogResponse::Confirmed; + if let Some(callback) = on_ok.take() { + callback(); + } + } + + ui.add_space(10.0); + + // Cancel button + let cancel_button = egui::Button::new(&self.cancel_text) + .fill(Color32::from_rgb(108, 117, 125)); // Gray for secondary + + if ui.add(cancel_button).clicked() { + final_response = ConfirmationDialogResponse::Canceled; + if let Some(callback) = on_cancel.take() { + callback(); + } + } + }); + }); + + ui.add_space(10.0); + }); + + // Handle window being closed via X button - treat as cancel + if !is_open && matches!(final_response, ConfirmationDialogResponse::None) { + final_response = ConfirmationDialogResponse::Canceled; + if let Some(callback) = on_cancel.take() { + callback(); + } + } + + if let Some(window_response) = window_response { + InnerResponse::new(final_response, window_response.response) + } else { + InnerResponse::new( + final_response, + ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()), + ) + } + } +} + +impl Widget for ConfirmationDialog +where + F: FnOnce(), + G: FnOnce(), +{ + fn ui(self, ui: &mut Ui) -> egui::Response { + let inner_response = self.show(ui); + inner_response.response + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + + #[test] + fn test_confirmation_dialog_creation() { + let dialog = ConfirmationDialog::new("Test Title", "Test Message") + .confirm_text("Yes") + .cancel_text("No") + .danger_mode(true); + + assert_eq!(dialog.title, "Test Title"); + assert_eq!(dialog.message, "Test Message"); + assert_eq!(dialog.confirm_text, "Yes"); + assert_eq!(dialog.cancel_text, "No"); + assert!(dialog.danger_mode); + assert!(dialog.is_open); + } + + #[test] + fn test_confirmation_dialog_with_callbacks() { + let ok_called = Arc::new(Mutex::new(false)); + let ok_called_clone = ok_called.clone(); + + let cancel_called = Arc::new(Mutex::new(false)); + let cancel_called_clone = cancel_called.clone(); + + let _dialog = ConfirmationDialog::new("Test", "Test message") + .on_confirm(move || { + *ok_called_clone.lock().unwrap() = true; + }) + .on_cancel(move || { + *cancel_called_clone.lock().unwrap() = true; + }); + + // Test that the dialog can be created with callbacks + // (We can't easily test the actual callback execution without a UI context) + assert!(!*ok_called.lock().unwrap()); + assert!(!*cancel_called.lock().unwrap()); + } +} diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index d987a778b..77958044d 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -1,3 +1,4 @@ +pub mod confirmation_dialog; pub mod contract_chooser_panel; pub mod dpns_subscreen_chooser_panel; pub mod entropy_grid; diff --git a/src/ui/components/styled.rs b/src/ui/components/styled.rs index a9f5ac01e..9cc441bab 100644 --- a/src/ui/components/styled.rs +++ b/src/ui/components/styled.rs @@ -9,6 +9,9 @@ use egui::{ Ui, Vec2, }; +// Re-export commonly used components +pub use super::confirmation_dialog::{ConfirmationDialog, ConfirmationDialogResponse}; + /// Styled button variants #[allow(dead_code)] pub(crate) enum ButtonVariant { diff --git a/src/ui/identities/transfer_screen.rs b/src/ui/identities/transfer_screen.rs index 15fb5beaa..52c3ebebd 100644 --- a/src/ui/identities/transfer_screen.rs +++ b/src/ui/identities/transfer_screen.rs @@ -5,7 +5,9 @@ use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::island_central_panel; +use crate::ui::components::styled::{ + ConfirmationDialog, ConfirmationDialogResponse, island_central_panel, +}; use crate::ui::components::top_panel::add_top_panel; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::{MessageType, Screen, ScreenLike}; @@ -112,90 +114,118 @@ impl TransferScreen { }); } - fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut app_action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Transfer") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - let identifier = if self.receiver_identity_id.is_empty() { - self.error_message = Some("Invalid identifier".to_string()); - self.transfer_credits_status = - TransferCreditsStatus::ErrorMessage("Invalid identifier".to_string()); - self.confirmation_popup = false; - return; - } else { - match Identifier::from_string_try_encodings( - &self.receiver_identity_id, - &[Encoding::Base58, Encoding::Hex], - ) { - Ok(identifier) => identifier, - Err(_) => { - self.error_message = Some("Invalid identifier".to_string()); - self.transfer_credits_status = TransferCreditsStatus::ErrorMessage( - "Invalid identifier".to_string(), - ); - self.confirmation_popup = false; - return; - } - } - }; - - let Some(selected_key) = self.selected_key.as_ref() else { - self.error_message = Some("No selected key".to_string()); - self.transfer_credits_status = - TransferCreditsStatus::ErrorMessage("No selected key".to_string()); - self.confirmation_popup = false; - return; - }; - - ui.label(format!( - "Are you sure you want to transfer {} Dash to {}", - self.amount, self.receiver_identity_id - )); - let parts: Vec<&str> = self.amount.split('.').collect(); - let mut credits: u128 = 0; - - // Process the whole number part if it exists. - if let Some(whole) = parts.first() { - if let Ok(whole_number) = whole.parse::() { - credits += whole_number * 100_000_000_000; // Whole Dash amount to credits - } - } + /// Handle the confirmation action when user clicks OK + fn confirmation_ok(&mut self) -> AppAction { + self.confirmation_popup = false; - // Process the fractional part if it exists. - if let Some(fraction) = parts.get(1) { - let fraction_length = fraction.len(); - let fraction_number = fraction.parse::().unwrap_or(0); - // Calculate the multiplier based on the number of digits in the fraction. - let multiplier = 10u128.pow(11 - fraction_length as u32); - credits += fraction_number * multiplier; // Fractional Dash to credits - } + // Validate identifier + let identifier = match self.validate_receiver_identifier() { + Ok(id) => id, + Err(error) => { + self.set_error_state(error); + return AppAction::None; + } + }; + + // Validate selected key + let selected_key = match self.selected_key.as_ref() { + Some(key) => key, + None => { + self.set_error_state("No selected key".to_string()); + return AppAction::None; + } + }; + + // Parse amount to credits + let credits = match self.parse_amount_to_credits() { + Ok(amount) => amount, + Err(error) => { + self.set_error_state(error); + return AppAction::None; + } + }; + + // Set waiting state and create backend task + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.transfer_credits_status = TransferCreditsStatus::WaitingForResult(now); + + AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::Transfer( + self.identity.clone(), + identifier, + credits as Credits, + Some(selected_key.id()), + ))) + } - if ui.button("Confirm").clicked() { - self.confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.transfer_credits_status = TransferCreditsStatus::WaitingForResult(now); - app_action = - AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::Transfer( - self.identity.clone(), - identifier, - credits as Credits, - Some(selected_key.id()), - ))); - } - if ui.button("Cancel").clicked() { - self.confirmation_popup = false; - } - }); - if !is_open { - self.confirmation_popup = false; + /// Handle the cancel action when user clicks Cancel or closes dialog + fn confirmation_cancel(&mut self) -> AppAction { + self.confirmation_popup = false; + AppAction::None + } + + /// Validate the receiver identity identifier + fn validate_receiver_identifier(&self) -> Result { + if self.receiver_identity_id.is_empty() { + return Err("Invalid identifier".to_string()); + } + + Identifier::from_string_try_encodings( + &self.receiver_identity_id, + &[Encoding::Base58, Encoding::Hex], + ) + .map_err(|_| "Invalid identifier".to_string()) + } + + /// Parse the amount string to credits + fn parse_amount_to_credits(&self) -> Result { + let parts: Vec<&str> = self.amount.split('.').collect(); + let mut credits: u128 = 0; + + // Process the whole number part if it exists + if let Some(whole) = parts.first() { + if let Ok(whole_number) = whole.parse::() { + credits += whole_number * 100_000_000_000; // Whole Dash amount to credits + } + } + + // Process the fractional part if it exists + if let Some(fraction) = parts.get(1) { + let fraction_length = fraction.len(); + let fraction_number = fraction + .parse::() + .map_err(|_| "Invalid amount format".to_string())?; + // Calculate the multiplier based on the number of digits in the fraction + let multiplier = 10u128.pow(11 - fraction_length as u32); + credits += fraction_number * multiplier; // Fractional Dash to credits + } + + Ok(credits) + } + + /// Set error state with the given message + fn set_error_state(&mut self, error: String) { + self.error_message = Some(error.clone()); + self.transfer_credits_status = TransferCreditsStatus::ErrorMessage(error); + } + + fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { + let msg = format!( + "Are you sure you want to transfer {} Dash to {}?", + self.amount, self.receiver_identity_id + ); + let response = ConfirmationDialog::new("Confirm Transfer", msg) + .confirm_text("Confirm") + .cancel_text("Cancel") + .show(ui); + + match response.inner { + ConfirmationDialogResponse::Confirmed => self.confirmation_ok(), + ConfirmationDialogResponse::Canceled => self.confirmation_cancel(), + ConfirmationDialogResponse::None => AppAction::None, } - app_action } pub fn show_success(&self, ui: &mut Ui) -> AppAction { From 7621f721ab5a126578123cabb593cde9851f9324 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:22:44 +0200 Subject: [PATCH 02/13] chore: update CLAUDE to create reusable components whenever appropriate --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 0bfa8aa2b..799f3a55b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,6 +43,8 @@ cross build --target x86_64-pc-windows-gnu --release - **Async Backend Tasks**: Communication via crossbeam channels with result handling - **Network Isolation**: Separate app contexts per network with independent databases - **Real-time Updates**: ZMQ listeners for core blockchain events on network-specific ports +- **Custom UI components**: we build a library of reusable widgets in `ui/components` whenever we need similar + widget displayed in more than 2 places ### Critical Dependencies - **dash-sdk**: Core Dash Platform SDK (git dependency, specific revision) From 1697c629c0da0c0b099e0a78ebec2c5738d97a81 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:20:50 +0200 Subject: [PATCH 03/13] feat: correct confirm dialog on set token price screen --- src/ui/tokens/set_token_price_screen.rs | 310 ++++++++++++++---------- 1 file changed, 178 insertions(+), 132 deletions(-) diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index 374071c9b..7589d078d 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -5,7 +5,7 @@ use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::wallet::Wallet; use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::island_central_panel; +use crate::ui::components::styled::{island_central_panel, ConfirmationDialog, ConfirmationDialogResponse}; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; @@ -489,127 +489,188 @@ impl SetTokenPriceScreen { } } - /// Renders a confirm popup with the final "Are you sure?" step - fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm SetPricingSchedule") - .collapsible(false) - .open(&mut is_open) - .frame( - egui::Frame::default() - .fill(Color32::from_rgb(245, 245, 245)) - .stroke(egui::Stroke::new(1.0, Color32::from_rgb(200, 200, 200))) - .shadow(egui::epaint::Shadow::default()) - .inner_margin(egui::Margin::same(20)) - .corner_radius(egui::CornerRadius::same(8)), - ) - .show(ui.ctx(), |ui| { - // Validate user input - let token_pricing_schedule_opt = match self.create_pricing_schedule() { - Ok(schedule) => schedule, - Err(error) => { - self.error_message = Some(error.clone()); - self.status = SetTokenPriceStatus::ErrorMessage(error); - self.show_confirmation_popup = false; - return; + /// Validate the current pricing configuration before showing confirmation dialog + fn validate_pricing_configuration(&self) -> Result<(), String> { + match self.pricing_type { + PricingType::RemovePricing => Ok(()), + PricingType::SinglePrice => { + if self.single_price.trim().is_empty() { + return Err("Please enter a price".to_string()); + } + match self.single_price.trim().parse::() { + Ok(price) if price > 0.0 => Ok(()), + Ok(_) => Err("Price must be greater than 0".to_string()), + Err(_) => Err("Invalid price format - must be a positive number".to_string()), + } + } + PricingType::TieredPricing => { + let mut valid_tiers = 0; + + for (amount_str, price_str) in &self.tiered_prices { + if amount_str.trim().is_empty() || price_str.trim().is_empty() { + continue; } - }; - // Show confirmation message based on pricing type - match &self.pricing_type { - PricingType::RemovePricing => { - ui.colored_label( - Color32::from_rgb(180, 100, 0), - "WARNING: Are you sure you want to remove the pricing schedule?", - ); - ui.label("This will make the token unavailable for direct purchase."); + let _amount = amount_str.trim().parse::().map_err(|_| { + format!( + "Invalid amount '{}' - must be a whole number", + amount_str.trim() + ) + })?; + + let price = price_str.trim().parse::().map_err(|_| { + format!( + "Invalid price '{}' - must be a positive number", + price_str.trim() + ) + })?; + + if price <= 0.0 { + return Err(format!( + "Price '{}' must be greater than 0", + price_str.trim() + )); } - PricingType::SinglePrice => { - if let Ok(dash_price) = self.single_price.trim().parse::() { - ui.label(format!( - "Are you sure you want to set a fixed price of {} Dash per token?", - dash_price - )); - } + + valid_tiers += 1; + } + + if valid_tiers == 0 { + return Err("Please add at least one valid pricing tier".to_string()); + } + + Ok(()) + } + } + } + + /// Generate the confirmation message for the set price dialog + /// + /// ## Panics + /// + /// Panics if the pricing type is not set correctly or if the single price is not a valid number. + fn confirmation_message(&self) -> String { + match &self.pricing_type { + PricingType::RemovePricing => { + "WARNING: Are you sure you want to remove the pricing schedule? This will make the token unavailable for direct purchase.".to_string() + } + PricingType::SinglePrice => { + if let Ok(dash_price) = self.single_price.trim().parse::() { + format!( + "Are you sure you want to set a fixed price of {} Dash per token?", + dash_price + ) + } else { + "Are you sure you want to set the pricing schedule?".to_string() + } + } + PricingType::TieredPricing => { + let mut message = "Are you sure you want to set the following tiered pricing?".to_string(); + for (amount_str, price_str) in &self.tiered_prices { + if amount_str.trim().is_empty() || price_str.trim().is_empty() { + continue; } - PricingType::TieredPricing => { - ui.label("Are you sure you want to set the following tiered pricing?"); - ui.add_space(5.0); - for (amount_str, price_str) in &self.tiered_prices { - if amount_str.trim().is_empty() || price_str.trim().is_empty() { - continue; - } - if let (Ok(amount), Ok(dash_price)) = ( - amount_str.trim().parse::(), - price_str.trim().parse::(), - ) { - ui.label(format!( - " - {} or more tokens: {} Dash each", - amount, dash_price - )); - } - } + if let (Ok(amount), Ok(dash_price)) = ( + amount_str.trim().parse::(), + price_str.trim().parse::(), + ) { + message.push_str(&format!( + " + - {} or more tokens: {} Dash each", + amount, dash_price + )); } } + message + } + } + } - ui.add_space(10.0); + /// Handle the confirmation action when user clicks OK + fn confirmation_ok(&mut self) -> AppAction { + self.show_confirmation_popup = false; + + // Validate user input and create pricing schedule + let token_pricing_schedule_opt = match self.create_pricing_schedule() { + Ok(schedule) => schedule, + Err(error) => { + // This should not happen if validation was done before opening dialog, + // but we handle it as a safety net + self.set_error_state(format!("Validation error: {}", error)); + return AppAction::None; + } + }; - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = SetTokenPriceStatus::WaitingForResult(now); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch the actual backend mint action - action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::SetDirectPurchasePrice { - identity: self.identity_token_info.identity.clone(), - data_contract: Arc::new( - self.identity_token_info.data_contract.contract.clone(), - ), - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - token_pricing_schedule: token_pricing_schedule_opt, - group_info, - }, - ))); - } + // Set waiting state + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = SetTokenPriceStatus::WaitingForResult(now); + + // Prepare group info + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; - // Cancel button - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + // Create and return the backend task + AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::SetDirectPurchasePrice { + identity: self.identity_token_info.identity.clone(), + data_contract: Arc::new(self.identity_token_info.data_contract.contract.clone()), + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("Expected a key"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + token_pricing_schedule: token_pricing_schedule_opt, + group_info, + }, + ))) + } + + /// Handle the cancel action when user clicks Cancel or closes dialog + fn confirmation_cancel(&mut self) -> AppAction { + self.show_confirmation_popup = false; + AppAction::None + } + + /// Set error state with the given message + fn set_error_state(&mut self, error: String) { + self.error_message = Some(error.clone()); + self.status = SetTokenPriceStatus::ErrorMessage(error); + } - if !is_open { - self.show_confirmation_popup = false; + /// Renders a confirm popup with the final "Are you sure?" step + fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { + let response = ConfirmationDialog::new( + "Confirm pricing schedule update", + self.confirmation_message(), + ) + .confirm_text("Confirm") + .cancel_text("Cancel") + .danger_mode(self.pricing_type == PricingType::RemovePricing) + .show(ui); + + match response.inner { + ConfirmationDialogResponse::Confirmed => self.confirmation_ok(), + ConfirmationDialogResponse::Canceled => self.confirmation_cancel(), + ConfirmationDialogResponse::None => AppAction::None, } - action } /// Renders a simple "Success!" screen after completion @@ -911,25 +972,9 @@ impl ScreenLike for SetTokenPriceScreen { }; // Set price button - let can_proceed = match self.pricing_type { - PricingType::RemovePricing => true, - PricingType::SinglePrice => { - if let Ok(price) = self.single_price.trim().parse::() { - price > 0.0 - } else { - false - } - }, - PricingType::TieredPricing => { - self.tiered_prices.iter().any(|(amount, price)| { - !amount.trim().is_empty() && !price.trim().is_empty() && - amount.trim().parse::().is_ok() && - if let Ok(p) = price.trim().parse::() { p > 0.0 } else { false } - }) - } - }; + let validation_result = self.validate_pricing_configuration(); - let button_color = if can_proceed { + let button_color = if validation_result.is_ok() { Color32::from_rgb(0, 128, 255) } else { Color32::from_rgb(100, 100, 100) @@ -939,10 +984,11 @@ impl ScreenLike for SetTokenPriceScreen { .fill(button_color) .corner_radius(3.0); - let button_response = ui.add_enabled(can_proceed, button); + let button_response = ui.add_enabled(validation_result.is_ok(), button); + - if !can_proceed { - button_response.on_hover_text("Please enter valid pricing information"); + if let Err(hover_message) = validation_result { + button_response.on_disabled_hover_text(hover_message); } else if button_response.clicked() { self.show_confirmation_popup = true; } From 898b8eae0296d7b4b6c625be8922cd09a920d7ce Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:08:58 +0200 Subject: [PATCH 04/13] chore: remove callbacks which are overkill --- src/ui/components/confirmation_dialog.rs | 86 +----------------------- 1 file changed, 3 insertions(+), 83 deletions(-) diff --git a/src/ui/components/confirmation_dialog.rs b/src/ui/components/confirmation_dialog.rs index 3db2e08da..fa0a4b7cb 100644 --- a/src/ui/components/confirmation_dialog.rs +++ b/src/ui/components/confirmation_dialog.rs @@ -49,15 +49,13 @@ pub enum ConfirmationDialogResponse { /// .show(ui); /// # } /// ``` -pub struct ConfirmationDialog { +pub struct ConfirmationDialog { title: String, message: String, confirm_text: String, cancel_text: String, danger_mode: bool, is_open: bool, - on_confirm: Option, - on_cancel: Option, } impl ConfirmationDialog { @@ -70,8 +68,6 @@ impl ConfirmationDialog { cancel_text: "Cancel".to_string(), danger_mode: false, is_open: true, - on_confirm: None, - on_cancel: None, } } @@ -100,50 +96,10 @@ impl ConfirmationDialog { } } -impl ConfirmationDialog -where - F: FnOnce(), - G: FnOnce(), -{ - /// Set a callback to execute when the user clicks the confirm button - pub fn on_confirm(self, callback: F2) -> ConfirmationDialog - where - F2: FnOnce(), - { - ConfirmationDialog { - title: self.title, - message: self.message, - confirm_text: self.confirm_text, - cancel_text: self.cancel_text, - danger_mode: self.danger_mode, - is_open: self.is_open, - on_confirm: Some(callback), - on_cancel: self.on_cancel, - } - } - - /// Set a callback to execute when the user clicks the cancel button or closes the dialog - pub fn on_cancel(self, callback: G2) -> ConfirmationDialog - where - G2: FnOnce(), - { - ConfirmationDialog { - title: self.title, - message: self.message, - confirm_text: self.confirm_text, - cancel_text: self.cancel_text, - danger_mode: self.danger_mode, - is_open: self.is_open, - on_confirm: self.on_confirm, - on_cancel: Some(callback), - } - } - +impl ConfirmationDialog { /// Show the dialog and return the user's response pub fn show(self, ui: &mut Ui) -> InnerResponse { let mut is_open = self.is_open; - let mut on_ok = self.on_confirm; - let mut on_cancel = self.on_cancel; if !is_open { return InnerResponse::new( @@ -185,9 +141,6 @@ where if ui.add(confirm_button).clicked() { final_response = ConfirmationDialogResponse::Confirmed; - if let Some(callback) = on_ok.take() { - callback(); - } } ui.add_space(10.0); @@ -198,9 +151,6 @@ where if ui.add(cancel_button).clicked() { final_response = ConfirmationDialogResponse::Canceled; - if let Some(callback) = on_cancel.take() { - callback(); - } } }); }); @@ -211,9 +161,6 @@ where // Handle window being closed via X button - treat as cancel if !is_open && matches!(final_response, ConfirmationDialogResponse::None) { final_response = ConfirmationDialogResponse::Canceled; - if let Some(callback) = on_cancel.take() { - callback(); - } } if let Some(window_response) = window_response { @@ -227,11 +174,7 @@ where } } -impl Widget for ConfirmationDialog -where - F: FnOnce(), - G: FnOnce(), -{ +impl Widget for ConfirmationDialog { fn ui(self, ui: &mut Ui) -> egui::Response { let inner_response = self.show(ui); inner_response.response @@ -241,7 +184,6 @@ where #[cfg(test)] mod tests { use super::*; - use std::sync::{Arc, Mutex}; #[test] fn test_confirmation_dialog_creation() { @@ -257,26 +199,4 @@ mod tests { assert!(dialog.danger_mode); assert!(dialog.is_open); } - - #[test] - fn test_confirmation_dialog_with_callbacks() { - let ok_called = Arc::new(Mutex::new(false)); - let ok_called_clone = ok_called.clone(); - - let cancel_called = Arc::new(Mutex::new(false)); - let cancel_called_clone = cancel_called.clone(); - - let _dialog = ConfirmationDialog::new("Test", "Test message") - .on_confirm(move || { - *ok_called_clone.lock().unwrap() = true; - }) - .on_cancel(move || { - *cancel_called_clone.lock().unwrap() = true; - }); - - // Test that the dialog can be created with callbacks - // (We can't easily test the actual callback execution without a UI context) - assert!(!*ok_called.lock().unwrap()); - assert!(!*cancel_called.lock().unwrap()); - } } From a7b30c1d9ee0c1aef257157b7b9fd4c2b58453c4 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 23 Jul 2025 17:52:37 +0200 Subject: [PATCH 05/13] chore: cargo fmt --- src/ui/tokens/set_token_price_screen.rs | 6 ++++-- src/ui/tokens/tokens_screen/groups.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index 7589d078d..0b5cbeaec 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -5,7 +5,9 @@ use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::wallet::Wallet; use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::{island_central_panel, ConfirmationDialog, ConfirmationDialogResponse}; +use crate::ui::components::styled::{ + ConfirmationDialog, ConfirmationDialogResponse, island_central_panel, +}; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; @@ -985,7 +987,7 @@ impl ScreenLike for SetTokenPriceScreen { .corner_radius(3.0); let button_response = ui.add_enabled(validation_result.is_ok(), button); - + if let Err(hover_message) = validation_result { button_response.on_disabled_hover_text(hover_message); diff --git a/src/ui/tokens/tokens_screen/groups.rs b/src/ui/tokens/tokens_screen/groups.rs index 191572984..6414cd930 100644 --- a/src/ui/tokens/tokens_screen/groups.rs +++ b/src/ui/tokens/tokens_screen/groups.rs @@ -164,7 +164,7 @@ impl TokensScreen { .members .iter() .enumerate() - .filter_map(|(i, m)| if i != j && !m.identity_str.is_empty() { + .filter_map(|(i, m)| if i != j && !m.identity_str.is_empty() { let identifier = Identifier::from_string(&m.identity_str, Encoding::Base58).ok()?; Some(identifier) } else { From 4cb4a32fb6f7589fdc23cdb79bac6789bd51c6e6 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:11:05 +0200 Subject: [PATCH 06/13] chore: doctest fix --- src/ui/components/confirmation_dialog.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/components/confirmation_dialog.rs b/src/ui/components/confirmation_dialog.rs index fa0a4b7cb..7c299ed00 100644 --- a/src/ui/components/confirmation_dialog.rs +++ b/src/ui/components/confirmation_dialog.rs @@ -31,8 +31,7 @@ pub enum ConfirmationDialogResponse { /// match response.inner { /// ConfirmationDialogResponse::Confirmed => println!("User confirmed"), /// ConfirmationDialogResponse::Canceled => println!("User canceled"), -/// ConfirmationDialogResponse::None => println!("Dialog still open"), -/// _ => {} +/// ConfirmationDialogResponse::None => println!("Dialog still open") /// }; /// # } /// ``` From 2b2a003feb932571d124f88f692d122b4346124d Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:12:14 +0200 Subject: [PATCH 07/13] chore: impl Component for ConfirmationDialog --- src/ui/components/confirmation_dialog.rs | 128 ++++++++++++++++------- src/ui/components/styled.rs | 2 +- src/ui/identities/transfer_screen.rs | 42 +++++--- src/ui/tokens/set_token_price_screen.rs | 41 +++++--- 4 files changed, 143 insertions(+), 70 deletions(-) diff --git a/src/ui/components/confirmation_dialog.rs b/src/ui/components/confirmation_dialog.rs index 7c299ed00..b7f5fa0c6 100644 --- a/src/ui/components/confirmation_dialog.rs +++ b/src/ui/components/confirmation_dialog.rs @@ -1,8 +1,9 @@ -use egui::{Color32, InnerResponse, RichText, Ui, Widget}; +use crate::ui::components::component_trait::{Component, ComponentResponse}; +use egui::{Color32, InnerResponse, RichText, Ui}; /// Response from showing a confirmation dialog #[derive(Debug, Clone, PartialEq)] -pub enum ConfirmationDialogResponse { +pub enum ConfirmationStatus { /// Dialog is still open, no action taken None, /// User clicked confirm button @@ -11,6 +12,43 @@ pub enum ConfirmationDialogResponse { Canceled, } +/// Response struct for the ConfirmationDialog component following the Component trait pattern +#[derive(Debug, Clone)] +pub struct ConfirmationDialogComponentResponse { + pub response: egui::Response, + pub changed: bool, + pub error_message: Option, + pub dialog_response: ConfirmationStatus, +} + +impl ComponentResponse for ConfirmationDialogComponentResponse { + type DomainType = ConfirmationStatus; + + fn has_changed(&self) -> bool { + self.changed + } + + fn is_valid(&self) -> bool { + self.error_message.is_none() + } + + fn changed_value(&self) -> &Option { + // Return Some(status) if dialog has a response, None if still open + static CONFIRMED: Option = Some(ConfirmationStatus::Confirmed); + static CANCELED: Option = Some(ConfirmationStatus::Canceled); + static NONE: Option = None; + + match self.dialog_response { + ConfirmationStatus::Confirmed => &CONFIRMED, + ConfirmationStatus::Canceled => &CANCELED, + ConfirmationStatus::None => &NONE, + } + } + + fn error_message(&self) -> Option<&str> { + self.error_message.as_deref() + } +} /// A reusable confirmation dialog component that implements the Widget trait /// /// This component provides a consistent modal dialog for confirming user actions @@ -20,34 +58,32 @@ pub enum ConfirmationDialogResponse { /// /// # Examples /// -/// Basic usage: +/// Basic usage with Component trait: /// ```rust -/// # use dash_evo_tool::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationDialogResponse}; +/// # use dash_evo_tool::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +/// # use dash_evo_tool::ui::components::component_trait::Component; /// # use egui::Ui; /// # fn example(ui: &mut Ui) { -/// let response = ConfirmationDialog::new("Confirm Action", "Are you sure?") -/// .show(ui); +/// // In your screen struct: +/// // confirmation_dialog: Option, /// -/// match response.inner { -/// ConfirmationDialogResponse::Confirmed => println!("User confirmed"), -/// ConfirmationDialogResponse::Canceled => println!("User canceled"), -/// ConfirmationDialogResponse::None => println!("Dialog still open") -/// }; -/// # } -/// ``` +/// // In your show method: +/// let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { +/// ConfirmationDialog::new("Confirm Action", "Are you sure?") +/// }); /// -/// With custom styling: -/// ```rust -/// # use dash_evo_tool::ui::components::confirmation_dialog::ConfirmationDialog; -/// # use egui::Ui; -/// # fn example(ui: &mut Ui) { -/// let response = ConfirmationDialog::new("Delete Item", "This action cannot be undone") -/// .confirm_text("Delete") -/// .cancel_text("Keep") -/// .danger_mode(true) -/// .show(ui); +/// let response = confirmation_dialog.show(ui); +/// +/// if let Some(status) = response.inner.changed_value() { +/// match status { +/// ConfirmationStatus::Confirmed => println!("User confirmed"), +/// ConfirmationStatus::Canceled => println!("User canceled/closed"), +/// ConfirmationStatus::None => {} // This won't happen in changed_value() +/// } +/// } /// # } /// ``` +/// pub struct ConfirmationDialog { title: String, message: String, @@ -57,6 +93,27 @@ pub struct ConfirmationDialog { is_open: bool, } +impl Component for ConfirmationDialog { + type DomainType = ConfirmationStatus; + type Response = ConfirmationDialogComponentResponse; + + fn show(&mut self, ui: &mut Ui) -> InnerResponse { + let inner_response = self.show_dialog(ui); + let changed = !matches!(inner_response.inner, ConfirmationStatus::None); + let response = inner_response.response; + + InnerResponse::new( + ConfirmationDialogComponentResponse { + response: response.clone(), + changed, + error_message: None, // Confirmation dialogs don't have validation errors + dialog_response: inner_response.inner, + }, + response, + ) + } +} + impl ConfirmationDialog { /// Create a new confirmation dialog with the given title and message pub fn new(title: impl Into, message: impl Into) -> Self { @@ -97,17 +154,17 @@ impl ConfirmationDialog { impl ConfirmationDialog { /// Show the dialog and return the user's response - pub fn show(self, ui: &mut Ui) -> InnerResponse { + pub fn show_dialog(&mut self, ui: &mut Ui) -> InnerResponse { let mut is_open = self.is_open; if !is_open { return InnerResponse::new( - ConfirmationDialogResponse::Canceled, + ConfirmationStatus::Canceled, ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()), ); } - let mut final_response = ConfirmationDialogResponse::None; + let mut final_response = ConfirmationStatus::None; let window_response = egui::Window::new(&self.title) .collapsible(false) .resizable(false) @@ -139,7 +196,7 @@ impl ConfirmationDialog { }; if ui.add(confirm_button).clicked() { - final_response = ConfirmationDialogResponse::Confirmed; + final_response = ConfirmationStatus::Confirmed; } ui.add_space(10.0); @@ -149,7 +206,7 @@ impl ConfirmationDialog { .fill(Color32::from_rgb(108, 117, 125)); // Gray for secondary if ui.add(cancel_button).clicked() { - final_response = ConfirmationDialogResponse::Canceled; + final_response = ConfirmationStatus::Canceled; } }); }); @@ -158,10 +215,13 @@ impl ConfirmationDialog { }); // Handle window being closed via X button - treat as cancel - if !is_open && matches!(final_response, ConfirmationDialogResponse::None) { - final_response = ConfirmationDialogResponse::Canceled; + if !is_open && matches!(final_response, ConfirmationStatus::None) { + final_response = ConfirmationStatus::Canceled; } + // Update the dialog's open state + self.is_open = is_open; + if let Some(window_response) = window_response { InnerResponse::new(final_response, window_response.response) } else { @@ -172,14 +232,6 @@ impl ConfirmationDialog { } } } - -impl Widget for ConfirmationDialog { - fn ui(self, ui: &mut Ui) -> egui::Response { - let inner_response = self.show(ui); - inner_response.response - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/ui/components/styled.rs b/src/ui/components/styled.rs index 9cc441bab..ad83e5799 100644 --- a/src/ui/components/styled.rs +++ b/src/ui/components/styled.rs @@ -10,7 +10,7 @@ use egui::{ }; // Re-export commonly used components -pub use super::confirmation_dialog::{ConfirmationDialog, ConfirmationDialogResponse}; +pub use super::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; /// Styled button variants #[allow(dead_code)] diff --git a/src/ui/identities/transfer_screen.rs b/src/ui/identities/transfer_screen.rs index 6389825f8..c908c33ec 100644 --- a/src/ui/identities/transfer_screen.rs +++ b/src/ui/identities/transfer_screen.rs @@ -6,13 +6,12 @@ use crate::model::amount::Amount; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::component_trait::{Component, ComponentResponse}; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::{ - ConfirmationDialog, ConfirmationDialogResponse, island_central_panel, -}; +use crate::ui::components::styled::island_central_panel; use crate::ui::components::top_panel::add_top_panel; -use crate::ui::components::{Component, ComponentResponse}; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::{MessageType, Screen, ScreenLike}; use dash_sdk::dpp::fee::Credits; @@ -53,6 +52,7 @@ pub struct TransferScreen { max_amount: u64, pub app_context: Arc, confirmation_popup: bool, + confirmation_dialog: Option, selected_wallet: Option>>, wallet_password: String, show_password: bool, @@ -87,6 +87,7 @@ impl TransferScreen { max_amount, app_context: app_context.clone(), confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -156,6 +157,7 @@ impl TransferScreen { /// Handle the confirmation action when user clicks OK fn confirmation_ok(&mut self) -> AppAction { self.confirmation_popup = false; + self.confirmation_dialog = None; // Reset the dialog for next use // Validate identifier let identifier = match self.validate_receiver_identifier() { @@ -203,6 +205,7 @@ impl TransferScreen { /// Handle the cancel action when user clicks Cancel or closes dialog fn confirmation_cancel(&mut self) -> AppAction { self.confirmation_popup = false; + self.confirmation_dialog = None; // Reset the dialog for next use AppAction::None } @@ -226,24 +229,33 @@ impl TransferScreen { } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { + // Prepare values before borrowing let Some(amount) = &self.amount else { - self.set_error_state("Amount is not set".to_string()); + self.set_error_state("Incorrect or empty amount".to_string()); return AppAction::None; }; + let receiver_id = self.receiver_identity_id.clone(); + let msg = format!( "Are you sure you want to transfer {} to {}?", - amount, self.receiver_identity_id + amount, receiver_id ); - let response = ConfirmationDialog::new("Confirm Transfer", msg) - .confirm_text("Confirm") - .cancel_text("Cancel") - .show(ui); - - match response.inner { - ConfirmationDialogResponse::Confirmed => self.confirmation_ok(), - ConfirmationDialogResponse::Canceled => self.confirmation_cancel(), - ConfirmationDialogResponse::None => AppAction::None, + + // Lazy initialization of the confirmation dialog + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Transfer", msg) + .confirm_text("Confirm") + .cancel_text("Cancel") + }); + + let response = confirmation_dialog.show(ui); + + // Handle the response using the Component pattern + match response.inner.dialog_response { + ConfirmationStatus::Confirmed => self.confirmation_ok(), + ConfirmationStatus::Canceled => self.confirmation_cancel(), + ConfirmationStatus::None => AppAction::None, } } diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index 0b5cbeaec..5dc55ccea 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -4,10 +4,10 @@ use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::wallet::Wallet; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::{ - ConfirmationDialog, ConfirmationDialogResponse, island_central_panel, -}; +use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; @@ -76,6 +76,7 @@ pub struct SetTokenPriceScreen { /// Confirmation popup show_confirmation_popup: bool, + confirmation_dialog: Option, // If needed for password-based wallet unlocking: selected_wallet: Option>>, @@ -208,6 +209,7 @@ impl SetTokenPriceScreen { error_message: None, app_context: app_context.clone(), show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -591,6 +593,7 @@ impl SetTokenPriceScreen { /// Handle the confirmation action when user clicks OK fn confirmation_ok(&mut self) -> AppAction { self.show_confirmation_popup = false; + self.confirmation_dialog = None; // Reset the dialog for next use // Validate user input and create pricing schedule let token_pricing_schedule_opt = match self.create_pricing_schedule() { @@ -648,6 +651,7 @@ impl SetTokenPriceScreen { /// Handle the cancel action when user clicks Cancel or closes dialog fn confirmation_cancel(&mut self) -> AppAction { self.show_confirmation_popup = false; + self.confirmation_dialog = None; // Reset the dialog for next use AppAction::None } @@ -659,19 +663,24 @@ impl SetTokenPriceScreen { /// Renders a confirm popup with the final "Are you sure?" step fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let response = ConfirmationDialog::new( - "Confirm pricing schedule update", - self.confirmation_message(), - ) - .confirm_text("Confirm") - .cancel_text("Cancel") - .danger_mode(self.pricing_type == PricingType::RemovePricing) - .show(ui); - - match response.inner { - ConfirmationDialogResponse::Confirmed => self.confirmation_ok(), - ConfirmationDialogResponse::Canceled => self.confirmation_cancel(), - ConfirmationDialogResponse::None => AppAction::None, + // Prepare values before borrowing + let confirmation_message = self.confirmation_message(); + let is_danger_mode = self.pricing_type == PricingType::RemovePricing; + + // Lazy initialization of the confirmation dialog + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm pricing schedule update", confirmation_message) + .confirm_text("Confirm") + .cancel_text("Cancel") + .danger_mode(is_danger_mode) + }); + + let response = confirmation_dialog.show(ui); + + match response.inner.dialog_response { + ConfirmationStatus::Confirmed => self.confirmation_ok(), + ConfirmationStatus::Canceled => self.confirmation_cancel(), + ConfirmationStatus::None => AppAction::None, } } From aecac41532fe6307e3367b2c77b3c078278a6576 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:40:21 +0200 Subject: [PATCH 08/13] chore: use WidgetText --- src/ui/components/confirmation_dialog.rs | 183 +++++++++++++---------- src/ui/identities/transfer_screen.rs | 4 +- src/ui/tokens/set_token_price_screen.rs | 4 +- 3 files changed, 110 insertions(+), 81 deletions(-) diff --git a/src/ui/components/confirmation_dialog.rs b/src/ui/components/confirmation_dialog.rs index b7f5fa0c6..232a713c5 100644 --- a/src/ui/components/confirmation_dialog.rs +++ b/src/ui/components/confirmation_dialog.rs @@ -1,5 +1,5 @@ use crate::ui::components::component_trait::{Component, ComponentResponse}; -use egui::{Color32, InnerResponse, RichText, Ui}; +use egui::{Color32, InnerResponse, Ui, WidgetText}; /// Response from showing a confirmation dialog #[derive(Debug, Clone, PartialEq)] @@ -12,6 +12,7 @@ pub enum ConfirmationStatus { Canceled, } +pub const NOTHING: Option<&str> = None; /// Response struct for the ConfirmationDialog component following the Component trait pattern #[derive(Debug, Clone)] pub struct ConfirmationDialogComponentResponse { @@ -49,46 +50,17 @@ impl ComponentResponse for ConfirmationDialogComponentResponse { self.error_message.as_deref() } } -/// A reusable confirmation dialog component that implements the Widget trait +/// A reusable confirmation dialog component that implements the Component trait /// /// This component provides a consistent modal dialog for confirming user actions -/// across the application. It supports customizable titles, messages, button text, -/// styling options including a danger mode for destructive actions, and callback -/// functions for handling user responses. -/// -/// # Examples -/// -/// Basic usage with Component trait: -/// ```rust -/// # use dash_evo_tool::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; -/// # use dash_evo_tool::ui::components::component_trait::Component; -/// # use egui::Ui; -/// # fn example(ui: &mut Ui) { -/// // In your screen struct: -/// // confirmation_dialog: Option, -/// -/// // In your show method: -/// let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { -/// ConfirmationDialog::new("Confirm Action", "Are you sure?") -/// }); -/// -/// let response = confirmation_dialog.show(ui); -/// -/// if let Some(status) = response.inner.changed_value() { -/// match status { -/// ConfirmationStatus::Confirmed => println!("User confirmed"), -/// ConfirmationStatus::Canceled => println!("User canceled/closed"), -/// ConfirmationStatus::None => {} // This won't happen in changed_value() -/// } -/// } -/// # } -/// ``` -/// +/// across the application. It supports customizable titles, messages, button text +/// with rich formatting (using WidgetText for styling), danger mode for destructive +/// actions, and optional buttons (confirm and cancel buttons can be hidden independently). pub struct ConfirmationDialog { - title: String, - message: String, - confirm_text: String, - cancel_text: String, + title: WidgetText, + message: WidgetText, + confirm_text: Option, + cancel_text: Option, danger_mode: bool, is_open: bool, } @@ -116,26 +88,26 @@ impl Component for ConfirmationDialog { impl ConfirmationDialog { /// Create a new confirmation dialog with the given title and message - pub fn new(title: impl Into, message: impl Into) -> Self { + pub fn new(title: impl Into, message: impl Into) -> Self { Self { title: title.into(), message: message.into(), - confirm_text: "Confirm".to_string(), - cancel_text: "Cancel".to_string(), + confirm_text: Some("Confirm".into()), + cancel_text: Some("Cancel".into()), danger_mode: false, is_open: true, } } - /// Set the text for the confirm button - pub fn confirm_text(mut self, text: impl Into) -> Self { - self.confirm_text = text.into(); + /// Set the text for the confirm button, or None to hide it + pub fn confirm_text(mut self, text: Option>) -> Self { + self.confirm_text = text.map(|t| t.into()); self } - /// Set the text for the cancel button - pub fn cancel_text(mut self, text: impl Into) -> Self { - self.cancel_text = text.into(); + /// Set the text for the cancel button, or None to hide it + pub fn cancel_text(mut self, text: Option>) -> Self { + self.cancel_text = text.map(|t| t.into()); self } @@ -165,7 +137,7 @@ impl ConfirmationDialog { } let mut final_response = ConfirmationStatus::None; - let window_response = egui::Window::new(&self.title) + let window_response = egui::Window::new(self.title.clone()) .collapsible(false) .resizable(false) .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) @@ -176,37 +148,40 @@ impl ConfirmationDialog { // Message content ui.add_space(10.0); - ui.label(&self.message); + ui.label(self.message.clone()); ui.add_space(20.0); // Buttons ui.horizontal(|ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // Confirm button - let confirm_button = if self.danger_mode { - egui::Button::new( - RichText::new(&self.confirm_text).color(Color32::WHITE), - ) - .fill(Color32::from_rgb(220, 53, 69)) // Red for danger - } else { - egui::Button::new( - RichText::new(&self.confirm_text).color(Color32::WHITE), - ) - .fill(Color32::from_rgb(0, 128, 255)) // Blue for primary - }; - - if ui.add(confirm_button).clicked() { - final_response = ConfirmationStatus::Confirmed; - } + // Confirm button (only if text is provided) + if let Some(confirm_text) = &self.confirm_text { + let confirm_button = if self.danger_mode { + egui::Button::new(confirm_text.clone()) + .fill(Color32::from_rgb(220, 53, 69)) // Red for danger + } else { + egui::Button::new(confirm_text.clone()) + .fill(Color32::from_rgb(0, 128, 255)) // Blue for primary + }; - ui.add_space(10.0); + if ui.add(confirm_button).clicked() { + final_response = ConfirmationStatus::Confirmed; + } - // Cancel button - let cancel_button = egui::Button::new(&self.cancel_text) - .fill(Color32::from_rgb(108, 117, 125)); // Gray for secondary + // Add space only if both buttons are present + if self.cancel_text.is_some() { + ui.add_space(10.0); + } + } - if ui.add(cancel_button).clicked() { - final_response = ConfirmationStatus::Canceled; + // Cancel button (only if text is provided) + if let Some(cancel_text) = &self.cancel_text { + let cancel_button = egui::Button::new(cancel_text.clone()) + .fill(Color32::from_rgb(108, 117, 125)); // Gray for secondary + + if ui.add(cancel_button).clicked() { + final_response = ConfirmationStatus::Canceled; + } } }); }); @@ -219,6 +194,18 @@ impl ConfirmationDialog { final_response = ConfirmationStatus::Canceled; } + // If no buttons are present, the user can only close via the X button or pressing Escape + // In that case, we treat it as canceled + if self.confirm_text.is_none() + && self.cancel_text.is_none() + && matches!(final_response, ConfirmationStatus::None) + { + // Check if user pressed Escape + if ui.input(|i| i.key_pressed(egui::Key::Escape)) { + final_response = ConfirmationStatus::Canceled; + } + } + // Update the dialog's open state self.is_open = is_open; @@ -239,15 +226,57 @@ mod tests { #[test] fn test_confirmation_dialog_creation() { let dialog = ConfirmationDialog::new("Test Title", "Test Message") - .confirm_text("Yes") - .cancel_text("No") + .confirm_text(Some("Yes")) + .cancel_text(Some("No")) .danger_mode(true); - assert_eq!(dialog.title, "Test Title"); - assert_eq!(dialog.message, "Test Message"); - assert_eq!(dialog.confirm_text, "Yes"); - assert_eq!(dialog.cancel_text, "No"); + assert_eq!(dialog.title.text(), "Test Title"); + assert_eq!(dialog.message.text(), "Test Message"); + assert!(dialog.confirm_text.is_some_and(|t| t.text() == "Yes")); + assert!(dialog.cancel_text.is_some_and(|t| t.text() == "No")); assert!(dialog.danger_mode); assert!(dialog.is_open); } + + #[test] + fn test_confirmation_dialog_no_buttons() { + let dialog = ConfirmationDialog::new("Test Title", "Test Message") + .confirm_text(NOTHING) + .cancel_text(NOTHING); + + assert_eq!(dialog.title.text(), "Test Title"); + assert_eq!(dialog.message.text(), "Test Message"); + assert!(dialog.confirm_text.is_none()); + assert!(dialog.cancel_text.is_none()); + assert!(!dialog.danger_mode); + assert!(dialog.is_open); + } + + #[test] + fn test_confirmation_dialog_only_confirm_button() { + let dialog = ConfirmationDialog::new("Test Title", "Test Message") + .confirm_text(Some("OK")) + .cancel_text(NOTHING); + + assert_eq!(dialog.title.text(), "Test Title"); + assert_eq!(dialog.message.text(), "Test Message"); + assert!(dialog.confirm_text.is_some()); + assert!(dialog.cancel_text.is_none()); + assert!(!dialog.danger_mode); + assert!(dialog.is_open); + } + + #[test] + fn test_confirmation_dialog_only_cancel_button() { + let dialog = ConfirmationDialog::new("Test Title", "Test Message") + .confirm_text(NOTHING) + .cancel_text(Some("Close")); + + assert_eq!(dialog.title.text(), "Test Title"); + assert_eq!(dialog.message.text(), "Test Message"); + assert!(dialog.confirm_text.is_none()); + assert!(dialog.cancel_text.is_some()); + assert!(!dialog.danger_mode); + assert!(dialog.is_open); + } } diff --git a/src/ui/identities/transfer_screen.rs b/src/ui/identities/transfer_screen.rs index c908c33ec..bed2f7758 100644 --- a/src/ui/identities/transfer_screen.rs +++ b/src/ui/identities/transfer_screen.rs @@ -245,8 +245,8 @@ impl TransferScreen { // Lazy initialization of the confirmation dialog let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { ConfirmationDialog::new("Confirm Transfer", msg) - .confirm_text("Confirm") - .cancel_text("Cancel") + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) }); let response = confirmation_dialog.show(ui); diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index 5dc55ccea..443bfe312 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -670,8 +670,8 @@ impl SetTokenPriceScreen { // Lazy initialization of the confirmation dialog let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { ConfirmationDialog::new("Confirm pricing schedule update", confirmation_message) - .confirm_text("Confirm") - .cancel_text("Cancel") + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) .danger_mode(is_danger_mode) }); From b8113da1a84cb778b269c32812d9caedcbb7b876 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:51:55 +0200 Subject: [PATCH 09/13] feat: Add Escape key handling for confirmation dialog --- src/ui/components/confirmation_dialog.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/ui/components/confirmation_dialog.rs b/src/ui/components/confirmation_dialog.rs index 232a713c5..efde292c0 100644 --- a/src/ui/components/confirmation_dialog.rs +++ b/src/ui/components/confirmation_dialog.rs @@ -56,6 +56,7 @@ impl ComponentResponse for ConfirmationDialogComponentResponse { /// across the application. It supports customizable titles, messages, button text /// with rich formatting (using WidgetText for styling), danger mode for destructive /// actions, and optional buttons (confirm and cancel buttons can be hidden independently). +/// The dialog can be dismissed by pressing Escape (treated as cancel) or clicking the X button. pub struct ConfirmationDialog { title: WidgetText, message: WidgetText, @@ -194,16 +195,11 @@ impl ConfirmationDialog { final_response = ConfirmationStatus::Canceled; } - // If no buttons are present, the user can only close via the X button or pressing Escape - // In that case, we treat it as canceled - if self.confirm_text.is_none() - && self.cancel_text.is_none() - && matches!(final_response, ConfirmationStatus::None) + // Handle Escape key press - always treat as cancel + if matches!(final_response, ConfirmationStatus::None) + && ui.input(|i| i.key_pressed(egui::Key::Escape)) { - // Check if user pressed Escape - if ui.input(|i| i.key_pressed(egui::Key::Escape)) { - final_response = ConfirmationStatus::Canceled; - } + final_response = ConfirmationStatus::Canceled; } // Update the dialog's open state From 89630893ee5dac3b17223d13a9773b1403b534f7 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:57:06 +0200 Subject: [PATCH 10/13] chore: button inactive when in progress --- src/backend_task/system_task/mod.rs | 2 +- src/ui/identities/transfer_screen.rs | 6 +++++- src/ui/tokens/set_token_price_screen.rs | 4 ++-- src/ui/tokens/tokens_screen/my_tokens.rs | 7 +++---- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/backend_task/system_task/mod.rs b/src/backend_task/system_task/mod.rs index d7a6383d2..2999b43fb 100644 --- a/src/backend_task/system_task/mod.rs +++ b/src/backend_task/system_task/mod.rs @@ -49,7 +49,7 @@ impl AppContext { theme_mode: ThemeMode, ) -> Result { let _guard = self.invalidate_settings_cache(); - + self.db .update_theme_preference(theme_mode) .map_err(|e| e.to_string())?; diff --git a/src/ui/identities/transfer_screen.rs b/src/ui/identities/transfer_screen.rs index bed2f7758..36ea850ce 100644 --- a/src/ui/identities/transfer_screen.rs +++ b/src/ui/identities/transfer_screen.rs @@ -432,7 +432,11 @@ impl ScreenLike for TransferScreen { // Transfer button let ready = self.amount.is_some() && !self.receiver_identity_id.is_empty() - && self.selected_key.is_some(); + && self.selected_key.is_some() + && !matches!( + self.transfer_credits_status, + TransferCreditsStatus::WaitingForResult(_), + ); let mut new_style = (**ui.style()).clone(); new_style.spacing.button_padding = egui::vec2(10.0, 5.0); ui.set_style(new_style); diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index 443bfe312..ce44307a0 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -984,6 +984,7 @@ impl ScreenLike for SetTokenPriceScreen { // Set price button let validation_result = self.validate_pricing_configuration(); + let button_active = validation_result.is_ok() && !matches!(self.status, SetTokenPriceStatus::WaitingForResult(_)); let button_color = if validation_result.is_ok() { Color32::from_rgb(0, 128, 255) @@ -995,8 +996,7 @@ impl ScreenLike for SetTokenPriceScreen { .fill(button_color) .corner_radius(3.0); - let button_response = ui.add_enabled(validation_result.is_ok(), button); - + let button_response = ui.add_enabled(button_active, button); if let Err(hover_message) = validation_result { button_response.on_disabled_hover_text(hover_message); diff --git a/src/ui/tokens/tokens_screen/my_tokens.rs b/src/ui/tokens/tokens_screen/my_tokens.rs index 6b6db3fd3..a7e3a02b2 100644 --- a/src/ui/tokens/tokens_screen/my_tokens.rs +++ b/src/ui/tokens/tokens_screen/my_tokens.rs @@ -523,10 +523,9 @@ impl TokensScreen { .token_configuration .conventions() .plural_form_by_language_code_or_default("en"); - let reward_amount = Amount::new( - explanation.total_amount, - decimal_places, - ).with_unit_name(unit_name); + let reward_amount = + Amount::new(explanation.total_amount, decimal_places) + .with_unit_name(unit_name); ui.label(format!("Total Estimated Rewards: {}", reward_amount)); ui.separator(); From e989369acccaa7d14c8fe53fff31ea2291fbd9e3 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:01:11 +0200 Subject: [PATCH 11/13] chore: fixes after merge --- src/ui/components/confirmation_dialog.rs | 56 +++++++++++++----------- src/ui/identities/transfer_screen.rs | 6 +-- src/ui/tokens/set_token_price_screen.rs | 6 +-- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/ui/components/confirmation_dialog.rs b/src/ui/components/confirmation_dialog.rs index efde292c0..7e2e940d0 100644 --- a/src/ui/components/confirmation_dialog.rs +++ b/src/ui/components/confirmation_dialog.rs @@ -4,8 +4,6 @@ use egui::{Color32, InnerResponse, Ui, WidgetText}; /// Response from showing a confirmation dialog #[derive(Debug, Clone, PartialEq)] pub enum ConfirmationStatus { - /// Dialog is still open, no action taken - None, /// User clicked confirm button Confirmed, /// User clicked cancel button or closed dialog @@ -19,7 +17,7 @@ pub struct ConfirmationDialogComponentResponse { pub response: egui::Response, pub changed: bool, pub error_message: Option, - pub dialog_response: ConfirmationStatus, + pub dialog_response: Option, } impl ComponentResponse for ConfirmationDialogComponentResponse { @@ -34,15 +32,10 @@ impl ComponentResponse for ConfirmationDialogComponentResponse { } fn changed_value(&self) -> &Option { - // Return Some(status) if dialog has a response, None if still open - static CONFIRMED: Option = Some(ConfirmationStatus::Confirmed); - static CANCELED: Option = Some(ConfirmationStatus::Canceled); - static NONE: Option = None; - - match self.dialog_response { - ConfirmationStatus::Confirmed => &CONFIRMED, - ConfirmationStatus::Canceled => &CANCELED, - ConfirmationStatus::None => &NONE, + if self.has_changed() { + &self.dialog_response + } else { + &None } } @@ -60,6 +53,7 @@ impl ComponentResponse for ConfirmationDialogComponentResponse { pub struct ConfirmationDialog { title: WidgetText, message: WidgetText, + status: Option, confirm_text: Option, cancel_text: Option, danger_mode: bool, @@ -72,7 +66,7 @@ impl Component for ConfirmationDialog { fn show(&mut self, ui: &mut Ui) -> InnerResponse { let inner_response = self.show_dialog(ui); - let changed = !matches!(inner_response.inner, ConfirmationStatus::None); + let changed = inner_response.inner.is_some(); let response = inner_response.response; InnerResponse::new( @@ -85,6 +79,15 @@ impl Component for ConfirmationDialog { response, ) } + + fn current_value(&self) -> Option { + // Return the current dialog state - None if still open, Some(status) if closed + if self.is_open { + None + } else { + Some(ConfirmationStatus::Canceled) // If dialog is closed, it was canceled + } + } } impl ConfirmationDialog { @@ -97,6 +100,7 @@ impl ConfirmationDialog { cancel_text: Some("Cancel".into()), danger_mode: false, is_open: true, + status: None, // No action taken yet } } @@ -127,17 +131,17 @@ impl ConfirmationDialog { impl ConfirmationDialog { /// Show the dialog and return the user's response - pub fn show_dialog(&mut self, ui: &mut Ui) -> InnerResponse { + fn show_dialog(&mut self, ui: &mut Ui) -> InnerResponse> { let mut is_open = self.is_open; if !is_open { return InnerResponse::new( - ConfirmationStatus::Canceled, + None, // no change ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()), ); } - let mut final_response = ConfirmationStatus::None; + let mut final_response = None; let window_response = egui::Window::new(self.title.clone()) .collapsible(false) .resizable(false) @@ -166,7 +170,7 @@ impl ConfirmationDialog { }; if ui.add(confirm_button).clicked() { - final_response = ConfirmationStatus::Confirmed; + final_response = Some(ConfirmationStatus::Confirmed); } // Add space only if both buttons are present @@ -181,7 +185,7 @@ impl ConfirmationDialog { .fill(Color32::from_rgb(108, 117, 125)); // Gray for secondary if ui.add(cancel_button).clicked() { - final_response = ConfirmationStatus::Canceled; + final_response = Some(ConfirmationStatus::Canceled); } } }); @@ -191,19 +195,21 @@ impl ConfirmationDialog { }); // Handle window being closed via X button - treat as cancel - if !is_open && matches!(final_response, ConfirmationStatus::None) { - final_response = ConfirmationStatus::Canceled; + if !is_open && final_response.is_none() { + final_response = Some(ConfirmationStatus::Canceled); } // Handle Escape key press - always treat as cancel - if matches!(final_response, ConfirmationStatus::None) - && ui.input(|i| i.key_pressed(egui::Key::Escape)) - { - final_response = ConfirmationStatus::Canceled; + if final_response.is_none() && ui.input(|i| i.key_pressed(egui::Key::Escape)) { + final_response = Some(ConfirmationStatus::Canceled); } - // Update the dialog's open state + // Update the dialog's state self.is_open = is_open; + // if user actually did something, update the status + if final_response.is_some() { + self.status = final_response.clone(); + } if let Some(window_response) = window_response { InnerResponse::new(final_response, window_response.response) diff --git a/src/ui/identities/transfer_screen.rs b/src/ui/identities/transfer_screen.rs index 03573d838..5596766e2 100644 --- a/src/ui/identities/transfer_screen.rs +++ b/src/ui/identities/transfer_screen.rs @@ -250,9 +250,9 @@ impl TransferScreen { // Handle the response using the Component pattern match response.inner.dialog_response { - ConfirmationStatus::Confirmed => self.confirmation_ok(), - ConfirmationStatus::Canceled => self.confirmation_cancel(), - ConfirmationStatus::None => AppAction::None, + Some(ConfirmationStatus::Confirmed) => self.confirmation_ok(), + Some(ConfirmationStatus::Canceled) => self.confirmation_cancel(), + None => AppAction::None, } } diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index ce44307a0..f3e42a24d 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -678,9 +678,9 @@ impl SetTokenPriceScreen { let response = confirmation_dialog.show(ui); match response.inner.dialog_response { - ConfirmationStatus::Confirmed => self.confirmation_ok(), - ConfirmationStatus::Canceled => self.confirmation_cancel(), - ConfirmationStatus::None => AppAction::None, + Some(ConfirmationStatus::Confirmed) => self.confirmation_ok(), + Some(ConfirmationStatus::Canceled) => self.confirmation_cancel(), + None => AppAction::None, } } From a8b40353d7fa29ffcff9dce13cd5303a3cb33e52 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:48:55 +0200 Subject: [PATCH 12/13] chore: some theme improvements --- doc/COMPONENT_DESIGN_PATTERN.md | 1 + src/ui/components/confirmation_dialog.rs | 58 ++++++++++++++++++++---- src/ui/theme.rs | 4 ++ 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/doc/COMPONENT_DESIGN_PATTERN.md b/doc/COMPONENT_DESIGN_PATTERN.md index a81d3c94b..d1a6d73df 100644 --- a/doc/COMPONENT_DESIGN_PATTERN.md +++ b/doc/COMPONENT_DESIGN_PATTERN.md @@ -89,6 +89,7 @@ impl ComponentResponse for MyComponentResponse { - [ ] Keep internal state private - [ ] **Be self-contained**: Handle validation, error display, hints, and formatting internally (preferably with configurable error display) - [ ] **Own your UX**: Component should manage its complete user experience +- [ ] Colors should be defined in `ComponentStyles` and optimized for light and dark mode ### ❌ Anti-Patterns to Avoid - Public mutable fields diff --git a/src/ui/components/confirmation_dialog.rs b/src/ui/components/confirmation_dialog.rs index 7e2e940d0..47149de3f 100644 --- a/src/ui/components/confirmation_dialog.rs +++ b/src/ui/components/confirmation_dialog.rs @@ -1,5 +1,8 @@ +use std::sync::Arc; + use crate::ui::components::component_trait::{Component, ComponentResponse}; -use egui::{Color32, InnerResponse, Ui, WidgetText}; +use crate::ui::theme::ComponentStyles; +use egui::{InnerResponse, Ui, WidgetText}; /// Response from showing a confirmation dialog #[derive(Debug, Clone, PartialEq)] @@ -161,15 +164,35 @@ impl ConfirmationDialog { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { // Confirm button (only if text is provided) if let Some(confirm_text) = &self.confirm_text { - let confirm_button = if self.danger_mode { - egui::Button::new(confirm_text.clone()) - .fill(Color32::from_rgb(220, 53, 69)) // Red for danger + let (fill_color, text_color) = if self.danger_mode { + ( + ComponentStyles::danger_button_fill(), + ComponentStyles::danger_button_text(), + ) + } else { + ( + ComponentStyles::primary_button_fill(), + ComponentStyles::primary_button_text(), + ) + }; + let confirm_label = if let WidgetText::RichText(rich_text) = + confirm_text + { + // preserve rich text formatting + rich_text.clone() } else { - egui::Button::new(confirm_text.clone()) - .fill(Color32::from_rgb(0, 128, 255)) // Blue for primary + Arc::new(egui::RichText::new(confirm_text.text()).color(text_color)) }; - if ui.add(confirm_button).clicked() { + let confirm_button = egui::Button::new(confirm_label) + .fill(fill_color) + .stroke(ComponentStyles::primary_button_stroke()); + + if ui + .add(confirm_button) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() + { final_response = Some(ConfirmationStatus::Confirmed); } @@ -181,10 +204,25 @@ impl ConfirmationDialog { // Cancel button (only if text is provided) if let Some(cancel_text) = &self.cancel_text { - let cancel_button = egui::Button::new(cancel_text.clone()) - .fill(Color32::from_rgb(108, 117, 125)); // Gray for secondary + let cancel_label = if let WidgetText::RichText(rich_text) = cancel_text + { + // preserve rich text formatting + rich_text.clone() + } else { + egui::RichText::new(cancel_text.text()) + .color(ComponentStyles::secondary_button_text()) + .into() + }; + + let cancel_button = egui::Button::new(cancel_label) + .fill(ComponentStyles::secondary_button_fill()) + .stroke(ComponentStyles::secondary_button_stroke()); - if ui.add(cancel_button).clicked() { + if ui + .add(cancel_button) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() + { final_response = Some(ConfirmationStatus::Canceled); } } diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 4b1485f99..4b2be45d8 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -453,6 +453,10 @@ impl ComponentStyles { DashColors::WHITE } + pub fn primary_button_stroke() -> Stroke { + Stroke::new(1.0, DashColors::DASH_BLUE) + } + pub fn secondary_button_fill() -> Color32 { DashColors::WHITE } From 7e07748444ee9cf14e5010ad598e9863a57e3ca3 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 13 Aug 2025 17:39:03 +0700 Subject: [PATCH 13/13] fmt and visual appeal --- src/ui/components/confirmation_dialog.rs | 61 +++++++++++--- src/ui/identities/keys/key_info_screen.rs | 98 ++++++++++++++++++----- 2 files changed, 125 insertions(+), 34 deletions(-) diff --git a/src/ui/components/confirmation_dialog.rs b/src/ui/components/confirmation_dialog.rs index 47149de3f..6f04f2a2b 100644 --- a/src/ui/components/confirmation_dialog.rs +++ b/src/ui/components/confirmation_dialog.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use crate::ui::components::component_trait::{Component, ComponentResponse}; -use crate::ui::theme::ComponentStyles; +use crate::ui::theme::{ComponentStyles, DashColors, Shape}; use egui::{InnerResponse, Ui, WidgetText}; /// Response from showing a confirmation dialog @@ -144,19 +144,53 @@ impl ConfirmationDialog { ); } + // Draw dark overlay behind the dialog for better visibility + let screen_rect = ui.ctx().screen_rect(); + let painter = ui.ctx().layer_painter(egui::LayerId::new( + egui::Order::Background, + egui::Id::new("confirmation_dialog_overlay"), + )); + painter.rect_filled( + screen_rect, + 0.0, + egui::Color32::from_rgba_unmultiplied(0, 0, 0, 120), // Semi-transparent black overlay + ); + let mut final_response = None; let window_response = egui::Window::new(self.title.clone()) .collapsible(false) .resizable(false) .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) .open(&mut is_open) + .frame(egui::Frame { + inner_margin: egui::Margin::same(16), + outer_margin: egui::Margin::same(0), + corner_radius: egui::CornerRadius::same(8), + shadow: egui::epaint::Shadow { + offset: [0, 8], + blur: 16, + spread: 0, + color: egui::Color32::from_rgba_unmultiplied(0, 0, 0, 100), + }, + fill: ui.style().visuals.window_fill, + stroke: egui::Stroke::new( + 1.0, + egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30), + ), + }) .show(ui.ctx(), |ui| { // Set minimum width for the dialog ui.set_min_width(300.0); - // Message content + let dark_mode = ui.ctx().style().visuals.dark_mode; + + // Message content with bold text and proper color ui.add_space(10.0); - ui.label(self.message.clone()); + ui.label( + egui::RichText::new(self.message.text()) + .strong() + .color(DashColors::text_primary(dark_mode)), + ); ui.add_space(20.0); // Buttons @@ -186,7 +220,13 @@ impl ConfirmationDialog { let confirm_button = egui::Button::new(confirm_label) .fill(fill_color) - .stroke(ComponentStyles::primary_button_stroke()); + .stroke(if self.danger_mode { + egui::Stroke::NONE + } else { + ComponentStyles::primary_button_stroke() + }) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_SM)) + .min_size(egui::Vec2::new(80.0, 32.0)); if ui .add(confirm_button) @@ -195,11 +235,6 @@ impl ConfirmationDialog { { final_response = Some(ConfirmationStatus::Confirmed); } - - // Add space only if both buttons are present - if self.cancel_text.is_some() { - ui.add_space(10.0); - } } // Cancel button (only if text is provided) @@ -216,7 +251,9 @@ impl ConfirmationDialog { let cancel_button = egui::Button::new(cancel_label) .fill(ComponentStyles::secondary_button_fill()) - .stroke(ComponentStyles::secondary_button_stroke()); + .stroke(ComponentStyles::secondary_button_stroke()) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_SM)) + .min_size(egui::Vec2::new(80.0, 32.0)); if ui .add(cancel_button) @@ -225,11 +262,11 @@ impl ConfirmationDialog { { final_response = Some(ConfirmationStatus::Canceled); } + + ui.add_space(8.0); // Add spacing between buttons } }); }); - - ui.add_space(10.0); }); // Handle window being closed via X button - treat as cancel diff --git a/src/ui/identities/keys/key_info_screen.rs b/src/ui/identities/keys/key_info_screen.rs index 03d8da0dc..1c12968d8 100644 --- a/src/ui/identities/keys/key_info_screen.rs +++ b/src/ui/identities/keys/key_info_screen.rs @@ -303,9 +303,16 @@ impl ScreenLike for KeyInfoScreen { .num_columns(2) .spacing([10.0, 10.0]) .show(ui, |ui| { - ui.label(RichText::new("Private Key (Hex):").strong().color(ui.visuals().text_color())); + ui.label( + RichText::new("Private Key (Hex):") + .strong() + .color(ui.visuals().text_color()), + ); let private_key_hex = hex::encode(clear); - ui.label(RichText::new(private_key_hex).color(ui.visuals().text_color())); + ui.label( + RichText::new(private_key_hex) + .color(ui.visuals().text_color()), + ); ui.end_row(); }); ui.add_space(10.0); @@ -330,14 +337,29 @@ impl ScreenLike for KeyInfoScreen { .num_columns(2) .spacing([10.0, 10.0]) .show(ui, |ui| { - ui.label(RichText::new("Private Key (WIF):").strong().color(ui.visuals().text_color())); + ui.label( + RichText::new("Private Key (WIF):") + .strong() + .color(ui.visuals().text_color()), + ); let private_key_wif = private_key.to_wif(); - ui.label(RichText::new(private_key_wif).color(ui.visuals().text_color())); + ui.label( + RichText::new(private_key_wif) + .color(ui.visuals().text_color()), + ); ui.end_row(); - - ui.label(RichText::new("Private Key (Hex):").strong().color(ui.visuals().text_color())); - let private_key_hex = hex::encode(private_key.inner.secret_bytes()); - ui.label(RichText::new(private_key_hex).color(ui.visuals().text_color())); + + ui.label( + RichText::new("Private Key (Hex):") + .strong() + .color(ui.visuals().text_color()), + ); + let private_key_hex = + hex::encode(private_key.inner.secret_bytes()); + ui.label( + RichText::new(private_key_hex) + .color(ui.visuals().text_color()), + ); ui.end_row(); }); } else { @@ -352,17 +374,33 @@ impl ScreenLike for KeyInfoScreen { .num_columns(2) .spacing([10.0, 10.0]) .show(ui, |ui| { - ui.label(RichText::new("Private Key (WIF):").strong().color(ui.visuals().text_color())); + ui.label( + RichText::new("Private Key (WIF):") + .strong() + .color(ui.visuals().text_color()), + ); let private_key_wif = private_key.to_wif(); - ui.label(RichText::new(private_key_wif).color(ui.visuals().text_color())); + ui.label( + RichText::new(private_key_wif) + .color(ui.visuals().text_color()), + ); ui.end_row(); - - ui.label(RichText::new("Private Key (Hex):").strong().color(ui.visuals().text_color())); - let private_key_hex = hex::encode(private_key.inner.secret_bytes()); - ui.label(RichText::new(private_key_hex).color(ui.visuals().text_color())); + + ui.label( + RichText::new("Private Key (Hex):") + .strong() + .color(ui.visuals().text_color()), + ); + let private_key_hex = hex::encode( + private_key.inner.secret_bytes(), + ); + ui.label( + RichText::new(private_key_hex) + .color(ui.visuals().text_color()), + ); ui.end_row(); }); - + self.decrypted_private_key = Some(private_key); } Err(e) => { @@ -392,17 +430,33 @@ impl ScreenLike for KeyInfoScreen { .num_columns(2) .spacing([10.0, 10.0]) .show(ui, |ui| { - ui.label(RichText::new("Private Key (WIF):").strong().color(ui.visuals().text_color())); + ui.label( + RichText::new("Private Key (WIF):") + .strong() + .color(ui.visuals().text_color()), + ); let private_key_wif = private_key.to_wif(); - ui.label(RichText::new(private_key_wif).color(ui.visuals().text_color())); + ui.label( + RichText::new(private_key_wif) + .color(ui.visuals().text_color()), + ); ui.end_row(); - - ui.label(RichText::new("Private Key (Hex):").strong().color(ui.visuals().text_color())); - let private_key_hex = hex::encode(private_key.inner.secret_bytes()); - ui.label(RichText::new(private_key_hex).color(ui.visuals().text_color())); + + ui.label( + RichText::new("Private Key (Hex):") + .strong() + .color(ui.visuals().text_color()), + ); + let private_key_hex = hex::encode( + private_key.inner.secret_bytes(), + ); + ui.label( + RichText::new(private_key_hex) + .color(ui.visuals().text_color()), + ); ui.end_row(); }); - + self.decrypted_private_key = Some(private_key); } Err(e) => {