android + ui: display cutouts (insets) refactoring, optimize platform-specific app, reorganize views

This commit is contained in:
ardocrat 2023-07-14 00:55:31 +03:00
parent f85f4c9ed7
commit dbe178f792
20 changed files with 541 additions and 508 deletions

View file

@ -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);

View file

@ -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);
}
}

View file

@ -2,6 +2,7 @@
<style name="Theme.Main" parent="Theme.AppCompat.NoActionBar">
<item name="android:statusBarColor">@color/yellow</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:navigationBarColor">@color/black</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
</style>
</resources>

View file

@ -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<Platform> {
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<Platform> PlatformApp<Platform> {
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<Platform: PlatformCallbacks> eframe::App for PlatformApp<Platform> {
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);
}

View file

@ -14,7 +14,7 @@
mod app;
pub use app::{App, PlatformApp};
pub use app::PlatformApp;
mod colors;
pub use colors::Colors;

View file

@ -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<Android> {
pub fn new(platform: Android) -> Self {
Self {
app: App::default(),
platform,
}
}
}
impl eframe::App for PlatformApp<Android> {
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);
}

View file

@ -31,23 +31,3 @@ impl PlatformCallbacks for Desktop {
fn exit(&self) {}
}
impl PlatformApp<Desktop> {
pub fn new(platform: Desktop) -> Self {
Self {
app: App::default(),
platform,
}
}
}
impl eframe::App for PlatformApp<Desktop> {
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
}
}

View file

@ -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<String>
}
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

View file

@ -12,5 +12,5 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod content;
pub use content::*;
mod accounts;
pub use accounts::*;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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::*;
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::*;

View file

@ -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<dyn NetworkTab>,
/// [`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.

View file

@ -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;
}

View file

@ -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.

View file

@ -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;
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;

View file

@ -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"))

View file

@ -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();
}
}

View file

@ -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<dyn Fn(&mut egui::Ui)>)
}
pub struct TitlePanel;
impl TitlePanel {
pub const DEFAULT_HEIGHT: f32 = 52.0;
pub fn test_ui(title: TitleContent, l: Option<TitleAction>, r: Option<TitleAction>, 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<TitleAction>, r: Option<TitleAction>, 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| {

View file

@ -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<T: 'static>(app: PlatformApp<T>) -> eframe::AppCreator
where PlatformApp<T>: 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);