diff --git a/src/ui/components/dashpay_subscreen_chooser_panel.rs b/src/ui/components/dashpay_subscreen_chooser_panel.rs index 529200da1..b78054c31 100644 --- a/src/ui/components/dashpay_subscreen_chooser_panel.rs +++ b/src/ui/components/dashpay_subscreen_chooser_panel.rs @@ -70,7 +70,6 @@ pub fn add_dashpay_subscreen_chooser_panel( .fill(DashColors::DASH_BLUE) .stroke(egui::Stroke::NONE) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) } else { egui::Button::new( RichText::new(display_name) @@ -80,7 +79,6 @@ pub fn add_dashpay_subscreen_chooser_panel( .fill(DashColors::glass_white(dark_mode)) .stroke(egui::Stroke::new(1.0, DashColors::border(dark_mode))) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) }; // Show the subscreen name as a clickable option diff --git a/src/ui/components/dpns_subscreen_chooser_panel.rs b/src/ui/components/dpns_subscreen_chooser_panel.rs index be3249308..8b91f5ce0 100644 --- a/src/ui/components/dpns_subscreen_chooser_panel.rs +++ b/src/ui/components/dpns_subscreen_chooser_panel.rs @@ -61,7 +61,6 @@ pub fn add_dpns_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext) .fill(DashColors::DASH_BLUE) .stroke(egui::Stroke::NONE) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) } else { egui::Button::new( RichText::new(subscreen.display_name()) @@ -71,7 +70,6 @@ pub fn add_dpns_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext) .fill(DashColors::glass_white(dark_mode)) .stroke(egui::Stroke::new(1.0, DashColors::border(dark_mode))) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) }; // Show the subscreen name as a clickable option diff --git a/src/ui/components/tokens_subscreen_chooser_panel.rs b/src/ui/components/tokens_subscreen_chooser_panel.rs index c2f2b99b5..f0af8c978 100644 --- a/src/ui/components/tokens_subscreen_chooser_panel.rs +++ b/src/ui/components/tokens_subscreen_chooser_panel.rs @@ -60,7 +60,6 @@ pub fn add_tokens_subscreen_chooser_panel(ctx: &Context, app_context: &AppContex .fill(DashColors::DASH_BLUE) .stroke(egui::Stroke::NONE) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) } else { egui::Button::new( RichText::new(subscreen.display_name()) @@ -70,7 +69,6 @@ pub fn add_tokens_subscreen_chooser_panel(ctx: &Context, app_context: &AppContex .fill(DashColors::glass_white(dark_mode)) .stroke(egui::Stroke::new(1.0, DashColors::border(dark_mode))) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) }; // Show the subscreen name as a clickable option diff --git a/src/ui/components/tools_subscreen_chooser_panel.rs b/src/ui/components/tools_subscreen_chooser_panel.rs index 3ff9a6156..f694e0499 100644 --- a/src/ui/components/tools_subscreen_chooser_panel.rs +++ b/src/ui/components/tools_subscreen_chooser_panel.rs @@ -128,7 +128,6 @@ pub fn add_tools_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext .fill(DashColors::DASH_BLUE) .stroke(egui::Stroke::NONE) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) } else { egui::Button::new( RichText::new(subscreen.display_name()) @@ -138,7 +137,6 @@ pub fn add_tools_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext .fill(DashColors::glass_white(dark_mode)) .stroke(egui::Stroke::new(1.0, DashColors::border(dark_mode))) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) }; // Show the subscreen name as a clickable option. Disable diff --git a/src/ui/dashpay/contact_requests.rs b/src/ui/dashpay/contact_requests.rs index c26de21f4..da27abd30 100644 --- a/src/ui/dashpay/contact_requests.rs +++ b/src/ui/dashpay/contact_requests.rs @@ -14,7 +14,7 @@ use crate::ui::components::wallet_unlock_popup::{ use crate::ui::components::{MessageBanner, ResultBannerExt}; use crate::ui::identities::get_selected_wallet; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; -use crate::ui::theme::DashColors; +use crate::ui::theme::{ComponentStyles, DashColors}; use crate::ui::{MessageType, Screen, ScreenLike, ScreenType}; use dash_sdk::dpp::document::DocumentV0Getters; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; @@ -650,53 +650,23 @@ impl ContactRequests { // Tabs let dark_mode = ui.ctx().style().visuals.dark_mode; ui.horizontal(|ui| { - let incoming_tab = egui::Button::new(RichText::new("Incoming").color( - if self.active_tab == RequestTab::Incoming { - DashColors::WHITE - } else { - DashColors::text_primary(dark_mode) - }, - )) - .fill(if self.active_tab == RequestTab::Incoming { - DashColors::DASH_BLUE - } else { - DashColors::glass_white(dark_mode) - }) - .stroke(if self.active_tab == RequestTab::Incoming { - egui::Stroke::NONE + let incoming_response = if self.active_tab == RequestTab::Incoming { + ComponentStyles::add_primary_button(ui, "Incoming") } else { - egui::Stroke::new(1.0, DashColors::border(dark_mode)) - }) - .corner_radius(egui::CornerRadius::same(4)) - .min_size(egui::Vec2::new(120.0, 28.0)); - - if ui.add(incoming_tab).clicked() { + ComponentStyles::add_secondary_button(ui, "Incoming", dark_mode) + }; + if incoming_response.clicked() { self.active_tab = RequestTab::Incoming; } ui.add_space(8.0); - let outgoing_tab = egui::Button::new(RichText::new("Outgoing").color( - if self.active_tab == RequestTab::Outgoing { - DashColors::WHITE - } else { - DashColors::text_primary(dark_mode) - }, - )) - .fill(if self.active_tab == RequestTab::Outgoing { - DashColors::DASH_BLUE - } else { - DashColors::glass_white(dark_mode) - }) - .stroke(if self.active_tab == RequestTab::Outgoing { - egui::Stroke::NONE + let outgoing_response = if self.active_tab == RequestTab::Outgoing { + ComponentStyles::add_primary_button(ui, "Outgoing") } else { - egui::Stroke::new(1.0, DashColors::border(dark_mode)) - }) - .corner_radius(egui::CornerRadius::same(4)) - .min_size(egui::Vec2::new(120.0, 28.0)); - - if ui.add(outgoing_tab).clicked() { + ComponentStyles::add_secondary_button(ui, "Outgoing", dark_mode) + }; + if outgoing_response.clicked() { self.active_tab = RequestTab::Outgoing; } }); diff --git a/src/ui/dashpay/contacts_list.rs b/src/ui/dashpay/contacts_list.rs index b851aa627..95b1776c2 100644 --- a/src/ui/dashpay/contacts_list.rs +++ b/src/ui/dashpay/contacts_list.rs @@ -8,7 +8,7 @@ use crate::ui::components::ResultBannerExt; use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::wallet_unlock_popup::WalletUnlockResult; use crate::ui::dashpay::contact_requests::ContactRequests; -use crate::ui::theme::DashColors; +use crate::ui::theme::{ComponentStyles, DashColors}; use crate::ui::{MessageType, ScreenLike, ScreenType}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::platform_value::string_encoding::Encoding; @@ -341,27 +341,12 @@ impl ContactsList { // Tab bar ui.horizontal(|ui| { - let contacts_tab = egui::Button::new(RichText::new("My Contacts").color( - if self.active_tab == ContactsTab::Contacts { - DashColors::WHITE - } else { - DashColors::text_primary(dark_mode) - }, - )) - .fill(if self.active_tab == ContactsTab::Contacts { - DashColors::DASH_BLUE - } else { - DashColors::glass_white(dark_mode) - }) - .stroke(if self.active_tab == ContactsTab::Contacts { - egui::Stroke::NONE + let contacts_response = if self.active_tab == ContactsTab::Contacts { + ComponentStyles::add_primary_button(ui, "My Contacts") } else { - egui::Stroke::new(1.0, DashColors::border(dark_mode)) - }) - .corner_radius(egui::CornerRadius::same(4)) - .min_size(egui::Vec2::new(120.0, 28.0)); - - if ui.add(contacts_tab).clicked() { + ComponentStyles::add_secondary_button(ui, "My Contacts", dark_mode) + }; + if contacts_response.clicked() { self.active_tab = ContactsTab::Contacts; } @@ -375,27 +360,12 @@ impl ContactsList { "Requests".to_string() }; - let requests_tab = egui::Button::new(RichText::new(requests_label).color( - if self.active_tab == ContactsTab::Requests { - DashColors::WHITE - } else { - DashColors::text_primary(dark_mode) - }, - )) - .fill(if self.active_tab == ContactsTab::Requests { - DashColors::DASH_BLUE - } else { - DashColors::glass_white(dark_mode) - }) - .stroke(if self.active_tab == ContactsTab::Requests { - egui::Stroke::NONE + let requests_response = if self.active_tab == ContactsTab::Requests { + ComponentStyles::add_primary_button(ui, requests_label) } else { - egui::Stroke::new(1.0, DashColors::border(dark_mode)) - }) - .corner_radius(egui::CornerRadius::same(4)) - .min_size(egui::Vec2::new(120.0, 28.0)); - - if ui.add(requests_tab).clicked() { + ComponentStyles::add_secondary_button(ui, requests_label, dark_mode) + }; + if requests_response.clicked() { self.active_tab = ContactsTab::Requests; } }); diff --git a/src/ui/dashpay/mod.rs b/src/ui/dashpay/mod.rs index d6d81f77c..35976c180 100644 --- a/src/ui/dashpay/mod.rs +++ b/src/ui/dashpay/mod.rs @@ -18,7 +18,7 @@ pub use profile_search::ProfileSearchScreen; use crate::app::AppAction; use crate::context::AppContext; use crate::ui::ScreenType; -use crate::ui::theme::DashColors; +use crate::ui::theme::{ComponentStyles, DashColors}; use chrono::{LocalResult, TimeZone, Utc}; use chrono_humanize::HumanTime; @@ -94,15 +94,7 @@ pub fn render_no_identities_card(ui: &mut Ui, app_context: &Arc) -> ui.add_space(15.0); - let button = egui::Button::new( - RichText::new("Load Identity") - .color(egui::Color32::WHITE) - .strong(), - ) - .fill(DashColors::DASH_BLUE) - .min_size(egui::vec2(150.0, 36.0)); - - if ui.add(button).clicked() { + if ComponentStyles::add_primary_button(ui, "Load Identity").clicked() { return AppAction::AddScreen( ScreenType::AddExistingIdentity.create_screen(app_context), ); diff --git a/src/ui/identities/top_up_identity_screen/by_platform_address.rs b/src/ui/identities/top_up_identity_screen/by_platform_address.rs index 59d2f21fb..fa27d2d13 100644 --- a/src/ui/identities/top_up_identity_screen/by_platform_address.rs +++ b/src/ui/identities/top_up_identity_screen/by_platform_address.rs @@ -9,7 +9,7 @@ use crate::ui::components::MessageBanner; use crate::ui::components::amount_input::AmountInput; use crate::ui::components::component_trait::{Component, ComponentResponse}; use crate::ui::identities::funding_common::WalletFundedScreenStep; -use crate::ui::theme::DashColors; +use crate::ui::theme::{ComponentStyles, DashColors}; use dash_sdk::dpp::address_funds::PlatformAddress; use dash_sdk::dpp::balances::credits::Credits; use dash_sdk::dpp::dashcore::Address; @@ -183,19 +183,7 @@ impl TopUpIdentityScreen { _ => "Top Up Identity", }; - let button = egui::Button::new( - RichText::new(button_text) - .color(egui::Color32::WHITE) - .strong(), - ) - .fill(if can_top_up { - DashColors::DASH_BLUE - } else { - DashColors::DASH_BLUE.gamma_multiply(0.5) - }) - .min_size(egui::vec2(120.0, 36.0)); - - if ui.add_enabled(can_top_up, button).clicked() { + if ComponentStyles::add_primary_button_enabled(ui, can_top_up, button_text).clicked() { match self.validate_and_top_up_from_platform() { Ok(top_up_action) => { action = top_up_action; diff --git a/src/ui/identities/transfer_screen.rs b/src/ui/identities/transfer_screen.rs index b22ede8f1..33c057dd2 100644 --- a/src/ui/identities/transfer_screen.rs +++ b/src/ui/identities/transfer_screen.rs @@ -36,7 +36,7 @@ use crate::ui::components::wallet_unlock_popup::{ WalletUnlockPopup, WalletUnlockResult, try_open_wallet_no_password, wallet_needs_unlock, }; use crate::ui::helpers::{TransactionType, add_key_chooser}; -use crate::ui::theme::{DashColors, ResponseExt}; +use crate::ui::theme::{ComponentStyles, DashColors, ResponseExt}; /// Transfer destination type #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -183,12 +183,6 @@ impl TransferScreen { fn render_destination_type_selector(&mut self, ui: &mut Ui) { let dark_mode = ui.ctx().style().visuals.dark_mode; - // Colors for selected/unselected states - let selected_fill = DashColors::DASH_BLUE; - let selected_text = Color32::WHITE; - let unselected_fill = DashColors::unselected_fill(dark_mode); - let unselected_text = DashColors::text_primary(dark_mode); - ui.horizontal(|ui| { ui.vertical(|ui| { ui.add_space(5.0); @@ -198,23 +192,12 @@ impl TransferScreen { // Identity button let identity_selected = self.destination_type == TransferDestinationType::Identity; - let identity_button = egui::Button::new( - RichText::new("Identity") - .color(if identity_selected { - selected_text - } else { - unselected_text - }) - .strong(), - ) - .fill(if identity_selected { - selected_fill + let identity_response = if identity_selected { + ComponentStyles::add_primary_button(ui, "Identity") } else { - unselected_fill - }) - .min_size(egui::vec2(120.0, 28.0)); - - if ui.add(identity_button).clicked() { + ComponentStyles::add_secondary_button(ui, "Identity", dark_mode) + }; + if identity_response.clicked() { self.destination_type = TransferDestinationType::Identity; } @@ -223,23 +206,12 @@ impl TransferScreen { // Platform Address button let platform_selected = self.destination_type == TransferDestinationType::PlatformAddress; - let platform_button = egui::Button::new( - RichText::new("Platform Address") - .color(if platform_selected { - selected_text - } else { - unselected_text - }) - .strong(), - ) - .fill(if platform_selected { - selected_fill + let platform_response = if platform_selected { + ComponentStyles::add_primary_button(ui, "Platform Address") } else { - unselected_fill - }) - .min_size(egui::vec2(140.0, 28.0)); - - if ui.add(platform_button).clicked() { + ComponentStyles::add_secondary_button(ui, "Platform Address", dark_mode) + }; + if platform_response.clicked() { self.destination_type = TransferDestinationType::PlatformAddress; } }); diff --git a/src/ui/identities/withdraw_screen.rs b/src/ui/identities/withdraw_screen.rs index 88a5ec9e9..4e4327a19 100644 --- a/src/ui/identities/withdraw_screen.rs +++ b/src/ui/identities/withdraw_screen.rs @@ -18,7 +18,7 @@ use crate::ui::components::wallet_unlock_popup::{ use crate::ui::components::{BannerHandle, MessageBanner, OptionBannerExt, ResultBannerExt}; use crate::ui::components::{Component, ComponentResponse}; use crate::ui::helpers::{TransactionType, add_key_chooser}; -use crate::ui::theme::{DashColors, ResponseExt}; +use crate::ui::theme::{ComponentStyles, DashColors, ResponseExt}; use crate::ui::{MessageType, Screen, ScreenLike}; use dash_sdk::dashcore_rpc::dashcore::{Address, Network}; use dash_sdk::dpp::fee::Credits; @@ -597,13 +597,6 @@ impl ScreenLike for WithdrawalScreen { ui.add_space(10.0); // Withdraw button - - let button = egui::Button::new(RichText::new("Withdraw").color(Color32::WHITE)) - .fill(DashColors::DASH_BLUE) - .frame(true) - .corner_radius(3.0) - .min_size(egui::vec2(60.0, 30.0)); - let has_valid_amount = self.withdrawal_amount.is_some(); let has_address_error = self.withdrawal_address_error.is_some(); let has_enough_balance = self.max_amount > estimated_fee; @@ -622,8 +615,7 @@ impl ScreenLike for WithdrawalScreen { String::new() }; - if ui - .add_enabled(ready, button) + if ComponentStyles::add_primary_button_enabled(ui, ready, "Withdraw") .disabled_tooltip(&hover_text) .clicked() && self.confirmation_dialog.is_none() diff --git a/src/ui/network_chooser_screen.rs b/src/ui/network_chooser_screen.rs index 213c3509c..653dc8f71 100644 --- a/src/ui/network_chooser_screen.rs +++ b/src/ui/network_chooser_screen.rs @@ -16,7 +16,7 @@ use crate::ui::components::styled::{ }; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::{BannerHandle, MessageBanner, OptionBannerExt}; -use crate::ui::theme::{DashColors, ResponseExt, Shape, ThemeMode}; +use crate::ui::theme::{ComponentStyles, DashColors, ResponseExt, Shape, ThemeMode}; use crate::ui::{MessageType, RootScreenType, ScreenLike}; use crate::utils::path::format_path_for_display; use dash_sdk::dash_spv::sync::{ProgressPercentage, SyncProgress as SpvSyncProgress, SyncState}; @@ -550,16 +550,12 @@ impl NetworkChooserScreen { if overall_state != OverallConnectionState::Disconnected { if current_backend_mode == CoreBackendMode::Spv { let is_stopping = spv_status == SpvStatus::Stopping; - let disconnect_button = egui::Button::new( - egui::RichText::new("Disconnect").color(DashColors::WHITE), - ) - .fill(DashColors::ERROR) - .stroke(egui::Stroke::NONE) - .corner_radius(Shape::RADIUS_MD) - .min_size(egui::vec2(120.0, 36.0)); if ui - .add_enabled(!is_stopping, disconnect_button) + .add_enabled_ui(!is_stopping, |ui| { + ComponentStyles::add_danger_button(ui, "Disconnect") + }) + .inner .clicked() { self.current_app_context().stop_spv(); @@ -616,37 +612,29 @@ impl NetworkChooserScreen { } }; - if show_connect_button { - let connect_button = egui::Button::new( - egui::RichText::new("Connect").color(DashColors::WHITE), - ) - .fill(DashColors::DASH_BLUE) - .stroke(egui::Stroke::NONE) - .corner_radius(Shape::RADIUS_MD) - .min_size(egui::vec2(120.0, 36.0)); - - if ui.add(connect_button).clicked() { - if current_backend_mode == CoreBackendMode::Spv { - if let Err(err) = self.current_app_context().start_spv() { - app_action = - AppAction::Custom(format!("Failed to start SPV: {}", err)); - } - } else { - // Core mode connect - let settings = - self.current_app_context().get_settings().ok().flatten(); - let dash_qt_path = settings - .and_then(|s| s.dash_qt_path) - .or_else(|| self.custom_dash_qt_path.clone()); - if let Some(path) = dash_qt_path { - app_action = AppAction::BackendTask(BackendTask::CoreTask( - CoreTask::StartDashQT( - self.current_network, - path, - self.overwrite_dash_conf, - ), - )); - } + if show_connect_button + && ComponentStyles::add_primary_button(ui, "Connect").clicked() + { + if current_backend_mode == CoreBackendMode::Spv { + if let Err(err) = self.current_app_context().start_spv() { + app_action = + AppAction::Custom(format!("Failed to start SPV: {}", err)); + } + } else { + // Core mode connect + let settings = + self.current_app_context().get_settings().ok().flatten(); + let dash_qt_path = settings + .and_then(|s| s.dash_qt_path) + .or_else(|| self.custom_dash_qt_path.clone()); + if let Some(path) = dash_qt_path { + app_action = AppAction::BackendTask(BackendTask::CoreTask( + CoreTask::StartDashQT( + self.current_network, + path, + self.overwrite_dash_conf, + ), + )); } } } diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 5f14e5e6c..8405a0656 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -795,7 +795,6 @@ impl ComponentStyles { .fill(Self::primary_button_fill()) .stroke(Self::primary_button_stroke()) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_SM)) - .min_size(Self::DIALOG_BUTTON_MIN_SIZE) } /// Returns a fully styled secondary (cancel/close) button with theme-aware colors. @@ -817,7 +816,6 @@ impl ComponentStyles { .fill(Self::secondary_button_fill(dark_mode)) .stroke(Self::secondary_button_stroke(dark_mode)) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_SM)) - .min_size(Self::DIALOG_BUTTON_MIN_SIZE) } /// Returns a fully styled danger (destructive action) button with red fill and white text. @@ -839,12 +837,16 @@ impl ComponentStyles { .fill(Self::danger_button_fill()) .stroke(egui::Stroke::NONE) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_SM)) - .min_size(Self::DIALOG_BUTTON_MIN_SIZE) } /// Add a primary button to the UI with pointer cursor on hover. + /// + /// Uses `add_sized` so the button's `AtomLayout` inherits + /// `horizontal_align = Center` from a `centered_and_justified` inner layout. + /// Without this the default top-down-left layout would left-align the text + /// atom inside the fill rect when the label is narrower than `DIALOG_BUTTON_MIN_SIZE`. pub fn add_primary_button(ui: &mut egui::Ui, label: impl Into) -> egui::Response { - ui.add(Self::primary_button(label)) + ui.add_sized(Self::DIALOG_BUTTON_MIN_SIZE, Self::primary_button(label)) .on_hover_cursor(CursorIcon::PointingHand) } @@ -877,25 +879,38 @@ impl ComponentStyles { .fill(DashColors::BUTTON_DISABLED) .stroke(egui::Stroke::NONE) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_SM)) - .min_size(Self::DIALOG_BUTTON_MIN_SIZE) }; - ui.add_enabled(enabled, button) - .on_hover_cursor(CursorIcon::PointingHand) + // `add_sized` wraps the widget in a `centered_and_justified` inner layout so the + // button's `AtomLayout` inherits `horizontal_align = Center`. This keeps the text + // centered within the fill rect for both the enabled and disabled branches, matching + // footprints so swapping between states doesn't jitter the cursor. + ui.add_enabled_ui(enabled, |ui| { + ui.add_sized(Self::DIALOG_BUTTON_MIN_SIZE, button) + }) + .inner + .on_hover_cursor(CursorIcon::PointingHand) } /// Add a secondary button to the UI with pointer cursor on hover. + /// + /// See `add_primary_button` for why `add_sized` is used. pub fn add_secondary_button( ui: &mut egui::Ui, label: impl Into, dark_mode: bool, ) -> egui::Response { - ui.add(Self::secondary_button(label, dark_mode)) - .on_hover_cursor(CursorIcon::PointingHand) + ui.add_sized( + Self::DIALOG_BUTTON_MIN_SIZE, + Self::secondary_button(label, dark_mode), + ) + .on_hover_cursor(CursorIcon::PointingHand) } /// Add a danger button to the UI with pointer cursor on hover. + /// + /// See `add_primary_button` for why `add_sized` is used. pub fn add_danger_button(ui: &mut egui::Ui, label: impl Into) -> egui::Response { - ui.add(Self::danger_button(label)) + ui.add_sized(Self::DIALOG_BUTTON_MIN_SIZE, Self::danger_button(label)) .on_hover_cursor(CursorIcon::PointingHand) } @@ -924,17 +939,24 @@ impl ComponentStyles { .frame(true) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) .stroke(egui::Stroke::NONE) - .min_size(Vec2::new(100.0, Self::TOOLBAR_BUTTON_HEIGHT)) } + /// Default minimum size for toolbar buttons. Use this with `add_sized` at callsites. + pub const TOOLBAR_BUTTON_MIN_SIZE: Vec2 = Vec2::new(100.0, Self::TOOLBAR_BUTTON_HEIGHT); + /// Add a toolbar button to the UI with pointer cursor on hover. + /// + /// See `add_primary_button` for why `add_sized` is used. pub fn add_toolbar_button( ui: &mut egui::Ui, label: impl Into, fill: egui::Color32, ) -> egui::Response { - ui.add(Self::toolbar_button(label, fill)) - .on_hover_cursor(CursorIcon::PointingHand) + ui.add_sized( + Self::TOOLBAR_BUTTON_MIN_SIZE, + Self::toolbar_button(label, fill), + ) + .on_hover_cursor(CursorIcon::PointingHand) } } diff --git a/src/ui/tokens/tokens_screen/my_tokens.rs b/src/ui/tokens/tokens_screen/my_tokens.rs index 5f53b036c..3625fa231 100644 --- a/src/ui/tokens/tokens_screen/my_tokens.rs +++ b/src/ui/tokens/tokens_screen/my_tokens.rs @@ -261,30 +261,14 @@ impl TokensScreen { match self.tokens_subscreen { TokensSubscreen::MyTokens => { - let button = egui::Button::new( - RichText::new("Import Token") - .color(egui::Color32::WHITE) - .strong(), - ) - .fill(DashColors::DASH_BLUE) - .min_size(egui::vec2(150.0, 36.0)); - - if ui.add(button).clicked() { + if ComponentStyles::add_primary_button(ui, "Import Token").clicked() { app_action = AppAction::AddScreen( ScreenType::AddTokenById.create_screen(&self.app_context), ); } } TokensSubscreen::SearchTokens | TokensSubscreen::TokenCreator => { - let button = egui::Button::new( - RichText::new("Refresh") - .color(egui::Color32::WHITE) - .strong(), - ) - .fill(DashColors::DASH_BLUE) - .min_size(egui::vec2(150.0, 36.0)); - - if ui.add(button).clicked() { + if ComponentStyles::add_primary_button(ui, "Refresh").clicked() { if let RefreshingStatus::Refreshing = self.refreshing_status { app_action = AppAction::None; } else { diff --git a/src/ui/tokens/tokens_screen/token_creator.rs b/src/ui/tokens/tokens_screen/token_creator.rs index f9ed0cacf..15031045a 100644 --- a/src/ui/tokens/tokens_screen/token_creator.rs +++ b/src/ui/tokens/tokens_screen/token_creator.rs @@ -11,7 +11,7 @@ use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::Identifier; use eframe::epaint::Color32; use egui::{ComboBox, Context, Frame, Margin, RichText, TextEdit, Ui}; -use crate::ui::theme::DashColors; +use crate::ui::theme::{ComponentStyles, DashColors}; use crate::ui::ScreenType; use crate::app::{AppAction, BackendTasksExecutionMode}; use crate::backend_task::BackendTask; @@ -117,15 +117,9 @@ impl TokensScreen { ui.add_space(15.0); - let button = egui::Button::new( - RichText::new("Load Identity") - .color(egui::Color32::WHITE) - .strong(), - ) - .fill(DashColors::DASH_BLUE) - .min_size(egui::vec2(150.0, 36.0)); - - if ui.add(button).clicked() { + if ComponentStyles::add_primary_button(ui, "Load Identity") + .clicked() + { load_identity_clicked = true; } @@ -404,19 +398,13 @@ impl TokensScreen { && self.selected_token_preset.is_some(); ui.horizontal(|ui| { - let button = egui::Button::new( - RichText::new("Create Token") - .color(egui::Color32::WHITE) - .strong(), + if ComponentStyles::add_primary_button_enabled( + ui, + can_create, + "Create Token", ) - .fill(if can_create { - DashColors::DASH_BLUE - } else { - egui::Color32::GRAY - }) - .min_size(egui::vec2(150.0, 36.0)); - - if ui.add_enabled(can_create, button).clicked() { + .clicked() + { // Auto-set plural name if empty (singular + "s") let singular = self.token_names_input[0].0.trim().to_string(); if self.token_names_input[0].1.trim().is_empty() { @@ -855,15 +843,9 @@ impl TokensScreen { // 6) "Register Token Contract" button ui.add_space(10.0); ui.horizontal(|ui| { - let register_button = egui::Button::new( - RichText::new("Register Token Contract") - .color(egui::Color32::WHITE) - .strong(), - ) - .fill(DashColors::DASH_BLUE) - .min_size(egui::vec2(200.0, 36.0)); - - if ui.add(register_button).clicked() { + if ComponentStyles::add_primary_button(ui, "Register Token Contract") + .clicked() + { match self.parse_token_build_args() { Ok(args) => { // If success, show the "confirmation popup" @@ -877,15 +859,7 @@ impl TokensScreen { } } - let view_json_button = egui::Button::new( - RichText::new("View JSON") - .color(egui::Color32::WHITE) - .strong(), - ) - .fill(DashColors::DASH_BLUE) - .min_size(egui::vec2(120.0, 36.0)); - - if ui.add(view_json_button).clicked() { + if ComponentStyles::add_primary_button(ui, "View JSON").clicked() { match self.parse_token_build_args() { Ok(args) => { // We have the parsed token creation arguments diff --git a/src/ui/tools/grovestark_screen.rs b/src/ui/tools/grovestark_screen.rs index a1e6944a9..5c85185ed 100644 --- a/src/ui/tools/grovestark_screen.rs +++ b/src/ui/tools/grovestark_screen.rs @@ -1125,7 +1125,6 @@ impl ScreenLike for GroveSTARKScreen { .fill(DashColors::DASH_BLUE) .stroke(egui::Stroke::NONE) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) } else { Button::new( RichText::new("🔐 Generate Proof") @@ -1135,7 +1134,6 @@ impl ScreenLike for GroveSTARKScreen { .fill(DashColors::glass_white(dark_mode)) .stroke(egui::Stroke::new(1.0, DashColors::border(dark_mode))) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) }; if ui.add(generate_button).clicked() { @@ -1155,7 +1153,6 @@ impl ScreenLike for GroveSTARKScreen { .fill(DashColors::DASH_BLUE) .stroke(egui::Stroke::NONE) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) } else { Button::new( RichText::new("✅ Verify Proof") @@ -1165,7 +1162,6 @@ impl ScreenLike for GroveSTARKScreen { .fill(DashColors::glass_white(dark_mode)) .stroke(egui::Stroke::new(1.0, DashColors::border(dark_mode))) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::Vec2::new(150.0, 28.0)) }; if ui.add(verify_button).clicked() { diff --git a/src/ui/wallets/send_screen.rs b/src/ui/wallets/send_screen.rs index a21f904dc..c518b02d8 100644 --- a/src/ui/wallets/send_screen.rs +++ b/src/ui/wallets/send_screen.rs @@ -19,7 +19,7 @@ use crate::ui::components::wallet_unlock_popup::{ WalletUnlockPopup, WalletUnlockResult, try_open_wallet_no_password, wallet_needs_unlock, }; use crate::ui::components::{BannerHandle, MessageBanner, OptionBannerExt}; -use crate::ui::theme::DashColors; +use crate::ui::theme::{ComponentStyles, DashColors}; use crate::ui::{MessageType, RootScreenType, ScreenLike}; use dash_sdk::dashcore_rpc::dashcore::Address; use dash_sdk::dashcore_rpc::dashcore::address::NetworkUnchecked; @@ -2537,16 +2537,7 @@ impl WalletSendScreen { self.get_transaction_type_description() }; - let send_button = - egui::Button::new(RichText::new(button_text).color(Color32::WHITE).strong()) - .fill(if can_send { - DashColors::DASH_BLUE - } else { - DashColors::DASH_BLUE.gamma_multiply(0.5) - }) - .min_size(egui::vec2(160.0, 36.0)); - - if ui.add_enabled(can_send, send_button).clicked() { + if ComponentStyles::add_primary_button_enabled(ui, can_send, button_text).clicked() { match self.validate_and_send() { Ok(send_action) => { self.set_send_progress_banner(ui.ctx()); @@ -3120,16 +3111,7 @@ impl WalletSendScreen { let button_text = if is_sending { "Sending..." } else { "Send" }; - let send_button = - egui::Button::new(RichText::new(button_text).color(Color32::WHITE).strong()) - .fill(if can_send { - DashColors::DASH_BLUE - } else { - DashColors::DASH_BLUE.gamma_multiply(0.5) - }) - .min_size(egui::vec2(160.0, 36.0)); - - if ui.add_enabled(can_send, send_button).clicked() { + if ComponentStyles::add_primary_button_enabled(ui, can_send, button_text).clicked() { match self.validate_and_send_advanced() { Ok(send_action) => { self.set_send_progress_banner(ui.ctx()); diff --git a/src/ui/wallets/single_key_send_screen.rs b/src/ui/wallets/single_key_send_screen.rs index 394db3ad4..9d06d08d1 100644 --- a/src/ui/wallets/single_key_send_screen.rs +++ b/src/ui/wallets/single_key_send_screen.rs @@ -821,25 +821,10 @@ impl SingleKeyWalletSendScreen { let is_rpc_mode = self.app_context.core_backend_mode() == CoreBackendMode::Rpc; let button_enabled = wallet_is_open && !self.sending && is_rpc_mode; - // Only force white label text when the button is actually clickable; - // otherwise let egui's default disabled visuals take over so the - // greyed-out state is visually unambiguous. - let send_label = - RichText::new(if self.sending { "Sending..." } else { "Send" }).strong(); - let send_label = if button_enabled { - send_label.color(Color32::WHITE) - } else { - send_label - }; - let send_button = egui::Button::new(send_label) - .fill(if button_enabled { - DashColors::DASH_BLUE - } else { - DashColors::DASH_BLUE.gamma_multiply(0.5) - }) - .min_size(egui::vec2(120.0, 36.0)); + let send_label = if self.sending { "Sending..." } else { "Send" }; - let mut response = ui.add_enabled(button_enabled, send_button); + let mut response = + ComponentStyles::add_primary_button_enabled(ui, button_enabled, send_label); if !is_rpc_mode { response = response.on_disabled_hover_text(SINGLE_KEY_REQUIRES_CORE); } diff --git a/src/ui/wallets/wallets_screen/dialogs.rs b/src/ui/wallets/wallets_screen/dialogs.rs index 6e10fe72e..4c9de1f4d 100644 --- a/src/ui/wallets/wallets_screen/dialogs.rs +++ b/src/ui/wallets/wallets_screen/dialogs.rs @@ -816,7 +816,13 @@ impl WalletsBalancesScreen { }); if ui - .add_enabled(can_fund, fund_button) + .add_enabled_ui(can_fund, |ui| { + ui.add_sized( + ComponentStyles::DIALOG_BUTTON_MIN_SIZE, + fund_button, + ) + }) + .inner .on_hover_cursor(egui::CursorIcon::PointingHand) .clicked() { diff --git a/tests/kittest/button_sizing.rs b/tests/kittest/button_sizing.rs new file mode 100644 index 000000000..8c719d168 --- /dev/null +++ b/tests/kittest/button_sizing.rs @@ -0,0 +1,110 @@ +//! Verifies the centering fix in `ComponentStyles` helpers does not +//! cap button max width — long labels must grow, short labels must floor. + +use dash_evo_tool::ui::theme::ComponentStyles; +use egui_kittest::Harness; +use egui_kittest::kittest::Queryable; + +fn button_rect(harness: &mut Harness<'_>, label: &str) -> egui::Rect { + harness.run(); + harness.get_by_label(label).rect() +} + +#[test] +fn primary_button_grows_for_long_label() { + let mut harness = Harness::builder() + .with_size(egui::vec2(800.0, 200.0)) + .build_ui(|ui| { + ui.horizontal(|ui| { + let _ = ComponentStyles::add_primary_button(ui, "Register Token Contract"); + }); + }); + let rect = button_rect(&mut harness, "Register Token Contract"); + eprintln!("Register Token Contract rect: {rect:?}"); + assert!( + rect.width() > 120.0, + "long label must grow past the 96px floor (actual width: {})", + rect.width() + ); +} + +#[test] +fn primary_button_floors_short_label() { + let mut harness = Harness::builder() + .with_size(egui::vec2(800.0, 200.0)) + .build_ui(|ui| { + ui.horizontal(|ui| { + let _ = ComponentStyles::add_primary_button(ui, "OK"); + }); + }); + let rect = button_rect(&mut harness, "OK"); + assert!( + rect.width() >= 90.0, + "short label must still honor ~96px min width (actual: {})", + rect.width() + ); + assert!( + rect.height() >= 30.0, + "short label must honor ~36px min height (actual: {})", + rect.height() + ); +} + +#[test] +fn secondary_button_grows_for_long_label() { + let mut harness = Harness::builder() + .with_size(egui::vec2(800.0, 200.0)) + .build_ui(|ui| { + ui.horizontal(|ui| { + let _ = ComponentStyles::add_secondary_button( + ui, + "Create Asset Lock Transaction", + false, + ); + }); + }); + let rect = button_rect(&mut harness, "Create Asset Lock Transaction"); + assert!( + rect.width() > 150.0, + "long label on secondary must grow (actual: {})", + rect.width() + ); +} + +#[test] +fn danger_button_grows_for_long_label() { + let mut harness = Harness::builder() + .with_size(egui::vec2(800.0, 200.0)) + .build_ui(|ui| { + ui.horizontal(|ui| { + let _ = ComponentStyles::add_danger_button(ui, "Remove From Local Database"); + }); + }); + let rect = button_rect(&mut harness, "Remove From Local Database"); + assert!( + rect.width() > 150.0, + "long label on danger must grow (actual: {})", + rect.width() + ); +} + +#[test] +fn primary_button_enabled_grows_for_long_label() { + let mut harness = Harness::builder() + .with_size(egui::vec2(800.0, 200.0)) + .build_ui(|ui| { + ui.horizontal(|ui| { + let _ = ComponentStyles::add_primary_button_enabled( + ui, + true, + "Register Token Contract", + ); + }); + }); + let rect = button_rect(&mut harness, "Register Token Contract"); + assert!( + rect.width() > 120.0, + "long label on enabled primary must grow (actual: {})", + rect.width() + ); +} diff --git a/tests/kittest/main.rs b/tests/kittest/main.rs index 387adf319..979ba6322 100644 --- a/tests/kittest/main.rs +++ b/tests/kittest/main.rs @@ -1,3 +1,4 @@ +mod button_sizing; mod confirmation_dialog; mod create_asset_lock_screen; mod identities_screen;