// 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::sync::atomic::{AtomicBool, Ordering}; use lazy_static::lazy_static; use egui::{Align, Context, CornerRadius, CursorIcon, Layout, Modifiers, ResizeDirection, Stroke, UiBuilder, ViewportCommand, StrokeKind}; use egui::epaint::{RectShape}; use crate::AppConfig; use crate::gui::Colors; use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View}; use crate::wallet::ExternalConnection; lazy_static! { /// State to check if platform Back button was pressed. static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false); } /// Implements ui entry point and contains platform-specific callbacks. pub struct App { /// Handles platform-specific functionality. pub platform: Platform, /// Main content. content: Content, /// Keyboard content. keyboard_content: KeyboardContent, /// Last window resize direction. resize_direction: Option, /// Flag to check if it's first draw. first_draw: bool } impl App { pub fn new(platform: Platform) -> Self { Self { platform, content: Content::default(), keyboard_content: KeyboardContent::default(), resize_direction: None, first_draw: true } } /// Called of first content draw. fn on_first_draw(&mut self, ctx: &Context) { // Set platform context. if View::is_desktop() { self.platform.set_context(ctx); } // Check connections availability. ExternalConnection::check(None, ctx); // Setup visuals. crate::setup_visuals(ctx); } /// Draw application content. pub fn ui(&mut self, ctx: &Context) { if self.first_draw { self.on_first_draw(ctx); self.first_draw = false; } // Handle Esc keyboard key event and platform Back button key event. let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed); if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) { // Pass event to content. self.content.on_back(&self.platform); if back_pressed { BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed); } // Request repaint to update previous content. ctx.request_repaint(); } // Handle Close event on desktop. if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) { if !self.content.exit_allowed { ctx.send_viewport_cmd(ViewportCommand::CancelClose); Content::show_exit_modal(); } else { let (w, h) = View::window_size(ctx); AppConfig::save_window_size(w, h); ctx.input(|i| { if let Some(rect) = i.viewport().outer_rect { AppConfig::save_window_pos(rect.left(), rect.top()); } }); } } // Show main content. egui::CentralPanel::default() .frame(egui::Frame { ..Default::default() }) .show(ctx, |ui| { if View::is_desktop() { let is_fullscreen = ui.ctx().input(|i| { i.viewport().fullscreen.unwrap_or(false) }); let os = egui::os::OperatingSystem::from_target_os(); match os { egui::os::OperatingSystem::Mac => { self.window_title_ui(ui, is_fullscreen); ui.add_space(-1.0); Self::title_panel_bg(ui, true); self.content.ui(ui, &self.platform); } egui::os::OperatingSystem::Windows => { Self::title_panel_bg(ui, false); self.content.ui(ui, &self.platform); } _ => { self.custom_frame_ui(ui, is_fullscreen); } } } else { Self::title_panel_bg(ui, false); self.content.ui(ui, &self.platform); } // Show soft keyboard content. self.keyboard_content.window_ui(ctx); // Provide incoming data to wallets. if let Some(data) = crate::consume_incoming_data() { if !data.is_empty() { self.content.wallets.on_data(ui, Some(data)); } } }); // Check if desktop window was focused after requested attention. if self.platform.user_attention_required() && ctx.input(|i| i.viewport().focused.unwrap_or(true)) { self.platform.clear_user_attention(); } } /// Draw custom desktop window frame content. fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) { let content_bg_rect = { let mut r = ui.max_rect(); if !is_fullscreen { r = r.shrink(Content::WINDOW_FRAME_MARGIN); } r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT; r }; let content_bg = RectShape::new(content_bg_rect, CornerRadius::ZERO, Colors::fill_lite(), View::default_stroke(), StrokeKind::Middle); // Draw content background. ui.painter().add(content_bg); let mut content_rect = ui.max_rect(); if !is_fullscreen { content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN); } // Draw window content. ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| { // Draw window title. self.window_title_ui(ui, is_fullscreen); ui.add_space(-1.0); // Draw title panel background. Self::title_panel_bg(ui, true); let content_rect = { let mut rect = ui.max_rect(); rect.min.y += Content::WINDOW_TITLE_HEIGHT; rect }; let mut content_ui = ui.new_child(UiBuilder::new() .max_rect(content_rect) .layout(*ui.layout())); // Draw main content. self.content.ui(&mut content_ui, &self.platform); }); // Setup resize areas. if !is_fullscreen { self.resize_area_ui(ui, ResizeDirection::North); self.resize_area_ui(ui, ResizeDirection::East); self.resize_area_ui(ui, ResizeDirection::South); self.resize_area_ui(ui, ResizeDirection::West); self.resize_area_ui(ui, ResizeDirection::NorthWest); self.resize_area_ui(ui, ResizeDirection::NorthEast); self.resize_area_ui(ui, ResizeDirection::SouthEast); self.resize_area_ui(ui, ResizeDirection::SouthWest); } } /// Draw title panel background. fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) { let title_rect = { let mut rect = ui.max_rect(); if window_title { rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5; } rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT; rect }; let title_bg = RectShape::filled(title_rect, CornerRadius::ZERO, Colors::yellow()); ui.painter().add(title_bg); } /// Draw custom window title content. fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) { let title_rect = { let mut rect = ui.max_rect(); rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT; rect }; let title_bg_rect = { let mut r = title_rect.clone(); r.max.y += TitlePanel::HEIGHT - 1.0; r }; let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac; let window_title_bg = RectShape::new(title_bg_rect, if is_fullscreen || is_mac { CornerRadius::ZERO } else { CornerRadius { nw: 8.0 as u8, ne: 8.0 as u8, sw: 0.0 as u8, se: 0.0 as u8, } }, Colors::yellow_dark(), Stroke::new(1.0, Colors::STROKE), StrokeKind::Middle); // Draw title background. ui.painter().add(window_title_bg); let painter = ui.painter(); let interact_rect = { let mut rect = title_rect.clone(); rect.max.x -= 128.0; rect.min.x += 85.0; if !is_fullscreen { rect.min.y += Content::WINDOW_FRAME_MARGIN; } rect }; let title_resp = ui.interact( interact_rect, egui::Id::new("window_title"), egui::Sense::drag(), ); // Interact with the window title (drag to move window): if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) { ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag); } // Paint the title. let dual_wallets_panel = ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) + View::get_right_inset() + View::get_left_inset(); let wallet_panel_opened = self.content.wallets.showing_wallet(); let show_app_name = if dual_wallets_panel { wallet_panel_opened && !AppConfig::show_wallets_at_dual_panel() } else if Content::is_dual_panel_mode(ui.ctx()) { wallet_panel_opened } else { Content::is_network_panel_open() || wallet_panel_opened }; let creating_wallet = self.content.wallets.creating_wallet(); let title_text = if creating_wallet || show_app_name { format!("Grim {}", crate::VERSION) } else { "ツ".to_string() }; painter.text( title_rect.center(), egui::Align2::CENTER_CENTER, title_text, egui::FontId::proportional(15.0), Colors::title(true), ); ui.scope_builder(UiBuilder::new().max_rect(title_rect), |ui| { ui.with_layout(Layout::right_to_left(Align::Center), |ui| { // Draw button to close window. View::title_button_small(ui, X, |_| { if Modal::opened().is_none() || Modal::opened_closeable() { Content::show_exit_modal(); } }); // Draw fullscreen button. let fullscreen_icon = if is_fullscreen { ARROWS_IN } else { ARROWS_OUT }; View::title_button_small(ui, fullscreen_icon, |ui| { ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen)); }); // Draw button to minimize window. View::title_button_small(ui, CARET_DOWN, |ui| { ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true)); }); // Draw application icon. let layout_size = ui.available_size(); ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { // Draw button to minimize window. let use_dark = AppConfig::dark_theme().unwrap_or(false); let theme_icon = if use_dark { SUN } else { MOON }; View::title_button_small(ui, theme_icon, |ui| { AppConfig::set_dark_theme(!use_dark); crate::setup_visuals(ui.ctx()); }); }); }); }); } /// Setup window resize area. fn resize_area_ui(&mut self, ui: &egui::Ui, direction: ResizeDirection) { let mut rect = ui.max_rect(); // Setup area id, cursor and area rect based on direction. let (id, cursor, rect) = match direction { ResizeDirection::North => ("n", CursorIcon::ResizeNorth, { rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0; rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN; rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0; rect }), ResizeDirection::East => ("e", CursorIcon::ResizeEast, { rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0; rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN; rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0; rect }), ResizeDirection::South => ("s", CursorIcon::ResizeSouth, { rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0; rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN; rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0; rect }), ResizeDirection::West => ("w", CursorIcon::ResizeWest, { rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0; rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN; rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0; rect }), ResizeDirection::NorthWest => ("nw", CursorIcon::ResizeNorthWest, { rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN * 2.0; rect.max.x = rect.max.y + Content::WINDOW_FRAME_MARGIN * 2.0; rect }), ResizeDirection::NorthEast => ("ne", CursorIcon::ResizeNorthEast, { rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0; rect.max.y = Content::WINDOW_FRAME_MARGIN * 2.0; rect }), ResizeDirection::SouthEast => ("se", CursorIcon::ResizeSouthEast, { rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0; rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0; rect }), ResizeDirection::SouthWest => ("sw", CursorIcon::ResizeSouthWest, { rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0; rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN * 2.0; rect }), }; // Setup resize area. let id = egui::Id::new("window_resize").with(id); let sense = egui::Sense::drag(); let area_resp = ui.interact(rect, id, sense).on_hover_cursor(cursor); if area_resp.dragged() { if self.resize_direction.is_none() { self.resize_direction = Some(direction.clone()); ui.ctx().send_viewport_cmd(ViewportCommand::BeginResize(direction)); } } if area_resp.drag_stopped() { self.resize_direction = None; } } } /// To draw with egui`s eframe (for wgpu, glow backends and wasm target). impl eframe::App for App { fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { self.ui(ctx); } fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { let os = egui::os::OperatingSystem::from_target_os(); let is_win = os == egui::os::OperatingSystem::Windows; let is_mac = os == egui::os::OperatingSystem::Mac; if !View::is_desktop() || is_win || is_mac { return Colors::fill_lite().to_normalized_gamma_f32(); } Colors::TRANSPARENT.to_normalized_gamma_f32() } } #[allow(dead_code)] #[cfg(target_os = "android")] #[allow(non_snake_case)] #[no_mangle] /// Handle Back key code event from Android. pub extern "C" fn Java_mw_gri_android_MainActivity_onBack( _env: jni::JNIEnv, _class: jni::objects::JObject, _activity: jni::objects::JObject, ) { BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed); }