diff --git a/locales/en.yml b/locales/en.yml index f19bb82..0d15bfa 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,4 +1,6 @@ -screen_accounts: +copy: Copy +paste: Paste +accounts: title: Accounts network: self: Network @@ -59,7 +61,7 @@ network_mining: enable_server: Enable server disable_server: Disable server info: 'Mining server is enabled, you can change its settings by selecting %{settings} at the bottom of the screen. Data is updating when devices are connected.' - info_settings: To change the settings of enabled server, you will need to restart the node. + restart_server_required: Server restart is required to apply changes. rewards_wallet: Wallet for rewards server: Stratum server address: Address @@ -85,8 +87,6 @@ network_settings: foreign_api_secret: Foreign API token disabled: Disabled enabled: Enabled - copy: Copy - paste: Paste ftl: The Future Time Limit (FTL) ftl_description: Limit on how far into the future, relative to a node's local time in seconds, the timestamp on a new block can be, in order for the block to be accepted. not_valid_value: Entered value is not valid diff --git a/locales/ru.yml b/locales/ru.yml index 213ec9a..5c7e1c0 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -1,4 +1,6 @@ -screen_accounts: +copy: Копировать +paste: Вставить +accounts: title: Аккаунты network: self: Сеть @@ -59,7 +61,7 @@ network_mining: enable_server: Включить сервер disable_server: Выключить сервер info: 'Сервер майнинга запущен, вы можете изменить его настройки, выбрав %{settings} внизу экрана. Данные обновляются, когда устройства подключены.' - info_settings: Для изменения настроек запущенного сервера потребуется перезапуск узла. + restart_server_required: Для применения изменений потребуется перезапуск Stratum сервера. rewards_wallet: Кошелёк для наград server: Stratum сервер address: Адрес @@ -85,8 +87,6 @@ network_settings: foreign_api_secret: Foreign API токен disabled: Отключен enabled: Включен - copy: Копировать - paste: Вставить ftl: Предел Будущего Времени (FTL) ftl_description: Насколько далеко в будущем, относительно локального времени узла в секундах, может находиться временная метка на новом блоке для его принятия. not_valid_value: Введено недопустимое значение diff --git a/src/gui/app.rs b/src/gui/app.rs index 07fc51f..61906c7 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -14,11 +14,10 @@ use egui::{Context, RichText, Stroke}; use egui::os::OperatingSystem; +use crate::gui::Colors; -use crate::gui::{Colors, Navigator}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::screens::Root; -use crate::gui::views::{ModalContainer, View}; +use crate::gui::views::{Modal, ModalContainer, Root, View}; use crate::node::Node; /// To be implemented at platform-specific module. @@ -27,16 +26,12 @@ pub struct PlatformApp { pub(crate) platform: Platform, } -/// Contains main ui content, handles application exit and visual style setup. +/// Contains main ui, handles exit and visual style setup. pub struct App { - /// Main ui container. + /// Main ui content. root: Root, - - /// Check if app exit is allowed on close event callback. + /// Check if app exit is allowed on close event of [`eframe::App`] platform implementation. pub(crate) exit_allowed: bool, - /// Called from callback of [`eframe::App`] platform implementation on close event. - pub(crate) exit_requested: bool, - /// Flag to show exit progress at modal. show_exit_progress: bool, /// List of allowed modal ids for this [`ModalContainer`]. @@ -47,14 +42,13 @@ impl Default for App { fn default() -> Self { let os = OperatingSystem::from_target_os(); // Exit from eframe only for non-mobile platforms. - let allow_to_exit = os == OperatingSystem::Android || os == OperatingSystem::IOS; + let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS; Self { root: Root::default(), - exit_allowed: allow_to_exit, - exit_requested: false, + exit_allowed, show_exit_progress: false, allowed_modal_ids: vec![ - Navigator::EXIT_MODAL + Self::EXIT_MODAL ] } } @@ -67,25 +61,22 @@ impl ModalContainer for App { } impl App { + /// Identifier for exit confirmation [`Modal`]. + pub const EXIT_MODAL: &'static str = "exit_confirmation"; + /// Draw content on main screen panel. pub fn ui(&mut self, ctx: &Context, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - // Show exit modal if window closing was requested. - if self.exit_requested { - Navigator::show_exit_modal(); - self.exit_requested = false; - } - // Draw main content. egui::CentralPanel::default() .frame(egui::Frame { fill: Colors::FILL, ..Default::default() }) .show(ctx, |ui| { - // Draw exit modal content if it's open or exit requested. - let modal_id = Navigator::is_modal_open(); - if modal_id.is_some() && self.can_show_modal(modal_id.unwrap()) { + // Draw modal content if it's open. + if self.can_draw_modal() { self.exit_modal_content(ui, frame, cb); } + // Draw main content. self.root.ui(ui, frame, cb); }); } @@ -95,7 +86,7 @@ impl App { ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - Navigator::modal_ui(ui, |ui, modal| { + Modal::ui(ui, |ui, modal| { if self.show_exit_progress { if !Node::is_running() { self.exit(frame, cb); @@ -156,13 +147,12 @@ impl App { cb.exit(); } OperatingSystem::IOS => { - //TODO: exit on iOS + //TODO: exit on iOS. } OperatingSystem::Nix | OperatingSystem::Mac | OperatingSystem::Windows => { self.exit_allowed = true; frame.close(); } - // Web OperatingSystem::Unknown => {} } } @@ -249,31 +239,10 @@ impl App { ctx.set_style(style); } -} -#[allow(dead_code)] -#[cfg(target_os = "android")] -#[allow(non_snake_case)] -#[no_mangle] -/// Calling when back button is pressed on Android. -pub extern "C" fn Java_mw_gri_android_MainActivity_onBackButtonPress( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, -) { - Navigator::back(); + /// Show exit confirmation modal. + pub fn show_exit_modal() { + let exit_modal = Modal::new(Self::EXIT_MODAL).title(t!("modal_exit.exit")); + Modal::show(exit_modal); + } } - -#[allow(dead_code)] -#[cfg(target_os = "android")] -#[allow(non_snake_case)] -#[no_mangle] -/// Calling on unexpected Android application termination (removal from recent apps). -pub extern "C" fn Java_mw_gri_android_MainActivity_onTermination( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - _activity: jni::objects::JObject, -) { - Node::stop(false); -} - diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 1d423f5..40c7952 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -16,13 +16,9 @@ mod app; pub use app::{App, PlatformApp}; -mod navigator; -pub use navigator::Navigator; - mod colors; pub use colors::Colors; pub mod platform; -pub mod screens; pub mod views; -pub mod icons; +pub mod icons; \ No newline at end of file diff --git a/src/gui/navigator.rs b/src/gui/navigator.rs deleted file mode 100644 index 06d6ef1..0000000 --- a/src/gui/navigator.rs +++ /dev/null @@ -1,156 +0,0 @@ -// 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::collections::BTreeSet; -use std::sync::{RwLock, RwLockWriteGuard}; -use std::sync::atomic::{AtomicBool, Ordering}; - -use lazy_static::lazy_static; - -use crate::gui::screens::ScreenId; -use crate::gui::views::Modal; - -lazy_static! { - /// Static [`Navigator`] state to be accessible from anywhere. - static ref NAVIGATOR_STATE: RwLock = RwLock::new(Navigator::default()); -} - -/// Logic of common navigation at ui for screens and modals. -pub struct Navigator { - /// Screen identifiers in navigation stack. - screen_stack: BTreeSet, - /// Indicator if side panel is open. - side_panel_open: AtomicBool, - /// Modal window to show. - modal: Option, -} - -impl Default for Navigator { - fn default() -> Self { - Self { - screen_stack: BTreeSet::new(), - side_panel_open: AtomicBool::new(false), - modal: None, - } - } -} - -impl Navigator { - /// Identifier for exit [`Modal`]. - pub const EXIT_MODAL: &'static str = "exit"; - - /// Initialize navigation from provided [`ScreenId`]. - pub fn init(from: ScreenId) { - let mut w_nav = NAVIGATOR_STATE.write().unwrap(); - w_nav.screen_stack.clear(); - w_nav.screen_stack.insert(from); - } - - /// Check if provided [`ScreenId`] is current. - pub fn is_current(id: &ScreenId) -> bool { - let r_nav = NAVIGATOR_STATE.read().unwrap(); - r_nav.screen_stack.last().unwrap() == id - } - - /// Navigate to screen with provided [`ScreenId`]. - pub fn to(id: ScreenId) { - NAVIGATOR_STATE.write().unwrap().screen_stack.insert(id); - } - - /// Go back at navigation stack, close showing modals first. - pub fn back() { - let mut w_nav = NAVIGATOR_STATE.write().unwrap(); - - // If Modal is showing and closeable, remove it from Navigator. - if w_nav.modal.is_some() { - let modal = w_nav.modal.as_ref().unwrap(); - if modal.is_closeable() { - w_nav.modal = None; - } - return; - } - - // Go back at screen stack or show exit confirmation Modal. - if w_nav.screen_stack.len() > 1 { - w_nav.screen_stack.pop_last(); - } else { - Self::show_exit_modal_nav(w_nav); - } - } - - /// Set exit confirmation [`Modal`]. - pub fn show_exit_modal() { - let w_nav = NAVIGATOR_STATE.write().unwrap(); - Self::show_exit_modal_nav(w_nav); - } - - /// Set exit confirmation [`Modal`] with provided [NAVIGATOR_STATE] lock. - fn show_exit_modal_nav(mut w_nav: RwLockWriteGuard) { - let m = Modal::new(Self::EXIT_MODAL).title(t!("modal_exit.exit")); - w_nav.modal = Some(m); - } - - /// Set [`Modal`] to show. - pub fn show_modal(modal: Modal) { - let mut w_nav = NAVIGATOR_STATE.write().unwrap(); - w_nav.modal = Some(modal); - } - - /// Check if [`Modal`] is open by returning its id, remove it from [`Navigator`] if it's closed. - pub fn is_modal_open() -> Option<&'static str> { - // Check if Modal is showing. - { - if NAVIGATOR_STATE.read().unwrap().modal.is_none() { - return None; - } - } - - // Check if Modal is open. - let (is_open, id) = { - let r_nav = NAVIGATOR_STATE.read().unwrap(); - let modal = r_nav.modal.as_ref().unwrap(); - (modal.is_open(), modal.id) - }; - - // If Modal is not open, remove it from navigator state. - if !is_open { - let mut w_nav = NAVIGATOR_STATE.write().unwrap(); - w_nav.modal = None; - return None; - } - Some(id) - } - - /// Draw showing [`Modal`] content if it's opened. - pub fn modal_ui(ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) { - if let Some(modal) = &NAVIGATOR_STATE.read().unwrap().modal { - if modal.is_open() { - modal.ui(ui, add_content); - } - } - } - - /// Change state of side panel to opposite. - pub fn toggle_side_panel() { - let r_nav = NAVIGATOR_STATE.read().unwrap(); - let is_open = r_nav.side_panel_open.load(Ordering::Relaxed); - r_nav.side_panel_open.store(!is_open, Ordering::Relaxed); - } - - /// Check if side panel is open. - pub fn is_side_panel_open() -> bool { - let r_nav = NAVIGATOR_STATE.read().unwrap(); - r_nav.side_panel_open.load(Ordering::Relaxed) - } -} diff --git a/src/gui/platform/desktop/mod.rs b/src/gui/platform/desktop/mod.rs index e866655..eaec138 100644 --- a/src/gui/platform/desktop/mod.rs +++ b/src/gui/platform/desktop/mod.rs @@ -47,7 +47,7 @@ impl eframe::App for PlatformApp { } fn on_close_event(&mut self) -> bool { - self.app.exit_requested = true; + App::show_exit_modal(); self.app.exit_allowed } } diff --git a/src/gui/screens/mod.rs b/src/gui/screens/mod.rs deleted file mode 100644 index 3051436..0000000 --- a/src/gui/screens/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -// 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. - -pub use account::Account; -pub use accounts::Accounts; -pub use root::Root; - -use crate::gui::platform::PlatformCallbacks; - -mod root; -mod accounts; -mod account; - -#[derive(Ord, Eq, PartialOrd, PartialEq)] -pub enum ScreenId { - Accounts, - Account, -} - -pub trait Screen { - fn id(&self) -> ScreenId; - fn ui(&mut self, - ui: &mut egui::Ui, - frame: &mut eframe::Frame, - cb: &dyn PlatformCallbacks); -} diff --git a/src/gui/screens/root.rs b/src/gui/screens/root.rs deleted file mode 100644 index 2551362..0000000 --- a/src/gui/screens/root.rs +++ /dev/null @@ -1,84 +0,0 @@ -// 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::min; -use crate::gui::Navigator; -use crate::gui::platform::PlatformCallbacks; -use crate::gui::screens::{Account, Accounts, Screen, ScreenId}; -use crate::gui::views::{NetworkContainer, View}; - -/// Main ui container. -pub struct Root { - screens: Vec>, - network_panel: NetworkContainer, -} - -impl Default for Root { - fn default() -> Self { - Navigator::init(ScreenId::Accounts); - - Self { - screens: vec![ - Box::new(Accounts::default()), - Box::new(Account::default()) - ], - network_panel: NetworkContainer::default() - } - } -} - -impl Root { - pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - let (is_panel_open, panel_width) = Self::side_panel_state_width(frame); - egui::SidePanel::left("network_panel") - .resizable(false) - .exact_width(panel_width) - .frame(egui::Frame::default()) - .show_animated_inside(ui, is_panel_open, |ui| { - self.network_panel.ui(ui, frame, cb); - }); - - egui::CentralPanel::default() - .frame(egui::Frame::default()) - .show_inside(ui, |ui| { - self.show_current_screen(ui, frame, cb); - }); - } - - /// Show current screen at central panel based on [`Navigator`] state. - fn show_current_screen(&mut self, - ui: &mut egui::Ui, - frame: &mut eframe::Frame, - cb: &dyn PlatformCallbacks) { - let Self { screens, .. } = self; - for screen in screens.iter_mut() { - if Navigator::is_current(&screen.id()) { - screen.ui(ui, frame, cb); - break; - } - } - } - - /// Get side panel state and width. - fn side_panel_state_width(frame: &mut eframe::Frame) -> (bool, f32) { - let dual_panel_mode = View::is_dual_panel_mode(frame); - let is_panel_open = dual_panel_mode || Navigator::is_side_panel_open(); - let panel_width = if dual_panel_mode { - min(frame.info().window_info.size.x as i64, View::SIDE_PANEL_MIN_WIDTH) as f32 - } else { - frame.info().window_info.size.x - }; - (is_panel_open, panel_width) - } -} \ No newline at end of file diff --git a/src/gui/screens/account.rs b/src/gui/views/accounts.rs similarity index 58% rename from src/gui/screens/account.rs rename to src/gui/views/accounts.rs index aa0e5bd..6bde807 100644 --- a/src/gui/screens/account.rs +++ b/src/gui/views/accounts.rs @@ -12,30 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::gui::platform::PlatformCallbacks; -use crate::gui::screens::ScreenId; - -pub struct Account { - -} - -impl Default for Account { - fn default() -> Self { - Self { - - } - } -} - -impl super::Screen for Account { - fn id(&self) -> ScreenId { - ScreenId::Account - } - - fn ui(&mut self, - ui: &mut egui::Ui, - frame: &mut eframe::Frame, - cb: &dyn PlatformCallbacks) { - - } -} \ No newline at end of file +mod content; +pub use content::*; \ No newline at end of file diff --git a/src/gui/screens/accounts.rs b/src/gui/views/accounts/content.rs similarity index 53% rename from src/gui/screens/accounts.rs rename to src/gui/views/accounts/content.rs index d29ab96..9d52b7b 100644 --- a/src/gui/screens/accounts.rs +++ b/src/gui/views/accounts/content.rs @@ -12,26 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::Frame; - -use crate::gui::icons::{ARROW_CIRCLE_LEFT, GLOBE, PLUS}; -use crate::gui::{Colors, Navigator}; +use crate::gui::Colors; +use crate::gui::icons::{GLOBE, PLUS}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::screens::{Screen, ScreenId}; -use crate::gui::views::{TitlePanel, TitleAction, View}; +use crate::gui::views::{Root, TitleAction, TitlePanel, View}; -#[derive(Default)] -pub struct Accounts; +/// Accounts central panel content. +pub struct AccountsContent { + /// List of accounts. + list: Vec +} -impl Screen for Accounts { - fn id(&self) -> ScreenId { - ScreenId::Accounts +impl Default for AccountsContent { + fn default() -> Self { + Self { + list: vec![], + } } +} - fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - TitlePanel::ui(t!("screen_accounts.title"), if !View::is_dual_panel_mode(frame) { +impl AccountsContent { + pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { + TitlePanel::ui(t!("accounts.title"), if !Root::is_dual_panel_mode(frame) { TitleAction::new(GLOBE, || { - Navigator::toggle_side_panel(); + Root::toggle_network_panel(); }) } else { None @@ -40,19 +44,13 @@ impl Screen for Accounts { }), ui); egui::CentralPanel::default() - .frame(Frame { + .frame(egui::Frame { stroke: View::DEFAULT_STROKE, fill: Colors::FILL_DARK, ..Default::default() }) .show_inside(ui, |ui| { - ui.label(format!("{}Here we go 10000 ツ", ARROW_CIRCLE_LEFT)); - if ui.button("TEST").clicked() { - Navigator::to(ScreenId::Account) - }; - if ui.button(format!("{}BACK ", ARROW_CIRCLE_LEFT)).clicked() { - Navigator::back() - }; + //TODO: accounts list }); } } \ No newline at end of file diff --git a/src/gui/views/mod.rs b/src/gui/views/mod.rs index 823ad8f..1dabc33 100644 --- a/src/gui/views/mod.rs +++ b/src/gui/views/mod.rs @@ -21,5 +21,11 @@ pub use title_panel::*; mod modal; pub use modal::*; +mod root; +pub use root::*; + mod network; -pub use network::*; \ No newline at end of file +pub use network::*; + +mod accounts; +pub use accounts::*; \ No newline at end of file diff --git a/src/gui/views/modal.rs b/src/gui/views/modal.rs index 1b6ab69..2b6e0df 100644 --- a/src/gui/views/modal.rs +++ b/src/gui/views/modal.rs @@ -14,21 +14,34 @@ use std::cmp::min; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{RwLock, RwLockWriteGuard}; use egui::{Align2, RichText, Rounding, Stroke, Vec2}; use egui::epaint::RectShape; +use lazy_static::lazy_static; use crate::gui::Colors; use crate::gui::views::View; -/// Contains modal ids to draw at current container if possible. +lazy_static! { + /// Showing [`Modal`] state to be accessible from different ui parts. + static ref MODAL_STATE: RwLock = RwLock::new(ModalState::default()); +} + +#[derive(Default)] +struct ModalState { + modal: Option +} + +/// Contains ids to draw current [`Modal`] at this ui container if it's possible. pub trait ModalContainer { - /// List of modal ids to show at current view container. + /// List of [`Modal`] ids to draw at current ui container. fn modal_ids(&self) -> &Vec<&'static str>; - /// Check if it's possible to show modal. - fn can_show_modal(&self, id: &'static str) -> bool { - self.modal_ids().contains(&id) + /// Check if it's possible to draw [`Modal`] at current ui container. + fn can_draw_modal(&self) -> bool { + let modal_id = Modal::opened(); + modal_id.is_some() && self.modal_ids().contains(&modal_id.unwrap()) } } @@ -38,7 +51,7 @@ pub enum ModalPosition { Center } -/// Stores data to draw dialog box/popup at UI, powered by [`egui::Window`]. +/// Stores data to draw modal [`egui::Window`] at ui. pub struct Modal { /// Identifier for modal. pub(crate) id: &'static str, @@ -67,34 +80,34 @@ impl Modal { } } - /// Setup position of Modal on the screen. + /// Setup position of [`Modal`] on the screen. pub fn position(mut self, position: ModalPosition) -> Self { self.position = position; self } - /// Check if Modal is open. + /// Check if [`Modal`] is open. pub fn is_open(&self) -> bool { self.open.load(Ordering::Relaxed) } - /// Mark Modal closed. + /// Mark [`Modal`] closed. pub fn close(&self) { self.open.store(false, Ordering::Relaxed); } - /// Setup possibility to close Modal. + /// Setup possibility to close [`Modal`]. pub fn closeable(self, closeable: bool) -> Self { self.closeable.store(closeable, Ordering::Relaxed); self } - /// Disable possibility to close Modal. + /// Disable possibility to close [`Modal`]. pub fn disable_closing(&self) { self.closeable.store(false, Ordering::Relaxed); } - /// Check if Modal is closeable. + /// Check if [`Modal`] is closeable. pub fn is_closeable(&self) -> bool { self.closeable.load(Ordering::Relaxed) } @@ -105,8 +118,64 @@ impl Modal { self } - /// Show Modal with provided content. - pub fn ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) { + /// Set [`Modal`] instance to show at ui. + pub fn show(modal: Modal) { + let mut w_nav = MODAL_STATE.write().unwrap(); + w_nav.modal = Some(modal); + } + + /// Remove [`Modal`] from [`MODAL_STATE`] if it's showing and can be closed. + /// Return `false` if Modal existed in [`MODAL_STATE`] before call. + pub fn on_back() -> bool { + let mut w_state = MODAL_STATE.write().unwrap(); + + // If Modal is showing and closeable, remove it from state. + if w_state.modal.is_some() { + let modal = w_state.modal.as_ref().unwrap(); + if modal.is_closeable() { + w_state.modal = None; + } + return false; + } + true + } + + /// Return id of opened [`Modal`] or remove its instance from [`MODAL_STATE`] if it was closed. + pub fn opened() -> Option<&'static str> { + // Check if Modal is showing. + { + if MODAL_STATE.read().unwrap().modal.is_none() { + return None; + } + } + + // Check if Modal is open. + let (is_open, id) = { + let r_state = MODAL_STATE.read().unwrap(); + let modal = r_state.modal.as_ref().unwrap(); + (modal.is_open(), modal.id) + }; + + // If Modal is not open, remove it from navigator state. + if !is_open { + let mut w_state = MODAL_STATE.write().unwrap(); + w_state.modal = None; + return None; + } + Some(id) + } + + /// Draw opened [`Modal`] content. + pub fn ui(ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) { + if let Some(modal) = &MODAL_STATE.read().unwrap().modal { + if modal.is_open() { + modal.draw(ui, add_content); + } + } + } + + /// Draw Modal with provided content. + fn draw(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) { let mut rect = ui.ctx().screen_rect(); egui::Window::new("modal_bg_window") .title_bar(false) diff --git a/src/gui/views/network.rs b/src/gui/views/network.rs index 0f7848d..a2ef28c 100644 --- a/src/gui/views/network.rs +++ b/src/gui/views/network.rs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod container; +mod content; mod metrics; mod mining; mod settings; mod node; mod configs; -pub use container::*; \ No newline at end of file +pub use content::*; \ No newline at end of file diff --git a/src/gui/views/network/configs/dandelion.rs b/src/gui/views/network/configs/dandelion.rs index 852f4da..34a6f29 100644 --- a/src/gui/views/network/configs/dandelion.rs +++ b/src/gui/views/network/configs/dandelion.rs @@ -14,7 +14,7 @@ use egui::{Id, RichText, TextStyle, Ui, Widget}; -use crate::gui::{Colors, Navigator}; +use crate::gui::Colors; use crate::gui::icons::{CLOCK_COUNTDOWN, GRAPH, TIMER, WATCH}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalPosition, View}; @@ -116,7 +116,7 @@ impl DandelionSetup { let epoch_modal = Modal::new(Self::EPOCH_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(epoch_modal); + Modal::show(epoch_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -201,7 +201,7 @@ impl DandelionSetup { let embargo_modal = Modal::new(Self::EMBARGO_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(embargo_modal); + Modal::show(embargo_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -286,7 +286,7 @@ impl DandelionSetup { let aggregation_modal = Modal::new(Self::AGGREGATION_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(aggregation_modal); + Modal::show(aggregation_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -371,7 +371,7 @@ impl DandelionSetup { let embargo_modal = Modal::new(Self::STEM_PROBABILITY_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(embargo_modal); + Modal::show(embargo_modal); cb.show_keyboard(); }); ui.add_space(6.0); diff --git a/src/gui/views/network/configs/node.rs b/src/gui/views/network/configs/node.rs index b921da9..c10b4a2 100644 --- a/src/gui/views/network/configs/node.rs +++ b/src/gui/views/network/configs/node.rs @@ -14,13 +14,14 @@ use eframe::emath::Align; use egui::{Id, Layout, RichText, TextStyle, Ui, Widget}; +use egui::os::OperatingSystem; use grin_core::global::ChainTypes; use crate::AppConfig; -use crate::gui::{Colors, Navigator}; +use crate::gui::Colors; use crate::gui::icons::{CLIPBOARD_TEXT, CLOCK_CLOCKWISE, COMPUTER_TOWER, COPY, PLUG, POWER, SHIELD, SHIELD_SLASH}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, ModalPosition, NetworkContainer, View}; +use crate::gui::views::{Modal, ModalPosition, NetworkContent, View}; use crate::gui::views::network::settings::NetworkSettings; use crate::node::{Node, NodeConfig}; @@ -114,7 +115,7 @@ impl NodeSetup { // Autorun node setup. ui.vertical_centered(|ui| { ui.add_space(6.0); - NetworkContainer::autorun_node_ui(ui); + NetworkContent::autorun_node_ui(ui); if Node::is_running() { ui.add_space(2.0); ui.label(RichText::new(t!("network_settings.restart_node_required")) @@ -224,7 +225,7 @@ impl NodeSetup { let port_modal = Modal::new(Self::API_PORT_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(port_modal); + Modal::show(port_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -338,7 +339,7 @@ impl NodeSetup { let port_modal = Modal::new(modal_id) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(port_modal); + Modal::show(port_modal); cb.show_keyboard(); }); } @@ -354,7 +355,7 @@ impl NodeSetup { ui.label(RichText::new(description) .size(18.0) .color(Colors::GRAY)); - ui.add_space(8.0); + ui.add_space(6.0); // Draw API port text edit. let text_edit_resp = egui::TextEdit::singleline(&mut self.secret_edit) @@ -367,36 +368,34 @@ impl NodeSetup { cb.show_keyboard(); } - ui.add_space(12.0); + // Show buttons to copy/paste text for Android. + if OperatingSystem::from_target_os() == OperatingSystem::Android { + ui.add_space(12.0); + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(12.0, 0.0); - // Show buttons to copy/paste text. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(12.0, 0.0); + let mut buttons_rect = ui.available_rect_before_wrap(); + buttons_rect.set_height(46.0); + ui.allocate_ui_at_rect(buttons_rect, |ui| { - let mut buttons_rect = ui.available_rect_before_wrap(); - buttons_rect.set_height(46.0); - ui.allocate_ui_at_rect(buttons_rect, |ui| { - - ui.columns(2, |columns| { - columns[0].with_layout(Layout::right_to_left(Align::Center), |ui| { - let copy_title = format!("{} {}", COPY, t!("network_settings.copy")); - View::button(ui, copy_title, Colors::WHITE, || { - cb.copy_string_to_buffer(self.secret_edit.clone()); - }); - }); - columns[1].with_layout(Layout::left_to_right(Align::Center), |ui| { - let paste_title = format!("{} {}", - CLIPBOARD_TEXT, - t!("network_settings.paste")); - View::button(ui, paste_title, Colors::WHITE, || { - self.secret_edit = cb.get_string_from_buffer(); + ui.columns(2, |columns| { + columns[0].with_layout(Layout::right_to_left(Align::Center), |ui| { + let copy_title = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_title, Colors::WHITE, || { + cb.copy_string_to_buffer(self.secret_edit.clone()); + }); + }); + columns[1].with_layout(Layout::left_to_right(Align::Center), |ui| { + let paste_title = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste_title, Colors::WHITE, || { + self.secret_edit = cb.get_string_from_buffer(); + }); + }); }); }); }); - }); - - }); + } // Show reminder to restart enabled node. NetworkSettings::node_restart_required_ui(ui); @@ -455,7 +454,7 @@ impl NodeSetup { let ftl_modal = Modal::new(Self::FTL_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(ftl_modal); + Modal::show(ftl_modal); cb.show_keyboard(); }); ui.add_space(6.0); diff --git a/src/gui/views/network/configs/p2p.rs b/src/gui/views/network/configs/p2p.rs index 5c005fa..061b8ca 100644 --- a/src/gui/views/network/configs/p2p.rs +++ b/src/gui/views/network/configs/p2p.rs @@ -17,7 +17,7 @@ use egui_extras::{Size, StripBuilder}; use grin_core::global::ChainTypes; use crate::AppConfig; -use crate::gui::{Colors, Navigator}; +use crate::gui::Colors; use crate::gui::icons::{HANDSHAKE, PLUG, TRASH, GLOBE_SIMPLE, PLUS_CIRCLE, ARROW_FAT_LINES_UP, ARROW_FAT_LINES_DOWN, ARROW_FAT_LINE_UP, PROHIBIT_INSET}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalPosition, View}; @@ -215,7 +215,7 @@ impl P2PSetup { let port_modal = Modal::new(Self::PORT_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(port_modal); + Modal::show(port_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -374,7 +374,7 @@ impl P2PSetup { let peer_modal = Modal::new(modal_id) .position(ModalPosition::CenterTop) .title(modal_title); - Navigator::show_modal(peer_modal); + Modal::show(peer_modal); cb.show_keyboard(); }); } @@ -561,7 +561,7 @@ impl P2PSetup { let ban_modal = Modal::new(Self::BAN_WINDOW_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(ban_modal); + Modal::show(ban_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -652,7 +652,7 @@ impl P2PSetup { let max_inbound_modal = Modal::new(Self::MAX_INBOUND_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(max_inbound_modal); + Modal::show(max_inbound_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -738,7 +738,7 @@ impl P2PSetup { let max_outbound = Modal::new(Self::MAX_OUTBOUND_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(max_outbound); + Modal::show(max_outbound); cb.show_keyboard(); }); ui.add_space(6.0); @@ -824,7 +824,7 @@ impl P2PSetup { let min_outbound = Modal::new(Self::MIN_OUTBOUND_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(min_outbound); + Modal::show(min_outbound); cb.show_keyboard(); }); ui.add_space(6.0); diff --git a/src/gui/views/network/configs/pool.rs b/src/gui/views/network/configs/pool.rs index 53d6b2d..568de3c 100644 --- a/src/gui/views/network/configs/pool.rs +++ b/src/gui/views/network/configs/pool.rs @@ -14,7 +14,7 @@ use egui::{Id, RichText, TextStyle, Ui, Widget}; -use crate::gui::{Colors, Navigator}; +use crate::gui::Colors; use crate::gui::icons::{BEZIER_CURVE, BOUNDING_BOX, CHART_SCATTER, CIRCLES_THREE, CLOCK_COUNTDOWN, HAND_COINS}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalPosition, View}; @@ -118,7 +118,7 @@ impl PoolSetup { let fee_modal = Modal::new(Self::FEE_BASE_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(fee_modal); + Modal::show(fee_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -203,7 +203,7 @@ impl PoolSetup { let reorg_modal = Modal::new(Self::REORG_PERIOD_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(reorg_modal); + Modal::show(reorg_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -288,7 +288,7 @@ impl PoolSetup { let size_modal = Modal::new(Self::POOL_SIZE_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(size_modal); + Modal::show(size_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -373,7 +373,7 @@ impl PoolSetup { let stem_modal = Modal::new(Self::STEMPOOL_SIZE_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(stem_modal); + Modal::show(stem_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -458,7 +458,7 @@ impl PoolSetup { let weight_modal = Modal::new(Self::MAX_WEIGHT_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(weight_modal); + Modal::show(weight_modal); cb.show_keyboard(); }); ui.add_space(6.0); diff --git a/src/gui/views/network/configs/stratum.rs b/src/gui/views/network/configs/stratum.rs index 46e1dab..8198b3a 100644 --- a/src/gui/views/network/configs/stratum.rs +++ b/src/gui/views/network/configs/stratum.rs @@ -14,7 +14,7 @@ use egui::{Id, RichText, TextStyle, Ui, Widget}; -use crate::gui::{Colors, Navigator}; +use crate::gui::Colors; use crate::gui::icons::{BARBELL, HARD_DRIVES, PLUG, TIMER}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalPosition, View}; @@ -98,13 +98,15 @@ impl StratumSetup { View::checkbox(ui, stratum_enabled, t!("network.autorun"), || { NodeConfig::toggle_stratum_autorun(); }); - ui.add_space(4.0); - // Show message to restart node after changing of stratum settings - ui.label(RichText::new(t!("network_mining.info_settings")) - .size(16.0) - .color(Colors::INACTIVE_TEXT) - ); + // Show reminder to restart running server. + if Node::get_stratum_stats().is_running { + ui.add_space(2.0); + ui.label(RichText::new(t!("network_mining.restart_server_required")) + .size(16.0) + .color(Colors::INACTIVE_TEXT) + ); + } ui.add_space(8.0); }); @@ -164,7 +166,7 @@ impl StratumSetup { let port_modal = Modal::new(Self::STRATUM_PORT_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(port_modal); + Modal::show(port_modal); cb.show_keyboard(); }); ui.add_space(12.0); @@ -206,6 +208,8 @@ impl StratumSetup { ui.label(RichText::new(t!("network_settings.port_unavailable")) .size(18.0) .color(Colors::RED)); + } else { + server_restart_required_ui(ui); } ui.add_space(12.0); @@ -269,7 +273,7 @@ impl StratumSetup { let time_modal = Modal::new(Self::ATTEMPT_TIME_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(time_modal); + Modal::show(time_modal); cb.show_keyboard(); }); ui.add_space(12.0); @@ -303,7 +307,7 @@ impl StratumSetup { .size(18.0) .color(Colors::RED)); } else { - NetworkSettings::node_restart_required_ui(ui); + server_restart_required_ui(ui); } ui.add_space(12.0); }); @@ -355,7 +359,7 @@ impl StratumSetup { let diff_modal = Modal::new(Self::MIN_SHARE_DIFF_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_settings.change_value")); - Navigator::show_modal(diff_modal); + Modal::show(diff_modal); cb.show_keyboard(); }); ui.add_space(6.0); @@ -389,7 +393,7 @@ impl StratumSetup { .size(18.0) .color(Colors::RED)); } else { - NetworkSettings::node_restart_required_ui(ui); + server_restart_required_ui(ui); } ui.add_space(12.0); }); @@ -423,4 +427,15 @@ impl StratumSetup { ui.add_space(6.0); }); } +} + +/// Reminder to restart enabled node to show on edit setting at [`Modal`]. +pub fn server_restart_required_ui(ui: &mut Ui) { + if Node::get_stratum_stats().is_running { + ui.add_space(12.0); + ui.label(RichText::new(t!("network_mining.restart_server_required")) + .size(16.0) + .color(Colors::GREEN) + ); + } } \ No newline at end of file diff --git a/src/gui/views/network/container.rs b/src/gui/views/network/content.rs similarity index 89% rename from src/gui/views/network/container.rs rename to src/gui/views/network/content.rs index c3e31e7..5f75f41 100644 --- a/src/gui/views/network/container.rs +++ b/src/gui/views/network/content.rs @@ -18,10 +18,10 @@ use egui_extras::{Size, StripBuilder}; use grin_chain::SyncStatus; use crate::AppConfig; -use crate::gui::{Colors, Navigator}; +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, TitlePanel, View}; +use crate::gui::views::{Modal, ModalContainer, Root, TitlePanel, View}; use crate::gui::views::network::configs::dandelion::DandelionSetup; use crate::gui::views::network::configs::node::NodeSetup; use crate::gui::views::network::configs::p2p::P2PSetup; @@ -39,6 +39,7 @@ pub trait NetworkTab { fn on_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks); } + #[derive(PartialEq)] pub enum NetworkTabType { Node, @@ -48,7 +49,7 @@ pub enum NetworkTabType { } impl NetworkTabType { - pub fn name(&self) -> String { + pub fn title(&self) -> String { match *self { NetworkTabType::Node => { t!("network.node") } NetworkTabType::Metrics => { t!("network.metrics") } @@ -58,12 +59,15 @@ impl NetworkTabType { } } -pub struct NetworkContainer { +/// Network side panel content. +pub struct NetworkContent { + /// Current tab view to show at ui. current_tab: Box, + /// [`Modal`] ids allowed at this ui container. modal_ids: Vec<&'static str>, } -impl Default for NetworkContainer { +impl Default for NetworkContent { fn default() -> Self { Self { current_tab: Box::new(NetworkNode::default()), @@ -106,18 +110,17 @@ impl Default for NetworkContainer { } } -impl ModalContainer for NetworkContainer { +impl ModalContainer for NetworkContent { fn modal_ids(&self) -> &Vec<&'static str> { self.modal_ids.as_ref() } } -impl NetworkContainer { +impl NetworkContent { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { // Show modal content if it's opened. - let modal_id = Navigator::is_modal_open(); - if modal_id.is_some() && self.can_show_modal(modal_id.unwrap()) { - Navigator::modal_ui(ui, |ui, modal| { + if self.can_draw_modal() { + Modal::ui(ui, |ui, modal| { self.current_tab.as_mut().on_modal_ui(ui, modal, cb); }); } @@ -169,24 +172,26 @@ impl NetworkContainer { // Setup vertical padding inside tab button. ui.style_mut().spacing.button_padding = egui::vec2(0.0, 8.0); + // Draw tab buttons. + let current_type = self.current_tab.get_type(); ui.columns(4, |columns| { columns[0].vertical_centered_justified(|ui| { - View::tab_button(ui, DATABASE, self.is_current_tab(NetworkTabType::Node), || { + View::tab_button(ui, DATABASE, current_type == NetworkTabType::Node, || { self.current_tab = Box::new(NetworkNode::default()); }); }); columns[1].vertical_centered_justified(|ui| { - View::tab_button(ui, GAUGE, self.is_current_tab(NetworkTabType::Metrics), || { + View::tab_button(ui, GAUGE, current_type == NetworkTabType::Metrics, || { self.current_tab = Box::new(NetworkMetrics::default()); }); }); columns[2].vertical_centered_justified(|ui| { - View::tab_button(ui, FACTORY, self.is_current_tab(NetworkTabType::Mining), || { + View::tab_button(ui, FACTORY, current_type == NetworkTabType::Mining, || { self.current_tab = Box::new(NetworkMining::default()); }); }); columns[3].vertical_centered_justified(|ui| { - View::tab_button(ui, FADERS, self.is_current_tab(NetworkTabType::Settings), || { + View::tab_button(ui, FADERS, current_type == NetworkTabType::Settings, || { self.current_tab = Box::new(NetworkSettings::default()); }); }); @@ -194,11 +199,6 @@ impl NetworkContainer { }); } - /// Check if current tab equals providing [`NetworkTabType`]. - fn is_current_tab(&self, tab_type: NetworkTabType) -> bool { - self.current_tab.get_type() == tab_type - } - /// Draw title content. fn title_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { StripBuilder::new(ui) @@ -217,10 +217,10 @@ impl NetworkContainer { self.title_text_ui(builder); }); strip.cell(|ui| { - if !View::is_dual_panel_mode(frame) { + if !Root::is_dual_panel_mode(frame) { ui.centered_and_justified(|ui| { View::title_button(ui, CARDHOLDER, || { - Navigator::toggle_side_panel(); + Root::toggle_network_panel(); }); }); } @@ -237,7 +237,7 @@ impl NetworkContainer { strip.cell(|ui| { ui.add_space(4.0); ui.vertical_centered(|ui| { - ui.label(RichText::new(self.current_tab.get_type().name().to_uppercase()) + ui.label(RichText::new(self.current_tab.get_type().title().to_uppercase()) .size(19.0) .color(Colors::TITLE)); }); @@ -291,7 +291,7 @@ impl NetworkContainer { }); } - /// Draw checkbox with setting to run node on app launch. + /// Draw checkbox to run integrated node on application launch. pub fn autorun_node_ui(ui: &mut egui::Ui) { let autostart = AppConfig::autostart_node(); View::checkbox(ui, autostart, t!("network.autorun"), || { diff --git a/src/gui/views/network/metrics.rs b/src/gui/views/network/metrics.rs index 75508a8..aef6da3 100644 --- a/src/gui/views/network/metrics.rs +++ b/src/gui/views/network/metrics.rs @@ -20,9 +20,10 @@ use crate::gui::Colors; use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HASH, HOURGLASS_LOW, HOURGLASS_MEDIUM, TIMER}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::network::{NetworkTab, NetworkTabType}; -use crate::gui::views::{Modal, NetworkContainer, View}; +use crate::gui::views::{Modal, NetworkContent, View}; use crate::node::Node; +/// Chain metrics tab content. #[derive(Default)] pub struct NetworkMetrics; @@ -39,7 +40,7 @@ impl NetworkTab for NetworkMetrics { let server_stats = Node::get_stats(); // Show message to enable node when it's not running. if !Node::is_running() { - NetworkContainer::disabled_node_ui(ui); + NetworkContent::disabled_node_ui(ui); return; } @@ -94,7 +95,7 @@ impl NetworkTab for NetworkMetrics { }); ui.add_space(4.0); - // Show difficulty adjustment window info + // Show difficulty adjustment window info. let difficulty_title = t!( "network_metrics.difficulty_window", "size" => stats.diff_stats.window_size @@ -122,7 +123,7 @@ impl NetworkTab for NetworkMetrics { }); ui.add_space(4.0); - // Show difficulty adjustment window blocks + // Show difficulty adjustment window blocks. let blocks_size = stats.diff_stats.last_blocks.len(); ScrollArea::vertical() .id_source("difficulty_scroll") @@ -130,7 +131,7 @@ impl NetworkTab for NetworkMetrics { .stick_to_bottom(true) .show_rows( ui, - DIFF_BLOCK_UI_HEIGHT, + BLOCK_ITEM_HEIGHT, blocks_size, |ui, row_range| { for index in row_range { @@ -144,7 +145,7 @@ impl NetworkTab for NetworkMetrics { } else { [false, false] }; - draw_diff_block(ui, db, rounding) + block_item_ui(ui, db, rounding) } }, ); @@ -153,9 +154,10 @@ impl NetworkTab for NetworkMetrics { fn on_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {} } -const DIFF_BLOCK_UI_HEIGHT: f32 = 78.30; +const BLOCK_ITEM_HEIGHT: f32 = 78.30; -fn draw_diff_block(ui: &mut egui::Ui, db: &DiffBlock, rounding: [bool; 2]) { +/// Draw block difficulty item. +fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: [bool; 2]) { // Add space before the first item. if rounding[0] { ui.add_space(4.0); @@ -164,8 +166,9 @@ fn draw_diff_block(ui: &mut egui::Ui, db: &DiffBlock, rounding: [bool; 2]) { ui.horizontal(|ui| { ui.add_space(6.0); ui.vertical(|ui| { + // Draw round background. let mut rect = ui.available_rect_before_wrap(); - rect.set_height(DIFF_BLOCK_UI_HEIGHT); + rect.set_height(BLOCK_ITEM_HEIGHT); ui.painter().rect( rect, Rounding { diff --git a/src/gui/views/network/mining.rs b/src/gui/views/network/mining.rs index 65b7954..372ef48 100644 --- a/src/gui/views/network/mining.rs +++ b/src/gui/views/network/mining.rs @@ -20,11 +20,12 @@ use grin_servers::WorkerStats; use crate::gui::Colors; use crate::gui::icons::{BARBELL, CLOCK_AFTERNOON, CPU, CUBE, FADERS, FOLDER_DASHED, FOLDER_NOTCH_MINUS, FOLDER_NOTCH_PLUS, HARD_DRIVES, PLUGS, PLUGS_CONNECTED, POLYGON}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, NetworkContainer, View}; +use crate::gui::views::{Modal, NetworkContent, View}; use crate::gui::views::network::{NetworkTab, NetworkTabType}; use crate::gui::views::network::configs::stratum::StratumSetup; use crate::node::{Node, NodeConfig}; +/// Mining tab content. #[derive(Default)] pub struct NetworkMining { stratum_server_setup: StratumSetup @@ -40,7 +41,7 @@ impl NetworkTab for NetworkMining { // Show message to enable node when it's not running. if !Node::is_running() { - NetworkContainer::disabled_node_ui(ui); + NetworkContent::disabled_node_ui(ui); return; } @@ -169,7 +170,7 @@ impl NetworkTab for NetworkMining { .id_source("stratum_workers_scroll") .show_rows( ui, - WORKER_UI_HEIGHT, + WORKER_ITEM_HEIGHT, workers_size, |ui, row_range| { for index in row_range { @@ -183,7 +184,7 @@ impl NetworkTab for NetworkMining { } else { [false, false] }; - draw_workers_stats(ui, worker, rounding) + worker_item_ui(ui, worker, rounding) } }, ); @@ -207,9 +208,10 @@ impl NetworkTab for NetworkMining { } } -const WORKER_UI_HEIGHT: f32 = 77.0; +const WORKER_ITEM_HEIGHT: f32 = 77.0; -fn draw_workers_stats(ui: &mut egui::Ui, ws: &WorkerStats, rounding: [bool; 2]) { +/// Draw worker statistics item. +fn worker_item_ui(ui: &mut egui::Ui, ws: &WorkerStats, rounding: [bool; 2]) { // Add space before the first item. if rounding[0] { ui.add_space(4.0); @@ -217,8 +219,9 @@ fn draw_workers_stats(ui: &mut egui::Ui, ws: &WorkerStats, rounding: [bool; 2]) ui.horizontal_wrapped(|ui| { ui.vertical_centered_justified(|ui| { + // Draw round background. let mut rect = ui.available_rect_before_wrap(); - rect.set_height(WORKER_UI_HEIGHT); + rect.set_height(WORKER_ITEM_HEIGHT); ui.painter().rect( rect, Rounding { @@ -232,94 +235,68 @@ fn draw_workers_stats(ui: &mut egui::Ui, ws: &WorkerStats, rounding: [bool; 2]) ); ui.add_space(2.0); - ui.horizontal_top(|ui| { + ui.horizontal(|ui| { + ui.add_space(5.0); + + // Draw worker connection status. let (status_text, status_icon, status_color) = match ws.is_connected { true => (t!("network_mining.connected"), PLUGS_CONNECTED, Colors::BLACK), false => (t!("network_mining.disconnected"), PLUGS, Colors::INACTIVE_TEXT) }; - ui.add_space(5.0); - ui.heading(RichText::new(status_icon) + let status_line_text = format!("{} {} {}", status_icon, ws.id, status_text); + ui.heading(RichText::new(status_line_text) .color(status_color) .size(18.0)); ui.add_space(2.0); - - // Draw worker ID. - ui.heading(RichText::new(&ws.id) - .color(status_color) - .size(18.0)); - ui.add_space(3.0); - - // Draw worker status. - ui.heading(RichText::new(status_text) - .color(status_color) - .size(18.0)); }); - ui.horizontal_top(|ui| { + ui.horizontal(|ui| { ui.add_space(6.0); - ui.heading(RichText::new(BARBELL) - .color(Colors::TITLE) - .size(16.0)); - ui.add_space(4.0); + // Draw difficulty. - ui.heading(RichText::new(ws.pow_difficulty.to_string()) + let diff_text = format!("{} {}", BARBELL, ws.pow_difficulty); + ui.heading(RichText::new(diff_text) .color(Colors::TITLE) .size(16.0)); ui.add_space(6.0); - ui.heading(RichText::new(FOLDER_NOTCH_PLUS) - .color(Colors::GREEN) - .size(16.0)); - ui.add_space(3.0); // Draw accepted shares. - ui.heading(RichText::new(ws.num_accepted.to_string()) + let accepted_text = format!("{} {}", FOLDER_NOTCH_PLUS, ws.num_accepted); + ui.heading(RichText::new(accepted_text) .color(Colors::GREEN) .size(16.0)); ui.add_space(6.0); - ui.heading(RichText::new(FOLDER_NOTCH_MINUS) - .color(Colors::RED) - .size(16.0)); - ui.add_space(3.0); // Draw rejected shares. - ui.heading(RichText::new(ws.num_rejected.to_string()) + let rejected_text = format!("{} {}", FOLDER_NOTCH_MINUS, ws.num_rejected); + ui.heading(RichText::new(rejected_text) .color(Colors::RED) .size(16.0)); ui.add_space(6.0); - ui.heading(RichText::new(FOLDER_DASHED) - .color(Colors::GRAY) - .size(16.0)); - ui.add_space(3.0); // Draw stale shares. - ui.heading(RichText::new(ws.num_stale.to_string()) + let stale_text = format!("{} {}", FOLDER_DASHED, ws.num_stale); + ui.heading(RichText::new(stale_text) .color(Colors::GRAY) .size(16.0)); ui.add_space(6.0); - ui.heading(RichText::new(CUBE) - .color(Colors::TITLE) - .size(16.0)); - ui.add_space(3.0); // Draw blocks found. - ui.heading(RichText::new(ws.num_blocks_found.to_string()) + let blocks_found_text = format!("{} {}", CUBE, ws.num_blocks_found); + ui.heading(RichText::new(blocks_found_text) .color(Colors::TITLE) .size(16.0)); }); - ui.horizontal_top(|ui| { + ui.horizontal(|ui| { ui.add_space(6.0); - ui.heading(RichText::new(CLOCK_AFTERNOON) - .color(Colors::TITLE) - .size(16.0)); - ui.add_space(4.0); // Draw block time let seen = ws.last_seen.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); let naive_datetime = NaiveDateTime::from_timestamp_opt(seen as i64, 0).unwrap(); let datetime: DateTime = DateTime::from_utc(naive_datetime, Utc); - ui.heading(RichText::new(datetime.to_string()) + let date_text = format!("{} {}", CLOCK_AFTERNOON, datetime); + ui.heading(RichText::new(date_text) .color(Colors::GRAY) .size(16.0)); - }); }); }); diff --git a/src/gui/views/network/node.rs b/src/gui/views/network/node.rs index 29a8cf8..ebd33dc 100644 --- a/src/gui/views/network/node.rs +++ b/src/gui/views/network/node.rs @@ -20,9 +20,10 @@ use crate::gui::Colors; use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, PLUGS_CONNECTED, SHARE_NETWORK}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, View}; -use crate::gui::views::network::{NetworkContainer, NetworkTab, NetworkTabType}; +use crate::gui::views::network::{NetworkContent, NetworkTab, NetworkTabType}; use crate::node::Node; +/// Integrated node tab content. #[derive(Default)] pub struct NetworkNode; @@ -35,7 +36,7 @@ impl NetworkTab for NetworkNode { let server_stats = Node::get_stats(); // Show message to enable node when it's not running. if !Node::is_running() { - NetworkContainer::disabled_node_ui(ui); + NetworkContent::disabled_node_ui(ui); return; } @@ -175,7 +176,7 @@ impl NetworkTab for NetworkNode { [false, false] }; ui.vertical_centered(|ui| { - draw_peer_stats(ui, ps, rounding); + peer_item_ui(ui, ps, rounding); }); } } @@ -185,7 +186,8 @@ impl NetworkTab for NetworkNode { fn on_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {} } -fn draw_peer_stats(ui: &mut egui::Ui, peer: &PeerStats, rounding: [bool; 2]) { +/// Draw connected peer info item. +fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: [bool; 2]) { ui.vertical(|ui| { // Draw round background. let mut rect = ui.available_rect_before_wrap(); diff --git a/src/gui/views/network/settings.rs b/src/gui/views/network/settings.rs index 4f808f5..52ee47f 100644 --- a/src/gui/views/network/settings.rs +++ b/src/gui/views/network/settings.rs @@ -14,7 +14,7 @@ use egui::{RichText, ScrollArea}; -use crate::gui::{Colors, Navigator}; +use crate::gui::Colors; use crate::gui::icons::ARROW_COUNTER_CLOCKWISE; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalPosition, View}; @@ -141,7 +141,7 @@ impl NetworkSettings { let reset_modal = Modal::new(Self::RESET_SETTINGS_MODAL) .position(ModalPosition::Center) .title(t!("modal.confirmation")); - Navigator::show_modal(reset_modal); + Modal::show(reset_modal); }); // Show reminder to restart enabled node. @@ -207,7 +207,7 @@ impl NetworkSettings { let port_modal = Modal::new(Self::NODE_RESTART_REQUIRED_MODAL) .position(ModalPosition::Center) .title(t!("network.settings")); - Navigator::show_modal(port_modal); + Modal::show(port_modal); } } diff --git a/src/gui/views/root.rs b/src/gui/views/root.rs new file mode 100644 index 0000000..db84446 --- /dev/null +++ b/src/gui/views/root.rs @@ -0,0 +1,110 @@ +// 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::min; +use std::sync::atomic::{AtomicBool, Ordering}; + +use lazy_static::lazy_static; +use crate::gui::App; + +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{AccountsContent, Modal, NetworkContent}; + +lazy_static! { + /// To check if side panel is open from any part of ui. + static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false); +} + +/// Main ui content, handles network panel state modal state. +#[derive(Default)] +pub struct Root { + network: NetworkContent, + accounts: AccountsContent, +} + +impl Root { + /// Default width of side panel at application UI. + pub const SIDE_PANEL_MIN_WIDTH: i64 = 400; + + pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { + let (is_panel_open, panel_width) = Self::side_panel_state_width(frame); + egui::SidePanel::left("network_panel") + .resizable(false) + .exact_width(panel_width) + .frame(egui::Frame::default()) + .show_animated_inside(ui, is_panel_open, |ui| { + self.network.ui(ui, frame, cb); + }); + + egui::CentralPanel::default() + .frame(egui::Frame::default()) + .show_inside(ui, |ui| { + self.accounts.ui(ui, frame, cb); + }); + } + + /// Get side panel state and width. + fn side_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_network_panel_open(); + let panel_width = if dual_panel_mode { + min(frame.info().window_info.size.x as i64, Self::SIDE_PANEL_MIN_WIDTH) as f32 + } else { + frame.info().window_info.size.x + }; + (is_panel_open, panel_width) + } + + /// Check if ui can show [`NetworkContent`] and [`AccountsContent`] 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; + // Screen is wide if width is greater than height or just 20% smaller. + let is_wide_screen = w > h || w + (w * 0.2) >= h; + // Dual panel mode is available when window is wide and its width is at least 2 times + // greater than minimal width of the side panel. + is_wide_screen && w >= Self::SIDE_PANEL_MIN_WIDTH as f32 * 2.0 + } + + /// Toggle [`Network`] panel state. + 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_network_panel_open() -> bool { + NETWORK_PANEL_OPEN.load(Ordering::Relaxed) + } + + /// Handle back button press event. + fn on_back() { + if Modal::on_back() { + App::show_exit_modal() + } + } +} + +#[allow(dead_code)] +#[cfg(target_os = "android")] +#[allow(non_snake_case)] +#[no_mangle] +/// Handle back button press event from Android. +pub extern "C" fn Java_mw_gri_android_MainActivity_onBackButtonPress( + _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/views.rs b/src/gui/views/views.rs index 8c9d307..6a1f3ab 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -26,22 +26,8 @@ impl View { /// Default stroke around views. pub const DEFAULT_STROKE: Stroke = Stroke { width: 1.0, color: Colors::STROKE }; - /// Default width of side panel at application UI. - pub const SIDE_PANEL_MIN_WIDTH: i64 = 400; - - /// Check if ui can show side panel and screen 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; - // Screen is wide if width is greater than height or just 20% smaller. - let is_wide_screen = w > h || w + (w * 0.2) >= h; - // Dual panel mode is available when window is wide and its width is at least 2 times - // greater than minimal width of the side panel. - is_wide_screen && w >= Self::SIDE_PANEL_MIN_WIDTH as f32 * 2.0 - } - - /// Show and cut long text with ﹍ character. - pub fn ellipsize_text(ui: &mut egui::Ui, text: String, size: f32, color: Color32) { + /// Cut long text with ﹍ character. + fn ellipsize(text: String, size: f32, color: Color32) -> LayoutJob { let mut job = LayoutJob::single_section(text, TextFormat { font_id: FontId::proportional(size), color, ..Default::default() }); @@ -51,7 +37,12 @@ impl View { overflow_character: Option::from('﹍'), ..Default::default() }; - ui.label(job); + job + } + + /// Show ellipsized text. + pub fn ellipsize_text(ui: &mut egui::Ui, text: String, size: f32, color: Color32) { + ui.label(Self::ellipsize(text, size, color)); } /// Draw horizontally centered sub-title with space below. @@ -121,7 +112,7 @@ impl View { /// Draw [`Button`] with specified background fill color. pub fn button(ui: &mut egui::Ui, text: String, fill_color: Color32, action: impl FnOnce()) { - let button_text = RichText::new(text.to_uppercase()).size(18.0).color(Colors::TEXT_BUTTON); + let button_text = Self::ellipsize(text.to_uppercase(), 18.0, Colors::TEXT_BUTTON); let br = Button::new(button_text) .stroke(Self::DEFAULT_STROKE) .fill(fill_color) diff --git a/src/node/node.rs b/src/node/node.rs index 9387ec4..5fd7a8d 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -260,7 +260,7 @@ impl Node { } } - if stratum_start_requested { + if stratum_start_requested && NODE_STATE.stratum_stats.read().is_running { NODE_STATE.start_stratum_needed.store(false, Ordering::Relaxed); } @@ -643,4 +643,17 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_exitAppAfterNodeStop( ) -> jni::sys::jboolean { let exit_needed = !Node::is_running() && NODE_STATE.exit_after_stop.load(Ordering::Relaxed); return exit_needed as jni::sys::jboolean; +} + +#[allow(dead_code)] +#[cfg(target_os = "android")] +#[allow(non_snake_case)] +#[no_mangle] +/// Handle unexpected application termination on Android (removal from recent apps). +pub extern "C" fn Java_mw_gri_android_MainActivity_onTermination( + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, +) { + Node::stop(false); } \ No newline at end of file