ui: modals, exit modal, app exit logic on Android, refactor display cutouts, optimize translations

This commit is contained in:
ardocrat 2023-06-02 02:05:34 +03:00
parent c3ce297373
commit d3e81826e8
26 changed files with 1051 additions and 484 deletions

View file

@ -88,15 +88,14 @@ public class BackgroundService extends Service {
public void onStop() {
mStopped = true;
stopForeground(Service.STOP_FOREGROUND_REMOVE);
if (mWakeLock.isHeld()) {
mWakeLock.release();
mWakeLock = null;
}
mHandler.removeCallbacks(mUpdateSyncStatus);
stopForeground(Service.STOP_FOREGROUND_REMOVE);
stopSelf();
}
public static void start(Context context) {

View file

@ -9,6 +9,8 @@ import android.view.KeyEvent;
import android.view.OrientationEventListener;
import com.google.androidgamesdk.GameActivity;
import java.util.concurrent.atomic.AtomicBoolean;
public class MainActivity extends GameActivity {
static {
@ -17,13 +19,15 @@ public class MainActivity extends GameActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// Setup HOME environment variable for native code configurations.
try {
Os.setenv("HOME", getExternalFilesDir("").getPath(), true);
} catch (ErrnoException e) {
throw new RuntimeException(e);
}
super.onCreate(savedInstanceState);
super.onCreate(null);
// Callback to update display cutouts at native code.
OrientationEventListener orientationEventListener = new OrientationEventListener(this,
SensorManager.SENSOR_DELAY_GAME) {
@Override
@ -34,9 +38,10 @@ public class MainActivity extends GameActivity {
if (orientationEventListener.canDetectOrientation()) {
orientationEventListener.enable();
}
onDisplayCutoutsChanged(Utils.getDisplayCutouts(MainActivity.this));
onDisplayCutoutsChanged(Utils.getDisplayCutouts(this));
BackgroundService.start(getApplicationContext());
// Start notification service.
BackgroundService.start(this);
}
native void onDisplayCutoutsChanged(int[] cutouts);
@ -52,23 +57,35 @@ public class MainActivity extends GameActivity {
public native void onBackButtonPress();
private boolean mManualExit;
private final AtomicBoolean mActivityDestroyed = new AtomicBoolean(false);
@Override
protected void onDestroy() {
if (!mManualExit) {
BackgroundService.stop(getApplicationContext());
// Temporary fix to prevent app hanging when closed from recent apps
Process.killProcess(Process.myPid());
onTermination();
}
// Temp fix: kill process after 3 seconds to prevent app hanging at next launch
new Thread(() -> {
try {
Thread.sleep(3000);
if (!mActivityDestroyed.get()) {
Process.killProcess(Process.myPid());
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
super.onDestroy();
mActivityDestroyed.set(true);
}
private boolean mManualExit = false;
// Called from native code
public void onExit() {
mManualExit = true;
BackgroundService.stop(getApplicationContext());
BackgroundService.stop(this);
finish();
}
public native void onTermination();
}

View file

@ -1,22 +1,13 @@
accounts: Accounts
integrated_node: Integrated Node
metrics: Metrics
settings: Settings
server_restarting: Server is restarting
server_down: Server is down
header: Header
block: Block
hash: Hash
height: Height
difficulty: Difficulty
time_utc: Time (UTC)
transactions: Transactions
main_pool: Main pool
stem_pool: Stem pool
data: Data
size: Size (GB)
peers: Peers
screen_accounts:
title: Accounts
network:
node: Integrated node
metrics: Metrics
mining: Mining
settings: Server settings
sync_status:
server_restarting: Server is restarting
server_down: Server is down
initial: Server is starting
no_sync: Server is running
awaiting_peers: Waiting for peers
@ -30,10 +21,29 @@ sync_status:
tx_hashset_save: Finalizing chain state
body_sync: Downloading blocks
body_sync_percent: 'Downloading blocks: %{percent}%'
shutdown: Shutting down
emission: Emission
inflation: Inflation
supply: Supply
block_time: Block time
reward: Reward
difficulty_at_window: 'Difficulty at window %{size}'
shutdown: Server is shutting down
network_node:
header: Header
block: Block
hash: Hash
height: Height
difficulty: Difficulty
time_utc: Time (UTC)
transactions: Transactions
main_pool: Main pool
stem_pool: Stem pool
data: Data
size: Size (GB)
peers: Peers
network_metrics:
emission: Emission
inflation: Inflation
supply: Supply
block_time: Block time
reward: Reward
difficulty_window: 'Difficulty at window %{size}'
modal:
cancel: Cancel
modal_exit:
description: Are you sure you want to quit the app?
exit: Exit

View file

@ -1,22 +1,13 @@
accounts: Аккаунты
integrated_node: Встроенный узел
metrics: Метрики
settings: Настройки
server_restarting: Сервер перезапускается
server_down: Сервер выключен
header: Заголовок
block: Блок
hash: Хэш
height: Высота
difficulty: Сложность
time_utc: Время (UTC)
transactions: Транзакции
main_pool: Основной пул
stem_pool: Stem пул
data: Данные
size: Размер (ГБ)
peers: Пиры
screen_accounts:
title: Аккаунты
network:
node: Встроенный узел
metrics: Метрики
mining: Майнинг
settings: Настройки сервера
sync_status:
server_restarting: Сервер перезапускается
server_down: Сервер выключен
initial: Запуск сервера
no_sync: Сервер запущен
awaiting_peers: Ожидание пиров
@ -30,10 +21,29 @@ sync_status:
tx_hashset_save: Сохранение состояния цепи
body_sync: Загрузка блоков
body_sync_percent: 'Загрузка блоков: %{percent}%'
shutdown: Выключение
emission: Эмиссия
inflation: Инфляция
supply: Предложение
block_time: Время блока
reward: Награда
difficulty_at_window: 'Сложность в окне %{size}'
shutdown: Выключение сервера
network_node:
header: Заголовок
block: Блок
hash: Хэш
height: Высота
difficulty: Сложность
time_utc: Время (UTC)
transactions: Транзакции
main_pool: Основной пул
stem_pool: Stem пул
data: Данные
size: Размер (ГБ)
peers: Пиры
network_metrics:
emission: Эмиссия
inflation: Инфляция
supply: Предложение
block_time: Время блока
reward: Награда
difficulty_window: 'Сложность в окне %{size}'
modal:
cancel: Отмена
modal_exit:
description: Вы уверены, что хотите выйти из приложения?
exit: Выход

View file

@ -13,11 +13,13 @@
// limitations under the License.
use eframe::{AppCreator, NativeOptions, Renderer, Theme};
use grin_core::global::ChainTypes;
use log::LevelFilter::Info;
#[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp;
use crate::gui::PlatformApp;
use crate::node::Node;
#[allow(dead_code)]
#[cfg(target_os = "android")]
@ -67,6 +69,9 @@ fn start(mut options: NativeOptions, app_creator: AppCreator) {
setup_i18n();
//TODO: Take network type and server check from config
Node::start(ChainTypes::Mainnet);
eframe::run_native("Grim", options, app_creator);
}

View file

@ -12,53 +12,109 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use eframe::epaint::Stroke;
use egui::{Context, Frame};
use egui::{Context, Stroke, Widget};
use egui::os::OperatingSystem;
use egui::style::Margin;
use crate::gui::colors::COLOR_LIGHT;
use crate::gui::Navigator;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::screens::Root;
use crate::gui::views::{Modal, ModalId, ModalLocation, ProgressLoading, View};
use crate::node::Node;
pub struct PlatformApp<Platform> {
pub(crate) app: App,
pub(crate) platform: Platform,
}
#[derive(Default)]
pub struct App {
root: Root,
}
impl Default for App {
fn default() -> Self {
Self {
root: Root::default(),
}
}
show_exit_progress: bool
}
impl App {
pub fn ui(&mut self, ctx: &Context, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
let Self { root } = self;
let modal_open = Navigator::is_modal_open(ModalLocation::Global);
egui::CentralPanel::default()
.frame(Frame {
inner_margin: Margin::same(0.0),
outer_margin: Margin::same(0.0),
stroke: Stroke::NONE,
.frame(egui::Frame {
fill: COLOR_LIGHT,
.. Default::default()
})
.show(ctx, |ui| {
root.ui(ui, frame, cb)
});
if modal_open {
self.show_global_modal(ui, frame, cb);
}
self.root.ui(ui, frame, cb);
}).response.enabled = !modal_open;
}
fn show_global_modal(&mut self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
cb: &dyn PlatformCallbacks) {
let location = ModalLocation::Global;
Navigator::modal_ui(ui, frame, location, |ui, frame, modal| {
match modal.id {
ModalId::Exit => {
if self.show_exit_progress {
if !Node::is_running() {
Self::exit(frame, cb);
} else {
ui.add_space(10.0);
let text = Node::get_sync_status_text(Node::get_sync_status());
ProgressLoading::new(text).ui(ui);
ui.add_space(10.0);
}
} else {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(t!("modal_exit.description"));
});
ui.add_space(10.0);
// 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::modal_button(ui, t!("modal_exit.exit"), || {
if !Node::is_running() {
Self::exit(frame, cb);
modal.close();
} else {
modal.disable_closing();
Node::stop();
self.show_exit_progress = true;
}
});
});
columns[1].vertical_centered_justified(|ui| {
View::modal_button(ui, t!("modal.cancel"), || {
modal.close();
});
});
});
ui.add_space(6.0);
}
}
}
});
}
fn exit(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 => {
frame.close();
}
// Web
OperatingSystem::Unknown => {}
}
}
}
pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool {
is_landscape(frame) && frame.info().window_info.size.x > 400.0
}
pub fn is_landscape(frame: &mut eframe::Frame) -> bool {
return frame.info().window_info.size.x > frame.info().window_info.size.y
}

View file

@ -13,14 +13,14 @@
// limitations under the License.
pub use app::App;
pub use app::PlatformApp;
mod app;
pub use app::{App, PlatformApp};
mod navigator;
pub use navigator::Navigator;
pub mod platform;
pub mod screens;
pub mod views;
pub mod icons;
pub mod colors;
mod app;
pub mod colors;

209
src/gui/navigator.rs Normal file
View file

@ -0,0 +1,209 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::BTreeSet;
use std::sync::{RwLock, RwLockWriteGuard};
use std::sync::atomic::{AtomicBool, Ordering};
use lazy_static::lazy_static;
use crate::gui::screens::ScreenId;
use crate::gui::views::{Modal, ModalId, ModalLocation};
lazy_static! {
/// Static [Navigator] state to be accessible from anywhere.
static ref NAVIGATOR_STATE: RwLock<Navigator> = RwLock::new(Navigator::default());
}
/// Logic of navigation for UI, stores screen identifiers stack, open modals and side panel state.
pub struct Navigator {
/// Screen identifiers in navigation stack.
screen_stack: BTreeSet<ScreenId>,
/// Indicator if side panel is open.
side_panel_open: AtomicBool,
/// Modal state to show globally above panel and screen.
global_modal: Option<Modal>,
/// Modal state to show on the side panel.
side_panel_modal: Option<Modal>,
/// Modal state to show on the screen.
screen_modal: Option<Modal>,
}
impl Default for Navigator {
fn default() -> Self {
Self {
screen_stack: BTreeSet::new(),
side_panel_open: AtomicBool::new(false),
global_modal: None,
side_panel_modal: None,
screen_modal: None,
}
}
}
impl Navigator {
/// Initialize navigation from provided [ScreenId].
pub fn init(from: ScreenId) {
let mut w_nav = NAVIGATOR_STATE.write().unwrap();
w_nav.screen_stack.clear();
w_nav.screen_stack.insert(from);
}
/// Check if provided [ScreenId] is current.
pub fn is_current(id: &ScreenId) -> bool {
let r_nav = NAVIGATOR_STATE.read().unwrap();
r_nav.screen_stack.last().unwrap() == id
}
/// Navigate to screen with provided [ScreenId].
pub fn to(id: ScreenId) {
NAVIGATOR_STATE.write().unwrap().screen_stack.insert(id);
}
/// Go back at navigation stack, close showing modals first.
pub fn back() {
let mut w_nav = NAVIGATOR_STATE.write().unwrap();
// If global Modal is showing and closeable, remove it from Navigator.
if w_nav.global_modal.is_some() {
let global_modal = w_nav.global_modal.as_ref().unwrap();
if global_modal.is_closeable() {
w_nav.global_modal = None;
}
return;
}
// If side panel Modal is showing and closeable, remove it from Navigator.
if w_nav.side_panel_modal.is_some() {
let side_panel_modal = w_nav.side_panel_modal.as_ref().unwrap();
if side_panel_modal.is_closeable() {
w_nav.side_panel_modal = None;
}
return;
}
// If screen Modal is showing and closeable, remove it from Navigator.
if w_nav.screen_modal.is_some() {
let screen_modal = w_nav.screen_modal.as_ref().unwrap();
if screen_modal.is_closeable() {
w_nav.screen_modal = None;
}
return;
}
// Go back at screen stack or show exit confirmation Modal.
if w_nav.screen_stack.len() > 1 {
w_nav.screen_stack.pop_last();
} else {
Self::open_exit_modal_nav(w_nav);
}
}
/// Open exit confirmation [Modal].
pub fn open_exit_modal() {
let w_nav = NAVIGATOR_STATE.write().unwrap();
Self::open_exit_modal_nav(w_nav);
}
/// Open exit confirmation [Modal] with provided [NAVIGATOR_STATE] lock.
fn open_exit_modal_nav(mut w_nav: RwLockWriteGuard<Navigator>) {
let m = Modal::new(ModalId::Exit, ModalLocation::Global).title(t!("modal_exit.exit"));
w_nav.global_modal = Some(m);
}
/// Open [Modal] at specified location.
pub fn open_modal(modal: Modal) {
let mut w_nav = NAVIGATOR_STATE.write().unwrap();
match modal.location {
ModalLocation::Global => {
w_nav.global_modal = Some(modal);
}
ModalLocation::SidePanel => {
w_nav.side_panel_modal = Some(modal);
}
ModalLocation::Screen => {
w_nav.screen_modal = Some(modal);
}
}
}
/// Check if [Modal] is open at specified location and remove it from [Navigator] if closed.
pub fn is_modal_open(location: ModalLocation) -> bool {
// Check if Modal is showing.
{
let r_nav = NAVIGATOR_STATE.read().unwrap();
let showing = match location {
ModalLocation::Global => { r_nav.global_modal.is_some() }
ModalLocation::SidePanel => { r_nav.side_panel_modal.is_some() }
ModalLocation::Screen => { r_nav.screen_modal.is_some() }
};
if !showing {
return false;
}
}
// Check if Modal is open.
let mut is_open = false;
{
let r_nav = NAVIGATOR_STATE.read().unwrap();
is_open = match location {
ModalLocation::Global => { r_nav.global_modal.as_ref().unwrap().is_open() }
ModalLocation::SidePanel => { r_nav.side_panel_modal.as_ref().unwrap().is_open() }
ModalLocation::Screen => {r_nav.screen_modal.as_ref().unwrap().is_open() }
};
}
// If Modal is not open, remove it from navigator state.
if !is_open {
let mut w_nav = NAVIGATOR_STATE.write().unwrap();
match location {
ModalLocation::Global => { w_nav.global_modal = None }
ModalLocation::SidePanel => { w_nav.side_panel_modal = None }
ModalLocation::Screen => { w_nav.screen_modal = None }
}
return false;
}
true
}
/// Show [Modal] with provided location at app UI.
pub fn modal_ui(ui: &mut egui::Ui,
frame: &mut eframe::Frame,
location: ModalLocation,
add_content: impl FnOnce(&mut egui::Ui, &mut eframe::Frame, &Modal)) {
let r_nav = NAVIGATOR_STATE.read().unwrap();
let modal = match location {
ModalLocation::Global => { &r_nav.global_modal }
ModalLocation::SidePanel => { &r_nav.side_panel_modal }
ModalLocation::Screen => { &r_nav.screen_modal }
};
if modal.is_some() {
modal.as_ref().unwrap().ui(ui, frame, add_content);
}
}
/// Change state of side panel to opposite.
pub fn toggle_side_panel() {
let r_nav = NAVIGATOR_STATE.read().unwrap();
let is_open = r_nav.side_panel_open.load(Ordering::Relaxed);
r_nav.side_panel_open.store(!is_open, Ordering::Relaxed);
}
/// Check if side panel is open.
pub fn is_side_panel_open() -> bool {
let r_nav = NAVIGATOR_STATE.read().unwrap();
r_nav.side_panel_open.load(Ordering::Relaxed)
}
}

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::atomic::{AtomicI32, Ordering};
use eframe::epaint::Stroke;
use lazy_static::lazy_static;
use winit::platform::android::activity::AndroidApp;
@ -34,22 +34,33 @@ impl Android {
}
impl PlatformCallbacks for Android {
fn show_keyboard(&mut self) {
fn show_keyboard(&self) {
self.android_app.show_soft_input(true);
}
fn hide_keyboard(&mut self) {
fn hide_keyboard(&self) {
self.android_app.hide_soft_input(true);
}
fn copy_string_to_buffer(&mut self, data: String) {
fn copy_string_to_buffer(&self, data: String) {
//TODO
}
fn get_string_from_buffer(&mut self) -> String {
fn get_string_from_buffer(&self) -> String {
//TODO
"".to_string()
}
fn exit(&self) {
use jni::objects::{JObject};
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
let activity = unsafe {
JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject)
};
env.call_method(activity, "onExit", "()V", &[]).unwrap();
}
}
//TODO

View file

@ -21,8 +21,9 @@ pub mod platform;
pub mod app;
pub trait PlatformCallbacks {
fn show_keyboard(&mut self);
fn hide_keyboard(&mut self);
fn copy_string_to_buffer(&mut self, data: String);
fn get_string_from_buffer(&mut self) -> String;
fn show_keyboard(&self);
fn hide_keyboard(&self);
fn copy_string_to_buffer(&self, data: String);
fn get_string_from_buffer(&self) -> String;
fn exit(&self);
}

View file

@ -13,7 +13,7 @@
// limitations under the License.
use crate::gui::platform::PlatformCallbacks;
use crate::gui::screens::{Navigator, ScreenId};
use crate::gui::screens::ScreenId;
pub struct Account {

View file

@ -14,48 +14,56 @@
use egui::Frame;
use crate::gui::app::is_dual_panel_mode;
use crate::gui::icons::{ARROW_CIRCLE_LEFT, GEAR_SIX, GLOBE};
use crate::gui::icons::{ARROW_CIRCLE_LEFT, GLOBE, PLUS};
use crate::gui::Navigator;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::screens::{Navigator, Screen, ScreenId};
use crate::gui::screens::{Screen, ScreenId};
use crate::gui::views::{TitlePanel, TitlePanelAction, View};
#[derive(Default)]
pub struct Accounts {}
pub struct Accounts {
title: String
}
impl Default for Accounts {
fn default() -> Self {
Self {
title: t!("screen_accounts.title").to_uppercase(),
}
}
}
impl Screen for Accounts {
fn id(&self) -> ScreenId {
ScreenId::Accounts
}
fn ui(&mut self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
cb: &dyn PlatformCallbacks) {
TitlePanel::new(t!("accounts"))
.left_action(
if !is_dual_panel_mode(frame) {
TitlePanelAction::new(GLOBE.into(), || {
Navigator::toggle_side_panel();
})
} else {
None
}
).right_action(TitlePanelAction::new(GEAR_SIX.into(), || {
//TODO: settings
})).ui(ui);
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
let Self { title } = self;
egui::CentralPanel::default().frame(Frame {
stroke: View::DEFAULT_STROKE,
.. Default::default()
}).show_inside(ui, |ui| {
ui.label(format!("{}Here we go 10000 ツ", ARROW_CIRCLE_LEFT));
if ui.button("TEST").clicked() {
Navigator::to(ScreenId::Account)
};
if ui.button(format!("{}BACK ", ARROW_CIRCLE_LEFT)).clicked() {
Navigator::to(ScreenId::Account)
};
});
TitlePanel::new(title)
.ui(if !View::is_dual_panel_mode(frame) {
TitlePanelAction::new(GLOBE, || {
Navigator::toggle_side_panel();
})
} else {
None
}, TitlePanelAction::new(PLUS, || {
//TODO: add account
}), ui);
egui::CentralPanel::default()
.frame(Frame {
stroke: View::DEFAULT_STROKE,
..Default::default()
})
.show_inside(ui, |ui| {
ui.label(format!("{}Here we go 10000 ツ", ARROW_CIRCLE_LEFT));
if ui.button("TEST").clicked() {
Navigator::to(ScreenId::Account)
};
if ui.button(format!("{}BACK ", ARROW_CIRCLE_LEFT)).clicked() {
Navigator::to(ScreenId::Account)
};
});
}
}

View file

@ -14,12 +14,10 @@
pub use account::Account;
pub use accounts::Accounts;
pub use navigator::Navigator;
pub use root::Root;
use crate::gui::platform::PlatformCallbacks;
mod navigator;
mod root;
mod accounts;
mod account;

View file

@ -1,78 +0,0 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::BTreeSet;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::RwLock;
use lazy_static::lazy_static;
use crate::gui::screens::ScreenId;
lazy_static! {
static ref NAVIGATOR_STATE: RwLock<Navigator> = RwLock::new(Navigator::default());
}
pub struct Navigator {
screens_stack: BTreeSet<ScreenId>,
side_panel_open: AtomicBool,
}
impl Default for Navigator {
fn default() -> Self {
Self {
screens_stack: BTreeSet::new(),
side_panel_open: AtomicBool::new(false)
}
}
}
impl Navigator {
pub fn init(from: ScreenId) {
let mut w_nav = NAVIGATOR_STATE.write().unwrap();
w_nav.screens_stack.clear();
w_nav.screens_stack.insert(from);
}
pub fn is_current(id: &ScreenId) -> bool {
let r_nav = NAVIGATOR_STATE.read().unwrap();
r_nav.screens_stack.last().unwrap() == id
}
pub fn to(id: ScreenId) {
NAVIGATOR_STATE.write().unwrap().screens_stack.insert(id);
}
pub fn back() {
let mut w_nav = NAVIGATOR_STATE.write().unwrap();
if w_nav.screens_stack.len() > 1 {
w_nav.screens_stack.pop_last();
} else {
}
}
pub fn toggle_side_panel() {
let w_nav = NAVIGATOR_STATE.write().unwrap();
w_nav.side_panel_open.store(
!w_nav.side_panel_open.load(Ordering::Relaxed),
Ordering::Relaxed
);
}
pub fn is_side_panel_open() -> bool {
let r_nav = NAVIGATOR_STATE.read().unwrap();
r_nav.side_panel_open.load(Ordering::Relaxed)
}
}

View file

@ -14,10 +14,10 @@
use std::cmp::min;
use crate::gui::app::is_dual_panel_mode;
use crate::gui::Navigator;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::screens::{Account, Accounts, Navigator, Screen, ScreenId};
use crate::gui::views::Network;
use crate::gui::screens::{Account, Accounts, Screen, ScreenId};
use crate::gui::views::{Network, View};
pub struct Root {
screens: Vec<Box<dyn Screen>>,
@ -26,7 +26,7 @@ pub struct Root {
impl Default for Root {
fn default() -> Self {
Navigator::init_from(ScreenId::Accounts);
Navigator::init(ScreenId::Accounts);
Self {
screens: (vec![
@ -40,31 +40,23 @@ impl Default for Root {
impl Root {
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
let is_network_panel_open = Navigator::is_side_panel_open() || is_dual_panel_mode(frame);
let (is_panel_open, panel_width) = dual_panel_state_width(frame);
egui::SidePanel::left("network_panel")
.resizable(false)
.exact_width(if is_dual_panel_mode(frame) {
min(frame.info().window_info.size.x as i64, 400) as f32
} else {
frame.info().window_info.size.x
})
.frame(egui::Frame {
.. Default::default()
})
.show_animated_inside(ui, is_network_panel_open, |ui| {
.exact_width(panel_width)
.frame(egui::Frame::default())
.show_animated_inside(ui, is_panel_open, |ui| {
self.network.ui(ui, frame, cb);
});
egui::CentralPanel::default().frame(egui::Frame {
..Default::default()
}).show_inside(ui, |ui| {
self.show_current_screen(ui, frame, cb);
});
egui::CentralPanel::default()
.frame(egui::Frame::default())
.show_inside(ui, |ui| {
self.show_current_screen(ui, frame, cb);
});
}
pub fn show_current_screen(&mut self,
fn show_current_screen(&mut self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
cb: &dyn PlatformCallbacks) {
@ -78,6 +70,18 @@ impl Root {
}
}
/// Get dual panel state and width
fn dual_panel_state_width(frame: &mut eframe::Frame) -> (bool, f32) {
let dual_panel_mode = View::is_dual_panel_mode(frame);
let is_panel_open = dual_panel_mode || Navigator::is_side_panel_open();
let panel_width = if dual_panel_mode {
min(frame.info().window_info.size.x as i64, View::SIDE_PANEL_MIN_WIDTH) as f32
} else {
frame.info().window_info.size.x
};
(is_panel_open, panel_width)
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]

View file

@ -13,22 +13,25 @@
// limitations under the License.
mod views;
pub use self::views::View;
pub use views::View;
mod title_panel;
pub use self::title_panel::{TitlePanel, TitlePanelAction};
pub use title_panel::*;
mod modal;
pub use modal::*;
mod network;
mod network_node;
mod network_tuning;
mod network_settings;
mod network_metrics;
mod network_mining;
pub use network::Network;
pub use self::network::Network;
mod progress_loading;
pub use progress_loading::ProgressLoading;
pub trait NetworkTab {
fn name(&self) -> &String;
fn ui(&mut self, ui: &mut egui::Ui);
}
}

260
src/gui/views/modal.rs Normal file
View file

@ -0,0 +1,260 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::cmp::min;
use std::sync::atomic::{AtomicBool, Ordering};
use egui::{Align2, Color32, RichText, Rounding, Sense, Stroke, Vec2};
use egui::epaint::RectShape;
use egui::style::Margin;
use egui_extras::{Size, StripBuilder};
use crate::gui::colors::{COLOR_DARK, COLOR_LIGHT, COLOR_YELLOW};
use crate::gui::views::View;
/// Identifier for [`Modal`] content to draw at [`Modal::ui`].
pub enum ModalId {
Exit
}
/// Location for [`Modal`] at application UI.
#[derive(Clone, Copy)]
pub enum ModalLocation {
/// To draw globally above side panel and screen.
Global,
/// To draw on the side panel.
SidePanel,
/// To draw on the screen.
Screen
}
/// Position of [`Modal`] on the screen at provided [`ModalLocation`].
pub enum ModalPosition {
/// Center-top position.
CenterTop,
/// Center of the location.
Center
}
/// Stores data to draw dialog box/popup at UI, powered by [`egui::Window`].
pub struct Modal {
/// Identifier for content.
pub(crate) id: ModalId,
/// Location at UI.
pub(crate) location: ModalLocation,
/// Position on the screen.
position: ModalPosition,
/// Flag to show the content.
open: AtomicBool,
/// To check if it can be closed.
closeable: AtomicBool,
/// Title text
title: Option<String>
}
impl Modal {
/// Default width of the content.
const DEFAULT_WIDTH: i64 = 380;
/// Create open and closeable [`Modal`] with center position.
pub fn new(id: ModalId, location: ModalLocation) -> Self {
Self {
id,
location,
position: ModalPosition::Center,
open: AtomicBool::new(true),
closeable: AtomicBool::new(true),
title: None
}
}
/// Setup position of [`Modal`] on the screen.
pub fn position(mut self, position: ModalPosition) -> Self {
self.position = position;
self
}
/// Check if [`Modal`] is open.
pub fn is_open(&self) -> bool {
self.open.load(Ordering::Relaxed)
}
/// Mark [`Modal`] closed.
pub fn close(&self) {
self.open.store(false, Ordering::Relaxed);
}
/// Setup possibility to close [`Modal`].
pub fn closeable(self, closeable: bool) -> Self {
self.closeable.store(closeable, Ordering::Relaxed);
self
}
/// Disable possibility to close [`Modal`].
pub fn disable_closing(&self) {
self.closeable.store(false, Ordering::Relaxed);
}
/// Check if [`Modal`] is closeable.
pub fn is_closeable(&self) -> bool {
self.closeable.load(Ordering::Relaxed)
}
/// Set title text.
pub fn title(mut self, title: String) -> Self {
self.title = Some(title.to_uppercase());
self
}
/// Show [`Modal`] with provided content.
pub fn ui(&self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
add_content: impl FnOnce(&mut egui::Ui, &mut eframe::Frame, &Modal)) {
let cw = min(frame.info().window_info.size.x as i64 - 20, Self::DEFAULT_WIDTH) as f32;
// Show background Window at full available size
egui::Window::new(self.window_id(true))
.title_bar(false)
.resizable(false)
.collapsible(false)
.fixed_pos(ui.next_widget_position())
.fixed_size(ui.available_size())
.frame(egui::Frame {
fill: Color32::from_black_alpha(100),
..Default::default()
})
.show(ui.ctx(), |ui| {
ui.set_min_size(ui.available_size());
});
// Show main content Window at give position
let layer_id = egui::Window::new(self.window_id(false))
.title_bar(false)
.resizable(false)
.collapsible(false)
.default_width(cw)
.anchor(self.modal_position(), Vec2::default())
.frame(egui::Frame {
rounding: Rounding::same(8.0),
fill: COLOR_YELLOW,
..Default::default()
})
.show(ui.ctx(), |ui| {
if self.title.is_some() {
self.draw_title(ui);
}
self.draw_content(ui, frame, add_content);
}).unwrap().response.layer_id;
// Always show main content Window above background Window
ui.ctx().move_to_top(layer_id);
}
/// Generate identifier for inner [`egui::Window`] parts based on [`ModalLocation`].
fn window_id(&self, background: bool) -> &'static str {
match self.location {
ModalLocation::Global => {
if background { "global.bg" } else { "global" }
}
ModalLocation::SidePanel => {
if background { "side_panel.bg" } else { "side_panel" }
}
ModalLocation::Screen => {
if background { "global.bg" } else { "global" }
}
}
}
/// Get [`egui::Window`] position based on [`ModalPosition`].
fn modal_position(&self) -> Align2 {
match self.position {
ModalPosition::CenterTop => { Align2::CENTER_TOP }
ModalPosition::Center => { Align2::CENTER_CENTER }
}
}
/// Draw provided content.
fn draw_content(&self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
add_content: impl FnOnce(&mut egui::Ui, &mut eframe::Frame, &Modal)) {
let mut rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
// Create background shape.
let rounding = if self.title.is_some() {
Rounding {
nw: 0.0,
ne: 0.0,
sw: 8.0,
se: 8.0,
}
} else {
Rounding::same(8.0)
};
let mut bg_shape = RectShape {
rect,
rounding,
fill: COLOR_LIGHT,
stroke: View::DEFAULT_STROKE,
};
let bg_idx = ui.painter().add(bg_shape);
// Draw main content.
let mut content_resp_rect = ui.allocate_ui_at_rect(rect, |ui| {
(add_content)(ui, frame, self);
}).response.rect;
// Setup background shape to be painted behind main content.
content_resp_rect.min -= egui::emath::vec2(6.0, 0.0);
content_resp_rect.max += egui::emath::vec2(6.0, 0.0);
bg_shape.rect = content_resp_rect;
ui.painter().set(bg_idx, bg_shape);
}
/// Draw the title.
fn draw_title(&self, ui: &mut egui::Ui) {
let rect = ui.available_rect_before_wrap();
// Create background shape.
let mut bg_shape = RectShape {
rect,
rounding: Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
},
fill: COLOR_YELLOW,
stroke: Stroke::NONE,
};
let bg_idx = ui.painter().add(bg_shape);
// Draw title content.
let title_resp = ui.allocate_ui_at_rect(rect, |ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(8.0);
ui.label(RichText::new(self.title.as_ref().unwrap()).size(20.0).color(COLOR_DARK));
ui.add_space(8.0);
});
}).response;
// Setup background shape to be painted behind title content.
bg_shape.rect = title_resp.rect;
ui.painter().set(bg_idx, bg_shape);
}
}

View file

@ -14,20 +14,16 @@
use std::time::Duration;
use eframe::emath::lerp;
use eframe::epaint::{Color32, FontId, Rgba, Stroke};
use eframe::epaint::text::{LayoutJob, TextFormat, TextWrapping};
use egui::RichText;
use egui::{Color32, lerp, Rgba, RichText, Stroke};
use egui::style::Margin;
use egui_extras::{Size, StripBuilder};
use grin_chain::SyncStatus;
use grin_core::global::ChainTypes;
use crate::gui::app::is_dual_panel_mode;
use crate::gui::colors::{COLOR_DARK, COLOR_YELLOW};
use crate::gui::colors::{COLOR_DARK, COLOR_GRAY_DARK, COLOR_YELLOW};
use crate::gui::icons::{CARDHOLDER, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE};
use crate::gui::Navigator;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::screens::Navigator;
use crate::gui::views::{NetworkTab, View};
use crate::gui::views::network_metrics::NetworkMetrics;
use crate::gui::views::network_node::NetworkNode;
@ -50,7 +46,6 @@ pub struct Network {
impl Default for Network {
fn default() -> Self {
Node::start(ChainTypes::Mainnet);
Self {
current_mode: Mode::Node,
node_view: NetworkNode::default(),
@ -60,11 +55,7 @@ impl Default for Network {
}
impl Network {
pub fn ui(&mut self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
_: &dyn PlatformCallbacks) {
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, _: &dyn PlatformCallbacks) {
egui::TopBottomPanel::top("network_title")
.resizable(false)
.frame(egui::Frame {
@ -88,44 +79,45 @@ impl Network {
self.draw_tabs(ui);
});
egui::CentralPanel::default().frame(egui::Frame {
stroke: View::DEFAULT_STROKE,
inner_margin: Margin::same(4.0),
fill: Color32::WHITE,
.. Default::default()
}).show_inside(ui, |ui| {
self.draw_tab_content(ui);
});
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::DEFAULT_STROKE,
inner_margin: Margin::same(4.0),
fill: Color32::WHITE,
.. Default::default()
})
.show_inside(ui, |ui| {
self.draw_tab_content(ui);
});
}
fn draw_tabs(&mut self, ui: &mut egui::Ui) {
//Setup spacing between tabs
ui.style_mut().spacing.item_spacing = egui::vec2(6.0, 0.0);
ui.scope(|ui| {
//Setup spacing between tabs
ui.style_mut().spacing.item_spacing = egui::vec2(6.0, 0.0);
ui.columns(4, |columns| {
columns[0].vertical_centered(|ui| {
View::tab_button(ui, DATABASE, self.current_mode == Mode::Node, || {
self.current_mode = Mode::Node;
ui.columns(4, |columns| {
columns[0].vertical_centered(|ui| {
View::tab_button(ui, DATABASE, self.current_mode == Mode::Node, || {
self.current_mode = Mode::Node;
});
});
columns[1].vertical_centered(|ui| {
View::tab_button(ui, GAUGE, self.current_mode == Mode::Metrics, || {
self.current_mode = Mode::Metrics;
});
});
columns[2].vertical_centered(|ui| {
View::tab_button(ui, FACTORY, self.current_mode == Mode::Miner, || {
self.current_mode = Mode::Miner;
});
});
columns[3].vertical_centered(|ui| {
View::tab_button(ui, FADERS, self.current_mode == Mode::Tuning, || {
self.current_mode = Mode::Tuning;
});
});
});
columns[1].vertical_centered(|ui| {
View::tab_button(ui, GAUGE, self.current_mode == Mode::Metrics, || {
self.current_mode = Mode::Metrics;
});
});
columns[2].vertical_centered(|ui| {
View::tab_button(ui, FACTORY, self.current_mode == Mode::Miner, || {
self.current_mode = Mode::Miner;
});
});
columns[3].vertical_centered(|ui| {
View::tab_button(ui, FADERS, self.current_mode == Mode::Tuning, || {
self.current_mode = Mode::Tuning;
});
});
});
}
@ -165,7 +157,7 @@ impl Network {
self.draw_title_text(builder);
});
strip.cell(|ui| {
if !is_dual_panel_mode(frame) {
if !View::is_dual_panel_mode(frame) {
ui.centered_and_justified(|ui| {
View::title_button(ui, CARDHOLDER, || {
Navigator::toggle_side_panel();
@ -195,13 +187,14 @@ impl Network {
};
builder
.size(Size::exact(19.0))
.size(Size::remainder())
.size(Size::exact(32.0))
.vertical(|mut strip| {
strip.cell(|ui| {
ui.centered_and_justified(|ui| {
ui.label(RichText::new(title_text.to_uppercase())
.size(19.0)
ui.add_space(2.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(title_text)
.size(18.0)
.color(COLOR_DARK));
});
});
@ -223,26 +216,17 @@ impl Network {
bright
};
// Draw sync text
let status_color_rgba = Rgba::from(COLOR_GRAY_DARK) * color_factor as f32;
let status_color = Color32::from(status_color_rgba);
View::ellipsize_text(ui, status_text, 15.0, status_color);
// Repaint based on sync status
if idle {
ui.ctx().request_repaint_after(Duration::from_millis(600));
} else {
ui.ctx().request_repaint();
}
// Draw sync text
let mut job = LayoutJob::single_section(status_text, TextFormat {
font_id: FontId::proportional(15.0),
color: Color32::from(Rgba::from(COLOR_DARK) * color_factor as f32),
.. Default::default()
});
job.wrap = TextWrapping {
max_rows: 1,
break_anywhere: false,
overflow_character: Option::from(''),
..Default::default()
};
ui.label(job);
});
});
});

View file

@ -29,7 +29,7 @@ pub struct NetworkMetrics {
impl Default for NetworkMetrics {
fn default() -> Self {
Self {
title: t!("metrics"),
title: t!("network.metrics").to_uppercase(),
}
}
}
@ -57,7 +57,7 @@ impl NetworkTab for NetworkMetrics {
// Show emission info
ui.vertical_centered_justified(|ui| {
View::sub_title(ui, format!("{} {}", COINS, t!("emission")));
View::sub_header(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
});
ui.add_space(4.0);
@ -68,19 +68,19 @@ impl NetworkTab for NetworkMetrics {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
format!("{}", BLOCK_REWARD),
t!("reward"),
t!("network_metrics.reward"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
format!("{:.2}%", rate),
t!("inflation"),
t!("network_metrics.inflation"),
[false, false, false, false]);
});
columns[2].vertical_centered(|ui| {
View::rounded_box(ui,
supply.to_string(),
t!("supply"),
t!("network_metrics.supply"),
[false, true, false, true]);
});
});
@ -88,27 +88,30 @@ impl NetworkTab for NetworkMetrics {
// Show difficulty adjustment window info
ui.vertical_centered_justified(|ui| {
let title = t!("difficulty_at_window", "size" => stats.diff_stats.window_size);
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, title));
let title = t!(
"network_metrics.difficulty_window",
"size" => stats.diff_stats.window_size
);
View::sub_header(ui, format!("{} {}", HOURGLASS_MEDIUM, title));
});
ui.add_space(4.0);
ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.diff_stats.height.to_string(),
t!("height"),
t!("network_node.height"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
format!("{}s", stats.diff_stats.average_block_time),
t!("block_time"),
t!("network_metrics.block_time"),
[false, false, false, false]);
});
columns[2].vertical_centered(|ui| {
View::rounded_box(ui,
stats.diff_stats.average_difficulty.to_string(),
t!("difficulty"),
t!("network_node.difficulty"),
[false, true, false, true]);
});
});
@ -119,7 +122,7 @@ impl NetworkTab for NetworkMetrics {
ScrollArea::vertical()
.auto_shrink([false; 2])
.stick_to_bottom(true)
.id_source("diff_scroll")
.id_source("difficulty_scroll")
.show_rows(
ui,
DIFF_BLOCK_UI_HEIGHT,

View file

@ -28,7 +28,7 @@ pub struct NetworkNode {
impl Default for NetworkNode {
fn default() -> Self {
Self {
title: t!("integrated_node"),
title: t!("network.node").to_uppercase(),
}
}
}
@ -54,20 +54,20 @@ impl NetworkTab for NetworkNode {
.show(ui, |ui| {
// Show header stats
ui.vertical_centered_justified(|ui| {
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("header")));
View::sub_header(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
});
ui.add_space(4.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.header_stats.last_block_h.to_string(),
t!("hash"),
t!("network_node.hash"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
stats.header_stats.height.to_string(),
t!("height"),
t!("network_node.height"),
[false, true, false, false]);
});
});
@ -75,14 +75,14 @@ impl NetworkTab for NetworkNode {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.header_stats.total_difficulty.to_string(),
t!("difficulty"),
t!("network_node.difficulty"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let h_ts = stats.header_stats.latest_timestamp;
View::rounded_box(ui,
format!("{}", h_ts.format("%d/%m/%Y %H:%M")),
t!("time_utc"),
t!("network_node.time_utc"),
[false, false, false, true]);
});
});
@ -90,20 +90,20 @@ impl NetworkTab for NetworkNode {
// Show block stats
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::sub_title(ui, format!("{} {}", CUBE, t!("block")));
View::sub_header(ui, format!("{} {}", CUBE, t!("network_node.block")));
});
ui.add_space(4.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.chain_stats.last_block_h.to_string(),
t!("hash"),
t!("network_node.hash"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
stats.chain_stats.height.to_string(),
t!("height"),
t!("network_node.height"),
[false, true, false, false]);
});
});
@ -111,14 +111,14 @@ impl NetworkTab for NetworkNode {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.chain_stats.total_difficulty.to_string(),
t!("difficulty"),
t!("network_node.difficulty"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let b_ts = stats.chain_stats.latest_timestamp;
View::rounded_box(ui,
format!("{}", b_ts.format("%d/%m/%Y %H:%M")),
t!("time_utc"),
t!("network_node.time_utc"),
[false, false, false, true]);
});
});
@ -126,7 +126,7 @@ impl NetworkTab for NetworkNode {
// Show data stats
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::sub_title(ui, format!("{} {}", SHARE_NETWORK, t!("data")));
View::sub_header(ui, format!("{} {}", SHARE_NETWORK, t!("network_node.data")));
});
ui.add_space(4.0);
ui.columns(2, |columns| {
@ -139,7 +139,7 @@ impl NetworkTab for NetworkNode {
};
View::rounded_box(ui,
tx_stat,
t!("main_pool"),
t!("network_node.main_pool"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
@ -151,7 +151,7 @@ impl NetworkTab for NetworkNode {
};
View::rounded_box(ui,
stem_tx_stat,
t!("stem_pool"),
t!("network_node.stem_pool"),
[false, true, false, false]);
});
});
@ -159,13 +159,13 @@ impl NetworkTab for NetworkNode {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.disk_usage_gb.to_string(),
t!("size"),
t!("network_node.size"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
stats.peer_count.to_string(),
t!("peers"),
t!("network_node.peers"),
[false, false, false, true]);
});
});
@ -174,7 +174,7 @@ impl NetworkTab for NetworkNode {
if stats.peer_count > 0 {
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::sub_title(ui,format!("{} {}", HANDSHAKE, t!("peers")));
View::sub_header(ui, format!("{} {}", HANDSHAKE, t!("network_node.peers")));
});
ui.add_space(4.0);

View file

@ -0,0 +1,39 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Response, RichText, Spinner, Ui, Widget};
use crate::gui::colors::COLOR_DARK;
pub struct ProgressLoading {
text: String
}
impl ProgressLoading {
pub fn new(text: String) -> Self {
Self {
text
}
}
}
impl Widget for ProgressLoading {
fn ui(self, ui: &mut Ui) -> Response {
ui.vertical_centered_justified(|ui| {
Spinner::new().size(36.0).color(COLOR_DARK).ui(ui);
ui.add_space(10.0);
ui.label(RichText::new(self.text).size(18.0).color(COLOR_DARK));
}).response
}
}

View file

@ -12,57 +12,36 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use eframe::epaint::{FontId, Stroke};
use eframe::epaint::text::{LayoutJob, TextFormat, TextWrapping};
use egui::style::Margin;
use egui_extras::{Size, StripBuilder};
use crate::gui::colors::{COLOR_DARK, COLOR_YELLOW};
use crate::gui::screens::Navigator;
use crate::gui::views::View;
pub struct TitlePanelAction {
pub(crate) icon: Box<str>,
pub struct TitlePanelAction<'action> {
pub(crate) icon: Box<&'action str>,
pub(crate) on_click: Box<dyn Fn()>,
}
impl TitlePanelAction {
pub fn new(icon: Box<str>, on_click: fn()) -> Option<Self> {
Option::from(Self { icon, on_click: Box::new(on_click) })
impl<'action> TitlePanelAction<'action> {
pub fn new(icon: &'action str, on_click: fn()) -> Option<Self> {
Option::from(Self { icon: Box::new(icon), on_click: Box::new(on_click) })
}
}
#[derive(Default)]
pub struct TitlePanelActions {
left: Option<TitlePanelAction>,
right: Option<TitlePanelAction>
}
pub struct TitlePanel {
title: String,
actions: TitlePanelActions,
}
impl TitlePanel {
pub fn new(title: String) -> Self {
Self {
title,
actions: TitlePanelActions::default()
}
const PANEL_SIZE: f32 = 52.0;
pub fn new(title: &String) -> Self {
Self { title: title.to_uppercase() }
}
pub fn left_action(mut self, action: Option<TitlePanelAction>) -> Self {
self.actions.left = action;
self
}
pub fn right_action(mut self, action: Option<TitlePanelAction>) -> Self {
self.actions.right = action;
self
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
let Self { actions, title } = self;
pub fn ui(&self, l: Option<TitlePanelAction>, r: Option<TitlePanelAction>, ui: &mut egui::Ui) {
let Self { title } = self;
egui::TopBottomPanel::top("title_panel")
.resizable(false)
@ -70,33 +49,29 @@ impl TitlePanel {
fill: COLOR_YELLOW,
inner_margin: Margin::same(0.0),
outer_margin: Margin::same(0.0),
stroke: Stroke::NONE,
stroke: egui::Stroke::NONE,
..Default::default()
})
.show_inside(ui, |ui| {
StripBuilder::new(ui)
.size(Size::exact(52.0))
.size(Size::exact(Self::PANEL_SIZE))
.vertical(|mut strip| {
strip.strip(|builder| {
builder
.size(Size::exact(52.0))
.size(Size::exact(Self::PANEL_SIZE))
.size(Size::remainder())
.size(Size::exact(52.0))
.size(Size::exact(Self::PANEL_SIZE))
.horizontal(|mut strip| {
strip.cell(|ui| {
show_action(ui, actions.left.as_ref());
});
strip.strip(|builder| {
builder
.size(Size::remainder())
.vertical(|mut strip| {
strip.cell(|ui| {
show_title(title, ui);
});
});
show_action(ui, l.as_ref());
});
strip.cell(|ui| {
show_action(ui, actions.right.as_ref());
ui.centered_and_justified(|ui| {
View::ellipsize_text(ui, title.into(), 20.0, COLOR_DARK);
});
});
strip.cell(|ui| {
show_action(ui, r.as_ref());
});
});
});
@ -116,21 +91,3 @@ fn show_action(ui: &mut egui::Ui, action: Option<&TitlePanelAction>) {
}
}
fn show_title(title: &String, ui: &mut egui::Ui) {
ui.centered_and_justified(|ui| {
let mut job = LayoutJob::single_section(title.to_uppercase(), TextFormat {
font_id: FontId::proportional(20.0),
color: COLOR_DARK,
.. Default::default()
});
job.wrap = TextWrapping {
max_rows: 1,
break_anywhere: false,
overflow_character: Option::from(''),
..Default::default()
};
ui.label(job);
});
}

View file

@ -12,35 +12,87 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use eframe::epaint::{Color32, FontId, Rounding, Stroke};
use eframe::epaint::text::{LayoutJob, TextFormat, TextWrapping};
use egui::{RichText, Sense, Widget};
use egui::epaint::{Color32, FontId, Rounding, Stroke};
use egui::text::{LayoutJob, TextFormat};
use egui::{Button, PointerState, Response, RichText, Sense, Widget};
use egui::epaint::text::TextWrapping;
use crate::gui::colors::{COLOR_DARK, COLOR_GRAY, COLOR_LIGHT, COLOR_GRAY_LIGHT, COLOR_GRAY_DARK};
pub struct View;
impl View {
/// Default stroke around views.
pub const DEFAULT_STROKE: Stroke = Stroke { width: 1.0, color: Color32::from_gray(190) };
/// Default width of side panel at application UI.
pub const SIDE_PANEL_MIN_WIDTH: i64 = 400;
/// Check if UI can show side panel and screen 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
}
/// Show and cut long text with character.
pub fn ellipsize_text(ui: &mut egui::Ui, text: String, size: f32, color: Color32) {
let mut job = LayoutJob::single_section(text, TextFormat {
font_id: FontId::proportional(size), color, .. Default::default()
});
job.wrap = TextWrapping {
max_rows: 1,
break_anywhere: false,
overflow_character: Option::from(''),
..Default::default()
};
ui.label(job);
}
/// Sub-header with uppercase characters and more lighter color.
pub fn sub_header(ui: &mut egui::Ui, text: String) {
ui.label(RichText::new(text.to_uppercase()).size(16.0).color(COLOR_GRAY_DARK));
}
/// Temporary button click optimization for touch screens.
fn on_button_click(ui: &mut egui::Ui, resp: Response, action: impl FnOnce()) {
// Clear pointer event if dragging is out of button area
if resp.dragged() && !ui.rect_contains_pointer(resp.rect) {
ui.input_mut().pointer = PointerState::default();
}
// Call click action if button is clicked or drag released
if resp.drag_released() || resp.clicked() {
(action)();
};
}
/// Title button with transparent background fill color, contains only icon.
pub fn title_button(ui: &mut egui::Ui, icon: &str, action: impl FnOnce()) {
ui.scope(|ui| {
// Disable stroke around title buttons on hover
ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE;
let b = egui::widgets::Button::new(
RichText::new(icon.to_string()).size(24.0).color(COLOR_DARK)
).fill(Color32::TRANSPARENT)
let wt = RichText::new(icon.to_string()).size(24.0).color(COLOR_DARK);
let br = Button::new(wt)
.fill(Color32::TRANSPARENT)
.ui(ui).interact(Sense::click_and_drag());
// Click optimization for touch screens
if b.drag_released() || b.clicked() {
(action)();
};
Self::on_button_click(ui, br, action);
});
}
pub fn tab_button(ui: &mut egui::Ui, icon: &str, active: bool, mut action: impl FnMut()) {
/// Tab button with white background fill color, contains only icon.
pub fn tab_button(ui: &mut egui::Ui, icon: &str, active: bool, action: impl FnOnce()) {
let text_color = match active {
true => { COLOR_GRAY_DARK }
false => { COLOR_DARK }
};
let wt = RichText::new(icon.to_string()).size(24.0).color(text_color);
let stroke = match active {
true => { Stroke::NONE }
false => { Self::DEFAULT_STROKE }
@ -50,26 +102,32 @@ impl View {
true => { COLOR_LIGHT }
false => { Color32::WHITE }
};
let b = egui::widgets::Button::new(
RichText::new(icon.to_string()).size(24.0).color(COLOR_DARK)
).min_size(ui.available_size_before_wrap())
let br = Button::new(wt)
.min_size(ui.available_size_before_wrap())
.stroke(stroke)
.fill(color)
.ui(ui).interact(Sense::click_and_drag());
// Click optimization for touch screens
if b.drag_released() || b.clicked() {
(action)();
};
Self::on_button_click(ui, br, action);
}
pub fn sub_title(ui: &mut egui::Ui, text: String) {
ui.label(RichText::new(text.to_uppercase()).size(16.0).color(COLOR_GRAY_DARK));
/// Modal button with white background fill color, contains text.
pub fn modal_button(ui: &mut egui::Ui, text: String, action: impl FnOnce()) {
let mut size = ui.available_size_before_wrap();
size.y = 36.0;
let wt = RichText::new(text.to_uppercase()).size(18.0).color(COLOR_GRAY_DARK);
let br = Button::new(wt)
.stroke(Self::DEFAULT_STROKE)
.min_size(size)
.fill(Color32::WHITE)
.ui(ui).interact(Sense::click_and_drag());
Self::on_button_click(ui, br, action);
}
/// Draw rounded box with some value and label in the middle
/// where is r = [top_left, top_right, bottom_left, bottom_right]
/// Draw rounded box with some value and label in the middle,
/// where is r = (top_left, top_right, bottom_left, bottom_right).
/// | VALUE |
/// | label |
pub fn rounded_box(ui: &mut egui::Ui, value: String, label: String, r: [bool; 4]) {

View file

@ -14,4 +14,4 @@
mod node;
pub use self::node::Node;
pub use node::Node;

View file

@ -25,25 +25,26 @@ use grin_config::config;
use grin_core::global;
use grin_core::global::ChainTypes;
use grin_servers::{Server, ServerStats};
use jni::objects::JString;
use jni::sys::jstring;
use lazy_static::lazy_static;
use log::info;
lazy_static! {
/// Static thread-aware state of [Node] to be updated from another thread.
static ref NODE_STATE: Arc<Node> = Arc::new(Node::default());
}
/// Provides [Server] control, holds current status and statistics.
pub struct Node {
/// Data for UI
/// Statistics data for UI.
stats: Arc<RwLock<Option<ServerStats>>>,
/// Chain type of launched server
/// Chain type of launched server.
chain_type: Arc<RwLock<ChainTypes>>,
/// Indicator if server is starting
/// Indicator if server is starting.
starting: AtomicBool,
/// Thread flag to stop the server and start it again
/// Thread flag to stop the server and start it again.
restart_needed: AtomicBool,
/// Thread flag to stop the server
/// Thread flag to stop the server.
stop_needed: AtomicBool,
}
@ -60,12 +61,12 @@ impl Default for Node {
}
impl Node {
/// Stop server
/// Stop [Server].
pub fn stop() {
NODE_STATE.stop_needed.store(true, Ordering::Relaxed);
}
/// Start server with provided chain type
/// Start [Server] with provided chain type.
pub fn start(chain_type: ChainTypes) {
if !Self::is_running() {
let mut w_chain_type = NODE_STATE.chain_type.write().unwrap();
@ -74,7 +75,7 @@ impl Node {
}
}
/// Restart server with provided chain type
/// Restart [Server] with provided chain type.
pub fn restart(chain_type: ChainTypes) {
if Self::is_running() {
let mut w_chain_type = NODE_STATE.chain_type.write().unwrap();
@ -85,52 +86,52 @@ impl Node {
}
}
/// Check if server is starting
/// Check if [Server] is starting.
pub fn is_starting() -> bool {
NODE_STATE.starting.load(Ordering::Relaxed)
}
/// Check if server is running
/// Check if [Server] is running.
pub fn is_running() -> bool {
Self::get_stats().is_some() || Self::is_starting()
}
/// Check if server is stopping
/// Check if [Server] is stopping.
pub fn is_stopping() -> bool {
NODE_STATE.stop_needed.load(Ordering::Relaxed)
}
/// Check if server is restarting
/// Check if [Server] is restarting.
pub fn is_restarting() -> bool {
NODE_STATE.restart_needed.load(Ordering::Relaxed)
}
/// Get server stats
/// Get [Server] statistics.
pub fn get_stats() -> RwLockReadGuard<'static, Option<ServerStats>> {
NODE_STATE.stats.read().unwrap()
}
/// Get server sync status, empty when server is not running
/// Get [Server] synchronization status, empty when it is not running.
pub fn get_sync_status() -> Option<SyncStatus> {
// return Shutdown status when node is stopping
// Return Shutdown status when node is stopping.
if Self::is_stopping() {
return Some(SyncStatus::Shutdown)
}
// return Initial status when node is starting
// Return Initial status when node is starting.
if Self::is_starting() {
return Some(SyncStatus::Initial)
}
let stats = Self::get_stats();
// return sync status when server is running (stats are not empty)
// Return sync status when server is running (stats are not empty).
if stats.is_some() {
return Some(stats.as_ref().unwrap().sync_status)
}
None
}
/// Start a thread to launch server and update state with server stats
/// Start a thread to launch [Server] and update [NODE_STATE] with server statistics.
fn start_server_thread() -> JoinHandle<()> {
thread::spawn(move || {
NODE_STATE.starting.store(true, Ordering::Relaxed);
@ -142,7 +143,7 @@ impl Node {
if Self::is_restarting() {
server.stop();
// Create new server with current chain type
// Create new server with current chain type.
server = start_server(&NODE_STATE.chain_type.read().unwrap());
NODE_STATE.restart_needed.store(false, Ordering::Relaxed);
@ -152,6 +153,7 @@ impl Node {
let mut w_stats = NODE_STATE.stats.write().unwrap();
*w_stats = None;
NODE_STATE.starting.store(false, Ordering::Relaxed);
NODE_STATE.stop_needed.store(false, Ordering::Relaxed);
break;
} else {
@ -166,18 +168,23 @@ impl Node {
}
}
}
thread::sleep(Duration::from_millis(300));
thread::sleep(Duration::from_millis(250));
}
})
}
/// Get synchronization status i18n text.
pub fn get_sync_status_text(sync_status: Option<SyncStatus>) -> String {
if Node::is_restarting() {
return t!("server_restarting")
return t!("sync_status.server_restarting")
}
if Node::is_stopping() {
return t!("sync_status.shutdown")
}
if sync_status.is_none() {
return t!("server_down")
return t!("sync_status.server_down")
}
match sync_status.unwrap() {
@ -249,8 +256,9 @@ impl Node {
}
/// Start server with provided chain type
/// Start [Server] with provided chain type.
fn start_server(chain_type: &ChainTypes) -> Server {
// Initialize config
let mut node_config_result = config::initial_setup_server(chain_type);
if node_config_result.is_err() {
// Remove config file on init error
@ -268,18 +276,6 @@ fn start_server(chain_type: &ChainTypes) -> Server {
let config = node_config.clone().unwrap();
let server_config = config.members.as_ref().unwrap().server.clone();
// Remove lock file (in case if we have running node from another app)
{
let mut lock_file = PathBuf::from(&server_config.db_root);
lock_file.push("grin.lock");
if lock_file.exists() {
match fs::remove_file(lock_file) {
Ok(_) => {}
Err(_) => { println!("Cannot remove grin.lock file") }
};
}
}
// Remove temporary file dir
{
let mut tmp_dir = PathBuf::from(&server_config.db_root);
@ -332,26 +328,28 @@ fn start_server(chain_type: &ChainTypes) -> Server {
let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) =
Box::leak(Box::new(oneshot::channel::<()>()));
let mut server_result = Server::new(server_config.clone(), None, api_chan);
if server_result.is_err() {
let mut db_path = PathBuf::from(&server_config.db_root);
db_path.push("grin.lock");
fs::remove_file(db_path).unwrap();
// Remove chain data on server start error
let dirs_to_remove: Vec<&str> = vec!["header", "lmdb", "txhashset"];
for dir in dirs_to_remove {
let mut path = PathBuf::from(&server_config.db_root);
path.push(dir);
fs::remove_dir_all(path).unwrap();
}
// Recreate server
let config = node_config.clone().unwrap();
let server_config = config.members.as_ref().unwrap().server.clone();
let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) =
Box::leak(Box::new(oneshot::channel::<()>()));
server_result = Server::new(server_config.clone(), None, api_chan);
}
//TODO: handle server errors
//
// if server_result.is_err() {
// let mut db_path = PathBuf::from(&server_config.db_root);
// db_path.push("grin.lock");
// fs::remove_file(db_path).unwrap();
//
// // Remove chain data on server start error
// let dirs_to_remove: Vec<&str> = vec!["header", "lmdb", "txhashset"];
// for dir in dirs_to_remove {
// let mut path = PathBuf::from(&server_config.db_root);
// path.push(dir);
// fs::remove_dir_all(path).unwrap();
// }
//
// // Recreate server
// let config = node_config.clone().unwrap();
// let server_config = config.members.as_ref().unwrap().server.clone();
// let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) =
// Box::leak(Box::new(oneshot::channel::<()>()));
// server_result = Server::new(server_config.clone(), None, api_chan);
// }
server_result.unwrap()
}
@ -360,6 +358,7 @@ fn start_server(chain_type: &ChainTypes) -> Server {
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Get sync status text for Android notification from [NODE_STATE] in Java string format.
pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncStatusText(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
@ -375,11 +374,25 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncStatusText(
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Get sync title for Android notification in Java string format.
pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncTitle(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) -> jstring {
let j_text = _env.new_string(t!("integrated_node"));
let j_text = _env.new_string(t!("network.node"));
return j_text.unwrap().into_raw();
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Calling on unexpected application termination (removal from recent apps)
pub extern "C" fn Java_mw_gri_android_MainActivity_onTermination(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) {
Node::stop();
}