From 2f5011c36f0387d687e02d4a838afd435434bb44 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sun, 23 Jul 2023 11:48:28 +0300 Subject: [PATCH] ui: modal show method refactoring, mnemonic word input, update translations --- locales/en.yml | 6 +- locales/ru.yml | 8 +- src/gui/views/modal.rs | 8 +- src/gui/views/network/settings.rs | 12 +- src/gui/views/network/setup/dandelion.rs | 25 +- src/gui/views/network/setup/node.rs | 22 +- src/gui/views/network/setup/p2p.rs | 38 +-- src/gui/views/network/setup/pool.rs | 30 +-- src/gui/views/network/setup/stratum.rs | 24 +- src/gui/views/root.rs | 5 +- src/gui/views/wallets/creation/creation.rs | 89 ++++--- src/gui/views/wallets/creation/mnemonic.rs | 262 ++++++++++++++------- src/gui/views/wallets/creation/types.rs | 51 +++- src/gui/views/wallets/wallets.rs | 22 +- 14 files changed, 376 insertions(+), 226 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 142e4ac..ff9c643 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -4,7 +4,6 @@ continue: Continue complete: Complete wallets: title: Wallets - new: New wallet create_desc: Create or import existing wallet from saved recovery phrase. add: Add wallet name: 'Name:' @@ -13,13 +12,12 @@ wallets: pass_empty: Enter password for wallet create: Create recover: Restore + saved_phrase: Saved phrase words_count: 'Words count:' - word_number: 'Word #%{number}' - word_empty: 'Enter word #%{number} from your recovery phrase' + enter_word: 'Enter word #%{number}:' not_valid_word: Entered word is not valid create_phrase_desc: Safely write down and save your recovery phrase. restore_phrase_desc: Enter words from your saved recovery phrase. - conf_phrase_desc: Select words in the same order as they are displayed in your recovery phrase. setup_conn_desc: Choose wallet connection method. network: self: Network diff --git a/locales/ru.yml b/locales/ru.yml index 22ca972..4eda04a 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -4,7 +4,6 @@ continue: Продолжить complete: Завершить wallets: title: Кошельки - new: Новый кошелёк create_desc: Создайте или импортируйте существующий кошелёк из сохранённой фразы восстановления. add: Добавить кошелёк name: 'Название:' @@ -13,13 +12,12 @@ wallets: pass_empty: Введите пароль для кошелька create: Создать recover: Восстановить + saved_phrase: Сохранённая фраза words_count: 'Количество слов:' - word_number: 'Слово #%{number}' - word_empty: 'Введите слово #%{number} из вашей фразы восстановления' + enter_word: 'Введите слово #%{number}:' not_valid_word: Введено недопустимое слово - create_phrase_desc: Безопасно запишите и сохраните свою фразу восстановления. + create_phrase_desc: Безопасно запишите и сохраните вашу фразу восстановления. restore_phrase_desc: Введите слова из вашей сохранённой фразы восстановления. - conf_phrase_desc: Выберите слова в таком же порядке, как они отображены в вашей фразе восстановления. setup_conn_desc: Выберите способ подключения кошелька network: self: Сеть diff --git a/src/gui/views/modal.rs b/src/gui/views/modal.rs index 7630e8e..9f010fa 100644 --- a/src/gui/views/modal.rs +++ b/src/gui/views/modal.rs @@ -114,16 +114,16 @@ impl Modal { self.closeable.load(Ordering::Relaxed) } - /// Set title text on Modal creation. + /// Set title text on [`Modal`] creation. pub fn title(mut self, title: String) -> Self { self.title = Some(title.to_uppercase()); self } - /// Set [`Modal`] instance to show at ui. - pub fn show(modal: Modal) { + /// Set [`Modal`] instance into state to show at ui. + pub fn show(self) { let mut w_nav = MODAL_STATE.write().unwrap(); - w_nav.modal = Some(modal); + w_nav.modal = Some(self); } /// Remove [`Modal`] from [`MODAL_STATE`] if it's showing and can be closed. diff --git a/src/gui/views/network/settings.rs b/src/gui/views/network/settings.rs index 625c678..09fd713 100644 --- a/src/gui/views/network/settings.rs +++ b/src/gui/views/network/settings.rs @@ -134,10 +134,10 @@ impl NetworkSettings { t!("network_settings.reset_settings")); View::button(ui, button_text, Colors::GOLD, || { // Show modal to confirm settings reset. - let reset_modal = Modal::new(Self::RESET_SETTINGS_MODAL) + Modal::new(Self::RESET_SETTINGS_MODAL) .position(ModalPosition::Center) - .title(t!("modal.confirmation")); - Modal::show(reset_modal); + .title(t!("modal.confirmation")) + .show(); }); // Show reminder to restart enabled node. @@ -200,10 +200,10 @@ impl NetworkSettings { pub fn show_node_restart_required_modal() { if Node::is_running() { // Show modal to apply changes by node restart. - let port_modal = Modal::new(Self::NODE_RESTART_REQUIRED_MODAL) + Modal::new(Self::NODE_RESTART_REQUIRED_MODAL) .position(ModalPosition::Center) - .title(t!("network.settings")); - Modal::show(port_modal); + .title(t!("network.settings")) + .show(); } } diff --git a/src/gui/views/network/setup/dandelion.rs b/src/gui/views/network/setup/dandelion.rs index 266f1f7..cbf2e54 100644 --- a/src/gui/views/network/setup/dandelion.rs +++ b/src/gui/views/network/setup/dandelion.rs @@ -113,10 +113,10 @@ impl DandelionSetup { // Setup values for modal. self.epoch_edit = epoch; // Show epoch setup modal. - let epoch_modal = Modal::new(Self::EPOCH_MODAL) + Modal::new(Self::EPOCH_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(epoch_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -198,11 +198,10 @@ impl DandelionSetup { // Setup values for modal. self.embargo_edit = embargo; // Show embargo setup modal. - let embargo_modal = Modal::new(Self::EMBARGO_MODAL) + Modal::new(Self::EMBARGO_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(embargo_modal); - cb.show_keyboard(); + .title(t!("network_settings.change_value")) + .show(); }); ui.add_space(6.0); } @@ -283,10 +282,10 @@ impl DandelionSetup { // Setup values for modal. self.aggregation_edit = agg; // Show aggregation setup modal. - let aggregation_modal = Modal::new(Self::AGGREGATION_MODAL) + Modal::new(Self::AGGREGATION_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(aggregation_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -368,10 +367,10 @@ impl DandelionSetup { // Setup values for modal. self.stem_prob_edit = stem_prob; // Show stem probability setup modal. - let embargo_modal = Modal::new(Self::STEM_PROBABILITY_MODAL) + Modal::new(Self::STEM_PROBABILITY_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(embargo_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); diff --git a/src/gui/views/network/setup/node.rs b/src/gui/views/network/setup/node.rs index c1f801d..55f1d0a 100644 --- a/src/gui/views/network/setup/node.rs +++ b/src/gui/views/network/setup/node.rs @@ -221,10 +221,10 @@ impl NodeSetup { self.api_port_available_edit = self.is_api_port_available; // Show API port modal. - let port_modal = Modal::new(Self::API_PORT_MODAL) + Modal::new(Self::API_PORT_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(port_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -335,10 +335,10 @@ impl NodeSetup { // Setup values for modal. self.secret_edit = secret_value.unwrap_or("".to_string()); // Show secret edit modal. - let port_modal = Modal::new(modal_id) + Modal::new(modal_id) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(port_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); } @@ -455,11 +455,11 @@ impl NodeSetup { View::button(ui, format!("{} {}", CLOCK_CLOCKWISE, ftl.clone()), Colors::BUTTON, || { // Setup values for modal. self.ftl_edit = ftl; - // Show stratum port modal. - let ftl_modal = Modal::new(Self::FTL_MODAL) + // Show ftl value setup modal. + Modal::new(Self::FTL_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(ftl_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -478,7 +478,7 @@ impl NodeSetup { .color(Colors::GRAY)); ui.add_space(8.0); - // Draw stratum port text edit. + // Draw ftl value text edit. let text_edit_resp = egui::TextEdit::singleline(&mut self.ftl_edit) .id(Id::from(modal.id)) .font(TextStyle::Heading) diff --git a/src/gui/views/network/setup/p2p.rs b/src/gui/views/network/setup/p2p.rs index 079a91c..85a0492 100644 --- a/src/gui/views/network/setup/p2p.rs +++ b/src/gui/views/network/setup/p2p.rs @@ -212,15 +212,15 @@ impl P2PSetup { self.port_edit = port; self.port_available_edit = self.is_port_available; // Show p2p port modal. - let port_modal = Modal::new(Self::PORT_MODAL) + Modal::new(Self::PORT_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(port_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); - // Show error when stratum server port is unavailable. + // Show error when p2p port is unavailable. if !self.is_port_available { ui.add_space(6.0); ui.label(RichText::new(t!("network_settings.port_unavailable")) @@ -371,10 +371,10 @@ impl P2PSetup { _ => Self::DNS_SEEDS_TITLE.to_string() }; // Show modal to add peer. - let peer_modal = Modal::new(modal_id) + Modal::new(modal_id) .position(ModalPosition::CenterTop) - .title(modal_title); - Modal::show(peer_modal); + .title(modal_title) + .show(); cb.show_keyboard(); }); } @@ -580,10 +580,10 @@ impl P2PSetup { // Setup values for modal. self.ban_window_edit = ban_window; // Show ban window period setup modal. - let ban_modal = Modal::new(Self::BAN_WINDOW_MODAL) + Modal::new(Self::BAN_WINDOW_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(ban_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -671,10 +671,10 @@ impl P2PSetup { // Setup values for modal. self.max_inbound_count = max_inbound; // Show maximum number of inbound peers setup modal. - let max_inbound_modal = Modal::new(Self::MAX_INBOUND_MODAL) + Modal::new(Self::MAX_INBOUND_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(max_inbound_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -757,10 +757,10 @@ impl P2PSetup { // Setup values for modal. self.max_outbound_count = max_outbound; // Show maximum number of outbound peers setup modal. - let max_outbound = Modal::new(Self::MAX_OUTBOUND_MODAL) + Modal::new(Self::MAX_OUTBOUND_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(max_outbound); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -843,10 +843,10 @@ impl P2PSetup { // Setup values for modal. self.min_outbound_count = min_outbound; // Show maximum number of outbound peers setup modal. - let min_outbound = Modal::new(Self::MIN_OUTBOUND_MODAL) + Modal::new(Self::MIN_OUTBOUND_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(min_outbound); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); diff --git a/src/gui/views/network/setup/pool.rs b/src/gui/views/network/setup/pool.rs index fe54c8e..21b7577 100644 --- a/src/gui/views/network/setup/pool.rs +++ b/src/gui/views/network/setup/pool.rs @@ -115,10 +115,10 @@ impl PoolSetup { // Setup values for modal. self.fee_base_edit = fee; // Show fee setup modal. - let fee_modal = Modal::new(Self::FEE_BASE_MODAL) + Modal::new(Self::FEE_BASE_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(fee_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -200,10 +200,10 @@ impl PoolSetup { // Setup values for modal. self.reorg_period_edit = period; // Show reorg period setup modal. - let reorg_modal = Modal::new(Self::REORG_PERIOD_MODAL) + Modal::new(Self::REORG_PERIOD_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(reorg_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -285,10 +285,10 @@ impl PoolSetup { // Setup values for modal. self.pool_size_edit = size; // Show pool size setup modal. - let size_modal = Modal::new(Self::POOL_SIZE_MODAL) + Modal::new(Self::POOL_SIZE_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(size_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -370,10 +370,10 @@ impl PoolSetup { // Setup values for modal. self.stempool_size_edit = size; // Show stempool size setup modal. - let stem_modal = Modal::new(Self::STEMPOOL_SIZE_MODAL) + Modal::new(Self::STEMPOOL_SIZE_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(stem_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -455,10 +455,10 @@ impl PoolSetup { // Setup values for modal. self.max_weight_edit = weight; // Show total tx weight setup modal. - let weight_modal = Modal::new(Self::MAX_WEIGHT_MODAL) + Modal::new(Self::MAX_WEIGHT_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(weight_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); diff --git a/src/gui/views/network/setup/stratum.rs b/src/gui/views/network/setup/stratum.rs index 91d9a69..0163af7 100644 --- a/src/gui/views/network/setup/stratum.rs +++ b/src/gui/views/network/setup/stratum.rs @@ -163,10 +163,10 @@ impl StratumSetup { self.stratum_port_edit = port; self.stratum_port_available_edit = self.is_port_available; // Show stratum port modal. - let port_modal = Modal::new(Self::STRATUM_PORT_MODAL) + Modal::new(Self::STRATUM_PORT_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(port_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(12.0); @@ -270,10 +270,10 @@ impl StratumSetup { self.attempt_time_edit = time; // Show attempt time modal. - let time_modal = Modal::new(Self::ATTEMPT_TIME_MODAL) + Modal::new(Self::ATTEMPT_TIME_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(time_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -293,7 +293,7 @@ impl StratumSetup { .color(Colors::GRAY)); ui.add_space(8.0); - // Draw stratum port text edit. + // Draw attempt time text edit. let text_edit_resp = egui::TextEdit::singleline(&mut self.attempt_time_edit) .id(Id::from(modal.id)) .font(TextStyle::Heading) @@ -360,11 +360,11 @@ impl StratumSetup { // Setup values for modal. self.min_share_diff_edit = diff; - // Show attempt time modal. - let diff_modal = Modal::new(Self::MIN_SHARE_DIFF_MODAL) + // Show share difficulty setup modal. + Modal::new(Self::MIN_SHARE_DIFF_MODAL) .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Modal::show(diff_modal); + .title(t!("network_settings.change_value")) + .show(); cb.show_keyboard(); }); ui.add_space(6.0); @@ -379,7 +379,7 @@ impl StratumSetup { .color(Colors::GRAY)); ui.add_space(8.0); - // Draw stratum port text edit. + // Draw share difficulty text edit. let text_edit_resp = egui::TextEdit::singleline(&mut self.min_share_diff_edit) .id(Id::from(modal.id)) .font(TextStyle::Heading) diff --git a/src/gui/views/root.rs b/src/gui/views/root.rs index 083e5a0..d502ae2 100644 --- a/src/gui/views/root.rs +++ b/src/gui/views/root.rs @@ -142,8 +142,9 @@ impl Root { /// Show exit confirmation modal. pub fn show_exit_modal() { - let exit_modal = Modal::new(Self::EXIT_MODAL_ID).title(t!("modal.confirmation")); - Modal::show(exit_modal); + Modal::new(Self::EXIT_MODAL_ID) + .title(t!("modal.confirmation")) + .show(); } /// Draw exit confirmation modal content. diff --git a/src/gui/views/wallets/creation/creation.rs b/src/gui/views/wallets/creation/creation.rs index 936a512..72d394b 100644 --- a/src/gui/views/wallets/creation/creation.rs +++ b/src/gui/views/wallets/creation/creation.rs @@ -66,11 +66,11 @@ impl Default for WalletCreation { impl WalletCreation { /// Wallet name/password input modal identifier. - pub const MODAL_ID: &'static str = "create_wallet_modal"; + pub const NAME_PASS_MODAL: &'static str = "name_pass_modal"; pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - // Show wallet creation step description and confirmation panel. - if self.can_go_back() { + // Show wallet creation step description and confirmation bottom panel. + if self.step.is_some() { egui::TopBottomPanel::bottom("wallet_creation_step_panel") .frame(egui::Frame { stroke: View::DEFAULT_STROKE, @@ -85,36 +85,55 @@ impl WalletCreation { .show_inside(ui, |ui| { ui.vertical_centered(|ui| { if let Some(step) = &self.step { - // Setup step description text. - let step_text = match step { + // Setup step description text and availability. + let (step_text, step_available) = match step { Step::EnterMnemonic => { let mode = &self.mnemonic_setup.mnemonic.mode; let size_value = self.mnemonic_setup.mnemonic.size.value(); - if mode == &PhraseMode::Generate { + let text = if mode == &PhraseMode::Generate { t!("wallets.create_phrase_desc", "number" => size_value) } else { t!("wallets.restore_phrase_desc", "number" => size_value) - } + }; + let available = !self + .mnemonic_setup + .mnemonic + .words + .contains(&"".to_string()); + (text, available) } - Step::ConfirmMnemonic => t!("wallets.conf_phrase_desc"), - Step::SetupConnection => t!("wallets.setup_conn_desc") + Step::ConfirmMnemonic => { + let text = t!("wallets.restore_phrase_desc"); + let available = !self + .mnemonic_setup + .mnemonic + .confirm_words + .contains(&"".to_string()); + (text, available) + }, + Step::SetupConnection => (t!("wallets.setup_conn_desc"), true) }; // Show step description. ui.label(RichText::new(step_text).size(16.0).color(Colors::GRAY)); - ui.add_space(4.0); - // Setup next step button text. - let (next_text, color) = if step == &Step::SetupConnection { - (format!("{} {}", CHECK, t!("complete")), Colors::GOLD) - } else { - let text = format!("{} {}", SHARE_FAT, t!("continue")); - (text, Colors::WHITE) - }; - // Show next step button. - View::button(ui, next_text.to_uppercase(), color, || { - self.forward(); - }); - ui.add_space(4.0); + // Show next step button if there are no empty words. + + if step_available { + // Setup button text. + let (next_text, color) = if step == &Step::SetupConnection { + (format!("{} {}", CHECK, t!("complete")), Colors::GOLD) + } else { + let text = format!("{} {}", SHARE_FAT, t!("continue")); + (text, Colors::WHITE) + }; + + ui.add_space(4.0); + // Show button. + View::button(ui, next_text.to_uppercase(), color, || { + self.forward(); + }); + ui.add_space(4.0); + } } }); }); @@ -165,16 +184,18 @@ impl WalletCreation { ui.add_space(8.0); let add_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add")); View::button(ui, add_text, Colors::BUTTON, || { - Self::show_modal(); + self.show_name_pass_modal(); }); }); } Some(step) => { match step { Step::EnterMnemonic => { - self.mnemonic_setup.enter_ui(ui, cb); + self.mnemonic_setup.ui(ui); + } + Step::ConfirmMnemonic => { + self.mnemonic_setup.confirm_ui(ui); } - Step::ConfirmMnemonic => {} Step::SetupConnection => {} } } @@ -230,10 +251,17 @@ impl WalletCreation { } /// Start wallet creation from showing [`Modal`] to enter name and password. - pub fn show_modal() { - Modal::show(Modal::new(Self::MODAL_ID) + pub fn show_name_pass_modal(&mut self) { + // Reset modal values. + self.hide_pass = false; + self.modal_just_opened = true; + self.name_edit = "".to_string(); + self.pass_edit = "".to_string(); + // Show modal. + Modal::new(Self::NAME_PASS_MODAL) .position(ModalPosition::CenterTop) - .title(t!("wallets.add"))); + .title(t!("wallets.add")) + .show(); } /// Draw wallet creation [`Modal`] content. @@ -332,11 +360,6 @@ impl WalletCreation { ui.columns(2, |columns| { columns[0].vertical_centered_justified(|ui| { View::button(ui, t!("modal.cancel"), Colors::WHITE, || { - // Clear values. - self.hide_pass = false; - self.modal_just_opened = true; - self.name_edit = "".to_string(); - self.pass_edit = "".to_string(); // Close modal. cb.hide_keyboard(); modal.close(); diff --git a/src/gui/views/wallets/creation/mnemonic.rs b/src/gui/views/wallets/creation/mnemonic.rs index 201a9ca..f1014e9 100644 --- a/src/gui/views/wallets/creation/mnemonic.rs +++ b/src/gui/views/wallets/creation/mnemonic.rs @@ -12,35 +12,46 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{RichText, ScrollArea}; +use egui::{Id, RichText, ScrollArea, TextStyle, Widget}; use crate::gui::Colors; use crate::gui::icons::PENCIL; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Root, View}; +use crate::gui::views::{Modal, ModalPosition, Root, View}; use crate::gui::views::wallets::creation::types::{Mnemonic, PhraseMode, PhraseSize}; /// Mnemonic phrase setup content. pub struct MnemonicSetup { /// Current mnemonic phrase. pub(crate) mnemonic: Mnemonic, - /// Word value for [`Modal`]. + + /// Current word number to edit at [`Modal`]. + word_num_edit: usize, + /// Entered word value for [`Modal`]. word_edit: String, + /// Flag to check if entered word is valid. + valid_word_edit: bool } impl Default for MnemonicSetup { fn default() -> Self { Self { mnemonic: Mnemonic::default(), + word_num_edit: 0, word_edit: "".to_string(), + valid_word_edit: true } } } impl MnemonicSetup { - pub fn enter_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + /// Identifier for word input [`Modal`]. + pub const WORD_INPUT_MODAL: &'static str = "word_input_modal"; + + /// Draw content for input step. + pub fn ui(&mut self, ui: &mut egui::Ui) { ScrollArea::vertical() - .id_source("mnemonic_words_list") + .id_source("input_mnemonic_words_list") .auto_shrink([false; 2]) .show(ui, |ui| { ui.add_space(10.0); @@ -53,7 +64,23 @@ impl MnemonicSetup { ui.add_space(6.0); // Show words setup. - self.word_list_ui(ui); + self.word_list_ui(ui, self.mnemonic.mode == PhraseMode::Import); + }); + } + + /// Draw content for confirmation step. + pub fn confirm_ui(&mut self, ui: &mut egui::Ui) { + ui.add_space(4.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("wallets.saved_phrase")).size(16.0).color(Colors::GRAY)); + }); + ui.add_space(6.0); + ScrollArea::vertical() + .id_source("confirm_mnemonic_words_list") + .auto_shrink([false; 2]) + .show(ui, |ui| { + // Show words setup. + self.word_list_ui(ui, true); }); } @@ -89,125 +116,103 @@ impl MnemonicSetup { // Show mnemonic phrase size setup. let mut size = self.mnemonic.size.clone(); ui.columns(5, |columns| { - columns[0].vertical_centered(|ui| { - let words12 = PhraseSize::Words12; - let text = words12.value().to_string(); - View::radio_value(ui, &mut size, words12, text); - }); - columns[1].vertical_centered(|ui| { - let words15 = PhraseSize::Words15; - let text = words15.value().to_string(); - View::radio_value(ui, &mut size, words15, text); - }); - columns[2].vertical_centered(|ui| { - let words18 = PhraseSize::Words18; - let text = words18.value().to_string(); - View::radio_value(ui, &mut size, words18, text); - }); - columns[3].vertical_centered(|ui| { - let words21 = PhraseSize::Words21; - let text = words21.value().to_string(); - View::radio_value(ui, &mut size, words21, text); - }); - columns[4].vertical_centered(|ui| { - let words24 = PhraseSize::Words24; - let text = words24.value().to_string(); - View::radio_value(ui, &mut size, words24, text); - }); + for (index, word) in PhraseSize::VALUES.iter().enumerate() { + columns[index].vertical_centered(|ui| { + let text = word.value().to_string(); + View::radio_value(ui, &mut size, word.clone(), text); + }); + } }); if size != self.mnemonic.size { self.mnemonic.set_size(size); } } - /// Calculate word list columns count based on available ui width. - fn calc_columns_count(ui: &mut egui::Ui) -> usize { - let w = ui.available_width(); - let min_panel_w = Root::SIDE_PANEL_MIN_WIDTH - 12.0; - let double_min_panel_w = min_panel_w * 2.0; - if w >= min_panel_w * 1.5 && w < double_min_panel_w { - 3 - } else if w >= double_min_panel_w { - 4 - } else { - 2 - } - } - - /// Draw word list for mnemonic phrase. - fn word_list_ui(&self, ui: &mut egui::Ui) { + /// Draw list of words for mnemonic phrase. + fn word_list_ui(&mut self, ui: &mut egui::Ui, edit_words: bool) { + ui.add_space(6.0); ui.scope(|ui| { // Setup spacing between columns. ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0); - if self.mnemonic.mode == PhraseMode::Generate { - ui.add_space(6.0) - } + // Select list of words based on current mode and edit flag. + let words = match self.mnemonic.mode { + PhraseMode::Generate => { + if edit_words { + self.mnemonic.confirm_words.clone() + } else { + self.mnemonic.words.clone() + } + } + PhraseMode::Import => self.mnemonic.words.clone() + }; let mut word_number = 0; - let cols = Self::calc_columns_count(ui); - let _ = self.mnemonic.words.chunks(cols).map(|chunk| { + let cols = list_columns_count(ui); + let _ = words.chunks(cols).map(|chunk| { let size = chunk.len(); word_number += 1; if size > 1 { ui.columns(cols, |columns| { columns[0].horizontal(|ui| { - self.word_item_ui(ui, word_number, chunk, 0); + let word = chunk.get(0).unwrap(); + self.word_item_ui(ui, word_number, word, edit_words); }); columns[1].horizontal(|ui| { word_number += 1; - self.word_item_ui(ui, word_number, chunk, 1); + let word = chunk.get(1).unwrap(); + self.word_item_ui(ui, word_number, word, edit_words); }); if size > 2 { columns[2].horizontal(|ui| { word_number += 1; - self.word_item_ui(ui, word_number, chunk, 2); + let word = chunk.get(2).unwrap(); + self.word_item_ui(ui, word_number, word, edit_words); }); } if size > 3 { columns[3].horizontal(|ui| { word_number += 1; - self.word_item_ui(ui, word_number, chunk, 3); + let word = chunk.get(3).unwrap(); + self.word_item_ui(ui, word_number, word, edit_words); }); } }); } else { ui.columns(cols, |columns| { columns[0].horizontal(|ui| { - self.word_item_ui(ui, word_number, chunk, 0); + let word = chunk.get(0).unwrap(); + self.word_item_ui(ui, word_number, word, edit_words); }); }); - ui.add_space(12.0); } }).collect::>(); }); + ui.add_space(6.0); } - /// Draw word item at given index from provided chunk. - fn word_item_ui(&self, - ui: &mut egui::Ui, - word_number: usize, - chunk: &[String], - index: usize) { - match self.mnemonic.mode { - PhraseMode::Generate => { - ui.add_space(12.0); - let word = chunk.get(index).unwrap(); - let text = format!("#{} {}", word_number, word); - ui.label(RichText::new(text).size(17.0).color(Colors::BLACK)); - } - PhraseMode::Import => { - let mut size = ui.available_size(); - size.x = 90.0; - ui.allocate_ui(size, |ui| { - View::button(ui, PENCIL.to_string(), Colors::BUTTON, || { - //TODO: open modal - }); - }); - ui.label(RichText::new(format!("#{}", word_number)) - .size(17.0) - .color(Colors::BLACK)); - } + /// Draw word list item for current mode. + fn word_item_ui(&mut self, ui: &mut egui::Ui, word_number: usize, word: &String, edit: bool) { + if edit { + ui.add_space(6.0); + View::button(ui, PENCIL.to_string(), Colors::BUTTON, || { + // Setup modal values. + self.word_num_edit = word_number; + self.word_edit = word.clone(); + self.valid_word_edit = true; + // Show word edit modal. + Modal::new(MnemonicSetup::WORD_INPUT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.saved_phrase")) + .show(); + }); + ui.label(RichText::new(format!("#{} {}", word_number, word)) + .size(17.0) + .color(Colors::BLACK)); + } else { + ui.add_space(12.0); + let text = format!("#{} {}", word_number, word); + ui.label(RichText::new(text).size(17.0).color(Colors::BLACK)); } } @@ -215,4 +220,95 @@ impl MnemonicSetup { pub fn reset(&mut self) { self.mnemonic = Mnemonic::default(); } + + /// Show word input [`Modal`] content. + pub fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_num_edit)) + .size(17.0) + .color(Colors::GRAY)); + ui.add_space(8.0); + + // Draw word value text edit. + let text_edit_resp = egui::TextEdit::singleline(&mut self.word_edit) + .id(Id::from(format!("{}{}", modal.id, self.word_num_edit))) + .font(TextStyle::Heading) + .desired_width(ui.available_width()) + .cursor_at_end(true) + .ui(ui); + text_edit_resp.request_focus(); + if text_edit_resp.clicked() { + cb.show_keyboard(); + } + + // Show error when specified word is not valid. + if !self.valid_word_edit { + ui.add_space(12.0); + ui.label(RichText::new(t!("wallets.not_valid_word")) + .size(17.0) + .color(Colors::RED)); + } + ui.add_space(12.0); + }); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::WHITE, || { + // Close modal. + cb.hide_keyboard(); + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("continue"), Colors::WHITE, || { + // Check if word is valid. + let word_index = self.word_num_edit - 1; + if !self.mnemonic.is_valid_word(&self.word_edit, word_index) { + self.valid_word_edit = false; + return; + } + // Select list where to save word. + let words = match self.mnemonic.mode { + PhraseMode::Generate => &mut self.mnemonic.confirm_words, + PhraseMode::Import => &mut self.mnemonic.words + }; + // Save word at list. + words.remove(word_index); + words.insert(word_index, self.word_edit.clone()); + // Close modal or go to next word to edit. + let close_modal = words.len() == self.word_num_edit + || !words.get(self.word_num_edit).unwrap().is_empty(); + if close_modal { + cb.hide_keyboard(); + modal.close(); + } else { + self.word_num_edit += 1; + self.word_edit = "".to_string(); + } + }); + }); + }); + ui.add_space(6.0); + }); + } +} + +/// Calculate word list columns count based on available ui width. +fn list_columns_count(ui: &mut egui::Ui) -> usize { + let w = ui.available_width(); + let min_panel_w = Root::SIDE_PANEL_MIN_WIDTH - 12.0; + let double_min_panel_w = min_panel_w * 2.0; + if w >= min_panel_w * 1.5 && w < double_min_panel_w { + 3 + } else if w >= double_min_panel_w { + 4 + } else { + 2 + } } \ No newline at end of file diff --git a/src/gui/views/wallets/creation/types.rs b/src/gui/views/wallets/creation/types.rs index 8673d99..a120d51 100644 --- a/src/gui/views/wallets/creation/types.rs +++ b/src/gui/views/wallets/creation/types.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use grin_keychain::mnemonic::from_entropy; +use grin_keychain::mnemonic::{from_entropy, search}; use rand::{Rng, thread_rng}; /// Wallet creation step. @@ -20,7 +20,7 @@ use rand::{Rng, thread_rng}; pub enum Step { /// Mnemonic phrase input. EnterMnemonic, - /// Mnemonic phrase confirmation for [`Mnemonic`]. + /// Mnemonic phrase confirmation. ConfirmMnemonic, /// Wallet connection setup. SetupConnection @@ -40,6 +40,14 @@ pub enum PhraseMode { pub enum PhraseSize { Words12, Words15, Words18, Words21, Words24 } impl PhraseSize { + pub const VALUES: [PhraseSize; 5] = [ + PhraseSize::Words12, + PhraseSize::Words15, + PhraseSize::Words18, + PhraseSize::Words21, + PhraseSize::Words24 + ]; + /// Gen words count number. pub fn value(&self) -> usize { match *self { @@ -69,8 +77,10 @@ pub struct Mnemonic { pub(crate) mode: PhraseMode, /// Size of phrase based on words count. pub(crate) size: PhraseSize, - /// Words for phrase. - pub(crate) words: Vec + /// Generated words. + pub(crate) words: Vec, + /// Words to confirm the phrase. + pub(crate) confirm_words: Vec } impl Default for Mnemonic { @@ -78,7 +88,8 @@ impl Default for Mnemonic { let size = PhraseSize::Words24; let mode = PhraseMode::Generate; let words = Self::generate_words(&mode, &size); - Self { mode, size, words } + let confirm_words = Self::empty_words(&size); + Self { mode, size, words, confirm_words } } } @@ -87,15 +98,28 @@ impl Mnemonic { pub fn set_mode(&mut self, mode: PhraseMode) { self.mode = mode; self.words = Self::generate_words(&self.mode, &self.size); + self.confirm_words = Self::empty_words(&self.size); } /// Change mnemonic phrase words [`PhraseSize`]. pub fn set_size(&mut self, size: PhraseSize) { self.size = size; self.words = Self::generate_words(&self.mode, &self.size); + self.confirm_words = Self::empty_words(&self.size); } - /// Setup words based on provided [`PhraseMode`] and [`PhraseSize`]. + /// Check if provided word is in BIP39 format and equal to non-empty generated word at index. + pub fn is_valid_word(&self, word: &String, index: usize) -> bool { + let valid = search(word).is_ok(); + let equal = if let Some(gen_word) = self.words.get(index) { + gen_word.is_empty() || gen_word == word + } else { + false + }; + valid && equal + } + + /// Generate list of words based on provided [`PhraseMode`] and [`PhraseSize`]. fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec { match mode { PhraseMode::Generate => { @@ -110,12 +134,17 @@ impl Mnemonic { .collect::>() }, PhraseMode::Import => { - let mut words = Vec::with_capacity(size.value()); - for _ in 0..size.value() { - words.push("".to_string()) - } - words + Self::empty_words(size) } } } + + /// Generate empty list of words based on provided [`PhraseSize`]. + fn empty_words(size: &PhraseSize) -> Vec { + let mut words = Vec::with_capacity(size.value()); + for _ in 0..size.value() { + words.push("".to_string()) + } + words + } } \ No newline at end of file diff --git a/src/gui/views/wallets/wallets.rs b/src/gui/views/wallets/wallets.rs index 447d9c0..e6393cb 100644 --- a/src/gui/views/wallets/wallets.rs +++ b/src/gui/views/wallets/wallets.rs @@ -20,7 +20,7 @@ use crate::gui::Colors; use crate::gui::icons::{ARROW_LEFT, GEAR, GLOBE, PLUS}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalContainer, Root, TitlePanel, TitleType, View}; -use crate::gui::views::wallets::creation::WalletCreation; +use crate::gui::views::wallets::creation::{MnemonicSetup, WalletCreation}; use crate::gui::views::wallets::wallet::WalletContent; /// Wallets content. @@ -45,7 +45,8 @@ impl Default for Wallets { item_content: None, creation_content: WalletCreation::default(), modal_ids: vec![ - WalletCreation::MODAL_ID + WalletCreation::NAME_PASS_MODAL, + MnemonicSetup::WORD_INPUT_MODAL ] } } @@ -63,7 +64,12 @@ impl Wallets { if self.can_draw_modal() { Modal::ui(ui, |ui, modal| { match modal.id { - WalletCreation::MODAL_ID => self.creation_content.modal_ui(ui, modal, cb), + WalletCreation::NAME_PASS_MODAL => { + self.creation_content.modal_ui(ui, modal, cb); + }, + MnemonicSetup::WORD_INPUT_MODAL => { + self.creation_content.mnemonic_setup.modal_ui(ui, modal, cb); + } _ => {} } }); @@ -72,9 +78,9 @@ impl Wallets { // Show title panel. self.title_ui(ui, frame); - // Show wallet content. let is_wallet_panel_open = Self::is_dual_panel_mode(ui, frame) || self.list.is_empty(); let wallet_panel_width = self.wallet_panel_width(ui, frame); + // Show wallet content. egui::SidePanel::right("wallet_panel") .resizable(false) .min_width(wallet_panel_width) @@ -105,7 +111,7 @@ impl Wallets { }); // Show wallet creation button if wallet panel is not open. if !is_wallet_panel_open { - self.add_wallet_btn_ui(ui); + self.create_wallet_btn_ui(ui); } } } @@ -114,7 +120,7 @@ impl Wallets { fn title_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { // Setup title text. let title_text = if self.creation_content.can_go_back() { - t!("wallets.new") + t!("wallets.add") } else { t!("wallets.title") }; @@ -178,7 +184,7 @@ impl Wallets { } /// Draw floating button to create the wallet. - fn add_wallet_btn_ui(&self, ui: &mut egui::Ui) { + fn create_wallet_btn_ui(&mut self, ui: &mut egui::Ui) { egui::Window::new("create_wallet_button") .title_bar(false) .resizable(false) @@ -187,7 +193,7 @@ impl Wallets { .frame(egui::Frame::default()) .show(ui.ctx(), |ui| { View::round_button(ui, PLUS, || { - WalletCreation::show_modal(); + self.creation_content.show_name_pass_modal(); }); }); }