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