ui: modals, exit modal, app exit logic on Android, refactor display cutouts, optimize translations
This commit is contained in:
parent
c3ce297373
commit
d3e81826e8
26 changed files with 1051 additions and 484 deletions
|
@ -88,15 +88,14 @@ public class BackgroundService extends Service {
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
mStopped = true;
|
mStopped = true;
|
||||||
|
|
||||||
|
stopForeground(Service.STOP_FOREGROUND_REMOVE);
|
||||||
|
|
||||||
if (mWakeLock.isHeld()) {
|
if (mWakeLock.isHeld()) {
|
||||||
mWakeLock.release();
|
mWakeLock.release();
|
||||||
mWakeLock = null;
|
mWakeLock = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mHandler.removeCallbacks(mUpdateSyncStatus);
|
mHandler.removeCallbacks(mUpdateSyncStatus);
|
||||||
|
|
||||||
stopForeground(Service.STOP_FOREGROUND_REMOVE);
|
|
||||||
stopSelf();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void start(Context context) {
|
public static void start(Context context) {
|
||||||
|
|
|
@ -9,6 +9,8 @@ import android.view.KeyEvent;
|
||||||
import android.view.OrientationEventListener;
|
import android.view.OrientationEventListener;
|
||||||
import com.google.androidgamesdk.GameActivity;
|
import com.google.androidgamesdk.GameActivity;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class MainActivity extends GameActivity {
|
public class MainActivity extends GameActivity {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
@ -17,13 +19,15 @@ public class MainActivity extends GameActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
// Setup HOME environment variable for native code configurations.
|
||||||
try {
|
try {
|
||||||
Os.setenv("HOME", getExternalFilesDir("").getPath(), true);
|
Os.setenv("HOME", getExternalFilesDir("").getPath(), true);
|
||||||
} catch (ErrnoException e) {
|
} catch (ErrnoException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(null);
|
||||||
|
|
||||||
|
// Callback to update display cutouts at native code.
|
||||||
OrientationEventListener orientationEventListener = new OrientationEventListener(this,
|
OrientationEventListener orientationEventListener = new OrientationEventListener(this,
|
||||||
SensorManager.SENSOR_DELAY_GAME) {
|
SensorManager.SENSOR_DELAY_GAME) {
|
||||||
@Override
|
@Override
|
||||||
|
@ -34,9 +38,10 @@ public class MainActivity extends GameActivity {
|
||||||
if (orientationEventListener.canDetectOrientation()) {
|
if (orientationEventListener.canDetectOrientation()) {
|
||||||
orientationEventListener.enable();
|
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);
|
native void onDisplayCutoutsChanged(int[] cutouts);
|
||||||
|
@ -52,23 +57,35 @@ public class MainActivity extends GameActivity {
|
||||||
|
|
||||||
public native void onBackButtonPress();
|
public native void onBackButtonPress();
|
||||||
|
|
||||||
|
private boolean mManualExit;
|
||||||
|
private final AtomicBoolean mActivityDestroyed = new AtomicBoolean(false);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
if (!mManualExit) {
|
if (!mManualExit) {
|
||||||
BackgroundService.stop(getApplicationContext());
|
onTermination();
|
||||||
// Temporary fix to prevent app hanging when closed from recent apps
|
}
|
||||||
|
// 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());
|
Process.killProcess(Process.myPid());
|
||||||
}
|
}
|
||||||
super.onDestroy();
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
super.onDestroy();
|
||||||
|
mActivityDestroyed.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean mManualExit = false;
|
|
||||||
|
|
||||||
// Called from native code
|
// Called from native code
|
||||||
public void onExit() {
|
public void onExit() {
|
||||||
mManualExit = true;
|
mManualExit = true;
|
||||||
BackgroundService.stop(getApplicationContext());
|
BackgroundService.stop(this);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public native void onTermination();
|
||||||
}
|
}
|
|
@ -1,22 +1,13 @@
|
||||||
accounts: Accounts
|
screen_accounts:
|
||||||
integrated_node: Integrated Node
|
title: Accounts
|
||||||
|
network:
|
||||||
|
node: Integrated node
|
||||||
metrics: Metrics
|
metrics: Metrics
|
||||||
settings: Settings
|
mining: Mining
|
||||||
|
settings: Server settings
|
||||||
|
sync_status:
|
||||||
server_restarting: Server is restarting
|
server_restarting: Server is restarting
|
||||||
server_down: Server is down
|
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
|
|
||||||
sync_status:
|
|
||||||
initial: Server is starting
|
initial: Server is starting
|
||||||
no_sync: Server is running
|
no_sync: Server is running
|
||||||
awaiting_peers: Waiting for peers
|
awaiting_peers: Waiting for peers
|
||||||
|
@ -30,10 +21,29 @@ sync_status:
|
||||||
tx_hashset_save: Finalizing chain state
|
tx_hashset_save: Finalizing chain state
|
||||||
body_sync: Downloading blocks
|
body_sync: Downloading blocks
|
||||||
body_sync_percent: 'Downloading blocks: %{percent}%'
|
body_sync_percent: 'Downloading blocks: %{percent}%'
|
||||||
shutdown: Shutting down
|
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
|
emission: Emission
|
||||||
inflation: Inflation
|
inflation: Inflation
|
||||||
supply: Supply
|
supply: Supply
|
||||||
block_time: Block time
|
block_time: Block time
|
||||||
reward: Reward
|
reward: Reward
|
||||||
difficulty_at_window: 'Difficulty at window %{size}'
|
difficulty_window: 'Difficulty at window %{size}'
|
||||||
|
modal:
|
||||||
|
cancel: Cancel
|
||||||
|
modal_exit:
|
||||||
|
description: Are you sure you want to quit the app?
|
||||||
|
exit: Exit
|
|
@ -1,22 +1,13 @@
|
||||||
accounts: Аккаунты
|
screen_accounts:
|
||||||
integrated_node: Встроенный узел
|
title: Аккаунты
|
||||||
|
network:
|
||||||
|
node: Встроенный узел
|
||||||
metrics: Метрики
|
metrics: Метрики
|
||||||
settings: Настройки
|
mining: Майнинг
|
||||||
|
settings: Настройки сервера
|
||||||
|
sync_status:
|
||||||
server_restarting: Сервер перезапускается
|
server_restarting: Сервер перезапускается
|
||||||
server_down: Сервер выключен
|
server_down: Сервер выключен
|
||||||
header: Заголовок
|
|
||||||
block: Блок
|
|
||||||
hash: Хэш
|
|
||||||
height: Высота
|
|
||||||
difficulty: Сложность
|
|
||||||
time_utc: Время (UTC)
|
|
||||||
transactions: Транзакции
|
|
||||||
main_pool: Основной пул
|
|
||||||
stem_pool: Stem пул
|
|
||||||
data: Данные
|
|
||||||
size: Размер (ГБ)
|
|
||||||
peers: Пиры
|
|
||||||
sync_status:
|
|
||||||
initial: Запуск сервера
|
initial: Запуск сервера
|
||||||
no_sync: Сервер запущен
|
no_sync: Сервер запущен
|
||||||
awaiting_peers: Ожидание пиров
|
awaiting_peers: Ожидание пиров
|
||||||
|
@ -30,10 +21,29 @@ sync_status:
|
||||||
tx_hashset_save: Сохранение состояния цепи
|
tx_hashset_save: Сохранение состояния цепи
|
||||||
body_sync: Загрузка блоков
|
body_sync: Загрузка блоков
|
||||||
body_sync_percent: 'Загрузка блоков: %{percent}%'
|
body_sync_percent: 'Загрузка блоков: %{percent}%'
|
||||||
shutdown: Выключение
|
shutdown: Выключение сервера
|
||||||
|
network_node:
|
||||||
|
header: Заголовок
|
||||||
|
block: Блок
|
||||||
|
hash: Хэш
|
||||||
|
height: Высота
|
||||||
|
difficulty: Сложность
|
||||||
|
time_utc: Время (UTC)
|
||||||
|
transactions: Транзакции
|
||||||
|
main_pool: Основной пул
|
||||||
|
stem_pool: Stem пул
|
||||||
|
data: Данные
|
||||||
|
size: Размер (ГБ)
|
||||||
|
peers: Пиры
|
||||||
|
network_metrics:
|
||||||
emission: Эмиссия
|
emission: Эмиссия
|
||||||
inflation: Инфляция
|
inflation: Инфляция
|
||||||
supply: Предложение
|
supply: Предложение
|
||||||
block_time: Время блока
|
block_time: Время блока
|
||||||
reward: Награда
|
reward: Награда
|
||||||
difficulty_at_window: 'Сложность в окне %{size}'
|
difficulty_window: 'Сложность в окне %{size}'
|
||||||
|
modal:
|
||||||
|
cancel: Отмена
|
||||||
|
modal_exit:
|
||||||
|
description: Вы уверены, что хотите выйти из приложения?
|
||||||
|
exit: Выход
|
|
@ -13,11 +13,13 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use eframe::{AppCreator, NativeOptions, Renderer, Theme};
|
use eframe::{AppCreator, NativeOptions, Renderer, Theme};
|
||||||
|
use grin_core::global::ChainTypes;
|
||||||
use log::LevelFilter::Info;
|
use log::LevelFilter::Info;
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
use winit::platform::android::activity::AndroidApp;
|
use winit::platform::android::activity::AndroidApp;
|
||||||
|
|
||||||
use crate::gui::PlatformApp;
|
use crate::gui::PlatformApp;
|
||||||
|
use crate::node::Node;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
|
@ -67,6 +69,9 @@ fn start(mut options: NativeOptions, app_creator: AppCreator) {
|
||||||
|
|
||||||
setup_i18n();
|
setup_i18n();
|
||||||
|
|
||||||
|
//TODO: Take network type and server check from config
|
||||||
|
Node::start(ChainTypes::Mainnet);
|
||||||
|
|
||||||
eframe::run_native("Grim", options, app_creator);
|
eframe::run_native("Grim", options, app_creator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
104
src/gui/app.rs
104
src/gui/app.rs
|
@ -12,53 +12,109 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use eframe::epaint::Stroke;
|
use egui::{Context, Stroke, Widget};
|
||||||
use egui::{Context, Frame};
|
use egui::os::OperatingSystem;
|
||||||
use egui::style::Margin;
|
use egui::style::Margin;
|
||||||
|
|
||||||
use crate::gui::colors::COLOR_LIGHT;
|
use crate::gui::colors::COLOR_LIGHT;
|
||||||
|
use crate::gui::Navigator;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::screens::Root;
|
use crate::gui::screens::Root;
|
||||||
|
use crate::gui::views::{Modal, ModalId, ModalLocation, ProgressLoading, View};
|
||||||
|
use crate::node::Node;
|
||||||
|
|
||||||
pub struct PlatformApp<Platform> {
|
pub struct PlatformApp<Platform> {
|
||||||
pub(crate) app: App,
|
pub(crate) app: App,
|
||||||
pub(crate) platform: Platform,
|
pub(crate) platform: Platform,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
root: Root,
|
root: Root,
|
||||||
}
|
show_exit_progress: bool
|
||||||
|
|
||||||
impl Default for App {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
root: Root::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn ui(&mut self, ctx: &Context, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
|
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()
|
egui::CentralPanel::default()
|
||||||
.frame(Frame {
|
.frame(egui::Frame {
|
||||||
inner_margin: Margin::same(0.0),
|
|
||||||
outer_margin: Margin::same(0.0),
|
|
||||||
stroke: Stroke::NONE,
|
|
||||||
fill: COLOR_LIGHT,
|
fill: COLOR_LIGHT,
|
||||||
.. Default::default()
|
.. Default::default()
|
||||||
})
|
})
|
||||||
.show(ctx, |ui| {
|
.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) {
|
||||||
pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool {
|
match OperatingSystem::from_target_os() {
|
||||||
is_landscape(frame) && frame.info().window_info.size.x > 400.0
|
OperatingSystem::Android => {
|
||||||
}
|
cb.exit();
|
||||||
|
}
|
||||||
pub fn is_landscape(frame: &mut eframe::Frame) -> bool {
|
OperatingSystem::IOS => {
|
||||||
return frame.info().window_info.size.x > frame.info().window_info.size.y
|
//TODO: exit on iOS
|
||||||
|
}
|
||||||
|
OperatingSystem::Nix | OperatingSystem::Mac | OperatingSystem::Windows => {
|
||||||
|
frame.close();
|
||||||
|
}
|
||||||
|
// Web
|
||||||
|
OperatingSystem::Unknown => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,14 +13,14 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
pub use app::App;
|
mod app;
|
||||||
pub use app::PlatformApp;
|
pub use app::{App, PlatformApp};
|
||||||
|
|
||||||
|
mod navigator;
|
||||||
|
pub use navigator::Navigator;
|
||||||
|
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod screens;
|
pub mod screens;
|
||||||
pub mod views;
|
pub mod views;
|
||||||
pub mod icons;
|
pub mod icons;
|
||||||
pub mod colors;
|
pub mod colors;
|
||||||
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
|
|
209
src/gui/navigator.rs
Normal file
209
src/gui/navigator.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
use std::sync::atomic::{AtomicI32, Ordering};
|
||||||
use eframe::epaint::Stroke;
|
use eframe::epaint::Stroke;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use winit::platform::android::activity::AndroidApp;
|
use winit::platform::android::activity::AndroidApp;
|
||||||
|
@ -34,22 +34,33 @@ impl Android {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformCallbacks for Android {
|
impl PlatformCallbacks for Android {
|
||||||
fn show_keyboard(&mut self) {
|
fn show_keyboard(&self) {
|
||||||
self.android_app.show_soft_input(true);
|
self.android_app.show_soft_input(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_keyboard(&mut self) {
|
fn hide_keyboard(&self) {
|
||||||
self.android_app.hide_soft_input(true);
|
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
|
//TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_string_from_buffer(&mut self) -> String {
|
fn get_string_from_buffer(&self) -> String {
|
||||||
//TODO
|
//TODO
|
||||||
"".to_string()
|
"".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
|
//TODO
|
||||||
|
|
|
@ -21,8 +21,9 @@ pub mod platform;
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
|
||||||
pub trait PlatformCallbacks {
|
pub trait PlatformCallbacks {
|
||||||
fn show_keyboard(&mut self);
|
fn show_keyboard(&self);
|
||||||
fn hide_keyboard(&mut self);
|
fn hide_keyboard(&self);
|
||||||
fn copy_string_to_buffer(&mut self, data: String);
|
fn copy_string_to_buffer(&self, data: String);
|
||||||
fn get_string_from_buffer(&mut self) -> String;
|
fn get_string_from_buffer(&self) -> String;
|
||||||
|
fn exit(&self);
|
||||||
}
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::screens::{Navigator, ScreenId};
|
use crate::gui::screens::ScreenId;
|
||||||
|
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
|
|
||||||
|
|
|
@ -14,41 +14,49 @@
|
||||||
|
|
||||||
use egui::Frame;
|
use egui::Frame;
|
||||||
|
|
||||||
use crate::gui::app::is_dual_panel_mode;
|
use crate::gui::icons::{ARROW_CIRCLE_LEFT, GLOBE, PLUS};
|
||||||
use crate::gui::icons::{ARROW_CIRCLE_LEFT, GEAR_SIX, GLOBE};
|
use crate::gui::Navigator;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
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};
|
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 {
|
impl Screen for Accounts {
|
||||||
fn id(&self) -> ScreenId {
|
fn id(&self) -> ScreenId {
|
||||||
ScreenId::Accounts
|
ScreenId::Accounts
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(&mut self,
|
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
|
||||||
ui: &mut egui::Ui,
|
let Self { title } = self;
|
||||||
frame: &mut eframe::Frame,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
TitlePanel::new(title)
|
||||||
TitlePanel::new(t!("accounts"))
|
.ui(if !View::is_dual_panel_mode(frame) {
|
||||||
.left_action(
|
TitlePanelAction::new(GLOBE, || {
|
||||||
if !is_dual_panel_mode(frame) {
|
|
||||||
TitlePanelAction::new(GLOBE.into(), || {
|
|
||||||
Navigator::toggle_side_panel();
|
Navigator::toggle_side_panel();
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}, TitlePanelAction::new(PLUS, || {
|
||||||
).right_action(TitlePanelAction::new(GEAR_SIX.into(), || {
|
//TODO: add account
|
||||||
//TODO: settings
|
}), ui);
|
||||||
})).ui(ui);
|
|
||||||
|
|
||||||
egui::CentralPanel::default().frame(Frame {
|
egui::CentralPanel::default()
|
||||||
|
.frame(Frame {
|
||||||
stroke: View::DEFAULT_STROKE,
|
stroke: View::DEFAULT_STROKE,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}).show_inside(ui, |ui| {
|
})
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
ui.label(format!("{}Here we go 10000 ツ", ARROW_CIRCLE_LEFT));
|
ui.label(format!("{}Here we go 10000 ツ", ARROW_CIRCLE_LEFT));
|
||||||
if ui.button("TEST").clicked() {
|
if ui.button("TEST").clicked() {
|
||||||
Navigator::to(ScreenId::Account)
|
Navigator::to(ScreenId::Account)
|
||||||
|
|
|
@ -14,12 +14,10 @@
|
||||||
|
|
||||||
pub use account::Account;
|
pub use account::Account;
|
||||||
pub use accounts::Accounts;
|
pub use accounts::Accounts;
|
||||||
pub use navigator::Navigator;
|
|
||||||
pub use root::Root;
|
pub use root::Root;
|
||||||
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
|
||||||
mod navigator;
|
|
||||||
mod root;
|
mod root;
|
||||||
mod accounts;
|
mod accounts;
|
||||||
mod account;
|
mod account;
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,10 +14,10 @@
|
||||||
|
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
|
|
||||||
use crate::gui::app::is_dual_panel_mode;
|
use crate::gui::Navigator;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::screens::{Account, Accounts, Navigator, Screen, ScreenId};
|
use crate::gui::screens::{Account, Accounts, Screen, ScreenId};
|
||||||
use crate::gui::views::Network;
|
use crate::gui::views::{Network, View};
|
||||||
|
|
||||||
pub struct Root {
|
pub struct Root {
|
||||||
screens: Vec<Box<dyn Screen>>,
|
screens: Vec<Box<dyn Screen>>,
|
||||||
|
@ -26,7 +26,7 @@ pub struct Root {
|
||||||
|
|
||||||
impl Default for Root {
|
impl Default for Root {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Navigator::init_from(ScreenId::Accounts);
|
Navigator::init(ScreenId::Accounts);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
screens: (vec![
|
screens: (vec![
|
||||||
|
@ -40,31 +40,23 @@ impl Default for Root {
|
||||||
|
|
||||||
impl Root {
|
impl Root {
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
|
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")
|
egui::SidePanel::left("network_panel")
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.exact_width(if is_dual_panel_mode(frame) {
|
.exact_width(panel_width)
|
||||||
min(frame.info().window_info.size.x as i64, 400) as f32
|
.frame(egui::Frame::default())
|
||||||
} else {
|
.show_animated_inside(ui, is_panel_open, |ui| {
|
||||||
frame.info().window_info.size.x
|
|
||||||
})
|
|
||||||
.frame(egui::Frame {
|
|
||||||
.. Default::default()
|
|
||||||
})
|
|
||||||
.show_animated_inside(ui, is_network_panel_open, |ui| {
|
|
||||||
self.network.ui(ui, frame, cb);
|
self.network.ui(ui, frame, cb);
|
||||||
});
|
});
|
||||||
|
|
||||||
egui::CentralPanel::default().frame(egui::Frame {
|
egui::CentralPanel::default()
|
||||||
..Default::default()
|
.frame(egui::Frame::default())
|
||||||
}).show_inside(ui, |ui| {
|
.show_inside(ui, |ui| {
|
||||||
self.show_current_screen(ui, frame, cb);
|
self.show_current_screen(ui, frame, cb);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_current_screen(&mut self,
|
fn show_current_screen(&mut self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
frame: &mut eframe::Frame,
|
frame: &mut eframe::Frame,
|
||||||
cb: &dyn PlatformCallbacks) {
|
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)]
|
#[allow(dead_code)]
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
|
|
@ -13,22 +13,25 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
mod views;
|
mod views;
|
||||||
pub use self::views::View;
|
pub use views::View;
|
||||||
|
|
||||||
mod title_panel;
|
mod title_panel;
|
||||||
pub use self::title_panel::{TitlePanel, TitlePanelAction};
|
pub use title_panel::*;
|
||||||
|
|
||||||
|
mod modal;
|
||||||
|
pub use modal::*;
|
||||||
|
|
||||||
mod network;
|
mod network;
|
||||||
mod network_node;
|
mod network_node;
|
||||||
mod network_tuning;
|
mod network_settings;
|
||||||
mod network_metrics;
|
mod network_metrics;
|
||||||
mod network_mining;
|
mod network_mining;
|
||||||
|
pub use network::Network;
|
||||||
|
|
||||||
pub use self::network::Network;
|
mod progress_loading;
|
||||||
|
pub use progress_loading::ProgressLoading;
|
||||||
|
|
||||||
pub trait NetworkTab {
|
pub trait NetworkTab {
|
||||||
fn name(&self) -> &String;
|
fn name(&self) -> &String;
|
||||||
fn ui(&mut self, ui: &mut egui::Ui);
|
fn ui(&mut self, ui: &mut egui::Ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
260
src/gui/views/modal.rs
Normal file
260
src/gui/views/modal.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,20 +14,16 @@
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use eframe::emath::lerp;
|
use egui::{Color32, lerp, Rgba, RichText, Stroke};
|
||||||
use eframe::epaint::{Color32, FontId, Rgba, Stroke};
|
|
||||||
use eframe::epaint::text::{LayoutJob, TextFormat, TextWrapping};
|
|
||||||
use egui::RichText;
|
|
||||||
use egui::style::Margin;
|
use egui::style::Margin;
|
||||||
use egui_extras::{Size, StripBuilder};
|
use egui_extras::{Size, StripBuilder};
|
||||||
use grin_chain::SyncStatus;
|
use grin_chain::SyncStatus;
|
||||||
use grin_core::global::ChainTypes;
|
use grin_core::global::ChainTypes;
|
||||||
|
|
||||||
use crate::gui::app::is_dual_panel_mode;
|
use crate::gui::colors::{COLOR_DARK, COLOR_GRAY_DARK, COLOR_YELLOW};
|
||||||
use crate::gui::colors::{COLOR_DARK, COLOR_YELLOW};
|
|
||||||
use crate::gui::icons::{CARDHOLDER, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE};
|
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::platform::PlatformCallbacks;
|
||||||
use crate::gui::screens::Navigator;
|
|
||||||
use crate::gui::views::{NetworkTab, View};
|
use crate::gui::views::{NetworkTab, View};
|
||||||
use crate::gui::views::network_metrics::NetworkMetrics;
|
use crate::gui::views::network_metrics::NetworkMetrics;
|
||||||
use crate::gui::views::network_node::NetworkNode;
|
use crate::gui::views::network_node::NetworkNode;
|
||||||
|
@ -50,7 +46,6 @@ pub struct Network {
|
||||||
|
|
||||||
impl Default for Network {
|
impl Default for Network {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Node::start(ChainTypes::Mainnet);
|
|
||||||
Self {
|
Self {
|
||||||
current_mode: Mode::Node,
|
current_mode: Mode::Node,
|
||||||
node_view: NetworkNode::default(),
|
node_view: NetworkNode::default(),
|
||||||
|
@ -60,11 +55,7 @@ impl Default for Network {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Network {
|
impl Network {
|
||||||
pub fn ui(&mut self,
|
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, _: &dyn PlatformCallbacks) {
|
||||||
ui: &mut egui::Ui,
|
|
||||||
frame: &mut eframe::Frame,
|
|
||||||
_: &dyn PlatformCallbacks) {
|
|
||||||
|
|
||||||
egui::TopBottomPanel::top("network_title")
|
egui::TopBottomPanel::top("network_title")
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.frame(egui::Frame {
|
.frame(egui::Frame {
|
||||||
|
@ -88,19 +79,20 @@ impl Network {
|
||||||
self.draw_tabs(ui);
|
self.draw_tabs(ui);
|
||||||
});
|
});
|
||||||
|
|
||||||
egui::CentralPanel::default().frame(egui::Frame {
|
egui::CentralPanel::default()
|
||||||
|
.frame(egui::Frame {
|
||||||
stroke: View::DEFAULT_STROKE,
|
stroke: View::DEFAULT_STROKE,
|
||||||
inner_margin: Margin::same(4.0),
|
inner_margin: Margin::same(4.0),
|
||||||
fill: Color32::WHITE,
|
fill: Color32::WHITE,
|
||||||
.. Default::default()
|
.. Default::default()
|
||||||
}).show_inside(ui, |ui| {
|
})
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
self.draw_tab_content(ui);
|
self.draw_tab_content(ui);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_tabs(&mut self, ui: &mut egui::Ui) {
|
fn draw_tabs(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.scope(|ui| {
|
||||||
//Setup spacing between tabs
|
//Setup spacing between tabs
|
||||||
ui.style_mut().spacing.item_spacing = egui::vec2(6.0, 0.0);
|
ui.style_mut().spacing.item_spacing = egui::vec2(6.0, 0.0);
|
||||||
|
|
||||||
|
@ -125,7 +117,7 @@ impl Network {
|
||||||
self.current_mode = Mode::Tuning;
|
self.current_mode = Mode::Tuning;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +157,7 @@ impl Network {
|
||||||
self.draw_title_text(builder);
|
self.draw_title_text(builder);
|
||||||
});
|
});
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
if !is_dual_panel_mode(frame) {
|
if !View::is_dual_panel_mode(frame) {
|
||||||
ui.centered_and_justified(|ui| {
|
ui.centered_and_justified(|ui| {
|
||||||
View::title_button(ui, CARDHOLDER, || {
|
View::title_button(ui, CARDHOLDER, || {
|
||||||
Navigator::toggle_side_panel();
|
Navigator::toggle_side_panel();
|
||||||
|
@ -195,13 +187,14 @@ impl Network {
|
||||||
};
|
};
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.size(Size::exact(19.0))
|
|
||||||
.size(Size::remainder())
|
.size(Size::remainder())
|
||||||
|
.size(Size::exact(32.0))
|
||||||
.vertical(|mut strip| {
|
.vertical(|mut strip| {
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
ui.centered_and_justified(|ui| {
|
ui.add_space(2.0);
|
||||||
ui.label(RichText::new(title_text.to_uppercase())
|
ui.vertical_centered(|ui| {
|
||||||
.size(19.0)
|
ui.label(RichText::new(title_text)
|
||||||
|
.size(18.0)
|
||||||
.color(COLOR_DARK));
|
.color(COLOR_DARK));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -223,26 +216,17 @@ impl Network {
|
||||||
bright
|
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
|
// Repaint based on sync status
|
||||||
if idle {
|
if idle {
|
||||||
ui.ctx().request_repaint_after(Duration::from_millis(600));
|
ui.ctx().request_repaint_after(Duration::from_millis(600));
|
||||||
} else {
|
} else {
|
||||||
ui.ctx().request_repaint();
|
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,7 +29,7 @@ pub struct NetworkMetrics {
|
||||||
impl Default for NetworkMetrics {
|
impl Default for NetworkMetrics {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: t!("metrics"),
|
title: t!("network.metrics").to_uppercase(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ impl NetworkTab for NetworkMetrics {
|
||||||
|
|
||||||
// Show emission info
|
// Show emission info
|
||||||
ui.vertical_centered_justified(|ui| {
|
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);
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
@ -68,19 +68,19 @@ impl NetworkTab for NetworkMetrics {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
format!("{}ツ", BLOCK_REWARD),
|
format!("{}ツ", BLOCK_REWARD),
|
||||||
t!("reward"),
|
t!("network_metrics.reward"),
|
||||||
[true, false, true, false]);
|
[true, false, true, false]);
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
format!("{:.2}%", rate),
|
format!("{:.2}%", rate),
|
||||||
t!("inflation"),
|
t!("network_metrics.inflation"),
|
||||||
[false, false, false, false]);
|
[false, false, false, false]);
|
||||||
});
|
});
|
||||||
columns[2].vertical_centered(|ui| {
|
columns[2].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
supply.to_string(),
|
supply.to_string(),
|
||||||
t!("supply"),
|
t!("network_metrics.supply"),
|
||||||
[false, true, false, true]);
|
[false, true, false, true]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -88,27 +88,30 @@ impl NetworkTab for NetworkMetrics {
|
||||||
|
|
||||||
// Show difficulty adjustment window info
|
// Show difficulty adjustment window info
|
||||||
ui.vertical_centered_justified(|ui| {
|
ui.vertical_centered_justified(|ui| {
|
||||||
let title = t!("difficulty_at_window", "size" => stats.diff_stats.window_size);
|
let title = t!(
|
||||||
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, title));
|
"network_metrics.difficulty_window",
|
||||||
|
"size" => stats.diff_stats.window_size
|
||||||
|
);
|
||||||
|
View::sub_header(ui, format!("{} {}", HOURGLASS_MEDIUM, title));
|
||||||
});
|
});
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
ui.columns(3, |columns| {
|
ui.columns(3, |columns| {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
stats.diff_stats.height.to_string(),
|
stats.diff_stats.height.to_string(),
|
||||||
t!("height"),
|
t!("network_node.height"),
|
||||||
[true, false, true, false]);
|
[true, false, true, false]);
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
format!("{}s", stats.diff_stats.average_block_time),
|
format!("{}s", stats.diff_stats.average_block_time),
|
||||||
t!("block_time"),
|
t!("network_metrics.block_time"),
|
||||||
[false, false, false, false]);
|
[false, false, false, false]);
|
||||||
});
|
});
|
||||||
columns[2].vertical_centered(|ui| {
|
columns[2].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
stats.diff_stats.average_difficulty.to_string(),
|
stats.diff_stats.average_difficulty.to_string(),
|
||||||
t!("difficulty"),
|
t!("network_node.difficulty"),
|
||||||
[false, true, false, true]);
|
[false, true, false, true]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -119,7 +122,7 @@ impl NetworkTab for NetworkMetrics {
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.auto_shrink([false; 2])
|
.auto_shrink([false; 2])
|
||||||
.stick_to_bottom(true)
|
.stick_to_bottom(true)
|
||||||
.id_source("diff_scroll")
|
.id_source("difficulty_scroll")
|
||||||
.show_rows(
|
.show_rows(
|
||||||
ui,
|
ui,
|
||||||
DIFF_BLOCK_UI_HEIGHT,
|
DIFF_BLOCK_UI_HEIGHT,
|
||||||
|
|
|
@ -28,7 +28,7 @@ pub struct NetworkNode {
|
||||||
impl Default for NetworkNode {
|
impl Default for NetworkNode {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: t!("integrated_node"),
|
title: t!("network.node").to_uppercase(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,20 +54,20 @@ impl NetworkTab for NetworkNode {
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
// Show header stats
|
// Show header stats
|
||||||
ui.vertical_centered_justified(|ui| {
|
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.add_space(4.0);
|
||||||
ui.columns(2, |columns| {
|
ui.columns(2, |columns| {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
stats.header_stats.last_block_h.to_string(),
|
stats.header_stats.last_block_h.to_string(),
|
||||||
t!("hash"),
|
t!("network_node.hash"),
|
||||||
[true, false, false, false]);
|
[true, false, false, false]);
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
stats.header_stats.height.to_string(),
|
stats.header_stats.height.to_string(),
|
||||||
t!("height"),
|
t!("network_node.height"),
|
||||||
[false, true, false, false]);
|
[false, true, false, false]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -75,14 +75,14 @@ impl NetworkTab for NetworkNode {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
stats.header_stats.total_difficulty.to_string(),
|
stats.header_stats.total_difficulty.to_string(),
|
||||||
t!("difficulty"),
|
t!("network_node.difficulty"),
|
||||||
[false, false, true, false]);
|
[false, false, true, false]);
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|ui| {
|
||||||
let h_ts = stats.header_stats.latest_timestamp;
|
let h_ts = stats.header_stats.latest_timestamp;
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
format!("{}", h_ts.format("%d/%m/%Y %H:%M")),
|
format!("{}", h_ts.format("%d/%m/%Y %H:%M")),
|
||||||
t!("time_utc"),
|
t!("network_node.time_utc"),
|
||||||
[false, false, false, true]);
|
[false, false, false, true]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -90,20 +90,20 @@ impl NetworkTab for NetworkNode {
|
||||||
// Show block stats
|
// Show block stats
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.vertical_centered_justified(|ui| {
|
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.add_space(4.0);
|
||||||
ui.columns(2, |columns| {
|
ui.columns(2, |columns| {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
stats.chain_stats.last_block_h.to_string(),
|
stats.chain_stats.last_block_h.to_string(),
|
||||||
t!("hash"),
|
t!("network_node.hash"),
|
||||||
[true, false, false, false]);
|
[true, false, false, false]);
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
stats.chain_stats.height.to_string(),
|
stats.chain_stats.height.to_string(),
|
||||||
t!("height"),
|
t!("network_node.height"),
|
||||||
[false, true, false, false]);
|
[false, true, false, false]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -111,14 +111,14 @@ impl NetworkTab for NetworkNode {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
stats.chain_stats.total_difficulty.to_string(),
|
stats.chain_stats.total_difficulty.to_string(),
|
||||||
t!("difficulty"),
|
t!("network_node.difficulty"),
|
||||||
[false, false, true, false]);
|
[false, false, true, false]);
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|ui| {
|
||||||
let b_ts = stats.chain_stats.latest_timestamp;
|
let b_ts = stats.chain_stats.latest_timestamp;
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
format!("{}", b_ts.format("%d/%m/%Y %H:%M")),
|
format!("{}", b_ts.format("%d/%m/%Y %H:%M")),
|
||||||
t!("time_utc"),
|
t!("network_node.time_utc"),
|
||||||
[false, false, false, true]);
|
[false, false, false, true]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -126,7 +126,7 @@ impl NetworkTab for NetworkNode {
|
||||||
// Show data stats
|
// Show data stats
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.vertical_centered_justified(|ui| {
|
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.add_space(4.0);
|
||||||
ui.columns(2, |columns| {
|
ui.columns(2, |columns| {
|
||||||
|
@ -139,7 +139,7 @@ impl NetworkTab for NetworkNode {
|
||||||
};
|
};
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
tx_stat,
|
tx_stat,
|
||||||
t!("main_pool"),
|
t!("network_node.main_pool"),
|
||||||
[true, false, false, false]);
|
[true, false, false, false]);
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|ui| {
|
||||||
|
@ -151,7 +151,7 @@ impl NetworkTab for NetworkNode {
|
||||||
};
|
};
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
stem_tx_stat,
|
stem_tx_stat,
|
||||||
t!("stem_pool"),
|
t!("network_node.stem_pool"),
|
||||||
[false, true, false, false]);
|
[false, true, false, false]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -159,13 +159,13 @@ impl NetworkTab for NetworkNode {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
stats.disk_usage_gb.to_string(),
|
stats.disk_usage_gb.to_string(),
|
||||||
t!("size"),
|
t!("network_node.size"),
|
||||||
[false, false, true, false]);
|
[false, false, true, false]);
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|ui| {
|
||||||
View::rounded_box(ui,
|
View::rounded_box(ui,
|
||||||
stats.peer_count.to_string(),
|
stats.peer_count.to_string(),
|
||||||
t!("peers"),
|
t!("network_node.peers"),
|
||||||
[false, false, false, true]);
|
[false, false, false, true]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -174,7 +174,7 @@ impl NetworkTab for NetworkNode {
|
||||||
if stats.peer_count > 0 {
|
if stats.peer_count > 0 {
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.vertical_centered_justified(|ui| {
|
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);
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
|
39
src/gui/views/progress_loading.rs
Normal file
39
src/gui/views/progress_loading.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,57 +12,36 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use eframe::epaint::{FontId, Stroke};
|
|
||||||
use eframe::epaint::text::{LayoutJob, TextFormat, TextWrapping};
|
|
||||||
use egui::style::Margin;
|
use egui::style::Margin;
|
||||||
use egui_extras::{Size, StripBuilder};
|
use egui_extras::{Size, StripBuilder};
|
||||||
|
|
||||||
use crate::gui::colors::{COLOR_DARK, COLOR_YELLOW};
|
use crate::gui::colors::{COLOR_DARK, COLOR_YELLOW};
|
||||||
use crate::gui::screens::Navigator;
|
|
||||||
use crate::gui::views::View;
|
use crate::gui::views::View;
|
||||||
|
|
||||||
pub struct TitlePanelAction {
|
pub struct TitlePanelAction<'action> {
|
||||||
pub(crate) icon: Box<str>,
|
pub(crate) icon: Box<&'action str>,
|
||||||
pub(crate) on_click: Box<dyn Fn()>,
|
pub(crate) on_click: Box<dyn Fn()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TitlePanelAction {
|
impl<'action> TitlePanelAction<'action> {
|
||||||
pub fn new(icon: Box<str>, on_click: fn()) -> Option<Self> {
|
pub fn new(icon: &'action str, on_click: fn()) -> Option<Self> {
|
||||||
Option::from(Self { icon, on_click: Box::new(on_click) })
|
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 {
|
pub struct TitlePanel {
|
||||||
title: String,
|
title: String,
|
||||||
actions: TitlePanelActions,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TitlePanel {
|
impl TitlePanel {
|
||||||
pub fn new(title: String) -> Self {
|
const PANEL_SIZE: f32 = 52.0;
|
||||||
Self {
|
|
||||||
title,
|
pub fn new(title: &String) -> Self {
|
||||||
actions: TitlePanelActions::default()
|
Self { title: title.to_uppercase() }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left_action(mut self, action: Option<TitlePanelAction>) -> Self {
|
pub fn ui(&self, l: Option<TitlePanelAction>, r: Option<TitlePanelAction>, ui: &mut egui::Ui) {
|
||||||
self.actions.left = action;
|
let Self { title } = self;
|
||||||
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;
|
|
||||||
|
|
||||||
egui::TopBottomPanel::top("title_panel")
|
egui::TopBottomPanel::top("title_panel")
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
|
@ -70,33 +49,29 @@ impl TitlePanel {
|
||||||
fill: COLOR_YELLOW,
|
fill: COLOR_YELLOW,
|
||||||
inner_margin: Margin::same(0.0),
|
inner_margin: Margin::same(0.0),
|
||||||
outer_margin: Margin::same(0.0),
|
outer_margin: Margin::same(0.0),
|
||||||
stroke: Stroke::NONE,
|
stroke: egui::Stroke::NONE,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.show_inside(ui, |ui| {
|
.show_inside(ui, |ui| {
|
||||||
StripBuilder::new(ui)
|
StripBuilder::new(ui)
|
||||||
.size(Size::exact(52.0))
|
.size(Size::exact(Self::PANEL_SIZE))
|
||||||
.vertical(|mut strip| {
|
.vertical(|mut strip| {
|
||||||
strip.strip(|builder| {
|
strip.strip(|builder| {
|
||||||
builder
|
builder
|
||||||
.size(Size::exact(52.0))
|
.size(Size::exact(Self::PANEL_SIZE))
|
||||||
.size(Size::remainder())
|
.size(Size::remainder())
|
||||||
.size(Size::exact(52.0))
|
.size(Size::exact(Self::PANEL_SIZE))
|
||||||
.horizontal(|mut strip| {
|
.horizontal(|mut strip| {
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
show_action(ui, actions.left.as_ref());
|
show_action(ui, l.as_ref());
|
||||||
});
|
});
|
||||||
strip.strip(|builder| {
|
|
||||||
builder
|
|
||||||
.size(Size::remainder())
|
|
||||||
.vertical(|mut strip| {
|
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
show_title(title, ui);
|
ui.centered_and_justified(|ui| {
|
||||||
});
|
View::ellipsize_text(ui, title.into(), 20.0, COLOR_DARK);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
show_action(ui, actions.right.as_ref());
|
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);
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -12,35 +12,87 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use eframe::epaint::{Color32, FontId, Rounding, Stroke};
|
use egui::epaint::{Color32, FontId, Rounding, Stroke};
|
||||||
use eframe::epaint::text::{LayoutJob, TextFormat, TextWrapping};
|
use egui::text::{LayoutJob, TextFormat};
|
||||||
use egui::{RichText, Sense, Widget};
|
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};
|
use crate::gui::colors::{COLOR_DARK, COLOR_GRAY, COLOR_LIGHT, COLOR_GRAY_LIGHT, COLOR_GRAY_DARK};
|
||||||
|
|
||||||
pub struct View;
|
pub struct View;
|
||||||
|
|
||||||
impl View {
|
impl View {
|
||||||
|
/// Default stroke around views.
|
||||||
pub const DEFAULT_STROKE: Stroke = Stroke { width: 1.0, color: Color32::from_gray(190) };
|
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()) {
|
pub fn title_button(ui: &mut egui::Ui, icon: &str, action: impl FnOnce()) {
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
// Disable stroke around title buttons on hover
|
// Disable stroke around title buttons on hover
|
||||||
ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE;
|
ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE;
|
||||||
|
|
||||||
let b = egui::widgets::Button::new(
|
let wt = RichText::new(icon.to_string()).size(24.0).color(COLOR_DARK);
|
||||||
RichText::new(icon.to_string()).size(24.0).color(COLOR_DARK)
|
let br = Button::new(wt)
|
||||||
).fill(Color32::TRANSPARENT)
|
.fill(Color32::TRANSPARENT)
|
||||||
.ui(ui).interact(Sense::click_and_drag());
|
.ui(ui).interact(Sense::click_and_drag());
|
||||||
|
|
||||||
// Click optimization for touch screens
|
Self::on_button_click(ui, br, action);
|
||||||
if b.drag_released() || b.clicked() {
|
|
||||||
(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 {
|
let stroke = match active {
|
||||||
true => { Stroke::NONE }
|
true => { Stroke::NONE }
|
||||||
false => { Self::DEFAULT_STROKE }
|
false => { Self::DEFAULT_STROKE }
|
||||||
|
@ -50,26 +102,32 @@ impl View {
|
||||||
true => { COLOR_LIGHT }
|
true => { COLOR_LIGHT }
|
||||||
false => { Color32::WHITE }
|
false => { Color32::WHITE }
|
||||||
};
|
};
|
||||||
|
let br = Button::new(wt)
|
||||||
let b = egui::widgets::Button::new(
|
.min_size(ui.available_size_before_wrap())
|
||||||
RichText::new(icon.to_string()).size(24.0).color(COLOR_DARK)
|
|
||||||
).min_size(ui.available_size_before_wrap())
|
|
||||||
.stroke(stroke)
|
.stroke(stroke)
|
||||||
.fill(color)
|
.fill(color)
|
||||||
.ui(ui).interact(Sense::click_and_drag());
|
.ui(ui).interact(Sense::click_and_drag());
|
||||||
|
|
||||||
// Click optimization for touch screens
|
Self::on_button_click(ui, br, action);
|
||||||
if b.drag_released() || b.clicked() {
|
|
||||||
(action)();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sub_title(ui: &mut egui::Ui, text: String) {
|
/// Modal button with white background fill color, contains text.
|
||||||
ui.label(RichText::new(text.to_uppercase()).size(16.0).color(COLOR_GRAY_DARK));
|
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
|
/// Draw rounded box with some value and label in the middle,
|
||||||
/// where is r = [top_left, top_right, bottom_left, bottom_right]
|
/// where is r = (top_left, top_right, bottom_left, bottom_right).
|
||||||
/// | VALUE |
|
/// | VALUE |
|
||||||
/// | label |
|
/// | label |
|
||||||
pub fn rounded_box(ui: &mut egui::Ui, value: String, label: String, r: [bool; 4]) {
|
pub fn rounded_box(ui: &mut egui::Ui, value: String, label: String, r: [bool; 4]) {
|
||||||
|
|
|
@ -14,4 +14,4 @@
|
||||||
|
|
||||||
mod node;
|
mod node;
|
||||||
|
|
||||||
pub use self::node::Node;
|
pub use node::Node;
|
127
src/node/node.rs
127
src/node/node.rs
|
@ -25,25 +25,26 @@ use grin_config::config;
|
||||||
use grin_core::global;
|
use grin_core::global;
|
||||||
use grin_core::global::ChainTypes;
|
use grin_core::global::ChainTypes;
|
||||||
use grin_servers::{Server, ServerStats};
|
use grin_servers::{Server, ServerStats};
|
||||||
use jni::objects::JString;
|
|
||||||
use jni::sys::jstring;
|
use jni::sys::jstring;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
/// Static thread-aware state of [Node] to be updated from another thread.
|
||||||
static ref NODE_STATE: Arc<Node> = Arc::new(Node::default());
|
static ref NODE_STATE: Arc<Node> = Arc::new(Node::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Provides [Server] control, holds current status and statistics.
|
||||||
pub struct Node {
|
pub struct Node {
|
||||||
/// Data for UI
|
/// Statistics data for UI.
|
||||||
stats: Arc<RwLock<Option<ServerStats>>>,
|
stats: Arc<RwLock<Option<ServerStats>>>,
|
||||||
/// Chain type of launched server
|
/// Chain type of launched server.
|
||||||
chain_type: Arc<RwLock<ChainTypes>>,
|
chain_type: Arc<RwLock<ChainTypes>>,
|
||||||
/// Indicator if server is starting
|
/// Indicator if server is starting.
|
||||||
starting: AtomicBool,
|
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,
|
restart_needed: AtomicBool,
|
||||||
/// Thread flag to stop the server
|
/// Thread flag to stop the server.
|
||||||
stop_needed: AtomicBool,
|
stop_needed: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,12 +61,12 @@ impl Default for Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
impl Node {
|
||||||
/// Stop server
|
/// Stop [Server].
|
||||||
pub fn stop() {
|
pub fn stop() {
|
||||||
NODE_STATE.stop_needed.store(true, Ordering::Relaxed);
|
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) {
|
pub fn start(chain_type: ChainTypes) {
|
||||||
if !Self::is_running() {
|
if !Self::is_running() {
|
||||||
let mut w_chain_type = NODE_STATE.chain_type.write().unwrap();
|
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) {
|
pub fn restart(chain_type: ChainTypes) {
|
||||||
if Self::is_running() {
|
if Self::is_running() {
|
||||||
let mut w_chain_type = NODE_STATE.chain_type.write().unwrap();
|
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 {
|
pub fn is_starting() -> bool {
|
||||||
NODE_STATE.starting.load(Ordering::Relaxed)
|
NODE_STATE.starting.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if server is running
|
/// Check if [Server] is running.
|
||||||
pub fn is_running() -> bool {
|
pub fn is_running() -> bool {
|
||||||
Self::get_stats().is_some() || Self::is_starting()
|
Self::get_stats().is_some() || Self::is_starting()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if server is stopping
|
/// Check if [Server] is stopping.
|
||||||
pub fn is_stopping() -> bool {
|
pub fn is_stopping() -> bool {
|
||||||
NODE_STATE.stop_needed.load(Ordering::Relaxed)
|
NODE_STATE.stop_needed.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if server is restarting
|
/// Check if [Server] is restarting.
|
||||||
pub fn is_restarting() -> bool {
|
pub fn is_restarting() -> bool {
|
||||||
NODE_STATE.restart_needed.load(Ordering::Relaxed)
|
NODE_STATE.restart_needed.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get server stats
|
/// Get [Server] statistics.
|
||||||
pub fn get_stats() -> RwLockReadGuard<'static, Option<ServerStats>> {
|
pub fn get_stats() -> RwLockReadGuard<'static, Option<ServerStats>> {
|
||||||
NODE_STATE.stats.read().unwrap()
|
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> {
|
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() {
|
if Self::is_stopping() {
|
||||||
return Some(SyncStatus::Shutdown)
|
return Some(SyncStatus::Shutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
// return Initial status when node is starting
|
// Return Initial status when node is starting.
|
||||||
if Self::is_starting() {
|
if Self::is_starting() {
|
||||||
return Some(SyncStatus::Initial)
|
return Some(SyncStatus::Initial)
|
||||||
}
|
}
|
||||||
|
|
||||||
let stats = Self::get_stats();
|
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() {
|
if stats.is_some() {
|
||||||
return Some(stats.as_ref().unwrap().sync_status)
|
return Some(stats.as_ref().unwrap().sync_status)
|
||||||
}
|
}
|
||||||
None
|
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<()> {
|
fn start_server_thread() -> JoinHandle<()> {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
NODE_STATE.starting.store(true, Ordering::Relaxed);
|
NODE_STATE.starting.store(true, Ordering::Relaxed);
|
||||||
|
@ -142,7 +143,7 @@ impl Node {
|
||||||
if Self::is_restarting() {
|
if Self::is_restarting() {
|
||||||
server.stop();
|
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());
|
server = start_server(&NODE_STATE.chain_type.read().unwrap());
|
||||||
|
|
||||||
NODE_STATE.restart_needed.store(false, Ordering::Relaxed);
|
NODE_STATE.restart_needed.store(false, Ordering::Relaxed);
|
||||||
|
@ -152,6 +153,7 @@ impl Node {
|
||||||
let mut w_stats = NODE_STATE.stats.write().unwrap();
|
let mut w_stats = NODE_STATE.stats.write().unwrap();
|
||||||
*w_stats = None;
|
*w_stats = None;
|
||||||
|
|
||||||
|
NODE_STATE.starting.store(false, Ordering::Relaxed);
|
||||||
NODE_STATE.stop_needed.store(false, Ordering::Relaxed);
|
NODE_STATE.stop_needed.store(false, Ordering::Relaxed);
|
||||||
break;
|
break;
|
||||||
} else {
|
} 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 {
|
pub fn get_sync_status_text(sync_status: Option<SyncStatus>) -> String {
|
||||||
if Node::is_restarting() {
|
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() {
|
if sync_status.is_none() {
|
||||||
return t!("server_down")
|
return t!("sync_status.server_down")
|
||||||
}
|
}
|
||||||
|
|
||||||
match sync_status.unwrap() {
|
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 {
|
fn start_server(chain_type: &ChainTypes) -> Server {
|
||||||
|
// Initialize config
|
||||||
let mut node_config_result = config::initial_setup_server(chain_type);
|
let mut node_config_result = config::initial_setup_server(chain_type);
|
||||||
if node_config_result.is_err() {
|
if node_config_result.is_err() {
|
||||||
// Remove config file on init error
|
// Remove config file on init error
|
||||||
|
@ -268,18 +276,6 @@ fn start_server(chain_type: &ChainTypes) -> Server {
|
||||||
let config = node_config.clone().unwrap();
|
let config = node_config.clone().unwrap();
|
||||||
let server_config = config.members.as_ref().unwrap().server.clone();
|
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
|
// Remove temporary file dir
|
||||||
{
|
{
|
||||||
let mut tmp_dir = PathBuf::from(&server_config.db_root);
|
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<()>) =
|
let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) =
|
||||||
Box::leak(Box::new(oneshot::channel::<()>()));
|
Box::leak(Box::new(oneshot::channel::<()>()));
|
||||||
let mut server_result = Server::new(server_config.clone(), None, api_chan);
|
let mut server_result = Server::new(server_config.clone(), None, api_chan);
|
||||||
if server_result.is_err() {
|
//TODO: handle server errors
|
||||||
let mut db_path = PathBuf::from(&server_config.db_root);
|
//
|
||||||
db_path.push("grin.lock");
|
// if server_result.is_err() {
|
||||||
fs::remove_file(db_path).unwrap();
|
// let mut db_path = PathBuf::from(&server_config.db_root);
|
||||||
|
// db_path.push("grin.lock");
|
||||||
// Remove chain data on server start error
|
// fs::remove_file(db_path).unwrap();
|
||||||
let dirs_to_remove: Vec<&str> = vec!["header", "lmdb", "txhashset"];
|
//
|
||||||
for dir in dirs_to_remove {
|
// // Remove chain data on server start error
|
||||||
let mut path = PathBuf::from(&server_config.db_root);
|
// let dirs_to_remove: Vec<&str> = vec!["header", "lmdb", "txhashset"];
|
||||||
path.push(dir);
|
// for dir in dirs_to_remove {
|
||||||
fs::remove_dir_all(path).unwrap();
|
// 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();
|
// // Recreate server
|
||||||
let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) =
|
// let config = node_config.clone().unwrap();
|
||||||
Box::leak(Box::new(oneshot::channel::<()>()));
|
// let server_config = config.members.as_ref().unwrap().server.clone();
|
||||||
server_result = Server::new(server_config.clone(), None, api_chan);
|
// 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()
|
server_result.unwrap()
|
||||||
}
|
}
|
||||||
|
@ -360,6 +358,7 @@ fn start_server(chain_type: &ChainTypes) -> Server {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[no_mangle]
|
#[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(
|
pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncStatusText(
|
||||||
_env: jni::JNIEnv,
|
_env: jni::JNIEnv,
|
||||||
_class: jni::objects::JObject,
|
_class: jni::objects::JObject,
|
||||||
|
@ -375,11 +374,25 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncStatusText(
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
/// Get sync title for Android notification in Java string format.
|
||||||
pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncTitle(
|
pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncTitle(
|
||||||
_env: jni::JNIEnv,
|
_env: jni::JNIEnv,
|
||||||
_class: jni::objects::JObject,
|
_class: jni::objects::JObject,
|
||||||
_activity: jni::objects::JObject,
|
_activity: jni::objects::JObject,
|
||||||
) -> jstring {
|
) -> 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();
|
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();
|
||||||
|
}
|
Loading…
Reference in a new issue