ui: settings content at separate panel

This commit is contained in:
ardocrat
2025-05-29 12:56:49 +03:00
parent ed50132d5e
commit 4d5cc93a38
7 changed files with 270 additions and 160 deletions
+4 -2
View File
@@ -59,8 +59,10 @@ impl<Platform: PlatformCallbacks> App<Platform> {
if View::is_desktop() {
self.platform.set_context(ctx);
}
// Check connections availability.
ExternalConnection::check(None, ctx);
// Check connections availability at dual panel mode.
if Content::is_dual_panel_mode(ctx) && AppConfig::show_connections_network_panel() {
ExternalConnection::check(None, ctx);
}
// Setup visuals.
crate::setup_visuals(ctx);
}
+16 -129
View File
@@ -12,31 +12,35 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::os::OperatingSystem;
use egui::RichText;
use lazy_static::lazy_static;
use std::fs;
use std::sync::atomic::{AtomicBool, Ordering};
use egui::os::OperatingSystem;
use egui::{Align, Layout, RichText, StrokeKind};
use lazy_static::lazy_static;
use crate::gui::Colors;
use crate::gui::icons::FILE_X;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::gui::views::wallets::WalletsContent;
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::node::Node;
use crate::{AppConfig, Settings};
use crate::gui::icons::{CHECK, CHECK_FAT, FILE_X};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::wallets::WalletsContent;
lazy_static! {
/// Global state to check if [`NetworkContent`] panel is open.
static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false);
/// Global state to check if [`NetworkContent`] panel is open.
static ref SETTINGS_PANEL_OPEN: AtomicBool = AtomicBool::new(false);
}
/// Contains main ui content, handles side panel state.
pub struct Content {
/// Side panel [`NetworkContent`] content.
network: NetworkContent,
/// Central panel [`WalletsContent`] content.
pub wallets: WalletsContent,
@@ -70,7 +74,6 @@ impl Default for Content {
first_draw: true,
allowed_modal_ids: vec![
Self::EXIT_CONFIRMATION_MODAL,
Self::SETTINGS_MODAL,
ANDROID_INTEGRATED_NODE_WARNING_MODAL,
CRASH_REPORT_MODAL
],
@@ -89,7 +92,6 @@ impl ModalContainer for Content {
cb: &dyn PlatformCallbacks) {
match modal.id {
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
Self::SETTINGS_MODAL => self.settings_modal_ui(ui),
ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui),
CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, cb),
_ => {}
@@ -100,8 +102,6 @@ impl ModalContainer for Content {
impl Content {
/// Identifier for exit confirmation [`Modal`].
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
/// Identifier for wallet opening [`Modal`].
pub const SETTINGS_MODAL: &'static str = "settings_modal";
/// Default width of side panel at application UI.
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
@@ -114,7 +114,10 @@ impl Content {
self.current_modal_ui(ui, cb);
let dual_panel = Self::is_dual_panel_mode(ui.ctx());
let (is_panel_open, panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
let (is_panel_open, mut panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
if self.network.showing_settings() {
panel_width = ui.available_width();
}
// Show network content.
egui::SidePanel::left("network_panel")
@@ -244,122 +247,6 @@ impl Content {
}
}
/// Draw creating wallet name/password input [`Modal`] content.
pub fn settings_modal_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(6.0);
// Show theme selection.
Self::theme_selection_ui(ui);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(format!("{}:", t!("language")))
.size(16.0)
.color(Colors::gray())
);
});
ui.add_space(8.0);
// Draw available list of languages to select.
let locales = rust_i18n::available_locales!();
for (index, locale) in locales.iter().enumerate() {
Self::language_item_ui(locale, ui, index, locales.len());
}
ui.add_space(8.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
Modal::close();
});
});
ui.add_space(6.0);
}
/// Draw theme selection content.
fn theme_selection_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("theme")).size(16.0).color(Colors::gray()));
});
let saved_use_dark = AppConfig::dark_theme().unwrap_or(false);
let mut selected_use_dark = saved_use_dark;
ui.add_space(8.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, false, t!("light"));
});
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, true, t!("dark"));
})
});
ui.add_space(8.0);
if saved_use_dark != selected_use_dark {
AppConfig::set_dark_theme(selected_use_dark);
crate::setup_visuals(ui.ctx());
}
}
/// Draw language selection item content.
fn language_item_ui(locale: &str, ui: &mut egui::Ui, index: usize, len: usize) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(50.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(),
StrokeKind::Middle);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select language.
let is_current = if let Some(lang) = AppConfig::locale() {
lang == locale
} else {
rust_i18n::locale() == locale
};
if !is_current {
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
rust_i18n::set_locale(locale);
AppConfig::save_locale(locale);
Modal::close();
});
} else {
ui.add_space(14.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.add_space(14.0);
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
// Draw language name.
ui.add_space(12.0);
let color = if is_current {
Colors::title(false)
} else {
Colors::gray()
};
ui.label(RichText::new(t!("lang_name", locale = locale))
.size(17.0)
.color(color));
ui.add_space(3.0);
});
});
});
}
/// Draw content for integrated node warning [`Modal`] on Android.
fn android_warning_modal_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(6.0);
+1 -1
View File
@@ -27,8 +27,8 @@ mod content;
pub use content::*;
pub mod network;
pub mod wallets;
pub mod settings;
mod camera;
pub use camera::*;
+37 -10
View File
@@ -17,11 +17,12 @@ use egui::scroll_area::ScrollBarVisibility;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, POWER};
use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, ARROW_LEFT, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, GEAR, POWER};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, TitlePanel, View};
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::settings::SettingsContent;
use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
use crate::node::{Node, NodeConfig, NodeError};
use crate::wallet::ExternalConnection;
@@ -32,6 +33,9 @@ pub struct NetworkContent {
node_tab_content: Box<dyn NodeTab>,
/// Connections content.
connections: ConnectionsContent,
/// Application settings content.
settings_content: Option<SettingsContent>,
}
impl Default for NetworkContent {
@@ -39,12 +43,14 @@ impl Default for NetworkContent {
Self {
node_tab_content: Box::new(NetworkNode::default()),
connections: ConnectionsContent::default(),
settings_content: None,
}
}
}
impl NetworkContent {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let show_settings = self.showing_settings();
let show_connections = AppConfig::show_connections_network_panel();
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
@@ -52,8 +58,8 @@ impl NetworkContent {
self.title_ui(ui, dual_panel, show_connections);
// Show integrated node tabs content.
if !show_connections {
egui::TopBottomPanel::bottom("node_tabs")
if !show_connections && !show_settings {
egui::TopBottomPanel::bottom("network_tabs_content")
.min_height(0.5)
.resizable(false)
.frame(egui::Frame {
@@ -83,14 +89,14 @@ impl NetworkContent {
});
}
// Show integrated node tab content.
egui::SidePanel::right("node_tab_content")
// Show settings or integrated node content.
egui::SidePanel::right("network_side_content")
.resizable(false)
.exact_width(ui.available_width())
.frame(egui::Frame {
..Default::default()
})
.show_animated_inside(ui, !show_connections, |ui| {
.show_animated_inside(ui, show_settings || !show_connections, |ui| {
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
@@ -103,7 +109,11 @@ impl NetworkContent {
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
if self.node_tab_content.get_type() != NodeTabType::Settings {
if let Some(c) = &mut self.settings_content {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
c.ui(ui);
});
} else if self.node_tab_content.get_type() != NodeTabType::Settings {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let node_err = Node::get_error();
if let Some(err) = node_err {
@@ -192,6 +202,11 @@ impl NetworkContent {
}
}
/// Check if application settings content is showing.
pub fn showing_settings(&self) -> bool {
self.settings_content.is_some()
}
/// Draw tab buttons at bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
@@ -229,11 +244,15 @@ impl NetworkContent {
/// Draw title content.
fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
let show_settings = self.showing_settings();
// Setup values for title panel.
let title_text = self.node_tab_content.get_type().title();
let subtitle_text = Node::get_sync_status_text();
let not_syncing = Node::not_syncing();
let title_content = if !show_connections {
let title_content = if show_settings {
TitleContentType::Title(t!("settings"))
} else if !show_connections {
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
} else {
TitleContentType::Title(t!("network.connections"))
@@ -241,16 +260,24 @@ impl NetworkContent {
// Draw title panel.
TitlePanel::new(Id::from("network_title_panel")).ui(TitleType::Single(title_content), |ui| {
if !show_connections {
if show_settings {
View::title_button_big(ui, ARROW_LEFT, |_| {
self.settings_content = None;
});
} else if !show_connections {
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |ui| {
AppConfig::toggle_show_connections_network_panel();
if AppConfig::show_connections_network_panel() {
ExternalConnection::check(None, ui.ctx());
}
});
} else if !dual_panel {
View::title_button_big(ui, GEAR, |_| {
self.settings_content = Some(SettingsContent::default());
});
}
}, |ui| {
if !dual_panel {
if !dual_panel && !show_settings {
View::title_button_big(ui, BRIEFCASE, |_| {
Content::toggle_network_panel();
});
+145
View File
@@ -0,0 +1,145 @@
// Copyright 2025 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 eframe::emath::Align;
use eframe::epaint::StrokeKind;
use egui::{Layout, RichText};
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CHECK_FAT};
use crate::gui::views::{Modal, View};
/// Application settings content.
pub struct SettingsContent {
}
impl Default for SettingsContent {
fn default() -> Self {
Self {
}
}
}
impl SettingsContent {
/// Draw settings content.
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(6.0);
// Show theme selection.
Self::theme_selection_ui(ui);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(format!("{}:", t!("language")))
.size(16.0)
.color(Colors::gray())
);
});
ui.add_space(8.0);
// Draw available list of languages to select.
let locales = rust_i18n::available_locales!();
for (index, locale) in locales.iter().enumerate() {
Self::language_item_ui(locale, ui, index, locales.len());
}
}
/// Draw theme selection content.
fn theme_selection_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("theme")).size(16.0).color(Colors::gray()));
});
let saved_use_dark = AppConfig::dark_theme().unwrap_or(false);
let mut selected_use_dark = saved_use_dark;
ui.add_space(8.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, false, t!("light"));
});
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, true, t!("dark"));
})
});
ui.add_space(8.0);
if saved_use_dark != selected_use_dark {
AppConfig::set_dark_theme(selected_use_dark);
crate::setup_visuals(ui.ctx());
}
ui.add_space(6.0);
}
/// Draw language selection item content.
fn language_item_ui(locale: &str, ui: &mut egui::Ui, index: usize, len: usize) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(50.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(),
StrokeKind::Middle);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select language.
let is_current = if let Some(lang) = AppConfig::locale() {
lang == locale
} else {
rust_i18n::locale() == locale
};
if !is_current {
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
rust_i18n::set_locale(locale);
AppConfig::save_locale(locale);
Modal::close();
});
} else {
ui.add_space(14.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.add_space(14.0);
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
// Draw language name.
ui.add_space(12.0);
let color = if is_current {
Colors::title(false)
} else {
Colors::gray()
};
ui.label(RichText::new(t!("lang_name", locale = locale))
.size(17.0)
.color(color));
ui.add_space(3.0);
});
});
});
}
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod content;
pub use content::*;
+51 -18
View File
@@ -21,6 +21,7 @@ use crate::gui::Colors;
use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, TitlePanel, View};
use crate::gui::views::settings::SettingsContent;
use crate::gui::views::types::{ModalContainer, ModalPosition, LinePosition, TitleContentType, TitleType};
use crate::gui::views::wallets::creation::WalletCreation;
use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletConnectionModal, WalletsModal};
@@ -49,6 +50,9 @@ pub struct WalletsContent {
/// Wallet creation content.
creation_content: Option<WalletCreation>,
/// Settings content.
settings_content: Option<SettingsContent>,
/// Flag to show [`Wallet`] list at dual panel mode.
show_wallets_at_dual_panel: bool,
@@ -70,6 +74,7 @@ impl Default for WalletsContent {
conn_selection_content: None,
wallet_content: None,
creation_content: None,
settings_content: None,
show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(),
modal_ids: vec![
ADD_WALLET_MODAL,
@@ -100,7 +105,7 @@ impl ModalContainer for WalletsContent {
);
});
}
if self.creation_content.is_some() {
if self.creating_wallet() {
self.add_wallet_modal_content = None;
}
},
@@ -147,11 +152,12 @@ impl WalletsContent {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.current_modal_ui(ui, cb);
let showing_settings = self.showing_settings();
let creating_wallet = self.creating_wallet();
let showing_wallet = self.showing_wallet() && !creating_wallet;
let showing_wallet = self.showing_wallet() && !creating_wallet && !showing_settings;
let dual_panel = Self::is_dual_panel_mode(ui);
let content_width = ui.available_width();
let list_hidden = creating_wallet || self.wallets.list().is_empty()
let list_hidden = showing_settings || creating_wallet || self.wallets.list().is_empty()
|| (showing_wallet && self.wallet_content.as_ref().unwrap().qr_scan_content.is_some())
|| (dual_panel && showing_wallet && !self.show_wallets_at_dual_panel)
|| (!dual_panel && showing_wallet);
@@ -253,7 +259,13 @@ impl WalletsContent {
..Default::default()
})
.show_inside(ui, |ui| {
if self.creation_content.is_some() {
if self.showing_settings() {
if let Some(c) = &mut self.settings_content {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
c.ui(ui);
});
}
} else if self.creating_wallet() {
let creation = self.creation_content.as_mut().unwrap();
let pass = creation.pass.clone();
let mut created = false;
@@ -306,6 +318,11 @@ impl WalletsContent {
self.creation_content.is_some()
}
/// Check if application settings are showing.
pub fn showing_settings(&self) -> bool {
self.settings_content.is_some()
}
/// Handle data from deeplink or opened file.
pub fn on_data(&mut self, ui: &mut egui::Ui, data: Option<String>) {
let wallets_size = self.wallets.list().len();
@@ -349,7 +366,11 @@ impl WalletsContent {
/// Handle Back key event returning `false` when event was handled.
pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) -> bool {
if self.creation_content.is_some() {
if self.showing_settings() {
// Close settings.
self.settings_content = None;
return false;
} else if self.creating_wallet() {
// Close wallet creation.
let creation = self.creation_content.as_mut().unwrap();
if creation.on_back() {
@@ -380,6 +401,7 @@ impl WalletsContent {
show_wallet: bool,
cb: &dyn PlatformCallbacks) {
let show_list = self.show_wallets_at_dual_panel;
let showing_settings = self.showing_settings();
let creating_wallet = self.creating_wallet();
let qr_scan = {
let mut scan = false;
@@ -390,7 +412,7 @@ impl WalletsContent {
};
// Setup title.
let title_content = if show_wallet && (!dual_panel
|| (dual_panel && !show_list)) && !creating_wallet {
|| (dual_panel && !show_list)) && !creating_wallet && !showing_settings {
let wallet_content = self.wallet_content.as_ref().unwrap();
let wallet_tab_type = wallet_content.current_tab.get_type();
let title_text = if qr_scan {
@@ -405,14 +427,17 @@ impl WalletsContent {
TitleType::Single(TitleContentType::WithSubTitle(title_text, subtitle_text, false))
}
} else {
let title_text = if qr_scan {
let title_text = if showing_settings {
t!("settings")
} else if qr_scan {
t!("scan_qr")
} else if creating_wallet {
t!("wallets.add")
} else {
t!("wallets.title")
};
let dual_title = !qr_scan && !creating_wallet && show_wallet && dual_panel;
let dual_title = !showing_settings && !qr_scan && !creating_wallet &&
show_wallet && dual_panel;
if dual_title {
let wallet_content = self.wallet_content.as_ref().unwrap();
let wallet_tab_type = wallet_content.current_tab.get_type();
@@ -430,8 +455,14 @@ impl WalletsContent {
};
// Draw title panel.
let mut show_settings = false;
let showing_settings = self.showing_settings();
TitlePanel::new(Id::new("wallets_title_panel")).ui(title_content, |ui| {
if show_wallet && !dual_panel {
if self.showing_settings() {
View::title_button_big(ui, ARROW_LEFT, |_| {
self.settings_content = None;
});
} else if show_wallet && !dual_panel {
View::title_button_big(ui, ARROW_LEFT, |_| {
let wallet_qr_scan = self.wallet_content
.as_ref()
@@ -445,7 +476,7 @@ impl WalletsContent {
}
self.wallet_content = None;
});
} else if self.creation_content.is_some() {
} else if self.creating_wallet() {
let mut close = false;
if let Some(creation) = self.creation_content.as_mut() {
View::title_button_big(ui, ARROW_LEFT, |_| {
@@ -478,16 +509,18 @@ impl WalletsContent {
View::title_button_big(ui, GLOBE, |_| {
Content::toggle_network_panel();
});
};
}
}, |ui| {
View::title_button_big(ui, GEAR, |_| {
// Show settings modal.
Modal::new(Content::SETTINGS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("settings"))
.show();
});
if !showing_settings {
View::title_button_big(ui, GEAR, |_| {
// Show application settings.
show_settings = true;
});
}
}, ui);
if show_settings {
self.settings_content = Some(SettingsContent::default());
}
}
/// Draw list of wallets.