android + ui: display cutouts (insets) refactoring, optimize platform-specific app, reorganize views
This commit is contained in:
parent
f85f4c9ed7
commit
dbe178f792
20 changed files with 541 additions and 508 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
332
src/gui/app.rs
332
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<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);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
|
||||
mod app;
|
||||
pub use app::{App, PlatformApp};
|
||||
pub use app::PlatformApp;
|
||||
|
||||
mod colors;
|
||||
pub use colors::Colors;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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::*;
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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::*;
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
|
@ -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"))
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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| {
|
||||
|
|
99
src/lib.rs
99
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<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);
|
||||
|
|
Loading…
Reference in a new issue