diff --git a/locales/en.yml b/locales/en.yml index 256e727..05fbeab 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -38,6 +38,7 @@ wallets: settings: Wallet settings network: self: Network + connections: Connections node: Integrated node metrics: Metrics mining: Mining diff --git a/locales/ru.yml b/locales/ru.yml index 10775ba..e117812 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -24,7 +24,7 @@ wallets: add_node: Добавить узел node_url: 'URL узла:' node_secret: 'API токен (необязательно):' - invalid_url: Введенный URL-адрес недействителен + invalid_url: Введённый URL-адрес недействителен open: Открыть кошелёк wrong_pass: Введён неправильный пароль locked: Заблокирован @@ -38,6 +38,7 @@ wallets: settings: Настройки кошелька network: self: Сеть + connections: Подключения node: Встроенный узел metrics: Показатели mining: Майнинг diff --git a/src/gui/views/network/connections/content.rs b/src/gui/views/network/connections/content.rs new file mode 100644 index 0000000..c016842 --- /dev/null +++ b/src/gui/views/network/connections/content.rs @@ -0,0 +1,343 @@ +// 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::{Align, Id, Layout, RichText, Rounding, TextStyle, Widget}; +use url::Url; + +use crate::AppConfig; +use crate::gui::Colors; +use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLAY, STOP, TRASH, X_CIRCLE}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, View}; +use crate::gui::views::types::{ModalContainer, ModalPosition}; +use crate::node::{Node, NodeConfig}; +use crate::wallet::{ConnectionsConfig, ExternalConnection}; + +/// Network connections content. +pub struct ConnectionsContent { + /// Flag to check if modal was just opened. + first_modal_launch: bool, + /// External connection URL value for [`Modal`]. + ext_node_url_edit: String, + /// External connection API secret value for [`Modal`]. + ext_node_secret_edit: String, + /// Flag to show URL format error. + ext_node_url_error: bool, + /// Flag to check if existing connection is editing. + edit_ext_conn: Option, + + /// [`Modal`] identifiers allowed at this ui container. + modal_ids: Vec<&'static str> +} + +impl Default for ConnectionsContent { + fn default() -> Self { + Self { + first_modal_launch: true, + ext_node_url_edit: "".to_string(), + ext_node_secret_edit: "".to_string(), + ext_node_url_error: false, + edit_ext_conn: None, + modal_ids: vec![ + Self::NETWORK_EXT_CONNECTION_MODAL + ] + } + } +} + +impl ModalContainer for ConnectionsContent { + fn modal_ids(&self) -> &Vec<&'static str> { + &self.modal_ids + } + + fn modal_ui(&mut self, + ui: &mut egui::Ui, + _: &mut eframe::Frame, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + match modal.id { + Self::NETWORK_EXT_CONNECTION_MODAL => self.ext_conn_modal_ui(ui, modal, cb), + _ => {} + } + } +} + +impl ConnectionsContent { + /// External connection [`Modal`] identifier. + pub const NETWORK_EXT_CONNECTION_MODAL: &'static str = "network_ext_connection_modal"; + + pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { + // Draw modal content for current ui container. + self.current_modal_ui(ui, frame, cb); + + // Show integrated node info content. + Self::integrated_node_item_ui(ui); + + ui.add_space(8.0); + ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::GRAY)); + ui.add_space(6.0); + + // Show external connections. + let ext_conn_list = ConnectionsConfig::external_connections(); + for (index, conn) in ext_conn_list.iter().enumerate() { + ui.horizontal_wrapped(|ui| { + // Draw connection list item. + self.ext_conn_item_ui(ui, conn, index, ext_conn_list.len(), cb); + }); + } + } + + /// Draw integrated node connection item content. + fn integrated_node_item_ui(ui: &mut egui::Ui) { + // Draw round background. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(78.0); + let rounding = View::item_rounding(0, 1, false); + let bg_color = Colors::FILL_DARK; + ui.painter().rect(rect, rounding, bg_color, View::HOVER_STROKE); + + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Setup padding for item buttons. + ui.style_mut().spacing.button_padding = egui::vec2(14.0, 0.0); + // Setup rounding for item buttons. + ui.style_mut().visuals.widgets.inactive.rounding = Rounding::same(8.0); + ui.style_mut().visuals.widgets.hovered.rounding = Rounding::same(8.0); + ui.style_mut().visuals.widgets.active.rounding = Rounding::same(8.0); + + // Draw button to show integrated node. + View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, || { + AppConfig::toggle_show_connections_network_panel(); + }); + + if !Node::is_running() { + // Draw button to start integrated node. + View::item_button(ui, Rounding::none(), PLAY, || { + Node::start(); + }); + } else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() { + // Show button to open closed wallet. + View::item_button(ui, Rounding::none(), STOP, || { + Node::stop(false); + }); + } + + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(7.0); + ui.vertical(|ui| { + ui.add_space(3.0); + ui.label(RichText::new(t!("network.node")) + .size(18.0) + .color(Colors::TITLE)); + + // Setup node API address text. + let api_address = NodeConfig::get_api_address(); + let address_text = format!("{} http://{}", COMPUTER_TOWER, api_address); + ui.label(RichText::new(address_text).size(15.0).color(Colors::TEXT)); + ui.add_space(1.0); + + // Setup node status text. + let status_icon = if !Node::is_running() { + X_CIRCLE + } else if Node::not_syncing() { + CHECK_CIRCLE + } else { + DOTS_THREE_CIRCLE + }; + let status_text = format!("{} {}", status_icon, Node::get_sync_status_text()); + ui.label(RichText::new(status_text).size(15.0).color(Colors::GRAY)); + ui.add_space(4.0); + }) + }); + }); + } + + /// Draw external connection item content. + fn ext_conn_item_ui(&mut self, + ui: &mut egui::Ui, + conn: &ExternalConnection, + index: usize, + len: usize, + cb: &dyn PlatformCallbacks) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(42.0); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(index, len, false); + ui.painter().rect(bg_rect, item_rounding, Colors::FILL, View::ITEM_STROKE); + + ui.vertical(|ui| { + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw buttons for non-default connections. + if conn.url != ExternalConnection::DEFAULT_EXTERNAL_NODE_URL { + let button_rounding = View::item_rounding(index, len, true); + View::item_button(ui, button_rounding, TRASH, || { + ConnectionsConfig::remove_external_connection(conn); + }); + View::item_button(ui, Rounding::none(), PENCIL, || { + // Setup values for Modal. + self.first_modal_launch = true; + self.ext_node_url_edit = conn.url.clone(); + self.ext_node_secret_edit = conn.secret.clone().unwrap_or("".to_string()); + self.ext_node_url_error = false; + self.edit_ext_conn = Some(conn.clone()); + // Show modal. + Modal::new(Self::NETWORK_EXT_CONNECTION_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.add_node")) + .show(); + cb.show_keyboard(); + }); + } + + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + // Draw connections URL. + let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url); + ui.label(RichText::new(conn_text) + .color(Colors::TEXT_BUTTON) + .size(16.0)); + }); + }); + }); + } + + /// Show [`Modal`] to add external connection. + pub fn show_add_ext_conn_modal(&mut self, cb: &dyn PlatformCallbacks) { + // Setup values for Modal. + self.first_modal_launch = true; + self.ext_node_url_edit = "".to_string(); + self.ext_node_secret_edit = "".to_string(); + self.ext_node_url_error = false; + self.edit_ext_conn = None; + // Show modal. + Modal::new(Self::NETWORK_EXT_CONNECTION_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.add_node")) + .show(); + cb.show_keyboard(); + } + + /// Draw external connection [`Modal`] content. + pub fn ext_conn_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.node_url")) + .size(17.0) + .color(Colors::GRAY)); + ui.add_space(8.0); + + // Draw node URL text edit. + let url_edit_resp = egui::TextEdit::singleline(&mut self.ext_node_url_edit) + .id(Id::from(modal.id).with("node_url_edit")) + .font(TextStyle::Heading) + .desired_width(ui.available_width()) + .cursor_at_end(true) + .ui(ui); + ui.add_space(8.0); + if self.first_modal_launch { + self.first_modal_launch = false; + url_edit_resp.request_focus(); + } + if url_edit_resp.clicked() { + cb.show_keyboard(); + } + + ui.label(RichText::new(t!("wallets.node_secret")) + .size(17.0) + .color(Colors::GRAY)); + ui.add_space(8.0); + + // Draw node API secret text edit. + let secret_edit_resp = egui::TextEdit::singleline(&mut self.ext_node_secret_edit) + .id(Id::from(modal.id).with("node_secret_edit")) + .font(TextStyle::Heading) + .desired_width(ui.available_width()) + .cursor_at_end(true) + .ui(ui); + ui.add_space(8.0); + if secret_edit_resp.clicked() { + cb.show_keyboard(); + } + + // Show error when specified URL is not valid. + if self.ext_node_url_error { + ui.add_space(12.0); + ui.label(RichText::new(t!("wallets.invalid_url")) + .size(17.0) + .color(Colors::RED)); + } + ui.add_space(12.0); + }); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::WHITE, || { + // Close modal. + cb.hide_keyboard(); + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Add connection button callback. + let mut on_add = || { + let error = Url::parse(self.ext_node_url_edit.as_str()).is_err(); + self.ext_node_url_error = error; + if !error { + // Save external connection. + let url = self.ext_node_url_edit.to_owned(); + let secret = if self.ext_node_secret_edit.is_empty() { + None + } else { + Some(self.ext_node_secret_edit.to_owned()) + }; + + // Update or create new connections. + let ext_conn = ExternalConnection::new(url.clone(), secret); + if let Some(edit_conn) = self.edit_ext_conn.clone() { + ConnectionsConfig::update_external_connection(edit_conn, ext_conn); + self.edit_ext_conn = None; + } else { + ConnectionsConfig::add_external_connection(ext_conn); + } + + // Close modal. + cb.hide_keyboard(); + modal.close(); + } + }; + + // Add connection on Enter button press. + View::on_enter_key(ui, || { + (on_add)(); + }); + + View::button(ui, t!("modal.save"), Colors::WHITE, on_add); + }); + }); + ui.add_space(6.0); + }); + } +} \ No newline at end of file diff --git a/src/gui/views/network/connections.rs b/src/gui/views/network/connections/mod.rs similarity index 63% rename from src/gui/views/network/connections.rs rename to src/gui/views/network/connections/mod.rs index 0d077e6..b3e1aa2 100644 --- a/src/gui/views/network/connections.rs +++ b/src/gui/views/network/connections/mod.rs @@ -12,21 +12,5 @@ // 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 +mod content; +pub use content::ConnectionsContent; \ No newline at end of file diff --git a/src/gui/views/network/content.rs b/src/gui/views/network/content.rs index 5535ed5..73ae3a2 100644 --- a/src/gui/views/network/content.rs +++ b/src/gui/views/network/content.rs @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::RichText; +use egui::{RichText, ScrollArea}; use egui::style::Margin; use crate::AppConfig; use crate::gui::Colors; -use crate::gui::icons::{CARDHOLDER, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, POWER}; +use crate::gui::icons::{CARDHOLDER, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, PLUS_CIRCLE, POWER}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings, Root, TitlePanel, View}; +use crate::gui::views::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings, Root, TitlePanel, View}; use crate::gui::views::network::types::{NetworkTab, NetworkTabType}; use crate::gui::views::types::TitleType; use crate::node::Node; @@ -28,38 +28,46 @@ use crate::node::Node; pub struct NetworkContent { /// Current tab content to show. current_tab: Box, + + /// Connections content. + connections: ConnectionsContent } impl Default for NetworkContent { fn default() -> Self { Self { - current_tab: Box::new(NetworkNode::default()) + current_tab: Box::new(NetworkNode::default()), + connections: ConnectionsContent::default(), } } } impl NetworkContent { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { + // Flag to show connections or integrated node content. + let show_connections = AppConfig::show_connections_network_panel(); + // Show title panel. - self.title_ui(ui, frame); + self.title_ui(ui, frame, show_connections, cb); // Show bottom tabs. - egui::TopBottomPanel::bottom("network_tabs") - .frame(egui::Frame { - fill: Colors::FILL, - inner_margin: Margin { - 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, - }, - ..Default::default() - }) - .show_inside(ui, |ui| { - self.tabs_ui(ui); - }); + if !show_connections { + egui::TopBottomPanel::bottom("network_tabs") + .frame(egui::Frame { + fill: Colors::FILL, + inner_margin: Margin { + 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, + }, + ..Default::default() + }) + .show_inside(ui, |ui| { + self.tabs_ui(ui); + }); + } - // Show tab content. egui::CentralPanel::default() .frame(egui::Frame { stroke: View::DEFAULT_STROKE, @@ -67,16 +75,48 @@ impl NetworkContent { left: View::get_left_inset() + 4.0, right: View::far_right_inset_margin(ui, frame) + 4.0, top: 3.0, - bottom: 4.0, + bottom: if show_connections { + View::get_bottom_inset() + 4.0 + } else { + 4.0 + }, + }, + fill: if show_connections { + Colors::FILL + } else { + Colors::WHITE }, - fill: Colors::WHITE, ..Default::default() }) .show_inside(ui, |ui| { - self.current_tab.ui(ui, frame, cb); + if show_connections { + ScrollArea::vertical() + .id_source("connections_content") + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(1.0); + ui.vertical_centered(|ui| { + // Setup wallet list width. + let mut rect = ui.available_rect_before_wrap(); + let mut width = ui.available_width(); + if !Root::is_dual_panel_mode(frame) { + width = f32::min(width, Root::SIDE_PANEL_WIDTH * 1.3) + } + rect.set_width(width); + + ui.allocate_ui(rect.size(), |ui| { + // Show connections content. + self.connections.ui(ui, frame, cb); + }); + }); + }); + } else { + // Show current tab content. + self.current_tab.ui(ui, frame, cb); + } }); - // Redraw content after delay if node is not syncing to update stats. + // Redraw after delay if node is not syncing to update stats. if Node::not_syncing() { ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY); } @@ -118,18 +158,32 @@ impl NetworkContent { } /// Draw title content. - fn title_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { + fn title_ui(&mut self, + ui: &mut egui::Ui, + frame: &mut eframe::Frame, + show_connections: bool, + cb: &dyn PlatformCallbacks) { // Setup values for title panel. let title_text = self.current_tab.get_type().title().to_uppercase(); 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); + let title_content = if !show_connections { + TitleType::WithSubTitle(title_text, subtitle_text, !not_syncing) + } else { + TitleType::Single(t!("network.connections").to_uppercase()) + }; // Draw title panel. TitlePanel::ui(title_content, |ui, _| { - View::title_button(ui, DOTS_THREE_OUTLINE_VERTICAL, || { - //TODO: Show connections - }); + if !show_connections { + View::title_button(ui, DOTS_THREE_OUTLINE_VERTICAL, || { + AppConfig::toggle_show_connections_network_panel(); + }); + } else { + View::title_button(ui, PLUS_CIRCLE, || { + self.connections.show_add_ext_conn_modal(cb); + }); + } }, |ui, frame| { if !Root::is_dual_panel_mode(frame) { View::title_button(ui, CARDHOLDER, || { diff --git a/src/gui/views/network/metrics.rs b/src/gui/views/network/metrics.rs index fe7f887..d0a6bbc 100644 --- a/src/gui/views/network/metrics.rs +++ b/src/gui/views/network/metrics.rs @@ -131,7 +131,7 @@ impl NetworkTab for NetworkMetrics { ui.add_space(4.0); } let db = stats.diff_stats.last_blocks.get(index).unwrap(); - block_item_ui(ui, db, View::item_rounding(index, blocks_size)) + block_item_ui(ui, db, View::item_rounding(index, blocks_size, false)); } }, ); diff --git a/src/gui/views/network/mining.rs b/src/gui/views/network/mining.rs index caa59c2..811e403 100644 --- a/src/gui/views/network/mining.rs +++ b/src/gui/views/network/mining.rs @@ -177,7 +177,8 @@ impl NetworkTab for NetworkMining { ui.add_space(4.0); } let worker = stratum_stats.worker_stats.get(index).unwrap(); - worker_item_ui(ui, worker, View::item_rounding(index, workers_size)); + let item_rounding = View::item_rounding(index, workers_size, false); + worker_item_ui(ui, worker, item_rounding); } }, ); diff --git a/src/gui/views/network/node.rs b/src/gui/views/network/node.rs index cc09353..4e6fbb6 100644 --- a/src/gui/views/network/node.rs +++ b/src/gui/views/network/node.rs @@ -164,7 +164,7 @@ impl NetworkTab for NetworkNode { View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_node.peers"))); let peers = &stats.peer_stats; for (index, ps) in peers.iter().enumerate() { - peer_item_ui(ui, ps, View::item_rounding(index, peers.len())); + peer_item_ui(ui, ps, View::item_rounding(index, peers.len(), false)); // Add space after the last item. if index == peers.len() - 1 { ui.add_space(5.0); diff --git a/src/gui/views/network/setup/p2p.rs b/src/gui/views/network/setup/p2p.rs index ac119f5..2004869 100644 --- a/src/gui/views/network/setup/p2p.rs +++ b/src/gui/views/network/setup/p2p.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Align, Id, Layout, RichText, Rounding, TextStyle, Widget}; +use egui::{Align, Id, Layout, RichText, TextStyle, Widget}; use egui_extras::{Size, StripBuilder}; use grin_core::global::ChainTypes; @@ -360,7 +360,7 @@ impl P2PSetup { for (index, peer) in peers.iter().enumerate() { ui.horizontal_wrapped(|ui| { // Draw peer list item. - peer_item_ui(ui, peer, peer_type, View::item_rounding(index, peers.len())); + peer_item_ui(ui, peer, peer_type, index, peers.len()); }); } @@ -889,7 +889,11 @@ impl P2PSetup { } /// Draw peer list item. -fn peer_item_ui(ui: &mut egui::Ui, peer_addr: &String, peer_type: &PeerType, rounding: Rounding) { +fn peer_item_ui(ui: &mut egui::Ui, + peer_addr: &String, + peer_type: &PeerType, + index: usize, + len: usize,) { // Setup layout size. let mut rect = ui.available_rect_before_wrap(); rect.set_height(42.0); @@ -897,13 +901,14 @@ fn peer_item_ui(ui: &mut egui::Ui, peer_addr: &String, peer_type: &PeerType, rou // Draw round background. let mut bg_rect = rect.clone(); bg_rect.min += egui::emath::vec2(6.0, 0.0); - ui.painter().rect(bg_rect, rounding, Colors::WHITE, View::ITEM_STROKE); + let item_rounding = View::item_rounding(index, len, false); + ui.painter().rect(bg_rect, item_rounding, Colors::WHITE, View::ITEM_STROKE); ui.vertical(|ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw delete button for non-default seed peers. if peer_type != &PeerType::DefaultSeed { - View::item_button(ui, [false, true], TRASH, || { + View::item_button(ui, View::item_rounding(index, len, true), TRASH, || { match peer_type { PeerType::CustomSeed => { NodeConfig::remove_custom_seed(peer_addr); diff --git a/src/gui/views/root.rs b/src/gui/views/root.rs index 8fe41ec..a92d74b 100644 --- a/src/gui/views/root.rs +++ b/src/gui/views/root.rs @@ -153,7 +153,7 @@ impl Root { /// Show exit confirmation modal. pub fn show_exit_modal() { Modal::new(Self::EXIT_MODAL_ID) - .title(t!("modal.confirmation")) + .title(t!("modal_exit.exit")) .show(); } diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs index 6beaefc..be921e7 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -31,7 +31,7 @@ impl View { /// Stroke for items. pub const ITEM_STROKE: Stroke = Stroke { width: 1.0, color: Colors::ITEM_STROKE }; /// Stroke for hovered items and buttons. - pub const ITEM_HOVER_STROKE: Stroke = Stroke { width: 1.0, color: Colors::ITEM_HOVER }; + pub const HOVER_STROKE: Stroke = Stroke { width: 1.0, color: Colors::ITEM_HOVER }; /// Callback on Enter key press event. pub fn on_enter_key(ui: &mut egui::Ui, cb: impl FnOnce()) { @@ -105,7 +105,7 @@ impl View { pub fn title_button(ui: &mut egui::Ui, icon: &str, action: impl FnOnce()) { ui.scope(|ui| { // Setup stroke around title buttons on click. - ui.style_mut().visuals.widgets.hovered.bg_stroke = Self::ITEM_HOVER_STROKE; + ui.style_mut().visuals.widgets.hovered.bg_stroke = Self::HOVER_STROKE; ui.style_mut().visuals.widgets.active.bg_stroke = Self::DEFAULT_STROKE; // Disable rounding. ui.style_mut().visuals.widgets.hovered.rounding = Rounding::none(); @@ -120,6 +120,7 @@ impl View { let br = Button::new(wt) .fill(Colors::TRANSPARENT) .ui(ui); + br.surrender_focus(); if Self::touched(ui, br) { (action)(); } @@ -146,13 +147,14 @@ impl View { ui.visuals_mut().widgets.active.weak_bg_fill = Colors::FILL; // Setup stroke colors. ui.visuals_mut().widgets.inactive.bg_stroke = Self::DEFAULT_STROKE; - ui.visuals_mut().widgets.hovered.bg_stroke = Self::ITEM_HOVER_STROKE; + ui.visuals_mut().widgets.hovered.bg_stroke = Self::HOVER_STROKE; ui.visuals_mut().widgets.active.bg_stroke = Self::ITEM_STROKE; } else { button = button.fill(Colors::FILL).stroke(Stroke::NONE); } let br = button.ui(ui); + br.surrender_focus(); if Self::touched(ui, br) { (action)(); } @@ -172,9 +174,7 @@ impl View { } /// Draw list item [`Button`] with given vertical padding and rounding on left and right sides. - pub fn item_button(ui: &mut egui::Ui, r: [bool; 2], icon: &'static str, action: impl FnOnce()) { - let rounding = Self::get_rounding([r[0], r[1], r[1], r[0]]); - + pub fn item_button(ui: &mut egui::Ui, r: Rounding, icon: &'static str, action: impl FnOnce()) { // Setup button size. let mut rect = ui.available_rect_before_wrap(); rect.set_width(32.0); @@ -190,14 +190,15 @@ impl View { ui.visuals_mut().widgets.active.weak_bg_fill = Colors::FILL; // Setup stroke colors. ui.visuals_mut().widgets.inactive.bg_stroke = Self::DEFAULT_STROKE; - ui.visuals_mut().widgets.hovered.bg_stroke = Self::ITEM_HOVER_STROKE; + ui.visuals_mut().widgets.hovered.bg_stroke = Self::HOVER_STROKE; ui.visuals_mut().widgets.active.bg_stroke = Self::ITEM_STROKE; // Show button. let br = Button::new(RichText::new(icon).size(20.0).color(Colors::ITEM_BUTTON)) - .rounding(rounding) + .rounding(r) .min_size(button_size) .ui(ui); + br.surrender_focus(); if Self::touched(ui, br) { (action)(); } @@ -243,8 +244,29 @@ impl View { }); } - /// Get rounding for provided corners clockwise. - fn get_rounding(corners: [bool; 4]) -> Rounding { + /// Calculate item background/button rounding based on item index. + pub fn item_rounding(index: usize, len: usize, is_button: bool) -> Rounding { + let corners = if is_button { + if len == 1 { + [false, true, true, false] + } else if index == 0 { + [false, true, false, false] + } else if index == len - 1 { + [false, false, true, false] + } else { + [false, false, false, false] + } + } else { + if len == 1 { + [true, true, true, true] + } else if index == 0 { + [true, true, false, false] + } else if index == len - 1 { + [false, false, true, true] + } else { + [false, false, false, false] + } + }; Rounding { nw: if corners[0] { 8.0 } else { 0.0 }, ne: if corners[1] { 8.0 } else { 0.0 }, @@ -253,20 +275,6 @@ impl View { } } - /// Calculate list item rounding based on item index. - pub fn item_rounding(index: usize, len: usize) -> Rounding { - let rounding = if len == 1 { - [true, true, true, true] - } else if index == 0 { - [true, true, false, false] - } else if index == len - 1 { - [false, false, true, true] - } else { - [false, false, false, false] - }; - Self::get_rounding(rounding) - } - /// Draw rounded box with some value and label in the middle, /// where is r = (top_left, top_right, bottom_left, bottom_right). /// | VALUE | diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index 0cba393..19170e4 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -351,10 +351,9 @@ impl WalletsContent { // Draw round background. let mut rect = ui.available_rect_before_wrap(); rect.set_height(78.0); - let rounding = View::item_rounding(0, 1); + let rounding = View::item_rounding(0, 1, false); let bg_color = if is_current { Colors::ITEM_CURRENT } else { Colors::FILL }; - let stroke = if is_current { View::ITEM_HOVER_STROKE } else { View::ITEM_HOVER_STROKE }; - ui.painter().rect(rect, rounding, bg_color, stroke); + ui.painter().rect(rect, rounding, bg_color, View::HOVER_STROKE); ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Setup padding for item buttons. @@ -366,18 +365,17 @@ impl WalletsContent { if !wallet.is_open() { // Show button to open closed wallet. - View::item_button(ui, [false, true], FOLDER_OPEN, || { + View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, || { self.wallets.select(Some(id)); self.show_open_wallet_modal(cb); }); } else if !is_selected { // Show button to select opened wallet. - View::item_button(ui, [false, true], CARET_RIGHT, || { + View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, || { self.wallets.select(Some(id)); }); - // Show button to close opened wallet. - View::item_button(ui, [false, false], LOCK_KEY, || { + View::item_button(ui, Rounding::none(), LOCK_KEY, || { let _ = wallet.close(); }); } diff --git a/src/gui/views/wallets/setup/connection.rs b/src/gui/views/wallets/setup/connection.rs index 38b3afc..114ac8e 100644 --- a/src/gui/views/wallets/setup/connection.rs +++ b/src/gui/views/wallets/setup/connection.rs @@ -42,7 +42,7 @@ pub struct ConnectionSetup { } /// External connection [`Modal`] identifier. -pub const EXT_CONNECTION_MODAL: &'static str = "ext_connection_modal"; +pub const ADD_EXT_CONNECTION_MODAL: &'static str = "add_ext_connection_modal"; impl Default for ConnectionSetup { fn default() -> Self { @@ -53,7 +53,7 @@ impl Default for ConnectionSetup { ext_node_secret_edit: "".to_string(), ext_node_url_error: false, modal_ids: vec![ - EXT_CONNECTION_MODAL + ADD_EXT_CONNECTION_MODAL ] } } @@ -70,7 +70,7 @@ impl ModalContainer for ConnectionSetup { modal: &Modal, cb: &dyn PlatformCallbacks) { match modal.id { - EXT_CONNECTION_MODAL => self.ext_conn_modal_ui(ui, modal, cb), + ADD_EXT_CONNECTION_MODAL => self.add_ext_conn_modal_ui(ui, modal, cb), _ => {} } } @@ -117,17 +117,7 @@ impl ConnectionSetup { // Show button to add new external node connection. let add_node_text = format!("{} {}", GLOBE_SIMPLE, t!("wallets.add_node")); View::button(ui, add_node_text, Colors::GOLD, || { - // Setup values for Modal. - self.first_modal_launch = true; - self.ext_node_url_edit = "".to_string(); - self.ext_node_secret_edit = "".to_string(); - self.ext_node_url_error = false; - // Show modal. - Modal::new(EXT_CONNECTION_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.add_node")) - .show(); - cb.show_keyboard(); + self.show_add_ext_conn_modal(cb); }); ui.add_space(12.0); @@ -143,11 +133,26 @@ impl ConnectionSetup { }); } - /// Draw external connection [`Modal`] content. - pub fn ext_conn_modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { + /// Show external connection adding [`Modal`]. + fn show_add_ext_conn_modal(&mut self, cb: &dyn PlatformCallbacks) { + // Setup values for Modal. + self.first_modal_launch = true; + self.ext_node_url_edit = "".to_string(); + self.ext_node_secret_edit = "".to_string(); + self.ext_node_url_error = false; + // Show modal. + Modal::new(ADD_EXT_CONNECTION_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.add_node")) + .show(); + cb.show_keyboard(); + } + + /// Draw external connection adding [`Modal`] content. + pub fn add_ext_conn_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.node_url")) @@ -217,7 +222,7 @@ impl ConnectionSetup { let error = Url::parse(self.ext_node_url_edit.as_str()).is_err(); self.ext_node_url_error = error; if !error { - // Save external connection. + // Add external connection. let url = self.ext_node_url_edit.to_owned(); let secret = if self.ext_node_secret_edit.is_empty() { None diff --git a/src/settings.rs b/src/settings.rs index b223804..2071ab5 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -40,8 +40,11 @@ pub struct AppConfig { pub auto_start_node: bool, /// Chain type for node and wallets. chain_type: ChainTypes, - /// Flag to show wallet list at dual panel wallets mode. + + /// Flag to initially show wallet list at dual panel wallets mode. show_wallets_at_dual_panel: bool, + /// Flag to initially show all connections at network panel. + show_connections_network_panel: bool, } impl Default for AppConfig { @@ -50,6 +53,7 @@ impl Default for AppConfig { auto_start_node: false, chain_type: ChainTypes::default(), show_wallets_at_dual_panel: true, + show_connections_network_panel: false, } } } @@ -113,6 +117,20 @@ impl AppConfig { w_app_config.show_wallets_at_dual_panel = !show; w_app_config.save(); } + + /// Toggle flag to show all connections at network panel. + pub fn show_connections_network_panel() -> bool { + let r_config = Settings::app_config_to_read(); + r_config.show_connections_network_panel + } + + /// Toggle flag to show all connections at network panel. + pub fn toggle_show_connections_network_panel() { + let show = Self::show_connections_network_panel(); + let mut w_app_config = Settings::app_config_to_update(); + w_app_config.show_connections_network_panel = !show; + w_app_config.save(); + } } /// Main application directory name. diff --git a/src/wallet/connections/config.rs b/src/wallet/connections/config.rs index fbeb399..b787c4e 100644 --- a/src/wallet/connections/config.rs +++ b/src/wallet/connections/config.rs @@ -63,6 +63,10 @@ impl ConnectionsConfig { /// Save external connection for the wallet in app config. pub fn add_external_connection(conn: ExternalConnection) { + // Do not update default connection. + if conn.url == ExternalConnection::DEFAULT_EXTERNAL_NODE_URL { + return; + } let mut w_config = CONNECTIONS_STATE.write().unwrap(); let mut exists = false; for mut c in w_config.external.iter_mut() { @@ -75,7 +79,25 @@ impl ConnectionsConfig { } // Create new connection if URL not exists. if !exists { - w_config.external.insert(0, conn); + w_config.external.push(conn); + } + w_config.save(); + } + + /// Save external connection for the wallet in app config. + pub fn update_external_connection(conn: ExternalConnection, updated: ExternalConnection) { + // Do not update default connection. + if conn.url == ExternalConnection::DEFAULT_EXTERNAL_NODE_URL { + return; + } + let mut w_config = CONNECTIONS_STATE.write().unwrap(); + for mut c in w_config.external.iter_mut() { + // Update connection if URL exists. + if c.url == conn.url { + c.url = updated.url.clone(); + c.secret = updated.secret.clone(); + break; + } } w_config.save(); } diff --git a/src/wallet/connections/external.rs b/src/wallet/connections/external.rs index 7cafd3a..84e5de6 100644 --- a/src/wallet/connections/external.rs +++ b/src/wallet/connections/external.rs @@ -25,7 +25,7 @@ pub struct ExternalConnection { impl ExternalConnection { /// Default external node URL. - const DEFAULT_EXTERNAL_NODE_URL: &'static str = "https://grinnnode.live:3413"; + pub const DEFAULT_EXTERNAL_NODE_URL: &'static str = "https://grinnnode.live:3413"; pub fn new(url: String, secret: Option) -> Self { Self { url, secret }