diff --git a/locales/en.yml b/locales/en.yml index f5beb4b..39eae3e 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,7 +1,23 @@ copy: Copy paste: Paste -accounts: - title: Accounts +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:' + pass: 'Password:' + name_empty: Enter name of wallet + pass_empty: Enter password for wallet + word_number: 'Word #%{number}' + word_empty: 'Enter word #%{number} from recovery phrase' + not_valid_word: Entered word is not valid + create: Create + import: Import + mnemonic_desc: 'Safely write down and save your %{}-word recovery phrase below. If you lose your device, you will need them to restore access to your funds.' + mnemonic_conf: Select words in the same order as they are displayed in your recovery phrase. network: self: Network node: Integrated node diff --git a/locales/ru.yml b/locales/ru.yml index 0ac9b08..8286ab3 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -1,7 +1,23 @@ copy: Копировать paste: Вставить -accounts: - title: Аккаунты +continue: Продолжить +complete: Завершить +wallets: + title: Кошельки + new: Новый кошелёк + create_desc: Создайте или импортируйте существующий кошелёк из сохранённой фразы восстановления. + add: Добавить кошелёк + name: 'Название:' + pass: 'Пароль:' + name_empty: Введите название кошелька + pass_empty: Введите пароль для кошелька + word_number: 'Слово #%{number}' + word_empty: 'Введите слово #%{number} из фразы восстановления' + not_valid_word: Введено недопустимое слово + create: Создать + import: Импортировать + mnemonic_desc: 'Безопасно запишите и сохраните свою фразу восстановления из %{} слов ниже. Они понадобятся вам, чтобы восстановить доступ к вашим средствам, если вы потеряете устройство.' + mnemonic_conf: Выберите слова в таком же порядке, как они отображены в вашей фразе восстановления. network: self: Сеть node: Встроенный узел diff --git a/src/gui/app.rs b/src/gui/app.rs index 7970278..f3fc8be 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -12,12 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::sync::atomic::{AtomicBool, Ordering}; + use egui::Context; +use lazy_static::lazy_static; use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::Root; +lazy_static! { + /// State to check if platform Back button was pressed. + static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false); +} + /// Implements ui entry point and contains platform-specific callbacks. pub struct PlatformApp { /// Platform specific callbacks handler. @@ -34,9 +42,13 @@ impl PlatformApp { impl eframe::App for PlatformApp { fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) { - // Handle Esc keyboard key event. - if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { - Root::on_back(); + // Handle Esc keyboard key event and platform Back button key event. + let back_button_pressed = back_button_pressed(); + if ctx.input(|i| i.key_pressed(egui::Key::Escape) || back_button_pressed) { + if back_button_pressed { + BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed); + } + self.root.on_back(); } // Show main content. @@ -56,6 +68,13 @@ impl eframe::App for PlatformApp { } } + + +/// Check if platform Back button was pressed. +fn back_button_pressed() -> bool { + BACK_BUTTON_PRESSED.load(Ordering::Relaxed) +} + #[allow(dead_code)] #[cfg(target_os = "android")] #[allow(non_snake_case)] @@ -66,8 +85,5 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onBack( _class: jni::objects::JObject, _activity: jni::objects::JObject, ) { - Root::on_back(); -} - - - + BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed); +} \ No newline at end of file diff --git a/src/gui/views/mod.rs b/src/gui/views/mod.rs index 1dabc33..38fdf46 100644 --- a/src/gui/views/mod.rs +++ b/src/gui/views/mod.rs @@ -27,5 +27,5 @@ pub use root::*; mod network; pub use network::*; -mod accounts; -pub use accounts::*; \ No newline at end of file +mod wallets; +pub use wallets::*; \ No newline at end of file diff --git a/src/gui/views/network/connections.rs b/src/gui/views/network/connections.rs new file mode 100644 index 0000000..0d077e6 --- /dev/null +++ b/src/gui/views/network/connections.rs @@ -0,0 +1,32 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::gui::platform::PlatformCallbacks; + +/// Network connections content. +pub struct ConnectionsContent { + +} + +impl Default for ConnectionsContent { + fn default() -> Self { + Self {} + } +} + +impl ConnectionsContent { + pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { + + } +} \ No newline at end of file diff --git a/src/gui/views/network/mod.rs b/src/gui/views/network/mod.rs index 069e213..c716bd1 100644 --- a/src/gui/views/network/mod.rs +++ b/src/gui/views/network/mod.rs @@ -28,4 +28,8 @@ mod setup; pub use setup::*; mod network; -pub use network::*; \ No newline at end of file +pub use network::*; + + +mod connections; +pub use connections::*; \ No newline at end of file diff --git a/src/gui/views/network/network.rs b/src/gui/views/network/network.rs index 34e9ffe..0b2a647 100644 --- a/src/gui/views/network/network.rs +++ b/src/gui/views/network/network.rs @@ -19,17 +19,18 @@ use crate::AppConfig; use crate::gui::Colors; use crate::gui::icons::{CARDHOLDER, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, POWER}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, ModalContainer, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings, Root, TitleAction, TitlePanel, TitleType, View}; +use crate::gui::views::{Modal, ModalContainer, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings, Root, TitlePanel, TitleType, View}; use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup}; use crate::node::Node; +/// Network tab content interface. pub trait NetworkTab { fn get_type(&self) -> NetworkTabType; fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks); fn on_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks); } - +/// Type of [`NetworkTab`] content. #[derive(PartialEq)] pub enum NetworkTabType { Node, @@ -108,15 +109,17 @@ impl ModalContainer for Network { impl Network { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - // Show modal content if it's opened. + // Show modal content for current ui container. if self.can_draw_modal() { Modal::ui(ui, |ui, modal| { self.current_tab.as_mut().on_modal_ui(ui, modal, cb); }); } + // Show title panel. self.title_ui(ui, frame); + // Show bottom tabs. egui::TopBottomPanel::bottom("network_tabs") .frame(egui::Frame { fill: Colors::FILL, @@ -127,6 +130,7 @@ impl Network { self.tabs_ui(ui); }); + // Show tab content. egui::CentralPanel::default() .frame(egui::Frame { stroke: View::DEFAULT_STROKE, @@ -147,7 +151,7 @@ impl Network { /// Calculate tabs inner margin based on display insets (cutouts). fn tabs_inner_margin(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> Margin { Margin { - left: View::far_left_inset_margin(ui) + 4.0, + left: View::get_left_inset() + 4.0, right: View::far_right_inset_margin(ui, frame) + 4.0, top: 4.0, bottom: View::get_bottom_inset() + 4.0, @@ -157,7 +161,7 @@ impl Network { /// Calculate content inner margin based on display insets (cutouts). fn content_inner_margin(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> Margin { Margin { - left: View::far_left_inset_margin(ui) + 4.0, + left: View::get_left_inset() + 4.0, right: View::far_right_inset_margin(ui, frame) + 4.0, top: 3.0, bottom: 4.0, @@ -206,27 +210,30 @@ impl Network { let subtitle_text = Node::get_sync_status_text(); let not_syncing = Node::not_syncing(); let title_content = TitleType::WithSubTitle(title_text, subtitle_text, !not_syncing); + // Draw title panel. - TitlePanel::ui(title_content, TitleAction::new(DOTS_THREE_OUTLINE_VERTICAL, || { - //TODO: Show connections - }), if !Root::is_dual_panel_mode(frame) { - TitleAction::new(CARDHOLDER, || { - Root::toggle_side_panel(); - }) - } else { - None + TitlePanel::ui(title_content, |ui, frame| { + View::title_button(ui, DOTS_THREE_OUTLINE_VERTICAL, || { + //TODO: Show connections + }); + }, |ui, frame| { + if !Root::is_dual_panel_mode(frame) { + View::title_button(ui, CARDHOLDER, || { + Root::toggle_network_panel(); + }); + } }, ui, frame); } /// Content to draw when node is disabled. pub fn disabled_node_ui(ui: &mut egui::Ui) { - View::center_content(ui, 162.0, |ui| { + View::center_content(ui, 156.0, |ui| { let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL); ui.label(RichText::new(text) .size(16.0) .color(Colors::INACTIVE_TEXT) ); - ui.add_space(10.0); + ui.add_space(8.0); View::button(ui, format!("{} {}", POWER, t!("network.enable_node")), Colors::GOLD, || { Node::start(); }); diff --git a/src/gui/views/root.rs b/src/gui/views/root.rs index c21e1f7..083e5a0 100644 --- a/src/gui/views/root.rs +++ b/src/gui/views/root.rs @@ -11,30 +11,31 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + use std::sync::atomic::{AtomicBool, Ordering}; + use egui::os::OperatingSystem; use egui::RichText; - use lazy_static::lazy_static; -use crate::gui::Colors; +use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Accounts, Modal, ModalContainer, Network, View}; +use crate::gui::views::{Wallets, Modal, ModalContainer, Network, View}; use crate::node::Node; lazy_static! { - /// To check if side panel is open from any part of ui. - static ref SIDE_PANEL_OPEN: AtomicBool = AtomicBool::new(false); + /// Global state to check if [`Network`] panel is open. + static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false); } /// Contains main ui content, handles side panel state. pub struct Root { - /// Side panel content. - side_panel: Network, - /// Central panel content. - central_content: Accounts, + /// Side panel [`Network`] content. + network: Network, + /// Central panel [`Wallets`] content. + wallets: Wallets, - /// Check if app exit is allowed on close event of [`eframe::App`] platform implementation. + /// Check if app exit is allowed on close event of [`eframe::App`] implementation. pub(crate) exit_allowed: bool, /// Flag to show exit progress at [`Modal`]. @@ -50,8 +51,8 @@ impl Default for Root { let os = OperatingSystem::from_target_os(); let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS; Self { - side_panel: Network::default(), - central_content: Accounts::default(), + network: Network::default(), + wallets: Wallets::default(), exit_allowed, show_exit_progress: false, allowed_modal_ids: vec![ @@ -75,33 +76,39 @@ impl Root { pub const SIDE_PANEL_MIN_WIDTH: f32 = 400.0; pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - // Show opened exit confirmation Modal content. + // Show opened exit confirmation modal content. if self.can_draw_modal() { self.exit_modal_content(ui, frame, cb); } - let (is_panel_open, panel_width) = Self::side_panel_state_width(frame); + let (is_panel_open, panel_width) = Self::network_panel_state_width(frame); + // Show network content. egui::SidePanel::left("network_panel") .resizable(false) .exact_width(panel_width) - .frame(egui::Frame::none()) + .frame(egui::Frame { + fill: Colors::WHITE, + ..Default::default() + }) .show_animated_inside(ui, is_panel_open, |ui| { - // Show network content on side panel. - self.side_panel.ui(ui, frame, cb); + self.network.ui(ui, frame, cb); }); + // Show wallets content. egui::CentralPanel::default() - .frame(egui::Frame::none()) + .frame(egui::Frame { + fill: Colors::FILL_DARK, + ..Default::default() + }) .show_inside(ui, |ui| { - // Show accounts content on central panel. - self.central_content.ui(ui, frame, cb); + self.wallets.ui(ui, frame, cb); }); } - /// Get side panel state and width. - fn side_panel_state_width(frame: &mut eframe::Frame) -> (bool, f32) { + /// Get [`Network`] panel state and width. + fn network_panel_state_width(frame: &mut eframe::Frame) -> (bool, f32) { let dual_panel_mode = Self::is_dual_panel_mode(frame); - let is_panel_open = dual_panel_mode || Self::is_side_panel_open(); + let is_panel_open = dual_panel_mode || Self::is_network_panel_open(); let panel_width = if dual_panel_mode { Self::SIDE_PANEL_MIN_WIDTH + View::get_left_inset() } else { @@ -110,7 +117,7 @@ impl Root { (is_panel_open, panel_width) } - /// Check if ui can show [`Network`] and [`Accounts`] at same time. + /// Check if ui can show [`Network`] and [`Wallets`] at same time. pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool { let w = frame.info().window_info.size.x; let h = frame.info().window_info.size.y; @@ -123,14 +130,14 @@ impl Root { } /// Toggle [`Network`] panel state. - pub fn toggle_side_panel() { - let is_open = SIDE_PANEL_OPEN.load(Ordering::Relaxed); - SIDE_PANEL_OPEN.store(!is_open, Ordering::Relaxed); + pub fn toggle_network_panel() { + let is_open = NETWORK_PANEL_OPEN.load(Ordering::Relaxed); + NETWORK_PANEL_OPEN.store(!is_open, Ordering::Relaxed); } - /// Check if side panel is open. - pub fn is_side_panel_open() -> bool { - SIDE_PANEL_OPEN.load(Ordering::Relaxed) + /// Check if [`Network`] panel is open. + pub fn is_network_panel_open() -> bool { + NETWORK_PANEL_OPEN.load(Ordering::Relaxed) } /// Show exit confirmation modal. @@ -215,26 +222,12 @@ impl Root { } } - /// Handle platform-specific Back key code event. - pub fn on_back() { + /// Handle Back key event. + pub fn on_back(&mut self) { if Modal::on_back() { - Self::show_exit_modal() + if self.wallets.on_back() { + Self::show_exit_modal() + } } } -} - -#[allow(dead_code)] -#[cfg(target_os = "android")] -#[allow(non_snake_case)] -#[no_mangle] -/// Handle Back key code event from Android. -pub extern "C" fn Java_mw_gri_android_MainActivity_onBack( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, -) { - Root::on_back(); -} - - - +} \ No newline at end of file diff --git a/src/gui/views/title_panel.rs b/src/gui/views/title_panel.rs index d2cb799..14770f3 100644 --- a/src/gui/views/title_panel.rs +++ b/src/gui/views/title_panel.rs @@ -19,18 +19,6 @@ use egui_extras::{Size, StripBuilder}; use crate::gui::Colors; use crate::gui::views::View; -/// Title action button. -pub struct TitleAction { - pub(crate) icon: Box<&'static str>, - pub(crate) on_click: Box, -} - -impl TitleAction { - pub fn new(icon: &'static str, on_click: fn()) -> Option { - Option::from(Self { icon: Box::new(icon), on_click: Box::new(on_click) }) - } -} - /// Represents title content, can be single title or with animated sub-title. pub enum TitleType { Single(String), @@ -44,8 +32,8 @@ impl TitlePanel { pub const DEFAULT_HEIGHT: f32 = 54.0; pub fn ui(title: TitleType, - left_action: Option, - right_action: Option, + mut left_content: impl FnMut(&mut egui::Ui, &mut eframe::Frame), + mut right_content: impl FnMut(&mut egui::Ui, &mut eframe::Frame), ui: &mut egui::Ui, frame: &mut eframe::Frame) { // Setup identifier. @@ -68,7 +56,9 @@ impl TitlePanel { .size(Size::exact(Self::DEFAULT_HEIGHT)) .horizontal(|mut strip| { strip.cell(|ui| { - Self::action_ui(ui, left_action); + ui.centered_and_justified(|ui| { + (left_content)(ui, frame); + }); }); match title { TitleType::Single(text) => { @@ -86,7 +76,9 @@ impl TitlePanel { } } strip.cell(|ui| { - Self::action_ui(ui, right_action); + ui.centered_and_justified(|ui| { + (right_content)(ui, frame); + }); }); }); }); @@ -102,18 +94,6 @@ impl TitlePanel { } } - /// Draw panel [`TitleAction`]. - fn action_ui(ui: &mut egui::Ui, action: Option) { - if action.is_some() { - let action = action.unwrap(); - ui.centered_and_justified(|ui| { - View::title_button(ui, &action.icon, || { - (action.on_click)(); - }); - }); - } - } - /// Draw title text for [`TitleType::WithSubTitle`] type. fn with_sub_title(builder: StripBuilder, title: String, subtitle: String, animate_sub: bool) { builder diff --git a/src/gui/views/wallets/creation/connection.rs b/src/gui/views/wallets/creation/connection.rs new file mode 100644 index 0000000..42d3e48 --- /dev/null +++ b/src/gui/views/wallets/creation/connection.rs @@ -0,0 +1,27 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::wallets::creation::StepControl; + +#[derive(Default)] +pub struct ConnectionSetup { + +} + +impl ConnectionSetup { + pub fn ui(&mut self, ui: &mut egui::Ui, step: &dyn StepControl, cb: &dyn PlatformCallbacks) { + + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/creation/content.rs b/src/gui/views/wallets/creation/content.rs new file mode 100644 index 0000000..b96a832 --- /dev/null +++ b/src/gui/views/wallets/creation/content.rs @@ -0,0 +1,310 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::{Margin, RichText, TextStyle, Widget}; +use egui_extras::{Size, StripBuilder}; + +use crate::gui::Colors; +use crate::gui::icons::{EYE, EYE_SLASH, PLUS_CIRCLE}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, ModalPosition, View}; +use crate::gui::views::wallets::creation::{ConnectionSetup, MnemonicSetup, StepControl}; +use crate::gui::views::wallets::creation::mnemonic::PhraseMode; + +/// Wallet creation step. +enum Step { + /// Mnemonic phrase input. + EnterMnemonic, + /// Mnemonic phrase confirmation for [`Mnemonic`]. + ConfirmMnemonic, + /// Wallet connection setup. + SetupConnection +} + +/// Wallet creation content. +pub struct WalletCreation { + /// Wallet creation ui step. + step: Option, + + /// Flag to check if [`Modal`] just was opened to focus on first field. + modal_just_opened: bool, + /// Wallet name value. + name_edit: String, + /// Password to encrypt created wallet. + pass_edit: String, + /// Flag to show/hide password at [`egui::TextEdit`] field. + hide_pass: bool, + + /// Mnemonic phrase setup content. + pub(crate) mnemonic_setup: MnemonicSetup, + /// Network setup content. + pub(crate) network_setup: ConnectionSetup, +} + +impl Default for WalletCreation { + fn default() -> Self { + Self { + step: None, + modal_just_opened: true, + name_edit: "".to_string(), + pass_edit: "".to_string(), + hide_pass: true, + mnemonic_setup: MnemonicSetup::default(), + network_setup: ConnectionSetup::default(), + } + } +} + + +impl StepControl for WalletCreation { + /// Go to next wallet creation [`Step`]. + fn next_step(&mut self) { + self.step = match &self.step { + None => Some(Step::EnterMnemonic), + Some(step) => { + match step { + Step::EnterMnemonic => { + if self.mnemonic_setup.get_mnemonic_mode() == &PhraseMode::Generate { + Some(Step::SetupConnection) + } else { + Some(Step::ConfirmMnemonic) + } + } + Step::ConfirmMnemonic => Some(Step::SetupConnection), + Step::SetupConnection => { + //TODO: Confirm mnemonic + None + } + } + } + } + } + + /// Go to previous wallet creation [`Step`]. + fn prev_step(&mut self) { + match &self.step { + None => {} + Some(step) => { + match step { + Step::EnterMnemonic => { + // Clear values if it needs to go back on first step. + self.step = None; + self.name_edit = "".to_string(); + self.pass_edit = "".to_string(); + self.mnemonic_setup.reset(); + } + Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic), + Step::SetupConnection => self.step = Some(Step::ConfirmMnemonic) + } + } + } + } +} + +impl WalletCreation { + /// Wallet name/password input modal identifier. + pub const MODAL_ID: &'static str = "create_wallet_modal"; + + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + // Show wallet creation step content. + egui::CentralPanel::default() + .frame(egui::Frame { + stroke: View::DEFAULT_STROKE, + inner_margin: Margin { + left: View::far_left_inset_margin(ui) + 4.0, + right: View::get_right_inset() + 4.0, + top: 3.0, + bottom: 4.0, + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + self.step_ui(ui, cb); + }); + } + + /// Draw wallet creation [`Step`] content. + fn step_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + match &self.step { + None => { + // Show wallet creation message if step is empty. + View::center_content(ui, 124.0 + View::get_bottom_inset(), |ui| { + let text = t!("wallets.create_desc"); + ui.label(RichText::new(text) + .size(16.0) + .color(Colors::INACTIVE_TEXT) + ); + ui.add_space(8.0); + let add_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add")); + View::button(ui, add_text, Colors::BUTTON, || { + Self::show_modal(); + }); + }); + } + Some(step) => { + match step { + Step::EnterMnemonic => { + self.mnemonic_setup.ui(ui, self, cb); + } + Step::ConfirmMnemonic => {} + Step::SetupConnection => {} + } + } + } + } + + /// Check if it's possible to go back for current step. + pub fn can_go_back(&self) -> bool { + self.step.is_some() + } + + /// Back button key event handling. + pub fn go_back(&mut self) { + self.prev_step(); + } + + /// Start wallet creation from showing [`Modal`] to enter name and password. + pub fn show_modal() { + Modal::show(Modal::new(Self::MODAL_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.add"))); + } + + /// Callback to go to next step for wallet creation from [`Modal`]. + fn on_modal_confirmation(&mut self, modal: &Modal, cb: &dyn PlatformCallbacks) { + // Check if input values are not empty. + if self.name_edit.is_empty() || self.pass_edit.is_empty() { + return; + } + self.step = Some(Step::EnterMnemonic); + cb.hide_keyboard(); + modal.close(); + } + + /// Draw wallet creation [`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.name")) + .size(17.0) + .color(Colors::GRAY)); + ui.add_space(8.0); + + // Show wallet name text edit. + let name_resp = egui::TextEdit::singleline(&mut self.name_edit) + .id(ui.id().with("wallet_name_edit")) + .font(TextStyle::Heading) + .desired_width(ui.available_width()) + .cursor_at_end(true) + .ui(ui); + ui.add_space(8.0); + if name_resp.clicked() { + cb.show_keyboard(); + } + + // Check if modal was just opened to show focus on name text input. + if self.modal_just_opened { + self.modal_just_opened = false; + cb.show_keyboard(); + name_resp.request_focus(); + } + + ui.label(RichText::new(t!("wallets.pass")) + .size(17.0) + .color(Colors::GRAY)); + ui.add_space(8.0); + + StripBuilder::new(ui) + .size(Size::exact(34.0)) + .vertical(|mut strip| { + strip.strip(|builder| { + builder + .size(Size::remainder()) + .size(Size::exact(48.0)) + .horizontal(|mut strip| { + strip.cell(|ui| { + ui.add_space(2.0); + // Draw wallet password text edit. + let pass_resp = egui::TextEdit::singleline(&mut self.pass_edit) + .id(ui.id().with("wallet_pass_edit")) + .font(TextStyle::Heading) + .desired_width(ui.available_width()) + .cursor_at_end(true) + .password(self.hide_pass) + .ui(ui); + if pass_resp.clicked() { + cb.show_keyboard(); + } + + // Hide keyboard if input fields has no focus. + if !pass_resp.has_focus() && !name_resp.has_focus() { + cb.hide_keyboard(); + } + }); + strip.cell(|ui| { + ui.vertical_centered(|ui| { + // Draw button to show/hide password. + let eye_icon = if self.hide_pass { EYE } else { EYE_SLASH }; + View::button(ui, eye_icon.to_string(), Colors::WHITE, || { + self.hide_pass = !self.hide_pass; + }); + }); + }); + }); + }) + }); + + // Show information when specified values are empty. + if self.name_edit.is_empty() { + ui.add_space(12.0); + ui.label(RichText::new(t!("wallets.name_empty")) + .size(17.0) + .color(Colors::INACTIVE_TEXT)); + } else if self.pass_edit.is_empty() { + ui.add_space(12.0); + ui.label(RichText::new(t!("wallets.pass_empty")) + .size(17.0) + .color(Colors::INACTIVE_TEXT)); + } + 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, || { + // 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(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("continue"), Colors::WHITE, || { + self.on_modal_confirmation(modal, cb); + }); + }); + }); + ui.add_space(6.0); + }); + } +} diff --git a/src/gui/views/wallets/creation/mnemonic.rs b/src/gui/views/wallets/creation/mnemonic.rs new file mode 100644 index 0000000..950718c --- /dev/null +++ b/src/gui/views/wallets/creation/mnemonic.rs @@ -0,0 +1,124 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use grin_keychain::mnemonic::from_entropy; +use rand::{Rng, thread_rng}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::wallets::creation::StepControl; + +/// Mnemonic phrase setup mode. +#[derive(PartialEq)] +pub enum PhraseMode { + /// Generate new mnemonic phrase. + Generate, + /// Import existing mnemonic phrase. + Import +} + +/// Mnemonic phrase type based on words count. +pub enum PhraseType { Words12, Words15, Words18, Words21, Words24 } + +impl PhraseType { + pub fn value(&self) -> usize { + match *self { + PhraseType::Words12 => 12, + PhraseType::Words15 => 15, + PhraseType::Words18 => 18, + PhraseType::Words21 => 21, + PhraseType::Words24 => 24 + } + } +} + +/// Mnemonic phrase container. +pub struct Mnemonic { + /// Phrase setup mode. + pub(crate) mode: PhraseMode, + /// Type of phrase based on words count. + size: PhraseType, + /// Words for phrase. + words: Vec +} + +impl Default for Mnemonic { + fn default() -> Self { + let size = PhraseType::Words12; + let size_value = size.value(); + Self { mode: PhraseMode::Generate, size, words: Vec::with_capacity(size_value) } + } +} + +impl Mnemonic { + /// Change mnemonic phrase setup [`PhraseMode`]. + fn set_mode(&mut self, mode: PhraseMode) { + self.mode = mode; + self.setup_words(); + } + + /// Change mnemonic phrase words [`PhraseType`]. + fn set_size(&mut self, size: PhraseType) { + self.size = size; + self.setup_words(); + } + + /// Setup words based on current [`PhraseMode`] and [`PhraseType`]. + fn setup_words(&mut self) { + self.words = match self.mode { + PhraseMode::Generate => { + let mut rng = thread_rng(); + let mut entropy: Vec = Vec::with_capacity(self.size.value()); + for _ in 0..self.size.value() { + entropy.push(rng.gen()); + } + from_entropy(&entropy).unwrap() + .split(" ") + .map(|s| s.to_string()) + .collect::>() + }, + PhraseMode::Import => Vec::with_capacity(self.size.value()) + }; + } +} + +/// Mnemonic phrase setup content. +pub struct MnemonicSetup { + /// Current mnemonic phrase. + mnemonic: Mnemonic, + /// Word value for [`Modal`]. + word_edit: String, +} + +impl Default for MnemonicSetup { + fn default() -> Self { + Self { + mnemonic: Mnemonic::default(), + word_edit: "".to_string(), + } + } +} + +impl MnemonicSetup { + pub fn ui(&self, ui: &mut egui::Ui, step: &dyn StepControl, cb: &dyn PlatformCallbacks) { + + } + + pub fn get_mnemonic_mode(&self) -> &PhraseMode { + &self.mnemonic.mode + } + + /// Reset mnemonic phrase to default values. + pub fn reset(&mut self) { + self.mnemonic = Mnemonic::default(); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/creation/mod.rs b/src/gui/views/wallets/creation/mod.rs new file mode 100644 index 0000000..f9b7b2a --- /dev/null +++ b/src/gui/views/wallets/creation/mod.rs @@ -0,0 +1,30 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod mnemonic; +pub use mnemonic::MnemonicSetup; + +mod connection; +pub use connection::ConnectionSetup; + +mod content; +pub use content::WalletCreation; + +/// Interface to provide moving between wallet creation steps. +pub trait StepControl { + /// Go to next wallet creation step. + fn next_step(&mut self); + /// Go to previous wallet creation Step. + fn prev_step(&mut self); +} \ No newline at end of file diff --git a/src/gui/views/accounts/mod.rs b/src/gui/views/wallets/mod.rs similarity index 91% rename from src/gui/views/accounts/mod.rs rename to src/gui/views/wallets/mod.rs index 56e29d6..3c3a46c 100644 --- a/src/gui/views/accounts/mod.rs +++ b/src/gui/views/wallets/mod.rs @@ -12,5 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod accounts; -pub use accounts::*; \ No newline at end of file +mod wallets; +mod creation; +mod wallet; + +pub use wallets::*; \ No newline at end of file diff --git a/src/gui/views/accounts/accounts.rs b/src/gui/views/wallets/wallet.rs similarity index 56% rename from src/gui/views/accounts/accounts.rs rename to src/gui/views/wallets/wallet.rs index 1df3fe5..08cc3c4 100644 --- a/src/gui/views/accounts/accounts.rs +++ b/src/gui/views/wallets/wallet.rs @@ -12,46 +12,41 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::gui::Colors; -use crate::gui::icons::{GLOBE, PLUS}; -use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Root, TitleAction, TitleType, TitlePanel, View}; +use egui::Margin; -/// Accounts content. -pub struct Accounts { - /// List of accounts. - list: Vec +use crate::gui::Colors; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::View; + +/// Selected wallet list item content. +pub struct WalletContent { + /// Current wallet instance. + item: String } -impl Default for Accounts { - fn default() -> Self { - Self { - list: vec![], - } +impl WalletContent { + fn new(item: String) -> Self { + Self { item } } } -impl Accounts { +impl WalletContent { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - let title_content = TitleType::Single(t!("accounts.title").to_uppercase()); - TitlePanel::ui(title_content, if !Root::is_dual_panel_mode(frame) { - TitleAction::new(GLOBE, || { - Root::toggle_side_panel(); - }) - } else { - None - }, TitleAction::new(PLUS, || { - //TODO: add account - }), ui, frame); - + // Show wallet content. egui::CentralPanel::default() .frame(egui::Frame { stroke: View::DEFAULT_STROKE, - fill: Colors::FILL_DARK, + fill: Colors::WHITE, + inner_margin: Margin { + left: View::far_left_inset_margin(ui) + 4.0, + right: View::far_right_inset_margin(ui, frame) + 4.0, + top: 3.0, + bottom: 4.0, + }, ..Default::default() }) .show_inside(ui, |ui| { - //TODO: accounts list + //TODO: wallet content }); } } \ No newline at end of file diff --git a/src/gui/views/wallets/wallets.rs b/src/gui/views/wallets/wallets.rs new file mode 100644 index 0000000..3a34caf --- /dev/null +++ b/src/gui/views/wallets/wallets.rs @@ -0,0 +1,199 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::cmp::max; + +use egui::{Align2, Margin, Vec2}; + +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::wallet::WalletContent; + +/// Wallets content. +pub struct Wallets { + /// List of wallets. + list: Vec, + + /// Selected list item content. + item_content: Option, + /// Wallet creation content. + creation_content: WalletCreation, + + /// [`Modal`] ids allowed at this ui container. + modal_ids: Vec<&'static str> +} + +impl Default for Wallets { + fn default() -> Self { + //TODO load list. + Self { + list: vec![], + item_content: None, + creation_content: WalletCreation::default(), + modal_ids: vec![ + WalletCreation::MODAL_ID + ] + } + } +} + +impl ModalContainer for Wallets { + fn modal_ids(&self) -> &Vec<&'static str> { + &self.modal_ids + } +} + +impl Wallets { + pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { + // Show modal content for current ui container. + if self.can_draw_modal() { + Modal::ui(ui, |ui, modal| { + match modal.id { + WalletCreation::MODAL_ID => self.creation_content.modal_ui(ui, modal, cb), + _ => {} + } + }); + } + + // 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); + egui::SidePanel::right("wallet_panel") + .resizable(false) + .min_width(wallet_panel_width) + .frame(egui::Frame { + fill: if self.list.is_empty() { Colors::FILL_DARK } else { Colors::WHITE }, + ..Default::default() + }) + .show_animated_inside(ui, is_wallet_panel_open, |ui| { + self.wallet_content_ui(ui, frame, cb); + }); + + // Show list of wallets. + if !self.list.is_empty() { + egui::CentralPanel::default() + .frame(egui::Frame { + stroke: View::DEFAULT_STROKE, + fill: Colors::FILL_DARK, + inner_margin: Margin { + left: View::far_left_inset_margin(ui) + 4.0, + right: View::far_right_inset_margin(ui, frame) + 4.0, + top: 3.0, + bottom: 4.0, + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + //TODO: wallets list + }); + // Show wallet creation button if wallet panel is not open. + if !is_wallet_panel_open { + self.add_wallet_btn_ui(ui); + } + } + } + + /// Draw title content. + fn title_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { + // Setup title text. + let title_content = TitleType::Single(t!("wallets.title").to_uppercase()); + + // Draw title panel. + TitlePanel::ui(title_content, |ui, frame| { + if !Root::is_dual_panel_mode(frame) { + View::title_button(ui, GLOBE, || { + Root::toggle_network_panel(); + }); + } else if self.creation_content.can_go_back() { + View::title_button(ui, ARROW_LEFT, || { + self.creation_content.go_back(); + }); + }; + }, |ui, frame| { + View::title_button(ui, GEAR, || { + //TODO: show settings. + }); + }, ui, frame); + } + + /// Draw [`WalletContent`] ui. + fn wallet_content_ui(&mut self, + ui: &mut egui::Ui, + frame: &mut eframe::Frame, + cb: &dyn PlatformCallbacks) { + if self.list.is_empty() || self.item_content.is_none() { + self.creation_content.ui(ui, cb) + } else { + self.item_content.as_mut().unwrap().ui(ui, frame, cb); + } + } + + /// Get [`WalletContent`] panel width. + fn wallet_panel_width(&self, ui: &mut egui::Ui, frame: &mut eframe::Frame) -> f32 { + if Self::is_dual_panel_mode(ui, frame) { + let min_width = (Root::SIDE_PANEL_MIN_WIDTH + View::get_right_inset()) as i64; + let available_width = if self.list.is_empty() { + ui.available_width() + } else { + ui.available_width() - Root::SIDE_PANEL_MIN_WIDTH + } as i64; + max(min_width, available_width) as f32 + } else { + let dual_panel_root = Root::is_dual_panel_mode(frame); + if dual_panel_root { + ui.available_width() + } else { + frame.info().window_info.size.x + } + } + } + + /// Check if ui can show [`Wallets`] list and [`WalletContent`] content at same time. + fn is_dual_panel_mode(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> bool { + let dual_panel_root = Root::is_dual_panel_mode(frame); + let max_width = ui.available_width(); + dual_panel_root && max_width >= (Root::SIDE_PANEL_MIN_WIDTH * 2.0) + View::get_right_inset() + } + + /// Draw floating button to create the wallet. + fn add_wallet_btn_ui(&self, ui: &mut egui::Ui) { + egui::Window::new("create_wallet_button") + .title_bar(false) + .resizable(false) + .collapsible(false) + .anchor(Align2::RIGHT_BOTTOM, Vec2::new(-8.0, -8.0)) + .frame(egui::Frame::default()) + .show(ui.ctx(), |ui| { + View::round_button(ui, PLUS, || { + WalletCreation::show_modal(); + }); + }); + } + + /// Handle Back key event. + /// Return `false` when event was handled. + pub fn on_back(&mut self) -> bool { + let can_go_back = self.creation_content.can_go_back(); + if can_go_back { + self.creation_content.go_back(); + } + !can_go_back + } +} \ No newline at end of file diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 4401524..2cbbf0d 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -12,6 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod wallet; +mod wallet; // pub use self::wallet::{init, init_from_seed}; diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 845ee06..6ddbed9 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -12,10 +12,3 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub fn create_from_seed() { - -} - -pub fn create() { - -} \ No newline at end of file