diff --git a/app/src/main/java/mw/gri/android/MainActivity.java b/app/src/main/java/mw/gri/android/MainActivity.java index 73bea32..5b8ecb6 100644 --- a/app/src/main/java/mw/gri/android/MainActivity.java +++ b/app/src/main/java/mw/gri/android/MainActivity.java @@ -1,15 +1,15 @@ package mw.gri.android; import android.content.*; -import android.content.pm.PackageManager; -import android.hardware.SensorManager; import android.os.Bundle; import android.os.Process; import android.system.ErrnoException; import android.system.Os; -import android.util.Log; import android.view.KeyEvent; -import android.view.OrientationEventListener; +import android.view.View; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import com.google.androidgamesdk.GameActivity; import java.util.concurrent.atomic.AtomicBoolean; @@ -45,46 +45,40 @@ public class MainActivity extends GameActivity { } super.onCreate(null); - // Callback to update display cutouts at native code. - OrientationEventListener orientationEventListener = new OrientationEventListener(this, - SensorManager.SENSOR_DELAY_NORMAL) { - @Override - public void onOrientationChanged(int orientation) { - onDisplayCutoutsChanged(Utils.getDisplayCutouts(MainActivity.this)); - } - }; - if (orientationEventListener.canDetectOrientation()) { - orientationEventListener.enable(); - } - // Register receiver to finish activity from the BackgroundService. registerReceiver(mBroadcastReceiver, new IntentFilter(FINISH_ACTIVITY_ACTION)); // Start notification service. BackgroundService.start(this); + + // Listener for display cutouts to pass values into native code. + View content = getWindow().getDecorView().findViewById(android.R.id.content); + ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + int[] cutouts = new int[]{0, 0, 0, 0}; + cutouts[0] = Utils.pxToDp(systemBars.top, this); + cutouts[1] = Utils.pxToDp(systemBars.right, this); + cutouts[2] = Utils.pxToDp(systemBars.bottom, this); + cutouts[3] = Utils.pxToDp(systemBars.left, this); + onDisplayCutouts(cutouts); + return insets; + }); } // Implemented into native code to handle display cutouts change. - native void onDisplayCutoutsChanged(int[] cutouts); - - @Override - protected void onResume() { - super.onResume(); - // Update display cutouts. - onDisplayCutoutsChanged(Utils.getDisplayCutouts(this)); - } + native void onDisplayCutouts(int[] cutouts); @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { - onBackButtonPress(); + onBack(); return true; } return super.onKeyDown(keyCode, event); } - // Implemented into native code to handle back button press. - public native void onBackButtonPress(); + // Implemented into native code to handle key code BACK event. + public native void onBack(); private boolean mManualExit; private final AtomicBoolean mActivityDestroyed = new AtomicBoolean(false); diff --git a/app/src/main/java/mw/gri/android/Utils.java b/app/src/main/java/mw/gri/android/Utils.java index be417aa..546b5ab 100644 --- a/app/src/main/java/mw/gri/android/Utils.java +++ b/app/src/main/java/mw/gri/android/Utils.java @@ -1,52 +1,10 @@ package mw.gri.android; -import android.app.Activity; import android.content.Context; -import android.os.Build; -import android.view.DisplayCutout; -import android.view.WindowInsets; -import android.view.WindowManager; -import androidx.core.graphics.Insets; -import androidx.core.view.DisplayCutoutCompat; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; public class Utils { - - public static int[] getDisplayCutouts(Activity context) { - int[] cutouts = new int[]{0, 0, 0, 0}; - if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { - WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - WindowInsets insets = windowManager.getCurrentWindowMetrics().getWindowInsets(); - android.graphics.Insets barsInsets = insets.getInsets(WindowInsets.Type.systemBars()); - android.graphics.Insets cutoutsInsets = insets.getInsets(WindowInsets.Type.displayCutout()); - cutouts[0] = pxToDp(Integer.max(barsInsets.top, cutoutsInsets.top), context); - cutouts[1] = pxToDp(Integer.max(barsInsets.right, cutoutsInsets.right), context); - cutouts[2] = pxToDp(Integer.max(barsInsets.bottom, cutoutsInsets.bottom), context); - cutouts[3] = pxToDp(Integer.max(barsInsets.left, cutoutsInsets.left), context); - } else if (Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.Q) { - DisplayCutout displayCutout = context.getWindowManager().getDefaultDisplay().getCutout(); - cutouts[0] = displayCutout.getSafeInsetBottom(); - cutouts[1] = displayCutout.getSafeInsetRight(); - cutouts[2] = displayCutout.getSafeInsetBottom(); - cutouts[3] = displayCutout.getSafeInsetLeft(); - } else { - WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(context.getWindow().getDecorView()); - if (insets != null) { - DisplayCutoutCompat displayCutout = insets.getDisplayCutout(); - Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - if (displayCutout != null) { - cutouts[0] = pxToDp(Integer.max(displayCutout.getSafeInsetTop(), systemBars.top), context); - cutouts[1] = pxToDp(Integer.max(displayCutout.getSafeInsetRight(), systemBars.right), context); - cutouts[2] = pxToDp(Integer.max(displayCutout.getSafeInsetBottom(), systemBars.bottom), context); - cutouts[3] = pxToDp(Integer.max(displayCutout.getSafeInsetLeft(), systemBars.left), context); - } - } - } - return cutouts; - } - - private static int pxToDp(int px, Context context) { + // Convert Pixels to DensityPixels + public static int pxToDp(int px, Context context) { return (int) (px / context.getResources().getDisplayMetrics().density); } } diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 90df0e7..90b5ad8 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -2,6 +2,7 @@ \ No newline at end of file diff --git a/src/gui/app.rs b/src/gui/app.rs index 61906c7..26ec926 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -12,237 +12,147 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Context, RichText, Stroke}; -use egui::os::OperatingSystem; +use std::sync::atomic::{AtomicI32, Ordering}; + +use egui::Context; +use lazy_static::lazy_static; + use crate::gui::Colors; - use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, ModalContainer, Root, View}; -use crate::node::Node; +use crate::gui::views::Root; -/// To be implemented at platform-specific module. +/// Implements ui entry point and contains platform-specific callbacks. pub struct PlatformApp { - pub(crate) app: App, + /// Platform specific callbacks handler. pub(crate) platform: Platform, -} - -/// Contains main ui, handles exit and visual style setup. -pub struct App { /// Main ui content. - root: Root, - /// Check if app exit is allowed on close event of [`eframe::App`] platform implementation. - pub(crate) exit_allowed: bool, - /// Flag to show exit progress at modal. - show_exit_progress: bool, - /// List of allowed modal ids for this [`ModalContainer`]. - allowed_modal_ids: Vec<&'static str> + root: Root } -impl Default for App { - fn default() -> Self { - let os = OperatingSystem::from_target_os(); - // Exit from eframe only for non-mobile platforms. - let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS; - Self { - root: Root::default(), - exit_allowed, - show_exit_progress: false, - allowed_modal_ids: vec![ - Self::EXIT_MODAL - ] - } +impl PlatformApp { + pub fn new(platform: Platform) -> Self { + Self { platform, root: Root::default() } } } -impl ModalContainer for App { - fn modal_ids(&self) -> &Vec<&'static str> { - &self.allowed_modal_ids - } -} +impl eframe::App for PlatformApp { + fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) { + // Show panels to support display cutouts (insets). + padding_panels(ctx); -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 main content. egui::CentralPanel::default() .frame(egui::Frame { fill: Colors::FILL, ..Default::default() }) .show(ctx, |ui| { - // 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); + self.root.ui(ui, frame, &self.platform); }); } - /// Draw exit confirmation modal content. - fn exit_modal_content(&mut self, - ui: &mut egui::Ui, - frame: &mut eframe::Frame, - cb: &dyn PlatformCallbacks) { - Modal::ui(ui, |ui, modal| { - if self.show_exit_progress { - if !Node::is_running() { - self.exit(frame, cb); - modal.close(); - } - ui.add_space(16.0); - ui.vertical_centered(|ui| { - View::small_loading_spinner(ui); - ui.add_space(12.0); - ui.label(RichText::new(t!("sync_status.shutdown")) - .size(18.0) - .color(Colors::TEXT)); - }); - ui.add_space(10.0); - } else { - ui.add_space(8.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("modal_exit.description")) - .size(18.0) - .color(Colors::TEXT)); - }); - ui.add_space(10.0); - - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal_exit.exit"), Colors::WHITE, || { - if !Node::is_running() { - self.exit(frame, cb); - modal.close(); - } else { - Node::stop(true); - modal.disable_closing(); - self.show_exit_progress = true; - } - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::WHITE, || { - modal.close(); - }); - }); - }); - ui.add_space(6.0); - }); - } - }); - } - - /// Platform-specific exit from the application. - fn exit(&mut self, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - match OperatingSystem::from_target_os() { - OperatingSystem::Android => { - cb.exit(); - } - OperatingSystem::IOS => { - //TODO: exit on iOS. - } - OperatingSystem::Nix | OperatingSystem::Mac | OperatingSystem::Windows => { - self.exit_allowed = true; - frame.close(); - } - OperatingSystem::Unknown => {} - } - } - - /// Setup application styles. - 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. - style.spacing.scroll_bar_width = 4.0; - // Disable spacing between items. - style.spacing.item_spacing = egui::vec2(0.0, 0.0); - // Setup radio button/checkbox size and spacing. - style.spacing.icon_width = 24.0; - style.spacing.icon_width_inner = 14.0; - style.spacing.icon_spacing = 10.0; - // Setup style - ctx.set_style(style); - - let mut visuals = egui::Visuals::light(); - // Setup selection color. - visuals.selection.stroke = Stroke { width: 1.0, color: Colors::TEXT }; - visuals.selection.bg_fill = Colors::GOLD; - // Disable stroke around panels by default - visuals.widgets.noninteractive.bg_stroke = Stroke::NONE; - // Setup visuals - ctx.set_visuals(visuals); - } - - /// Setup application fonts. - pub fn setup_fonts(ctx: &Context) { - use egui::FontFamily::Proportional; - - let mut fonts = egui::FontDefinitions::default(); - - fonts.font_data.insert( - "phosphor".to_owned(), - egui::FontData::from_static(include_bytes!( - "../../fonts/phosphor.ttf" - )).tweak(egui::FontTweak { - scale: 1.0, - y_offset_factor: -0.30, - y_offset: 0.0, - baseline_offset_factor: 0.30, - }), - ); - fonts - .families - .entry(Proportional) - .or_default() - .insert(0, "phosphor".to_owned()); - - fonts.font_data.insert( - "noto".to_owned(), - egui::FontData::from_static(include_bytes!( - "../../fonts/noto_sc_reg.otf" - )).tweak(egui::FontTweak { - scale: 1.0, - y_offset_factor: -0.25, - y_offset: 0.0, - baseline_offset_factor: 0.17, - }), - ); - fonts - .families - .entry(Proportional) - .or_default() - .insert(0, "noto".to_owned()); - - ctx.set_fonts(fonts); - - use egui::FontId; - use egui::TextStyle::*; - - let mut style = (*ctx.style()).clone(); - style.text_styles = [ - (Heading, FontId::new(20.0, Proportional)), - (Body, FontId::new(16.0, Proportional)), - (Button, FontId::new(18.0, Proportional)), - (Small, FontId::new(12.0, Proportional)), - (Monospace, FontId::new(16.0, Proportional)), - ].into(); - - ctx.set_style(style); - } - - /// 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); + fn on_close_event(&mut self) -> bool { + Root::show_exit_modal(); + self.root.exit_allowed } } + +/// Draw panels to support display cutouts (insets). +fn padding_panels(ctx: &Context) { + egui::TopBottomPanel::top("top_padding_panel") + .frame(egui::Frame { + inner_margin: egui::style::Margin::same(0.0), + fill: Colors::YELLOW, + ..Default::default() + }) + .show_separator_line(false) + .resizable(false) + .exact_height(get_top_display_cutout()) + .show(ctx, |_ui| {}); + + egui::TopBottomPanel::bottom("bottom_padding_panel") + .frame(egui::Frame { + inner_margin: egui::style::Margin::same(0.0), + fill: Colors::BLACK, + ..Default::default() + }) + .show_separator_line(false) + .resizable(false) + .exact_height(get_bottom_display_cutout()) + .show(ctx, |_ui| {}); + + egui::SidePanel::right("right_padding_panel") + .frame(egui::Frame { + inner_margin: egui::style::Margin::same(0.0), + fill: Colors::YELLOW, + ..Default::default() + }) + .show_separator_line(false) + .resizable(false) + .max_width(get_right_display_cutout()) + .show(ctx, |_ui| {}); + + egui::SidePanel::left("left_padding_panel") + .frame(egui::Frame { + inner_margin: egui::style::Margin::same(0.0), + fill: Colors::YELLOW, + ..Default::default() + }) + .show_separator_line(false) + .resizable(false) + .max_width(get_left_display_cutout()) + .show(ctx, |_ui| {}); +} + +/// Get top display cutout (inset) size. +pub fn get_top_display_cutout() -> f32 { + TOP_DISPLAY_CUTOUT.load(Ordering::Relaxed) as f32 +} + +/// Get right display cutout (inset) size. +pub fn get_right_display_cutout() -> f32 { + RIGHT_DISPLAY_CUTOUT.load(Ordering::Relaxed) as f32 +} + +/// Get bottom display cutout (inset) size. +pub fn get_bottom_display_cutout() -> f32 { + BOTTOM_DISPLAY_CUTOUT.load(Ordering::Relaxed) as f32 +} + +/// Get left display cutout (inset) size. +pub fn get_left_display_cutout() -> f32 { + LEFT_DISPLAY_CUTOUT.load(Ordering::Relaxed) as f32 +} + +/// Fields to handle platform-specific display cutouts (insets). +lazy_static! { + static ref TOP_DISPLAY_CUTOUT: AtomicI32 = AtomicI32::new(0); + static ref RIGHT_DISPLAY_CUTOUT: AtomicI32 = AtomicI32::new(0); + static ref BOTTOM_DISPLAY_CUTOUT: AtomicI32 = AtomicI32::new(0); + static ref LEFT_DISPLAY_CUTOUT: AtomicI32 = AtomicI32::new(0); +} + +#[allow(dead_code)] +#[cfg(target_os = "android")] +#[allow(non_snake_case)] +#[no_mangle] +/// Callback from Java code to update display cutouts (insets). +pub extern "C" fn Java_mw_gri_android_MainActivity_onDisplayCutouts( + _env: jni::JNIEnv, + _class: jni::objects::JObject, + cutouts: jni::sys::jarray +) { + use jni::objects::{JObject, JPrimitiveArray}; + + let mut array: [i32; 4] = [0; 4]; + unsafe { + let j_obj = JObject::from_raw(cutouts); + let j_arr = JPrimitiveArray::from(j_obj); + _env.get_int_array_region(j_arr, 0, array.as_mut()).unwrap(); + } + TOP_DISPLAY_CUTOUT.store(array[0], Ordering::Relaxed); + RIGHT_DISPLAY_CUTOUT.store(array[1], Ordering::Relaxed); + BOTTOM_DISPLAY_CUTOUT.store(array[2], Ordering::Relaxed); + LEFT_DISPLAY_CUTOUT.store(array[3], Ordering::Relaxed); +} + diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 40c7952..0fc208c 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -14,7 +14,7 @@ mod app; -pub use app::{App, PlatformApp}; +pub use app::PlatformApp; mod colors; pub use colors::Colors; diff --git a/src/gui/platform/android/mod.rs b/src/gui/platform/android/mod.rs index 383517b..da1b479 100644 --- a/src/gui/platform/android/mod.rs +++ b/src/gui/platform/android/mod.rs @@ -12,11 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::atomic::{AtomicI32, Ordering}; -use lazy_static::lazy_static; use winit::platform::android::activity::AndroidApp; - -use crate::gui::{App, PlatformApp}; use crate::gui::platform::PlatformCallbacks; #[derive(Clone)] @@ -89,95 +85,4 @@ impl PlatformCallbacks for Android { }; env.call_method(activity, "onExit", "()V", &[]).unwrap(); } -} - -impl PlatformApp { - pub fn new(platform: Android) -> Self { - Self { - app: App::default(), - platform, - } - } -} - -impl eframe::App for PlatformApp { - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - padding_panels(ctx); - self.app.ui(ctx, frame, &self.platform); - } -} - -fn padding_panels(ctx: &egui::Context) { - egui::TopBottomPanel::top("top_padding_panel") - .frame(egui::Frame { - inner_margin: egui::style::Margin::same(0.0), - fill: ctx.style().visuals.panel_fill, - ..Default::default() - }) - .show_separator_line(false) - .resizable(false) - .exact_height(DISPLAY_CUTOUT_TOP.load(Ordering::Relaxed) as f32) - .show(ctx, |_ui| {}); - - egui::TopBottomPanel::bottom("bottom_padding_panel") - .frame(egui::Frame { - inner_margin: egui::style::Margin::same(0.0), - fill: ctx.style().visuals.panel_fill, - ..Default::default() - }) - .show_separator_line(false) - .resizable(false) - .exact_height(DISPLAY_CUTOUT_BOTTOM.load(Ordering::Relaxed) as f32) - .show(ctx, |_ui| {}); - - egui::SidePanel::right("right_padding_panel") - .frame(egui::Frame { - inner_margin: egui::style::Margin::same(0.0), - fill: ctx.style().visuals.panel_fill, - ..Default::default() - }) - .show_separator_line(false) - .resizable(false) - .max_width(DISPLAY_CUTOUT_RIGHT.load(Ordering::Relaxed) as f32) - .show(ctx, |_ui| {}); - - egui::SidePanel::left("left_padding_panel") - .frame(egui::Frame { - inner_margin: egui::style::Margin::same(0.0), - fill: ctx.style().visuals.panel_fill, - ..Default::default() - }) - .show_separator_line(false) - .resizable(false) - .max_width(DISPLAY_CUTOUT_LEFT.load(Ordering::Relaxed) as f32) - .show(ctx, |_ui| {}); -} - -lazy_static! { - static ref DISPLAY_CUTOUT_TOP: AtomicI32 = AtomicI32::new(0); - static ref DISPLAY_CUTOUT_RIGHT: AtomicI32 = AtomicI32::new(0); - static ref DISPLAY_CUTOUT_BOTTOM: AtomicI32 = AtomicI32::new(0); - static ref DISPLAY_CUTOUT_LEFT: AtomicI32 = AtomicI32::new(0); -} - -#[allow(non_snake_case)] -#[no_mangle] -/// Callback from Java code to update display cutouts. -pub extern "C" fn Java_mw_gri_android_MainActivity_onDisplayCutoutsChanged( - _env: jni::JNIEnv, - _class: jni::objects::JObject, - cutouts: jni::sys::jarray -) { - use jni::objects::{JObject, JPrimitiveArray}; - - let mut array: [i32; 4] = [0; 4]; - unsafe { - let j_obj = JObject::from_raw(cutouts); - let j_arr = JPrimitiveArray::from(j_obj); - _env.get_int_array_region(j_arr, 0, array.as_mut()).unwrap(); - } - DISPLAY_CUTOUT_TOP.store(array[0], Ordering::Relaxed); - DISPLAY_CUTOUT_RIGHT.store(array[1], Ordering::Relaxed); - DISPLAY_CUTOUT_BOTTOM.store(array[2], Ordering::Relaxed); - DISPLAY_CUTOUT_LEFT.store(array[3], Ordering::Relaxed); } \ No newline at end of file diff --git a/src/gui/platform/desktop/mod.rs b/src/gui/platform/desktop/mod.rs index eaec138..534ab58 100644 --- a/src/gui/platform/desktop/mod.rs +++ b/src/gui/platform/desktop/mod.rs @@ -31,23 +31,3 @@ impl PlatformCallbacks for Desktop { fn exit(&self) {} } - -impl PlatformApp { - pub fn new(platform: Desktop) -> Self { - Self { - app: App::default(), - platform, - } - } -} - -impl eframe::App for PlatformApp { - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - self.app.ui(ctx, frame, &self.platform); - } - - fn on_close_event(&mut self) -> bool { - App::show_exit_modal(); - self.app.exit_allowed - } -} diff --git a/src/gui/views/accounts/content.rs b/src/gui/views/accounts/accounts.rs similarity index 90% rename from src/gui/views/accounts/content.rs rename to src/gui/views/accounts/accounts.rs index 9d52b7b..112a0f4 100644 --- a/src/gui/views/accounts/content.rs +++ b/src/gui/views/accounts/accounts.rs @@ -17,13 +17,13 @@ use crate::gui::icons::{GLOBE, PLUS}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Root, TitleAction, TitlePanel, View}; -/// Accounts central panel content. -pub struct AccountsContent { +/// Accounts content. +pub struct Accounts { /// List of accounts. list: Vec } -impl Default for AccountsContent { +impl Default for Accounts { fn default() -> Self { Self { list: vec![], @@ -31,11 +31,11 @@ impl Default for AccountsContent { } } -impl AccountsContent { +impl Accounts { 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, || { - Root::toggle_network_panel(); + Root::toggle_side_panel(); }) } else { None diff --git a/src/gui/views/accounts.rs b/src/gui/views/accounts/mod.rs similarity index 94% rename from src/gui/views/accounts.rs rename to src/gui/views/accounts/mod.rs index 6bde807..56e29d6 100644 --- a/src/gui/views/accounts.rs +++ b/src/gui/views/accounts/mod.rs @@ -12,5 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod content; -pub use content::*; \ No newline at end of file +mod accounts; +pub use accounts::*; \ No newline at end of file diff --git a/src/gui/views/network/metrics.rs b/src/gui/views/network/metrics.rs index aef6da3..5210f29 100644 --- a/src/gui/views/network/metrics.rs +++ b/src/gui/views/network/metrics.rs @@ -20,7 +20,7 @@ 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, NetworkContent, View}; +use crate::gui::views::{Modal, Network, View}; use crate::node::Node; /// Chain metrics tab content. @@ -40,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() { - NetworkContent::disabled_node_ui(ui); + Network::disabled_node_ui(ui); return; } diff --git a/src/gui/views/network/mining.rs b/src/gui/views/network/mining.rs index 0c3bbca..68ad511 100644 --- a/src/gui/views/network/mining.rs +++ b/src/gui/views/network/mining.rs @@ -20,9 +20,9 @@ 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, NetworkContent, View}; +use crate::gui::views::{Modal, Network, View}; use crate::gui::views::network::{NetworkTab, NetworkTabType}; -use crate::gui::views::network::setup::stratum::StratumSetup; +use crate::gui::views::network::setup::StratumSetup; use crate::node::{Node, NodeConfig}; /// Mining tab content. @@ -41,7 +41,7 @@ impl NetworkTab for NetworkMining { // Show message to enable node when it's not running. if !Node::is_running() { - NetworkContent::disabled_node_ui(ui); + Network::disabled_node_ui(ui); return; } diff --git a/src/gui/views/network.rs b/src/gui/views/network/mod.rs similarity index 83% rename from src/gui/views/network.rs rename to src/gui/views/network/mod.rs index f506d7c..069e213 100644 --- a/src/gui/views/network.rs +++ b/src/gui/views/network/mod.rs @@ -12,11 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod content; mod metrics; -mod mining; -mod settings; -mod node; -mod setup; +pub use metrics::*; -pub use content::*; \ No newline at end of file +mod mining; +pub use mining::*; + +mod settings; +pub use settings::*; + +mod node; +pub use node::*; + +mod setup; +pub use setup::*; + +mod network; +pub use network::*; \ No newline at end of file diff --git a/src/gui/views/network/content.rs b/src/gui/views/network/network.rs similarity index 83% rename from src/gui/views/network/content.rs rename to src/gui/views/network/network.rs index d6fb801..33bb76e 100644 --- a/src/gui/views/network/content.rs +++ b/src/gui/views/network/network.rs @@ -21,16 +21,8 @@ use crate::AppConfig; 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, Root, TitlePanel, View}; -use crate::gui::views::network::setup::dandelion::DandelionSetup; -use crate::gui::views::network::setup::node::NodeSetup; -use crate::gui::views::network::setup::p2p::P2PSetup; -use crate::gui::views::network::setup::pool::PoolSetup; -use crate::gui::views::network::setup::stratum::StratumSetup; -use crate::gui::views::network::metrics::NetworkMetrics; -use crate::gui::views::network::mining::NetworkMining; -use crate::gui::views::network::node::NetworkNode; -use crate::gui::views::network::settings::NetworkSettings; +use crate::gui::views::{Modal, ModalContainer, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings, Root, TitleAction, TitleContent, TitlePanel, View}; +use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup}; use crate::node::Node; pub trait NetworkTab { @@ -59,15 +51,15 @@ impl NetworkTabType { } } -/// Network side panel content. -pub struct NetworkContent { +/// Network content. +pub struct Network { /// 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 NetworkContent { +impl Default for Network { fn default() -> Self { Self { current_tab: Box::new(NetworkNode::default()), @@ -110,13 +102,13 @@ impl Default for NetworkContent { } } -impl ModalContainer for NetworkContent { +impl ModalContainer for Network { fn modal_ids(&self) -> &Vec<&'static str> { self.modal_ids.as_ref() } } -impl NetworkContent { +impl Network { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { // Show modal content if it's opened. if self.can_draw_modal() { @@ -140,7 +132,8 @@ impl NetworkContent { egui::TopBottomPanel::bottom("network_tabs") .frame(egui::Frame { - outer_margin: Margin::same(4.0), + fill: Colors::FILL, + inner_margin: Margin::same(4.0), ..Default::default() }) .show_inside(ui, |ui| { @@ -201,31 +194,43 @@ impl NetworkContent { /// Draw title content. fn title_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { - StripBuilder::new(ui) - .size(Size::exact(52.0)) - .size(Size::remainder()) - .size(Size::exact(52.0)) - .horizontal(|mut strip| { - strip.cell(|ui| { - ui.centered_and_justified(|ui| { - View::title_button(ui, DOTS_THREE_OUTLINE_VERTICAL, || { - //TODO: Show connections - }); - }); - }); - strip.strip(|builder| { - self.title_text_ui(builder); - }); - strip.cell(|ui| { - if !Root::is_dual_panel_mode(frame) { - ui.centered_and_justified(|ui| { - View::title_button(ui, CARDHOLDER, || { - Root::toggle_network_panel(); - }); - }); - } - }); - }); + let title_content = TitleContent::Custom("network_title".to_string(), Box::new(|ui| { + })); + TitlePanel::test_ui(title_content, TitleAction::new(DOTS_THREE_OUTLINE_VERTICAL, || { + //TODO: Show connections + }), if !Root::is_dual_panel_mode(frame) { + TitleAction::new(CARDHOLDER, || { + Root::toggle_side_panel(); + }) + } else { + None + }, ui); + + // StripBuilder::new(ui) + // .size(Size::exact(52.0)) + // .size(Size::remainder()) + // .size(Size::exact(52.0)) + // .horizontal(|mut strip| { + // strip.cell(|ui| { + // ui.centered_and_justified(|ui| { + // View::title_button(ui, DOTS_THREE_OUTLINE_VERTICAL, || { + // //TODO: Show connections + // }); + // }); + // }); + // strip.strip(|builder| { + // self.title_text_ui(builder); + // }); + // strip.cell(|ui| { + // if !Root::is_dual_panel_mode(frame) { + // ui.centered_and_justified(|ui| { + // View::title_button(ui, CARDHOLDER, || { + // Root::toggle_side_panel(); + // }); + // }); + // } + // }); + // }); } /// Draw title text. diff --git a/src/gui/views/network/node.rs b/src/gui/views/network/node.rs index ebd33dc..f4ffc78 100644 --- a/src/gui/views/network/node.rs +++ b/src/gui/views/network/node.rs @@ -20,7 +20,7 @@ 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::{NetworkContent, NetworkTab, NetworkTabType}; +use crate::gui::views::network::{Network, NetworkTab, NetworkTabType}; use crate::node::Node; /// Integrated node tab content. @@ -36,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() { - NetworkContent::disabled_node_ui(ui); + Network::disabled_node_ui(ui); return; } diff --git a/src/gui/views/network/settings.rs b/src/gui/views/network/settings.rs index cc72775..676b827 100644 --- a/src/gui/views/network/settings.rs +++ b/src/gui/views/network/settings.rs @@ -19,11 +19,7 @@ use crate::gui::icons::ARROW_COUNTER_CLOCKWISE; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalPosition, View}; use crate::gui::views::network::{NetworkTab, NetworkTabType}; -use crate::gui::views::network::setup::dandelion::DandelionSetup; -use crate::gui::views::network::setup::node::NodeSetup; -use crate::gui::views::network::setup::p2p::P2PSetup; -use crate::gui::views::network::setup::pool::PoolSetup; -use crate::gui::views::network::setup::stratum::StratumSetup; +use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup}; use crate::node::{Node, NodeConfig}; /// Integrated node settings tab content. diff --git a/src/gui/views/network/setup.rs b/src/gui/views/network/setup/mod.rs similarity index 75% rename from src/gui/views/network/setup.rs rename to src/gui/views/network/setup/mod.rs index 6dae75d..9770fca 100644 --- a/src/gui/views/network/setup.rs +++ b/src/gui/views/network/setup/mod.rs @@ -12,8 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod stratum; -pub mod node; -pub mod p2p; -pub mod pool; -pub mod dandelion; \ No newline at end of file +mod node; +pub use node::NodeSetup; + +mod p2p; +pub use p2p::P2PSetup; + +mod pool; +pub use pool::PoolSetup; + +mod dandelion; +pub use dandelion::DandelionSetup; + +mod stratum; +pub use stratum::StratumSetup; \ No newline at end of file diff --git a/src/gui/views/network/setup/node.rs b/src/gui/views/network/setup/node.rs index c10b4a2..8078011 100644 --- a/src/gui/views/network/setup/node.rs +++ b/src/gui/views/network/setup/node.rs @@ -21,7 +21,7 @@ use crate::AppConfig; 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, NetworkContent, View}; +use crate::gui::views::{Modal, ModalPosition, Network, View}; use crate::gui::views::network::settings::NetworkSettings; use crate::node::{Node, NodeConfig}; @@ -115,7 +115,7 @@ impl NodeSetup { // Autorun node setup. ui.vertical_centered(|ui| { ui.add_space(6.0); - NetworkContent::autorun_node_ui(ui); + Network::autorun_node_ui(ui); if Node::is_running() { ui.add_space(2.0); ui.label(RichText::new(t!("network_settings.restart_node_required")) diff --git a/src/gui/views/root.rs b/src/gui/views/root.rs index db84446..52b4e17 100644 --- a/src/gui/views/root.rs +++ b/src/gui/views/root.rs @@ -14,84 +14,216 @@ use std::cmp::min; use std::sync::atomic::{AtomicBool, Ordering}; +use egui::os::OperatingSystem; +use egui::RichText; use lazy_static::lazy_static; -use crate::gui::App; +use crate::gui::app::{get_left_display_cutout, get_right_display_cutout}; +use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{AccountsContent, Modal, NetworkContent}; +use crate::gui::views::{Accounts, Modal, ModalContainer, Network, View}; +use crate::node::Node; lazy_static! { /// To check if side panel is open from any part of ui. - static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false); + static ref SIDE_PANEL_OPEN: AtomicBool = AtomicBool::new(false); } -/// Main ui content, handles network panel state modal state. -#[derive(Default)] +/// Contains main ui content, handles side panel state. pub struct Root { - network: NetworkContent, - accounts: AccountsContent, + /// Side panel content. + side_panel: Network, + /// Central panel content. + central_content: Accounts, + + /// Check if app exit is allowed on close event of [`eframe::App`] platform implementation. + pub(crate) exit_allowed: bool, + + /// Flag to show exit progress at [`Modal`]. + show_exit_progress: bool, + + /// List of allowed [`Modal`] ids for this [`ModalContainer`]. + allowed_modal_ids: Vec<&'static str> +} + +impl Default for Root { + fn default() -> Self { + // Exit from eframe only for non-mobile platforms. + let os = OperatingSystem::from_target_os(); + let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS; + Self { + side_panel: Network::default(), + central_content: Accounts::default(), + exit_allowed, + show_exit_progress: false, + allowed_modal_ids: vec![ + Self::EXIT_MODAL_ID + ], + } + } +} + +impl ModalContainer for Root { + fn modal_ids(&self) -> &Vec<&'static str> { + &self.allowed_modal_ids + } } impl Root { + /// Identifier for exit confirmation [`Modal`]. + pub const EXIT_MODAL_ID: &'static str = "exit_confirmation"; + /// 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) { + // Show opened exit confirmation Modal content. + if self.can_draw_modal() { + self.exit_modal_content(ui, frame, cb); + } + + // Show network content on side panel. 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()) + .frame(egui::Frame::none()) .show_animated_inside(ui, is_panel_open, |ui| { - self.network.ui(ui, frame, cb); + self.side_panel.ui(ui, frame, cb); }); + // Show accounts content on central panel. egui::CentralPanel::default() - .frame(egui::Frame::default()) + .frame(egui::Frame::none()) .show_inside(ui, |ui| { - self.accounts.ui(ui, frame, cb); + self.central_content.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 is_panel_open = dual_panel_mode || Self::is_side_panel_open(); + let side_cutouts = get_left_display_cutout() + get_right_display_cutout(); let panel_width = if dual_panel_mode { - min(frame.info().window_info.size.x as i64, Self::SIDE_PANEL_MIN_WIDTH) as f32 + let available_width = (frame.info().window_info.size.x - side_cutouts) as i64; + min(available_width, Self::SIDE_PANEL_MIN_WIDTH) as f32 } else { - frame.info().window_info.size.x + frame.info().window_info.size.x - side_cutouts }; (is_panel_open, panel_width) } - /// Check if ui can show [`NetworkContent`] and [`AccountsContent`] at same time. + /// Check if ui can show [`Network`] and [`Accounts`] 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 + // greater than minimal width of the side panel plus display cutouts from both sides. + let side_cutouts = get_left_display_cutout() + get_right_display_cutout(); + is_wide_screen && w >= (Self::SIDE_PANEL_MIN_WIDTH as f32 * 2.0) + side_cutouts } /// 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); + pub fn toggle_side_panel() { + let is_open = SIDE_PANEL_OPEN.load(Ordering::Relaxed); + SIDE_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) + pub fn is_side_panel_open() -> bool { + SIDE_PANEL_OPEN.load(Ordering::Relaxed) } - /// Handle back button press event. - fn on_back() { + /// Show exit confirmation modal. + pub fn show_exit_modal() { + let exit_modal = Modal::new(Self::EXIT_MODAL_ID).title(t!("modal_exit.exit")); + Modal::show(exit_modal); + } + + /// Draw exit confirmation modal content. + fn exit_modal_content(&mut self, + ui: &mut egui::Ui, + frame: &mut eframe::Frame, + cb: &dyn PlatformCallbacks) { + Modal::ui(ui, |ui, modal| { + if self.show_exit_progress { + if !Node::is_running() { + self.exit(frame, cb); + modal.close(); + } + ui.add_space(16.0); + ui.vertical_centered(|ui| { + View::small_loading_spinner(ui); + ui.add_space(12.0); + ui.label(RichText::new(t!("sync_status.shutdown")) + .size(18.0) + .color(Colors::TEXT)); + }); + ui.add_space(10.0); + } else { + ui.add_space(8.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("modal_exit.description")) + .size(18.0) + .color(Colors::TEXT)); + }); + ui.add_space(10.0); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal_exit.exit"), Colors::WHITE, || { + if !Node::is_running() { + self.exit(frame, cb); + modal.close(); + } else { + Node::stop(true); + modal.disable_closing(); + self.show_exit_progress = true; + } + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::WHITE, || { + modal.close(); + }); + }); + }); + ui.add_space(6.0); + }); + } + }); + } + + /// Platform-specific exit from the application. + fn exit(&mut self, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { + match OperatingSystem::from_target_os() { + OperatingSystem::Android => { + cb.exit(); + } + OperatingSystem::IOS => { + //TODO: exit on iOS. + } + OperatingSystem::Nix | OperatingSystem::Mac | OperatingSystem::Windows => { + self.exit_allowed = true; + frame.close(); + } + OperatingSystem::Unknown => {} + } + } + + /// Handle platform-specific Back key code event. + pub fn on_back() { if Modal::on_back() { - App::show_exit_modal() + Self::show_exit_modal() } } } @@ -100,11 +232,14 @@ impl Root { #[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( +/// 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, ) { Root::on_back(); -} \ No newline at end of file +} + + + diff --git a/src/gui/views/title_panel.rs b/src/gui/views/title_panel.rs index 68b8a64..64f6a91 100644 --- a/src/gui/views/title_panel.rs +++ b/src/gui/views/title_panel.rs @@ -30,20 +30,64 @@ impl TitleAction { } } +/// Represents title content, can be text or callback to draw custom title. +pub enum TitleContent { + Text(String), + /// First argument is identifier for panel. + Custom(String, Box) +} + pub struct TitlePanel; impl TitlePanel { pub const DEFAULT_HEIGHT: f32 = 52.0; + pub fn test_ui(title: TitleContent, l: Option, r: Option, ui: &mut egui::Ui) { + let id = match &title { + TitleContent::Text(text) => Id::from(text.clone()), + TitleContent::Custom(text, _) => Id::from(text.clone()) + }; + egui::TopBottomPanel::top(id) + .resizable(false) + .exact_height(Self::DEFAULT_HEIGHT) + .frame(egui::Frame { + outer_margin: Margin::same(-1.0), + fill: Colors::YELLOW, + ..Default::default() + }) + .show_inside(ui, |ui| { + StripBuilder::new(ui) + .size(Size::exact(Self::DEFAULT_HEIGHT)) + .size(Size::remainder()) + .size(Size::exact(Self::DEFAULT_HEIGHT)) + .horizontal(|mut strip| { + strip.cell(|ui| { + Self::draw_action(ui, l); + }); + strip.cell(|ui| { + match title { + TitleContent::Text(text) => { + Self::draw_title(ui, text); + } + TitleContent::Custom(_, cb) => { + (cb)(ui); + } + } + }); + strip.cell(|ui| { + Self::draw_action(ui, r); + }); + }); + }); + } + pub fn ui(title: String, l: Option, r: Option, ui: &mut egui::Ui) { egui::TopBottomPanel::top(Id::from(title.clone())) .resizable(false) .exact_height(Self::DEFAULT_HEIGHT) .frame(egui::Frame { + outer_margin: Margin::same(-1.0), fill: Colors::YELLOW, - inner_margin: Margin::same(0.0), - outer_margin: Margin::same(0.0), - stroke: egui::Stroke::NONE, ..Default::default() }) .show_inside(ui, |ui| { diff --git a/src/lib.rs b/src/lib.rs index fc7892e..a345c74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,12 +17,13 @@ extern crate rust_i18n; use std::sync::Arc; +use egui::{Context, Stroke}; #[cfg(target_os = "android")] use winit::platform::android::activity::AndroidApp; pub use settings::{AppConfig, Settings}; -use crate::gui::{App, PlatformApp}; +use crate::gui::{Colors, PlatformApp}; use crate::gui::platform::PlatformCallbacks; use crate::node::Node; @@ -37,6 +38,7 @@ mod settings; #[allow(dead_code)] #[cfg(target_os = "android")] #[no_mangle] +/// Android platform entry point. fn android_main(app: AndroidApp) { #[cfg(debug_assertions)] { @@ -54,7 +56,7 @@ fn android_main(app: AndroidApp) { use winit::platform::android::EventLoopBuilderExtAndroid; let mut options = eframe::NativeOptions::default(); - // Use limits are guaranteed to be compatible with Android devices. + // Setup limits that are guaranteed to be compatible with Android devices. options.wgpu_options.device_descriptor = Arc::new(|adapter| { let base_limits = wgpu::Limits::downlevel_webgl2_defaults(); wgpu::DeviceDescriptor { @@ -73,16 +75,17 @@ fn android_main(app: AndroidApp) { start(options, app_creator(PlatformApp::new(platform))); } +/// [`PlatformApp`] setup for [`eframe`]. pub fn app_creator(app: PlatformApp) -> eframe::AppCreator where PlatformApp: eframe::App, T: PlatformCallbacks { Box::new(|cc| { - App::setup_visuals(&cc.egui_ctx); - App::setup_fonts(&cc.egui_ctx); - //TODO: Setup storage + setup_visuals(&cc.egui_ctx); + setup_fonts(&cc.egui_ctx); Box::new(app) }) } +/// Entry point to start ui with [`eframe`]. pub fn start(mut options: eframe::NativeOptions, app_creator: eframe::AppCreator) { options.default_theme = eframe::Theme::Light; options.renderer = eframe::Renderer::Wgpu; @@ -97,13 +100,97 @@ pub fn start(mut options: eframe::NativeOptions, app_creator: eframe::AppCreator let _ = eframe::run_native("Grim", options, app_creator); } +/// Setup application [`egui::Style`] and [`egui::Visuals`]. +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. + style.spacing.scroll_bar_width = 4.0; + // Disable spacing between items. + style.spacing.item_spacing = egui::vec2(0.0, 0.0); + // Setup radio button/checkbox size and spacing. + style.spacing.icon_width = 24.0; + style.spacing.icon_width_inner = 14.0; + style.spacing.icon_spacing = 10.0; + // Setup style + ctx.set_style(style); + + let mut visuals = egui::Visuals::light(); + // Setup selection color. + visuals.selection.stroke = Stroke { width: 1.0, color: Colors::TEXT }; + visuals.selection.bg_fill = Colors::GOLD; + // Disable stroke around panels by default + visuals.widgets.noninteractive.bg_stroke = Stroke::NONE; + // Setup visuals + ctx.set_visuals(visuals); +} + +/// Setup application fonts. +pub fn setup_fonts(ctx: &Context) { + use egui::FontFamily::Proportional; + + let mut fonts = egui::FontDefinitions::default(); + + fonts.font_data.insert( + "phosphor".to_owned(), + egui::FontData::from_static(include_bytes!( + "../fonts/phosphor.ttf" + )).tweak(egui::FontTweak { + scale: 1.0, + y_offset_factor: -0.30, + y_offset: 0.0, + baseline_offset_factor: 0.30, + }), + ); + fonts + .families + .entry(Proportional) + .or_default() + .insert(0, "phosphor".to_owned()); + + fonts.font_data.insert( + "noto".to_owned(), + egui::FontData::from_static(include_bytes!( + "../fonts/noto_sc_reg.otf" + )).tweak(egui::FontTweak { + scale: 1.0, + y_offset_factor: -0.25, + y_offset: 0.0, + baseline_offset_factor: 0.17, + }), + ); + fonts + .families + .entry(Proportional) + .or_default() + .insert(0, "noto".to_owned()); + + ctx.set_fonts(fonts); + + use egui::FontId; + use egui::TextStyle::*; + + let mut style = (*ctx.style()).clone(); + style.text_styles = [ + (Heading, FontId::new(20.0, Proportional)), + (Body, FontId::new(16.0, Proportional)), + (Button, FontId::new(18.0, Proportional)), + (Small, FontId::new(12.0, Proportional)), + (Monospace, FontId::new(16.0, Proportional)), + ].into(); + + ctx.set_style(style); +} + +/// Setup translations. fn setup_i18n() { const DEFAULT_LOCALE: &str = "en"; let locale = sys_locale::get_locale().unwrap_or(String::from(DEFAULT_LOCALE)); let locale_str = if locale.contains("-") { locale.split("-").next().unwrap_or(DEFAULT_LOCALE) } else { - DEFAULT_LOCALE + locale.as_str() }; if _rust_i18n_available_locales().contains(&locale_str) { rust_i18n::set_locale(locale_str);