ui: wallet creation button, exit modal buttons order, back button behaviour at wallet, remember window position and size on exit, desktop icon

This commit is contained in:
ardocrat
2024-04-17 01:30:28 +03:00
parent 648a18b5b2
commit c749bce26a
18 changed files with 623 additions and 130 deletions
+48 -2
View File
@@ -18,11 +18,11 @@ use crate::node::NodeConfig;
use crate::Settings;
use crate::wallet::ConnectionsConfig;
/// Application configuration.
/// Application configuration, stored at toml file.
#[derive(Serialize, Deserialize)]
pub struct AppConfig {
/// Run node server on startup.
pub auto_start_node: bool,
pub(crate) auto_start_node: bool,
/// Chain type for node and wallets.
pub(crate) chain_type: ChainTypes,
@@ -30,8 +30,19 @@ pub struct AppConfig {
show_wallets_at_dual_panel: bool,
/// Flag to show all connections at network panel or integrated node info.
show_connections_network_panel: bool,
/// Width of the desktop window.
width: f32,
/// Height of the desktop window.
height: f32,
/// Position of the desktop window.
x: Option<f32>, y: Option<f32>
}
pub const DEFAULT_WIDTH: f32 = 1200.0;
pub const DEFAULT_HEIGHT: f32 = 720.0;
impl Default for AppConfig {
fn default() -> Self {
Self {
@@ -39,6 +50,10 @@ impl Default for AppConfig {
chain_type: ChainTypes::default(),
show_wallets_at_dual_panel: false,
show_connections_network_panel: false,
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
x: None,
y: None,
}
}
}
@@ -124,4 +139,35 @@ impl AppConfig {
w_app_config.show_connections_network_panel = !show;
w_app_config.save();
}
/// Save desktop window width and height.
pub fn save_window_size(w: f32, h: f32) {
let mut w_app_config = Settings::app_config_to_update();
w_app_config.width = w;
w_app_config.height = h;
w_app_config.save();
}
/// Get desktop window width and height.
pub fn window_size() -> (f32, f32) {
let r_config = Settings::app_config_to_read();
(r_config.width, r_config.height)
}
/// Save desktop window position.
pub fn save_window_pos(x: f32, y: f32) {
let mut w_app_config = Settings::app_config_to_update();
w_app_config.x = Some(x);
w_app_config.y = Some(y);
w_app_config.save();
}
/// Get desktop window position.
pub fn window_pos() -> Option<(f32, f32)> {
let r_config = Settings::app_config_to_read();
if r_config.x.is_some() && r_config.y.is_some() {
return Some((r_config.x.unwrap(), r_config.y.unwrap()))
}
None
}
}
+10
View File
@@ -17,6 +17,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use egui::{Context, Modifiers};
use lazy_static::lazy_static;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::Root;
@@ -59,6 +60,15 @@ impl<Platform: PlatformCallbacks> eframe::App for PlatformApp<Platform> {
if !self.root.exit_allowed {
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
Root::show_exit_modal();
} else {
ctx.input(|i| {
if let Some(rect) = i.viewport().inner_rect {
AppConfig::save_window_size(rect.width(), rect.height());
}
if let Some(rect) = i.viewport().outer_rect {
AppConfig::save_window_pos(rect.left(), rect.top());
}
});
}
}
+6 -5
View File
@@ -22,6 +22,7 @@ use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, NetworkContent, View, WalletsContent};
use crate::gui::views::types::ModalContainer;
use crate::node::Node;
use crate::AppConfig;
lazy_static! {
/// Global state to check if [`NetworkContent`] panel is open.
@@ -200,6 +201,11 @@ impl Root {
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::WHITE, || {
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button_ui(ui, t!("modal_exit.exit"), Colors::WHITE, |ui| {
if !Node::is_running() {
self.exit_allowed = true;
@@ -212,11 +218,6 @@ impl Root {
}
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::WHITE, || {
modal.close();
});
});
});
ui.add_space(6.0);
});
+2 -42
View File
@@ -18,7 +18,7 @@ use std::sync::{Arc, RwLock};
use lazy_static::lazy_static;
use egui::{Align, Button, CursorIcon, Layout, PointerState, Rect, Response, RichText, Sense, Spinner, TextBuffer, TextStyle, Widget};
use egui::epaint::{CircleShape, Color32, FontId, RectShape, Rounding, Stroke};
use egui::epaint::{Color32, FontId, RectShape, Rounding, Stroke};
use egui::epaint::text::TextWrapping;
use egui::os::OperatingSystem;
use egui::text::{LayoutJob, TextFormat};
@@ -271,46 +271,6 @@ impl View {
});
}
/// Draw circle [`Button`] with icon.
pub fn circle_button(ui: &mut egui::Ui, icon: &'static str, action: impl FnOnce()) {
ui.scope(|ui| {
// Setup colors.
ui.visuals_mut().widgets.inactive.bg_fill = Colors::GOLD;
ui.visuals_mut().widgets.hovered.bg_fill = Colors::GOLD;
ui.visuals_mut().widgets.active.bg_fill = Colors::YELLOW;
// Setup radius.
let mut r = 44.0 * 0.5;
let size = egui::Vec2::splat(2.0 * r + 5.0);
let (rect, mut br) = ui.allocate_at_least(size, Sense::click_and_drag());
br = br.on_hover_cursor(CursorIcon::PointingHand);
let mut icon_color = Colors::TEXT;
// Increase radius and change icon size and color on-hover.
if br.hovered() {
r = r * 1.04;
icon_color = Colors::TITLE;
}
let visuals = ui.style().interact(&br);
ui.painter().add(CircleShape {
center: rect.center(),
radius: r,
fill: visuals.bg_fill,
stroke: Self::DEFAULT_STROKE
});
ui.allocate_ui_at_rect(rect, |ui| {
ui.centered_and_justified(|ui| {
ui.label(RichText::new(icon).color(icon_color).size(25.0));
});
});
if Self::touched(ui, br) {
(action)();
}
});
}
/// Default height of [`egui::TextEdit`] view.
const TEXT_EDIT_HEIGHT: f32 = 37.0;
@@ -524,7 +484,7 @@ impl View {
pub fn center_content(ui: &mut egui::Ui, height: f32, content: impl FnOnce(&mut egui::Ui)) {
ui.vertical_centered(|ui| {
let mut rect = ui.available_rect_before_wrap();
let side_margin = 24.0;
let side_margin = 28.0;
rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0);
rect.max -= egui::emath::vec2(side_margin, 0.0);
ui.allocate_ui_at_rect(rect, |ui| {
+46 -53
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Align2, Id, Layout, Margin, RichText, Rounding, ScrollArea, Widget};
use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea, Widget};
use crate::AppConfig;
use crate::gui::Colors;
@@ -159,13 +159,43 @@ impl WalletsContent {
}
});
// Flag to check if wallet list is hidden on the screen.
let list_hidden = content_width == 0.0
|| (dual_panel && show_wallet && !self.show_wallets_at_dual_panel)
|| (!dual_panel && show_wallet);
// Setup flag to show wallets bottom panel if wallet is not showing
// at non-dual panel mode and network is no open or showing at dual panel mode.
let show_bottom_panel =
(!show_wallet && !dual_panel && !Root::is_network_panel_open()) ||
(dual_panel && show_wallet);
// Show wallets bottom panel.
egui::TopBottomPanel::bottom("wallets_bottom_panel")
.frame(egui::Frame {
fill: Colors::FILL,
stroke: View::DEFAULT_STROKE,
inner_margin: Margin {
left: View::get_left_inset() + 4.0,
right: View::far_right_inset_margin(ui) + 4.0,
top: 4.0,
bottom: View::get_bottom_inset() + 4.0,
},
..Default::default()
})
.show_animated_inside(ui, !list_hidden && show_bottom_panel, |ui| {
// Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
ui.vertical_centered(|ui| {
View::tab_button(ui, PLUS, false, || {
self.creation_content.show_name_pass_modal(cb);
});
});
});
// Show non-empty list if wallet is not creating.
if !empty_list && !create_wallet {
// Flag to check if wallet list is hidden on the screen.
let list_hidden = content_width == 0.0
|| (dual_panel && show_wallet && !self.show_wallets_at_dual_panel)
|| (!dual_panel && show_wallet);
// Show wallet list panel.
egui::CentralPanel::default()
.frame(egui::Frame {
@@ -192,23 +222,8 @@ impl WalletsContent {
if list_hidden {
return;
}
// Setup flag to show wallet creation button if wallet is not showing
// at non-dual panel mode or showing at dual panel mode.
let show_creation_button
= (!show_wallet && !dual_panel) || (dual_panel && show_wallet);
// Show list of wallets.
let scroll =
self.wallet_list_ui(ui, dual_panel, show_creation_button, cb);
if show_creation_button {
// Setup right margin for button.
let mut right_margin = if dual_panel { wallet_panel_width } else { 0.0 };
if scroll { right_margin += 6.0 }
// Show wallet creation button.
self.create_wallet_btn_ui(ui, right_margin, cb);
}
self.wallet_list_ui(ui, dual_panel, cb);
});
}
}
@@ -309,13 +324,11 @@ impl WalletsContent {
}
}
/// Draw list of wallets. Returns `true` if scroller is showing.
/// Draw list of wallets.
fn wallet_list_ui(&mut self,
ui: &mut egui::Ui,
dual_panel: bool,
show_creation_btn: bool,
cb: &dyn PlatformCallbacks) -> bool {
let mut scroller_showing = false;
cb: &dyn PlatformCallbacks) {
ui.scope(|ui| {
// Setup scroll bar color.
ui.style_mut().visuals.widgets.inactive.bg_fill = Colors::ITEM_HOVER;
@@ -344,22 +357,14 @@ impl WalletsContent {
self.wallets.select(Some(wallet.get_config().id));
self.show_open_wallet_modal(cb);
}
// Draw wallet list item.
self.wallet_item_ui(ui, wallet, cb);
ui.add_space(5.0);
}
// Add space for wallet creation button.
if show_creation_btn {
ui.add_space(52.0);
}
});
});
});
// Scroller is showing if content size is larger than content on the screen.
scroller_showing = scroll.content_size.y > scroll.inner_rect.size().y;
});
scroller_showing
}
/// Draw wallet list item.
@@ -485,24 +490,6 @@ impl WalletsContent {
});
}
/// Draw floating button to show wallet creation [`Modal`].
fn create_wallet_btn_ui(&mut self,
ui: &mut egui::Ui,
right_margin: f32,
cb: &dyn PlatformCallbacks) {
egui::Window::new("create_wallet_button")
.title_bar(false)
.resizable(false)
.collapsible(false)
.anchor(Align2::RIGHT_BOTTOM, egui::Vec2::new(-6.0 - right_margin, -6.0))
.frame(egui::Frame::default())
.show(ui.ctx(), |ui| {
View::circle_button(ui, PLUS, || {
self.creation_content.show_name_pass_modal(cb);
});
});
}
/// Show [`Modal`] to open selected wallet.
pub fn show_open_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) {
// Reset modal values.
@@ -599,8 +586,14 @@ impl WalletsContent {
let can_go_back = self.creation_content.can_go_back();
if can_go_back {
self.creation_content.back();
return false
} else {
if self.wallets.is_selected_open() {
self.wallets.select(None);
return false
}
}
!can_go_back
true
}
}
+6 -7
View File
@@ -75,8 +75,8 @@ impl WalletCreation {
stroke: View::DEFAULT_STROKE,
fill: Colors::FILL_DARK,
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
left: View::far_left_inset_margin(ui) + 6.0,
right: View::get_right_inset() + 6.0,
top: 4.0,
bottom: View::get_bottom_inset() + 4.0,
},
@@ -180,7 +180,7 @@ impl WalletCreation {
self.copy_or_paste_button_ui(ui, cb);
} else {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
// Show copy or paste button for mnemonic phrase step.
@@ -239,7 +239,6 @@ impl WalletCreation {
(text, Colors::WHITE)
};
// Show next step button.
View::button(ui, next_text.to_uppercase(), color, || {
self.step = if let Some(step) = &self.step {
@@ -297,10 +296,10 @@ impl WalletCreation {
match &self.step {
None => {
// Show wallet creation message if step is empty.
View::center_content(ui, 415.0 + View::get_bottom_inset(), |ui| {
View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| {
ui.add(
egui::Image::new(egui::include_image!("../../../../../img/logo.png"))
.fit_to_exact_size(vec2(200.0, 200.0))
.fit_to_exact_size(vec2(180.0, 180.0))
);
ui.add_space(-15.0);
ui.label(RichText::new("GRIM")
@@ -362,7 +361,7 @@ impl WalletCreation {
pub fn show_name_pass_modal(&mut self, cb: &dyn PlatformCallbacks) {
// Reset modal values.
self.modal_just_opened = true;
self.name_edit = String::from("");
self.name_edit = t!("wallets.default_wallet");
self.pass_edit = String::from("");
// Show modal.
Modal::new(Self::NAME_PASS_MODAL)
+1 -1
View File
@@ -210,7 +210,7 @@ impl WalletContent {
ui.vertical(|ui| {
ui.add_space(3.0);
// Show spendable amount.
let amount = amount_to_hr_string(data.info.amount_currently_spendable, false);
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.label(RichText::new(amount_text).size(18.0).color(Colors::BLACK));
ui.add_space(-2.0);
+3 -3
View File
@@ -79,7 +79,7 @@ impl WalletInfo {
// Show non-zero awaiting confirmation amount.
if amount_awaiting_conf != 0 {
let awaiting_conf = amount_to_hr_string(amount_awaiting_conf, false);
let awaiting_conf = amount_to_hr_string(amount_awaiting_conf, true);
let rounding = if amount_awaiting_fin != 0 || amount_locked != 0 {
[false, false, false, false]
} else {
@@ -93,7 +93,7 @@ impl WalletInfo {
// Show non-zero awaiting finalization amount.
if amount_awaiting_fin != 0 {
let awaiting_conf = amount_to_hr_string(amount_awaiting_fin, false);
let awaiting_conf = amount_to_hr_string(amount_awaiting_fin, true);
let rounding = if amount_locked != 0 {
[false, false, false, false]
} else {
@@ -107,7 +107,7 @@ impl WalletInfo {
// Show non-zero locked amount.
if amount_locked != 0 {
let awaiting_conf = amount_to_hr_string(amount_locked, false);
let awaiting_conf = amount_to_hr_string(amount_locked, true);
View::rounded_box(ui,
format!("{} ツ", awaiting_conf),
t!("wallets.locked_amount"),
+3 -1
View File
@@ -122,8 +122,10 @@ pub fn setup_visuals(ctx: &Context) {
let mut style = (*ctx.style()).clone();
// Setup spacing for buttons.
style.spacing.button_padding = egui::vec2(12.0, 8.0);
// Make scroll-bar thinner.
// Make scroll-bar thinner and lighter.
style.spacing.scroll.bar_width = 4.0;
style.spacing.scroll.bar_outer_margin = -2.0;
style.spacing.scroll.foreground_color = false;
// Disable spacing between items.
style.spacing.item_spacing = egui::vec2(0.0, 0.0);
// Setup radio button/checkbox size and spacing.
+26 -1
View File
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use egui::{IconData, pos2};
use grim::AppConfig;
pub fn main() {
#[allow(dead_code)]
#[cfg(not(target_os = "android"))]
@@ -31,8 +35,29 @@ fn real_main() {
use grim::gui::PlatformApp;
let platform = Desktop::default();
// Desktop window size.
let (width, height) = AppConfig::window_size();
// Setup an icon.
let icon = image::open("img/icon.png").expect("Failed to open icon path").to_rgba8();
let (icon_width, icon_height) = icon.dimensions();
let mut viewport = egui::ViewportBuilder::default()
.with_inner_size([width, height])
.with_icon(Arc::new(IconData {
rgba: icon.into_raw(),
width: icon_width,
height: icon_height,
}));
// Desktop window position.
if let Some((x, y)) = AppConfig::window_pos() {
viewport = viewport.with_position(pos2(x, y));
}
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([1200.0, 720.0]),
viewport,
..Default::default()
};
grim::start(options, grim::app_creator(PlatformApp::new(platform)));
+2 -2
View File
@@ -62,7 +62,7 @@ pub struct Wallet {
reopen: Arc<AtomicBool>,
/// Flag to check if wallet is open.
is_open: Arc<AtomicBool>,
/// Flag to check if wallet is loading.
/// Flag to check if wallet is closing.
closing: Arc<AtomicBool>,
/// Flag to check if wallet was deleted to remove it from the list.
deleted: Arc<AtomicBool>,
@@ -306,7 +306,7 @@ impl Wallet {
}
}
/// Check if wallet was open.
/// Check if wallet is open.
pub fn is_open(&self) -> bool {
self.is_open.load(Ordering::Relaxed)
}