Compare commits

...

50 commits

Author SHA1 Message Date
1d9b7d9698 wallet: do not lock whole balance on send
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-01-14 17:55:50 +03:00
82c05588bc readme: update title
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:59:22 +03:00
1cddd05bc0 readme: update img tag
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:58:29 +03:00
8ad0d1c461 readme: update images
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:56:48 +03:00
a22a75913c img: add grin logo
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:55:55 +03:00
e797da0ed8 img: add cover
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:26:00 +03:00
6936c14ed2 tor: remove macos tls fix
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:06:34 +03:00
c626ed5a48 tor: clear data on launch, update arti to 0.26.0
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-01-13 19:40:09 +03:00
d79d05ef5a android: debug build without keystore 2025-01-13 16:54:27 +03:00
ardocrat
094a5b8969 release: v0.2.3 2024-10-27 20:12:12 +03:00
ardocrat
12a75f8370 macos: future version update 2024-10-27 19:45:00 +03:00
ardocrat
1c14b9aa93 tx: fix confirmation status for new block, do not show Slatepack message after finalization 2024-10-27 19:02:17 +03:00
ardocrat
8ea388554a github: macos target 11.0 2024-10-27 18:07:22 +03:00
ardocrat
1531c201bb github: macos 12 2024-10-27 00:46:53 +03:00
ardocrat
ed522c56ae github: macos zig linker 2024-10-27 00:40:58 +03:00
ardocrat
4b454ab2f3 github: macos last os 2024-10-27 00:29:27 +03:00
ardocrat
f6fbf7226e fix: window size saving 2024-10-26 23:54:47 +03:00
ardocrat
ebd09ab1c8 camera: update nokhwa, eye for macos, ability to switch camera when another camera not loaded 2024-10-26 23:25:55 +03:00
ardocrat
75cf7edc96 fix: modal padding and window border on desktop 2024-10-26 23:23:39 +03:00
ardocrat
5c8b9c40be build: provide version for android release 2024-10-26 02:17:28 +03:00
ardocrat
dcaf9945c8 ui: wgpu renderer for macos, desktop content background fix, do not show left line for camera content at dual panel mode 2024-10-26 02:16:47 +03:00
ardocrat
f9426287d5 macos: release on darwin without zig, info.plist camera usage description and version update 2024-10-25 20:03:57 +03:00
ardocrat
77281e3ab9 github: fix macos arm sdk 2024-10-23 00:37:41 +03:00
ardocrat
64439ad3d3 github: fix macos deployment target 2024-10-23 00:11:45 +03:00
ardocrat
9494c1292e github: macos coreutils 2024-10-22 23:47:19 +03:00
ardocrat
accf123d49 github: macos build 2024-10-22 23:24:02 +03:00
ardocrat
d77598c259 github: fix macos sdk env 2024-10-22 04:39:08 +03:00
ardocrat
4e6dff52fe github: macos install zig 2024-10-22 04:14:21 +03:00
ardocrat
92d0aac250 github: fix macos sdk unzip 2024-10-22 04:08:26 +03:00
ardocrat
5ef310558a release: v0.2.2 2024-10-22 03:51:01 +03:00
ardocrat
683821b667 build: fix version script 2024-10-22 03:50:48 +03:00
ardocrat
da4cf71fac github: build macos on linux with SDK 10.15 2024-10-22 03:19:34 +03:00
ardocrat
f81ceae940 txs: new block confirmation time 2024-10-22 02:11:25 +03:00
ardocrat
fa6301a1db stratum: fix wallet name after selection, do not panic after stop 2024-10-22 00:12:13 +03:00
ardocrat
442fc425f7 ui: update to egui 0.29.1, wallet qr scan content, panels strokes and colors refactoring, check closeable modal at desktop title, fix app socket name 2024-10-21 12:03:09 +03:00
ardocrat
ea61588ede build: check android lib result 2024-10-12 19:58:14 +03:00
ardocrat
7f67aa134a build: increment on android development 2024-10-12 15:33:23 +03:00
ardocrat
d7d1c53c52 build: incremental release on desktop development 2024-10-12 15:26:01 +03:00
ardocrat
18f52f877a node: remove delay after server start 2024-10-12 15:24:15 +03:00
ardocrat
c13195bd61 stratum: prevent crash at connections thread 2024-10-10 21:51:28 +03:00
ardocrat
e40d5b6474 node: single function to get api secrets 2024-10-10 21:11:50 +03:00
ardocrat
92e5d38755 build: update grin 5.3.3, arti 0.23.0 (fork arti-hyper crate) and non-egui dependencies 2024-10-09 12:58:59 +03:00
ardocrat
ec7e795ba9 build: camera features 2024-10-09 10:13:55 +03:00
ardocrat
af220b2a09 camera: remove eye-rs to fix build for mac, horizontally flip image 2024-10-08 23:23:04 +03:00
ardocrat
846e30cb38 app: better panic handling, macos single app instance 2024-10-08 17:11:45 +03:00
ardocrat
d371d4368b wallet: disable tor listener by default 2024-10-08 14:59:51 +03:00
ardocrat
85fc8101e4 ui: show tx modal on error if exists 2024-10-08 02:37:51 +03:00
ardocrat
e2f58a8938 android: update gradle 2024-10-07 20:55:23 +03:00
ardocrat
7e6954afd9 fix: opened file data providing 2024-10-07 19:45:29 +03:00
ardocrat
bed041a1c3 git: ignore android release artifacts 2024-09-21 00:33:46 +03:00
72 changed files with 4656 additions and 4681 deletions

View file

@ -94,20 +94,26 @@ jobs:
macos_release: macos_release:
name: MacOS Release name: MacOS Release
runs-on: macos-latest runs-on: macos-12
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install coreutils
run: brew install coreutils
- name: Zig Setup - name: Zig Setup
uses: goto-bus-stop/setup-zig@v2 uses: goto-bus-stop/setup-zig@v2
with: with:
version: 0.12.1 version: 0.12.1
- name: Install coreutils
run: brew install coreutils
- name: Install cargo-zigbuild - name: Install cargo-zigbuild
run: cargo install cargo-zigbuild run: cargo install cargo-zigbuild
- name: Download SDK
run: wget https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.0.sdk.tar.xz
- name: Setup SDK env
run: tar xf ${{ github.workspace }}/MacOSX11.0.sdk.tar.xz && echo "SDKROOT=${{ github.workspace }}/MacOSX11.0.sdk" >> $GITHUB_ENV
- name: Setup platform env
run: echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV
- name: Release x86 - name: Release x86
run: | run: |
rustup target add x86_64-apple-darwin rustup target add x86_64-apple-darwin
@ -140,8 +146,6 @@ jobs:
run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
- name: Release Universal - name: Release Universal
run: | run: |
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
cargo zigbuild --release --target universal2-apple-darwin cargo zigbuild --release --target universal2-apple-darwin
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive Universal - name: Archive Universal
@ -152,7 +156,7 @@ jobs:
cd .. cd ..
- name: Checksum Release Universal - name: Checksum Release Universal
working-directory: target/universal2-apple-darwin/release working-directory: target/universal2-apple-darwin/release
shell: bash shell: pwsh
run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1

3
.gitignore vendored
View file

@ -1,9 +1,12 @@
*.iml *.iml
android/.idea
android/.gradle android/.gradle
android/local.properties android/local.properties
android/keystore android/keystore
android/keystore.asc android/keystore.asc
android/keystore.properties android/keystore.properties
android/*.apk
android/*sha256sum.txt
/.idea /.idea
.DS_Store .DS_Store
/captures /captures

4400
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "grim" name = "grim"
version = "0.2.1" version = "0.2.3"
authors = ["Ardocrat <ardocrat@proton.me>"] authors = ["Ardocrat <ardocrat@proton.me>"]
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere." description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
license = "Apache-2.0" license = "Apache-2.0"
@ -25,108 +25,110 @@ codegen-units = 1
panic = "abort" panic = "abort"
[dependencies] [dependencies]
log = "0.4" log = "0.4.22"
## node ## node
openssl-sys = { version = "0.9.82", features = ["vendored"] } openssl-sys = { version = "0.9.103", features = ["vendored"] }
grin_api = "5.3.1" grin_api = "5.3.3"
grin_chain = "5.3.1" grin_chain = "5.3.3"
grin_config = "5.3.1" grin_config = "5.3.3"
grin_core = "5.3.1" grin_core = "5.3.3"
grin_p2p = "5.3.1" grin_p2p = "5.3.3"
grin_servers = "5.3.1" grin_servers = "5.3.3"
grin_keychain = "5.3.1" grin_keychain = "5.3.3"
grin_util = "5.3.1" grin_util = "5.3.3"
## wallet ## wallet
grin_wallet_impls = "5.3.1" grin_wallet_impls = "5.3.3"
grin_wallet_api = "5.3.1" grin_wallet_api = "5.3.3"
grin_wallet_libwallet = "5.3.1" grin_wallet_libwallet = "5.3.3"
grin_wallet_util = "5.3.1" grin_wallet_util = "5.3.3"
grin_wallet_controller = "5.3.1" grin_wallet_controller = "5.3.3"
## ui ## ui
egui = { version = "0.28.1", default-features = false } egui = { version = "0.29.1", default-features = false }
egui_extras = { version = "0.28.1", features = ["image", "svg"] } egui_extras = { version = "0.29.1", features = ["image", "svg"] }
rust-i18n = "2.3.1" rust-i18n = "2.3.1"
## other ## other
backtrace = "0.3" anyhow = "1.0.89"
thiserror = "1.0.58" pin-project = "1.1.6"
futures = "0.3" backtrace = "0.3.74"
thiserror = "1.0.64"
futures = "0.3.31"
dirs = "5.0.1" dirs = "5.0.1"
sys-locale = "0.3.0" sys-locale = "0.3.1"
chrono = "0.4.31" chrono = "0.4.38"
parking_lot = "0.12.1" parking_lot = "0.12.3"
lazy_static = "1.4.0" lazy_static = "1.5.0"
toml = "0.8.2" toml = "0.8.19"
serde = "1.0.170" serde = "1.0.210"
local-ip-address = "0.6.1" local-ip-address = "0.6.3"
url = "2.4.0" url = "2.5.2"
rand = "0.8.5" rand = "0.8.5"
serde_derive = "1.0.197" serde_derive = "1.0.210"
serde_json = "1.0.115" serde_json = "1.0.128"
tokio = { version = "1.37.0", features = ["full"] } tokio = { version = "1.40.0", features = ["full"] }
image = "0.25.1" image = "0.25.2"
rqrr = "0.7.1" rqrr = "0.8.0"
qrcodegen = "1.8.0" qrcodegen = "1.8.0"
qrcode = "0.14.0" qrcode = "0.14.1"
ur = "0.4.1" ur = "0.4.1"
gif = "0.13.1" gif = "0.13.1"
rkv = { version = "0.19.0", features = ["lmdb"] } rkv = { version = "0.19.0", features = ["lmdb"] }
## tor ## tor
arti-client = { version = "0.19.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] } arti-client = { version = "0.26.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.19.0", features = ["static"] } tor-rtcompat = { version = "0.26.0", features = ["static"] }
tor-config = "0.19.0" tor-config = "0.26.0"
fs-mistrust = "0.7.9" fs-mistrust = "0.8.0"
tor-hsservice = "0.19.0" tor-hsservice = "0.26.0"
tor-hsrproxy = "0.19.0" tor-hsrproxy = "0.26.0"
tor-keymgr = "0.19.0" tor-keymgr = "0.26.0"
tor-llcrypto = "0.19.0" tor-llcrypto = "0.26.0"
tor-hscrypto = "0.19.0" tor-hscrypto = "0.26.0"
arti-hyper = "0.19.0" tor-error = "0.26.0"
sha2 = "0.10.0" sha2 = "0.10.8"
ed25519-dalek = "2.1.1" ed25519-dalek = "2.1.1"
curve25519-dalek = "4.1.2" curve25519-dalek = "4.1.3"
hyper = { version = "0.14.28", features = ["full"] } hyper = { version = "0.14.30", features = ["full"] }
hyper-tls = "0.5.0" hyper-tls = "0.5.0"
tls-api = "0.9.0" tls-api = "0.12.0"
tls-api-native-tls = "0.9.0" tls-api-native-tls = "0.12.1"
## stratum server ## stratum server
tokio-old = {version = "0.2", features = ["full"], package = "tokio" } tokio-old = {version = "0.2", features = ["full"], package = "tokio" }
tokio-util-old = { version = "0.2", features = ["codec"], package = "tokio-util" } tokio-util-old = { version = "0.2", features = ["codec"], package = "tokio-util" }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "android")))'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
eye = { version = "0.5.0", default-features = false } nokhwa = { version = "0.10.5", default-features = false, features = ["input-v4l"] }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
nokhwa = { version = "0.10.4", default-features = false, features = ["input-msmf"] } nokhwa = { version = "0.10.5", default-features = false, features = ["input-msmf"] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
tls-api-openssl = "0.9.0" eye = { git = "https://github.com/raymanfx/eye-rs", rev = "5b7e3f7a1e79966091692896c568aab042e449ef", default-features = false }
openpnp_capture_sys = "0.4.0"
[target.'cfg(not(target_os = "android"))'.dependencies] [target.'cfg(not(target_os = "android"))'.dependencies]
env_logger = "0.11.3" env_logger = "0.11.3"
winit = { version = "0.29.15" } winit = { version = "0.30.5" }
eframe = { version = "0.28.1", features = ["wgpu", "glow"] } eframe = { version = "0.29.1", features = ["wgpu", "glow"] }
arboard = "3.2.0" arboard = "3.2.0"
rfd = "0.14.1" rfd = "0.15.0"
dark-light = "1.1.1"
interprocess = { version = "2.2.1", features = ["tokio"] } interprocess = { version = "2.2.1", features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13.1" android_logger = "0.14.1"
jni = "0.21.1" jni = "0.21.1"
wgpu = "22.1.0"
android-activity = { version = "0.6.0", features = ["game-activity"] } android-activity = { version = "0.6.0", features = ["game-activity"] }
wgpu = "0.20.1" winit = { version = "0.30.5", features = ["android-game-activity"] }
winit = { version = "0.29.15", features = ["android-game-activity"] } eframe = { version = "0.29.1", features = ["wgpu", "android-game-activity"] }
eframe = { version = "0.28.1", features = ["wgpu", "android-game-activity"] }
[patch.crates-io] [patch.crates-io]
openpnp_capture = { git = "https://github.com/ardocrat/openpnp-capture-rs", rev = "f9b06f627c5e5d42c672d117650af700846ca6cf" }
egui_extras = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
egui = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
eframe = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
### patch grin store ### patch grin store
#grin_store = { path = "../grin-store" } #grin_store = { path = "../grin-store" }
### fix cross-compilation support for macos
openpnp_capture_sys = { git = "https://github.com/ardocrat/openpnp-capture-rs", branch = "cross_compilation_support" }

View file

@ -1,11 +1,11 @@
# <img height="22" src="https://github.com/ardocrat/grim/blob/master/android/app/src/main/ic_launcher-playstore.png?raw=true"> Grim <img height="20" src="https://github.com/mimblewimble/site/blob/master/assets/images/grin-logo.png?raw=true"> <img height="20" src="https://github.com/ardocrat/grim/blob/master/img/logo.png?raw=true"> # Grim <img height="20" src="https://gri.mw/code/GUI/grim/raw/branch/master/img/grin-logo.png"/> <img height="20" src="https://gri.mw/code/GUI/grim/raw/branch/master/img/logo.png"/>
Cross-platform GUI for [GRiN ツ](https://grin.mw) in [Rust](https://www.rust-lang.org/) Cross-platform GUI for [GRiN ツ](https://grin.mw) in [Rust](https://www.rust-lang.org/)
for maximum compatibility with original [Mimblewimble](https://github.com/mimblewimble/grin) implementation. for maximum compatibility with original [Mimblewimble](https://github.com/mimblewimble/grin) implementation.
Initially supported platforms are Linux, Mac, Windows, limited Android and possible web support with help of [egui](https://github.com/emilk/egui) - immediate mode GUI library in pure Rust. Initially supported platforms are Linux, Mac, Windows, limited Android and possible web support with help of [egui](https://github.com/emilk/egui) - immediate mode GUI library in pure Rust.
Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog. Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog.
![image](https://github.com/user-attachments/assets/a925b1c8-02c9-4b08-b888-0315d11138b6) ![image](https://gri.mw/code/GUI/grim/raw/branch/master/img/cover.png)
## Build instructions ## Build instructions

View file

@ -11,7 +11,7 @@ android {
minSdk 24 minSdk 24
targetSdk 33 targetSdk 33
versionCode 3 versionCode 3
versionName "0.2.1" versionName "0.2.3"
} }
def keystorePropertiesFile = rootProject.file("keystore.properties") def keystorePropertiesFile = rootProject.file("keystore.properties")

View file

@ -1,10 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '8.1.1' apply false id 'com.android.application' version '8.6.1' apply false
id 'com.android.library' version '8.1.1' apply false id 'com.android.library' version '8.6.1' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
} }

View file

@ -19,5 +19,4 @@ android.useAndroidX=true
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false android.nonFinalResIds=false

View file

@ -1,6 +1,6 @@
#Mon May 02 15:39:12 BST 2022 #Mon May 02 15:39:12 BST 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

BIN
img/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
img/grin-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -1,77 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleDisplayName</key>
<key>CFBundleDisplayName</key> <string>Grim</string>
<string>Grim</string> <key>CFBundleExecutable</key>
<string>grim</string>
<key>CFBundleExecutable</key> <key>CFBundleIconFile</key>
<string>grim</string> <string>AppIcon</string>
<key>CFBundleIconName</key>
<key>CFBundleIconFile</key> <string>AppIcon</string>
<string>AppIcon</string> <key>CFBundleIdentifier</key>
<string>mw.gri.macos</string>
<key>CFBundleIconName</key> <key>CFBundleInfoDictionaryVersion</key>
<string>AppIcon</string> <string>6.0</string>
<key>CFBundleName</key>
<key>CFBundleIdentifier</key> <string>Grim</string>
<string>mw.gri.macos</string> <key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleShortVersionString</key>
<string>6.0</string> <string>0.2.3</string>
<key>CFBundleSupportedPlatforms</key>
<key>CFBundleName</key> <array>
<string>Grim</string> <string>MacOSX</string>
</array>
<key>CFBundlePackageType</key> <key>CFBundleVersion</key>
<string>APPL</string> <string>1</string>
<key>NSCameraUsageDescription</key>
<key>CFBundleShortVersionString</key> <string>Grim needs an access to your camera to scan QR code.</string>
<string>0.1.0</string> <key>CFBundleDocumentTypes</key>
<array>
<key>CFBundleSupportedPlatforms</key> <dict>
<array> <key>CFBundleTypeName</key>
<string>MacOSX</string> <string>Apple SimpleText document</string>
</array> <key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleVersion</key> <key>LSItemContentTypes</key>
<string>1</string> <array>
<string>com.apple.traditional-mac-plain-text</string>
<key>CFBundleDocumentTypes</key> </array>
<array> <key>NSDocumentClass</key>
<dict> <string>Document</string>
<key>CFBundleTypeName</key> </dict>
<string>Apple SimpleText document</string> <dict>
<key>CFBundleTypeRole</key> <key>CFBundleTypeName</key>
<string>Viewer</string> <string>Unknown document</string>
<key>LSItemContentTypes</key> <key>CFBundleTypeRole</key>
<array> <string>Viewer</string>
<string>com.apple.traditional-mac-plain-text</string> <key>LSItemContentTypes</key>
</array> <array>
<key>NSDocumentClass</key> <string>public.data</string>
<string>Document</string> </array>
</dict> <key>NSDocumentClass</key>
<dict> <string>Document</string>
<key>CFBundleTypeName</key> </dict>
<string>Unknown document</string> </array>
<key>CFBundleTypeRole</key> <key>LSApplicationCategoryType</key>
<string>Viewer</string> <string>public.app-category.finance</string>
<key>LSItemContentTypes</key> <key>NSHumanReadableCopyright</key>
<array> <string>2024</string>
<string>public.data</string> </dict>
</array> </plist>
<key>NSDocumentClass</key>
<string>Document</string>
</dict>
</array>
<key>LSApplicationCategoryType</key>
<string>public.app-category.finance</string>
<key>NSHumanReadableCopyright</key>
<string>2024</string>
</dict>
</plist>

View file

@ -1 +1,2 @@
!.gitignore !.gitignore
grim

View file

@ -1,11 +1,11 @@
#!/bin/bash #!/bin/bash
set -e set -e
case $2 in case $1 in
x86_64|arm|universal) x86_64|arm|universal)
;; ;;
*) *)
echo "Usage: release_macos.sh [version] [platform]\n - platform: 'x86_64', 'arm', 'universal'" >&2 echo "Usage: release_macos.sh [platform] [version]\n - platform: 'x86_64', 'arm', 'universal'" >&2
exit 1 exit 1
esac esac
@ -24,17 +24,28 @@ cd ${BASEDIR}
cd .. cd ..
# Setup platform # Setup platform
rustup target add x86_64-apple-darwin [[ $1 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
rustup target add aarch64-apple-darwin [[ $1 == "arm" ]] && arch+=(aarch64-apple-darwin)
[[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin) if [[ "$OSTYPE" != "darwin"* ]]; then
[[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin) # Start release build on non-MacOS with zig linker, requires zig 0.12.1
[[ $2 == "universal" ]]; arch+=(universal2-apple-darwin) rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin
[[ $1 == "universal" ]]; arch+=(universal2-apple-darwin)
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
else
rustup target add ${arch}
if [[ $1 == "universal" ]]; then
cargo build --release --target x86_64-apple-darwin
cargo build --release --target aarch64-apple-darwin
lipo -create -output target/grim target/aarch64-apple-darwin/release/grim target/x86_64-apple-darwin/release/grim
else
cargo build --release --target ${arch}
fi
fi
# Start release build with zig linker, requires zig 0.12.1 rm -f .intentionally-empty-file.o
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
rm -rf .intentionally-empty-file.o
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
@ -43,8 +54,7 @@ yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
#rcodesign sign --pem-file cert.pem macos/Grim.app #rcodesign sign --pem-file cert.pem macos/Grim.app
# Create release package # Create release package
FILE_NAME=grim-v$1-macos-$2.zip FILE_NAME=grim-v$2-macos-$1.zip
rm -rf target/${arch}/release/${FILE_NAME}
cd macos cd macos
zip -r ${FILE_NAME} Grim.app zip -r ${FILE_NAME} Grim.app
mv ${FILE_NAME} ../target/${arch}/release mv ${FILE_NAME} ../target/${arch}/release

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
usage="Usage: android.sh [type] [platform]\n - type: 'build', 'release', ''\n - platform, for build type: 'v7', 'v8', 'x86'" usage="Usage: android.sh [type] [platform|version]\n - type: 'build', 'release'\n - platform, for 'build' type: 'v7', 'v8', 'x86'\n - optional version for 'release' (needed on MacOS), example: '0.2.2'"
case $1 in case $1 in
build|release) build|release)
;; ;;
@ -20,8 +20,8 @@ if [[ $1 == "build" ]]; then
fi fi
# Setup build directory # Setup build directory
BASEDIR=$(cd $(dirname $0) && pwd) BASEDIR=$(cd "$(dirname "$0")" && pwd)
cd ${BASEDIR} cd "${BASEDIR}" || exit 1
cd .. cd ..
# Install platforms and tools # Install platforms and tools
@ -34,35 +34,37 @@ success=1
### Build native code ### Build native code
function build_lib() { function build_lib() {
[[ $1 == "v7" ]] && arch=(armeabi-v7a) [[ $1 == "v7" ]] && arch=armeabi-v7a
[[ $1 == "v8" ]] && arch=(arm64-v8a) [[ $1 == "v8" ]] && arch=arm64-v8a
[[ $1 == "x86" ]] && arch=(x86_64) [[ $1 == "x86" ]] && arch=x86_64
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
# Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s # Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" # Uncomment lines below for the 1st build:
cargo ndk -t ${arch} build --profile release-apk #export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
unset CPPFLAGS && unset CFLAGS #cargo ndk -t ${arch} build --profile release-apk
cargo ndk -t ${arch} -o android/app/src/main/jniLibs build --profile release-apk #unset CPPFLAGS && unset CFLAGS
cargo ndk -t "${arch}" -o android/app/src/main/jniLibs build --profile release-apk
if [ $? -eq 0 ] if [ $? -eq 0 ]
then then
success=1 success=1
else
success=0
fi fi
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
rm -f Cargo.toml-e
} }
### Build application ### Build application
function build_apk() { function build_apk() {
version=$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml) cd android || exit 1
cd android
./gradlew clean ./gradlew clean
# Build signed apk if keystore exists # Build signed apk if keystore exists
if [ ! -f keystore.properties ]; then if [ ! -f keystore.properties ]; then
./gradlew assembleRelease ./gradlew assembleDebug
apk_path=app/build/outputs/apk/release/app-release.apk apk_path=app/build/outputs/apk/debug/app-debug.apk
else else
./gradlew assembleSignedRelease ./gradlew assembleSignedRelease
apk_path=app/build/outputs/apk/signedRelease/app-signedRelease.apk apk_path=app/build/outputs/apk/signedRelease/app-signedRelease.apk
@ -72,22 +74,27 @@ function build_apk() {
# Launch application at all connected devices. # Launch application at all connected devices.
for SERIAL in $(adb devices | grep -v List | cut -f 1); for SERIAL in $(adb devices | grep -v List | cut -f 1);
do do
adb -s $SERIAL install ${apk_path} adb -s "$SERIAL" install ${apk_path}
sleep 1s sleep 1s
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity; adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
done done
else else
if [[ "$OSTYPE" != "darwin"* ]]; then
version=$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml)
else
version=v$2
fi
# Setup release file name # Setup release file name
name=grim-${version}-android-$1.apk name=grim-${version}-android-$1.apk
[[ $1 == "arm" ]] && name=grim-${version}-android.apk [[ $1 == "arm" ]] && name=grim-${version}-android.apk
rm -rf ${name} rm -f "${name}"
mv ${apk_path} ${name} mv ${apk_path} "${name}"
# Calculate checksum # Calculate checksum
checksum=grim-${version}-android-$1-sha256sum.txt checksum=grim-${version}-android-$1-sha256sum.txt
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt [[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
rm -rf ${checksum} rm -f "${checksum}"
sha256sum ${name} > ${checksum} sha256sum "${name}" > "${checksum}"
fi fi
cd .. cd ..
@ -96,7 +103,7 @@ function build_apk() {
rm -rf android/app/src/main/jniLibs/* rm -rf android/app/src/main/jniLibs/*
if [[ $1 == "build" ]]; then if [[ $1 == "build" ]]; then
build_lib $2 build_lib "$2"
[ $success -eq 1 ] && build_apk [ $success -eq 1 ] && build_apk
else else
rm -rf target/release-apk rm -rf target/release-apk
@ -106,8 +113,8 @@ else
build_lib "v7" build_lib "v7"
[ $success -eq 1 ] && build_lib "v8" [ $success -eq 1 ] && build_lib "v8"
[ $success -eq 1 ] && build_apk "arm" [ $success -eq 1 ] && build_apk "arm" "$2"
rm -rf android/app/src/main/jniLibs/* rm -rf android/app/src/main/jniLibs/*
[ $success -eq 1 ] && build_lib "x86" [ $success -eq 1 ] && build_lib "x86"
[ $success -eq 1 ] && build_apk "x86_64" [ $success -eq 1 ] && build_apk "x86_64" "$2"
fi fi

View file

@ -1,25 +1,27 @@
#!/bin/bash #!/bin/bash
case $1 in case $1 in
debug|release) debug|build)
;; ;;
*) *)
echo "Usage: build_run.sh [type] where is type is 'debug' or 'release'" >&2 echo "Usage: build_run.sh [type] where is type is 'debug' or 'build'" >&2
exit 1 exit 1
esac esac
# Setup build directory # Setup build directory
BASEDIR=$(cd $(dirname $0) && pwd) BASEDIR=$(cd "$(dirname $0)" && pwd)
cd ${BASEDIR} cd "${BASEDIR}" || return
cd .. cd ..
# Build application # Build application
type=$1 type=$1
[[ ${type} == "release" ]] && release_param+=(--release) [[ ${type} == "build" ]] && release_param+=(--release)
cargo build ${release_param[@]} cargo --config profile.release.incremental=true build "${release_param[@]}"
# Start application # Start application
if [ $? -eq 0 ] if [ $? -eq 0 ]
then then
./target/${type}/grim path=${type}
fi [[ ${type} == "build" ]] && path="release"
./target/"${path}"/grim
fi

View file

@ -77,10 +77,12 @@ fi
# Update version in build.gradle # Update version in build.gradle
sed -i 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle sed -i'.bak' -e 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
rm -f android/app/build.gradle.bak
# Update version in Cargo.toml # Update version in Cargo.toml
sed -i "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml sed -i'.bak' -e "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
rm -f Cargo.toml.bak
# Update Cargo.lock as this changes when # Update Cargo.lock as this changes when
# updating the version in your manifest # updating the version in your manifest
@ -96,4 +98,4 @@ git commit -m "release: v$VERSION_NEXT"
# Create a tag and push to master branch # Create a tag and push to master branch
git tag "v$VERSION_NEXT" master git tag "v$VERSION_NEXT" master
git push origin master --follow-tags #git push origin master --follow-tags

View file

@ -14,7 +14,7 @@
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use egui::{Align, Context, CursorIcon, Layout, Modifiers, Rect, ResizeDirection, Rounding, Stroke, ViewportCommand}; use egui::{Align, Context, CursorIcon, Layout, Modifiers, ResizeDirection, Rounding, Stroke, UiBuilder, ViewportCommand};
use egui::epaint::{RectShape}; use egui::epaint::{RectShape};
use egui::os::OperatingSystem; use egui::os::OperatingSystem;
@ -22,7 +22,7 @@ use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X}; use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, TitlePanel, View}; use crate::gui::views::{Content, Modal, TitlePanel, View};
use crate::wallet::ExternalConnection; use crate::wallet::ExternalConnection;
lazy_static! { lazy_static! {
@ -32,17 +32,14 @@ lazy_static! {
/// Implements ui entry point and contains platform-specific callbacks. /// Implements ui entry point and contains platform-specific callbacks.
pub struct App<Platform> { pub struct App<Platform> {
/// Platform specific callbacks handler. /// Handles platform-specific functionality.
pub platform: Platform, pub platform: Platform,
/// Main content. /// Main content.
content: Content, content: Content,
/// Last window resize direction. /// Last window resize direction.
resize_direction: Option<ResizeDirection>, resize_direction: Option<ResizeDirection>,
/// Flag to check if it's first draw. /// Flag to check if it's first draw.
first_draw: bool, first_draw: bool
} }
impl<Platform: PlatformCallbacks> App<Platform> { impl<Platform: PlatformCallbacks> App<Platform> {
@ -55,24 +52,29 @@ impl<Platform: PlatformCallbacks> App<Platform> {
} }
} }
/// Called of first content draw.
fn on_first_draw(&mut self, ctx: &Context) {
// Set platform context.
if View::is_desktop() {
self.platform.set_context(ctx);
}
// Check connections availability.
ExternalConnection::check(None, ctx);
// Setup visuals.
crate::setup_visuals(ctx);
}
/// Draw application content. /// Draw application content.
pub fn ui(&mut self, ctx: &Context) { pub fn ui(&mut self, ctx: &Context) {
if self.first_draw { if self.first_draw {
// Set platform context. self.on_first_draw(ctx);
if View::is_desktop() {
self.platform.set_context(ctx);
}
// Check external connections availability.
ExternalConnection::check(None, ctx);
self.first_draw = false; self.first_draw = false;
} }
// Handle Esc keyboard key event and platform Back button key event. // Handle Esc keyboard key event and platform Back button key event.
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed); let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) { if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
self.content.on_back(); self.content.on_back(&self.platform);
if back_pressed { if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed); BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
} }
@ -86,10 +88,9 @@ impl<Platform: PlatformCallbacks> App<Platform> {
ctx.send_viewport_cmd(ViewportCommand::CancelClose); ctx.send_viewport_cmd(ViewportCommand::CancelClose);
Content::show_exit_modal(); Content::show_exit_modal();
} else { } else {
let (w, h) = View::window_size(ctx);
AppConfig::save_window_size(w, h);
ctx.input(|i| { ctx.input(|i| {
if let Some(rect) = i.viewport().inner_rect {
AppConfig::save_window_size(rect.width(), rect.height());
}
if let Some(rect) = i.viewport().outer_rect { if let Some(rect) = i.viewport().outer_rect {
AppConfig::save_window_pos(rect.left(), rect.top()); AppConfig::save_window_pos(rect.left(), rect.top());
} }
@ -97,21 +98,26 @@ impl<Platform: PlatformCallbacks> App<Platform> {
} }
} }
// Show main content with custom frame on desktop. // Show main content.
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
..Default::default() ..Default::default()
}) })
.show(ctx, |ui| { .show(ctx, |ui| {
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac; if View::is_desktop() {
if View::is_desktop() && !is_mac_os { let is_fullscreen = ui.ctx().input(|i| {
self.desktop_window_ui(ui); i.viewport().fullscreen.unwrap_or(false)
} else { });
if is_mac_os { if OperatingSystem::from_target_os() != OperatingSystem::Mac {
self.window_title_ui(ui); self.desktop_window_ui(ui, is_fullscreen);
} else {
self.window_title_ui(ui, is_fullscreen);
ui.add_space(-1.0); ui.add_space(-1.0);
Self::title_panel_bg(ui);
self.content.ui(ui, &self.platform);
} }
self.content.ui(ui, &self.platform); } else {
self.mobile_window_ui(ui);
} }
// Provide incoming data to wallets. // Provide incoming data to wallets.
@ -129,76 +135,52 @@ impl<Platform: PlatformCallbacks> App<Platform> {
} }
} }
/// Draw custom resizeable window content. /// Draw mobile platform window content.
fn desktop_window_ui(&mut self, ui: &mut egui::Ui) { fn mobile_window_ui(&mut self, ui: &mut egui::Ui) {
let is_fullscreen = ui.ctx().input(|i| { Self::title_panel_bg(ui);
i.viewport().fullscreen.unwrap_or(false) self.content.ui(ui, &self.platform);
}); }
let title_stroke_rect = { /// Draw desktop platform window content.
let mut rect = ui.max_rect(); fn desktop_window_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
let content_bg_rect = {
let mut r = ui.max_rect();
if !is_fullscreen { if !is_fullscreen {
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN); r = r.shrink(Content::WINDOW_FRAME_MARGIN);
} }
rect.max.y = if !is_fullscreen { r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
Content::WINDOW_FRAME_MARGIN r
} else {
0.0
} + Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
rect
}; };
let title_stroke = RectShape { let content_bg = RectShape::new(content_bg_rect,
rect: title_stroke_rect, Rounding::ZERO,
rounding: Rounding { Colors::fill_lite(),
nw: 8.0, View::default_stroke());
ne: 8.0, // Draw content background.
sw: 0.0, ui.painter().add(content_bg);
se: 0.0,
},
fill: Colors::yellow(),
stroke: Stroke {
width: 1.0,
color: egui::Color32::from_gray(200)
},
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
// Draw title stroke.
ui.painter().add(title_stroke);
let content_stroke_rect = {
let mut rect = ui.max_rect();
if !is_fullscreen {
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
}
let top = Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
rect.min += egui::vec2(0.0, top);
rect
};
let content_stroke = RectShape {
rect: content_stroke_rect,
rounding: Rounding::ZERO,
fill: Colors::fill(),
stroke: Stroke {
width: 1.0,
color: Colors::stroke()
},
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
// Draw content stroke.
ui.painter().add(content_stroke);
// Draw window content.
let mut content_rect = ui.max_rect(); let mut content_rect = ui.max_rect();
if !is_fullscreen { if !is_fullscreen {
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN); content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
} }
ui.allocate_ui_at_rect(content_rect, |ui| { // Draw window content.
self.window_title_ui(ui); ui.allocate_new_ui(UiBuilder::new().max_rect(content_rect), |ui| {
self.window_content(ui); // Draw window title.
self.window_title_ui(ui, is_fullscreen);
ui.add_space(-1.0);
// Draw title panel background.
Self::title_panel_bg(ui);
let content_rect = {
let mut rect = ui.max_rect();
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
rect
};
let mut content_ui = ui.new_child(UiBuilder::new()
.max_rect(content_rect)
.layout(*ui.layout()));
// Draw main content.
self.content.ui(&mut content_ui, &self.platform);
}); });
// Setup resize areas. // Setup resize areas.
@ -214,57 +196,53 @@ impl<Platform: PlatformCallbacks> App<Platform> {
} }
} }
/// Draw window content for desktop. /// Draw title panel background.
fn window_content(&mut self, ui: &mut egui::Ui) { fn title_panel_bg(ui: &mut egui::Ui) {
let content_rect = { let title_rect = {
let mut rect = ui.max_rect(); let mut rect = ui.max_rect();
rect.min.y += Content::WINDOW_TITLE_HEIGHT; if View::is_desktop() {
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
}
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
rect rect
}; };
// Draw main content. let title_bg = RectShape::filled(title_rect, Rounding::ZERO, Colors::yellow());
let mut content_ui = ui.child_ui(content_rect, *ui.layout(), None); ui.painter().add(title_bg);
self.content.ui(&mut content_ui, &self.platform);
} }
/// Draw custom window title content. /// Draw custom window title content.
fn window_title_ui(&self, ui: &mut egui::Ui) { fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
let content_rect = ui.max_rect();
let title_rect = { let title_rect = {
let mut rect = content_rect; let mut rect = ui.max_rect();
rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT; rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
rect rect
}; };
let is_fullscreen = ui.ctx().input(|i| { let title_bg_rect = {
i.viewport().fullscreen.unwrap_or(false) let mut r = title_rect.clone();
}); r.max.y += TitlePanel::HEIGHT - 1.0;
r
let window_title_bg = RectShape {
rect: title_rect,
rounding: if is_fullscreen {
Rounding::ZERO
} else {
Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
}
},
fill: Colors::yellow_dark(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
}; };
let is_mac = OperatingSystem::from_target_os() == OperatingSystem::Mac;
let window_title_bg = RectShape::new(title_bg_rect, if is_fullscreen || is_mac {
Rounding::ZERO
} else {
Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
}
}, Colors::yellow_dark(), Stroke::new(1.0, Colors::STROKE));
// Draw title background. // Draw title background.
ui.painter().add(window_title_bg); ui.painter().add(window_title_bg);
let painter = ui.painter(); let painter = ui.painter();
let interact_rect = { let interact_rect = {
let mut rect = title_rect; let mut rect = title_rect.clone();
rect.max.x -= 128.0;
rect.min.x += 85.0;
if !is_fullscreen { if !is_fullscreen {
rect.min.y += Content::WINDOW_FRAME_MARGIN; rect.min.y += Content::WINDOW_FRAME_MARGIN;
} }
@ -273,17 +251,20 @@ impl<Platform: PlatformCallbacks> App<Platform> {
let title_resp = ui.interact( let title_resp = ui.interact(
interact_rect, interact_rect,
egui::Id::new("window_title"), egui::Id::new("window_title"),
egui::Sense::click_and_drag(), egui::Sense::drag(),
); );
// Interact with the window title (drag to move window):
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
}
// Paint the title. // Paint the title.
let dual_wallets_panel = let dual_wallets_panel = ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) +
ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) View::get_right_inset() + View::get_left_inset();
+ View::get_right_inset() + View::get_left_inset();
let wallet_panel_opened = self.content.wallets.showing_wallet(); let wallet_panel_opened = self.content.wallets.showing_wallet();
let show_app_name = if dual_wallets_panel { let show_app_name = if dual_wallets_panel {
wallet_panel_opened && !AppConfig::show_wallets_at_dual_panel() wallet_panel_opened && !AppConfig::show_wallets_at_dual_panel()
} else if Content::is_dual_panel_mode(ui) { } else if Content::is_dual_panel_mode(ui.ctx()) {
wallet_panel_opened wallet_panel_opened
} else { } else {
Content::is_network_panel_open() || wallet_panel_opened Content::is_network_panel_open() || wallet_panel_opened
@ -302,20 +283,13 @@ impl<Platform: PlatformCallbacks> App<Platform> {
Colors::title(true), Colors::title(true),
); );
// Interact with the window title (drag to move window): ui.allocate_new_ui(UiBuilder::new().max_rect(title_rect), |ui| {
if !is_fullscreen && title_resp.double_clicked() {
ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
}
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
}
ui.allocate_ui_at_rect(title_rect, |ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
// Draw button to close window. // Draw button to close window.
View::title_button_small(ui, X, |_| { View::title_button_small(ui, X, |_| {
Content::show_exit_modal(); if Modal::opened().is_none() || Modal::opened_closeable() {
Content::show_exit_modal();
}
}); });
// Draw fullscreen button. // Draw fullscreen button.
@ -427,16 +401,11 @@ impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
} }
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
if View::is_desktop() { let is_mac = OperatingSystem::from_target_os() == OperatingSystem::Mac;
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac; if !View::is_desktop() || is_mac {
if is_mac_os { return Colors::fill_lite().to_normalized_gamma_f32();
Colors::fill().to_normalized_gamma_f32()
} else {
egui::Rgba::TRANSPARENT.to_array()
}
} else {
Colors::fill().to_normalized_gamma_f32()
} }
Colors::TRANSPARENT.to_normalized_gamma_f32()
} }
} }

View file

@ -46,6 +46,9 @@ const FILL_DARK: Color32 = Color32::from_gray(24);
const FILL_DEEP: Color32 = Color32::from_gray(238); const FILL_DEEP: Color32 = Color32::from_gray(238);
const FILL_DEEP_DARK: Color32 = Color32::from_gray(18); const FILL_DEEP_DARK: Color32 = Color32::from_gray(18);
const FILL_LITE: Color32 = Color32::from_gray(249);
const FILL_LITE_DARK: Color32 = Color32::from_gray(16);
const TEXT: Color32 = Color32::from_gray(80); const TEXT: Color32 = Color32::from_gray(80);
const TEXT_DARK: Color32 = Color32::from_gray(185); const TEXT_DARK: Color32 = Color32::from_gray(185);
@ -58,13 +61,9 @@ const TEXT_BUTTON_DARK: Color32 = Color32::from_gray(195);
const TITLE: Color32 = Color32::from_gray(60); const TITLE: Color32 = Color32::from_gray(60);
const TITLE_DARK: Color32 = Color32::from_gray(205); const TITLE_DARK: Color32 = Color32::from_gray(205);
const BUTTON: Color32 = Color32::from_gray(249);
const BUTTON_DARK: Color32 = Color32::from_gray(16);
const GRAY: Color32 = Color32::from_gray(120); const GRAY: Color32 = Color32::from_gray(120);
const GRAY_DARK: Color32 = Color32::from_gray(145); const GRAY_DARK: Color32 = Color32::from_gray(145);
const STROKE: Color32 = Color32::from_gray(200);
const STROKE_DARK: Color32 = Color32::from_gray(50); const STROKE_DARK: Color32 = Color32::from_gray(50);
const INACTIVE_TEXT: Color32 = Color32::from_gray(150); const INACTIVE_TEXT: Color32 = Color32::from_gray(150);
@ -86,6 +85,7 @@ fn use_dark() -> bool {
impl Colors { impl Colors {
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0); pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
pub const STROKE: Color32 = Color32::from_gray(200);
pub fn white_or_black(black_in_white: bool) -> Color32 { pub fn white_or_black(black_in_white: bool) -> Color32 {
if use_dark() { if use_dark() {
@ -167,6 +167,14 @@ impl Colors {
} }
} }
pub fn fill_lite() -> Color32 {
if use_dark() {
FILL_LITE_DARK
} else {
FILL_LITE
}
}
pub fn checkbox() -> Color32 { pub fn checkbox() -> Color32 {
if use_dark() { if use_dark() {
CHECKBOX_DARK CHECKBOX_DARK
@ -199,14 +207,6 @@ impl Colors {
} }
} }
pub fn button() -> Color32 {
if use_dark() {
BUTTON_DARK
} else {
BUTTON
}
}
pub fn gray() -> Color32 { pub fn gray() -> Color32 {
if use_dark() { if use_dark() {
GRAY_DARK GRAY_DARK
@ -219,7 +219,7 @@ impl Colors {
if use_dark() { if use_dark() {
STROKE_DARK STROKE_DARK
} else { } else {
STROKE Self::STROKE
} }
} }

View file

@ -15,7 +15,7 @@
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::thread; use std::thread;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -30,6 +30,10 @@ pub struct Desktop {
/// Context to repaint content and handle viewport commands. /// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>, ctx: Arc<RwLock<Option<egui::Context>>>,
/// Cameras amount.
cameras_amount: Arc<AtomicUsize>,
/// Camera index.
camera_index: Arc<AtomicUsize>,
/// Flag to check if camera stop is needed. /// Flag to check if camera stop is needed.
stop_camera: Arc<AtomicBool>, stop_camera: Arc<AtomicBool>,
@ -37,6 +41,127 @@ pub struct Desktop {
attention_required: Arc<AtomicBool>, attention_required: Arc<AtomicBool>,
} }
impl Desktop {
pub fn new() -> Self {
Self {
ctx: Arc::new(RwLock::new(None)),
cameras_amount: Arc::new(AtomicUsize::new(0)),
camera_index: Arc::new(AtomicUsize::new(0)),
stop_camera: Arc::new(AtomicBool::new(false)),
attention_required: Arc::new(AtomicBool::new(false)),
}
}
#[allow(dead_code)]
#[cfg(not(target_os = "macos"))]
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
camera_index: Arc<AtomicUsize>,
stop_camera: Arc<AtomicBool>) {
use nokhwa::Camera;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
use nokhwa::utils::ApiBackend;
let devices = nokhwa::query(ApiBackend::Auto).unwrap();
cameras_amount.store(devices.len(), Ordering::Relaxed);
let index = camera_index.load(Ordering::Relaxed);
if devices.is_empty() || index >= devices.len() {
return;
}
thread::spawn(move || {
let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
let requested = RequestedFormat::new::<RgbFormat>(
RequestedFormatType::AbsoluteHighestFrameRate
);
// Create and open camera.
if let Ok(mut camera) = Camera::new(index, requested) {
if let Ok(_) = camera.open_stream() {
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
if let Ok(frame) = camera.frame() {
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((frame.buffer().to_vec(), 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
}
camera.stop_stream().unwrap();
};
}
});
}
#[allow(dead_code)]
#[cfg(target_os = "macos")]
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
camera_index: Arc<AtomicUsize>,
stop_camera: Arc<AtomicBool>) {
use image::{ExtendedColorType, ImageBuffer, ImageEncoder, Rgb};
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
use image::codecs::jpeg::JpegEncoder;
let index = camera_index.load(Ordering::Relaxed);
let devices = PlatformContext::default().devices().unwrap_or(vec![]);
cameras_amount.store(devices.len(), Ordering::Relaxed);
if devices.is_empty() || index >= devices.len() {
return;
}
// Capture images at separate thread.
let uri = devices[camera_index.load(Ordering::Relaxed)].uri.clone();
thread::spawn(move || {
if let Ok(dev) = PlatformContext::default().open_device(&uri) {
let streams = dev.streams().unwrap_or(vec![]);
if streams.is_empty() {
return;
}
let stream_desc = streams[0].clone();
let w = stream_desc.width;
let h = stream_desc.height;
if let Ok(mut stream) = dev.start_stream(&stream_desc) {
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
let frame = stream.next()
.expect("Stream is dead")
.expect("Failed to capture a frame");
let mut out = vec![];
if let Some(buf) = ImageBuffer::<Rgb<u8>, &[u8]>::from_raw(w, h, &frame) {
JpegEncoder::new(&mut out)
.write_image(buf.as_raw(), w, h, ExtendedColorType::Rgb8)
.unwrap_or_default();
} else {
out = frame.to_vec();
}
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((out, 0));
}
}
}
});
}
}
impl PlatformCallbacks for Desktop { impl PlatformCallbacks for Desktop {
fn set_context(&mut self, ctx: &egui::Context) { fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write(); let mut w_ctx = self.ctx.write();
@ -47,7 +172,7 @@ impl PlatformCallbacks for Desktop {
let r_ctx = self.ctx.read(); let r_ctx = self.ctx.read();
if r_ctx.is_some() { if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap(); let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(egui::ViewportCommand::Close); ctx.send_viewport_cmd(ViewportCommand::Close);
} }
} }
@ -71,15 +196,13 @@ impl PlatformCallbacks for Desktop {
let mut w_image = LAST_CAMERA_IMAGE.write(); let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None; *w_image = None;
} }
// Setup stop camera flag. // Setup stop camera flag.
let stop_camera = self.stop_camera.clone(); let stop_camera = self.stop_camera.clone();
stop_camera.store(false, Ordering::Relaxed); stop_camera.store(false, Ordering::Relaxed);
// Capture images at separate thread. Self::start_camera_capture(self.cameras_amount.clone(),
thread::spawn(move || { self.camera_index.clone(),
Self::start_camera_capture(stop_camera); stop_camera);
});
} }
fn stop_camera(&self) { fn stop_camera(&self) {
@ -96,11 +219,20 @@ impl PlatformCallbacks for Desktop {
} }
fn can_switch_camera(&self) -> bool { fn can_switch_camera(&self) -> bool {
false let amount = self.cameras_amount.load(Ordering::Relaxed);
amount > 1
} }
fn switch_camera(&self) { fn switch_camera(&self) {
return; self.stop_camera();
let index = self.camera_index.load(Ordering::Relaxed);
let amount = self.cameras_amount.load(Ordering::Relaxed);
if index == amount - 1 {
self.camera_index.store(0, Ordering::Relaxed);
} else {
self.camera_index.store(index + 1, Ordering::Relaxed);
}
self.start_camera();
} }
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> { fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
@ -171,94 +303,6 @@ impl PlatformCallbacks for Desktop {
} }
} }
impl Desktop {
pub fn new() -> Self {
Self {
stop_camera: Arc::new(AtomicBool::new(false)),
ctx: Arc::new(RwLock::new(None)),
attention_required: Arc::new(AtomicBool::new(false)),
}
}
#[allow(dead_code)]
#[cfg(target_os = "windows")]
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
use nokhwa::Camera;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
let index = CameraIndex::Index(0);
let requested = RequestedFormat::new::<RgbFormat>(
RequestedFormatType::AbsoluteHighestFrameRate
);
// Create and open camera.
let mut camera = Camera::new(index, requested).unwrap();
if let Ok(_) = camera.open_stream() {
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
if let Ok(frame) = camera.frame() {
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((frame.buffer().to_vec(), 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
}
camera.stop_stream().unwrap();
};
}
#[allow(dead_code)]
#[cfg(not(target_os = "windows"))]
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
use image::ImageEncoder;
let ctx = PlatformContext::default();
let devices = ctx.devices().unwrap();
if let Ok(dev) = ctx.open_device(&devices[0].uri) {
let streams = dev.streams().unwrap();
let stream_desc = streams[0].clone();
let w = stream_desc.width;
let h = stream_desc.height;
let mut stream = dev.start_stream(&stream_desc).unwrap();
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame");
let mut out = vec![];
if let Some(buf) = image::ImageBuffer::<image::Rgb<u8>, &[u8]>::from_raw(w, h, &frame) {
image::codecs::jpeg::JpegEncoder::new(&mut out)
.write_image(buf.as_raw(), w, h, image::ExtendedColorType::Rgb8).unwrap();
} else {
out = frame.to_vec();
}
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((out, 0));
}
}
}
}
lazy_static! { lazy_static! {
/// Last captured image from started camera. /// Last captured image from started camera.
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None)); static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));

View file

@ -15,11 +15,9 @@
use std::sync::Arc; use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::thread; use std::thread;
use eframe::emath::Align;
use egui::load::SizedTexture; use egui::load::SizedTexture;
use egui::{Layout, Pos2, Rect, RichText, TextureOptions, Widget}; use egui::{Pos2, Rect, RichText, TextureOptions, UiBuilder, Widget};
use image::{DynamicImage, EncodableLayout, ImageFormat}; use image::{DynamicImage, EncodableLayout};
use grin_util::ZeroingString; use grin_util::ZeroingString;
use grin_wallet_libwallet::SlatepackAddress; use grin_wallet_libwallet::SlatepackAddress;
use grin_keychain::mnemonic::WORDS; use grin_keychain::mnemonic::WORDS;
@ -36,16 +34,15 @@ use crate::wallet::WalletUtils;
pub struct CameraContent { pub struct CameraContent {
/// QR code scanning progress and result. /// QR code scanning progress and result.
qr_scan_state: Arc<RwLock<QrScanState>>, qr_scan_state: Arc<RwLock<QrScanState>>,
/// Uniform Resources URIs collected from QR code scanning. /// Uniform Resources URIs collected from QR code scanning.
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>, ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>
} }
impl Default for CameraContent { impl Default for CameraContent {
fn default() -> Self { fn default() -> Self {
Self { Self {
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())), qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
ur_data: Arc::new(RwLock::new(None)), ur_data: Arc::new(RwLock::new(None))
} }
} }
} }
@ -53,102 +50,112 @@ impl Default for CameraContent {
impl CameraContent { impl CameraContent {
/// Draw camera content. /// Draw camera content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw last image from camera or loader. ui.ctx().request_repaint();
if let Some(img_data) = cb.camera_image() { let rect = if let Some(img_data) = cb.camera_image() {
// Load image to draw. if let Ok(img) =
if let Ok(mut img) = image::load_from_memory(&*img_data.0) {
image::load_from_memory_with_format(&*img_data.0, ImageFormat::Jpeg) {
// Process image to find QR code. // Process image to find QR code.
self.scan_qr(&img); self.scan_qr(&img);
// Setup image rotation.
img = match img_data.1 { // Draw image.
90 => img.rotate90(), let img_rect = self.image_ui(ui, img, img_data.1);
180 => img.rotate180(),
270 => img.rotate270(),
_ => img
};
// Convert to ColorImage to add at content.
let color_img = match &img {
DynamicImage::ImageRgb8(image) => {
egui::ColorImage::from_rgb(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
)
},
other => {
let image = other.to_rgba8();
egui::ColorImage::from_rgba_unmultiplied(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
)
},
};
// Create image texture.
let texture = ui.ctx().load_texture("camera_image",
color_img.clone(),
TextureOptions::default());
let img_size = egui::emath::vec2(color_img.width() as f32,
color_img.height() as f32);
let sized_img = SizedTexture::new(texture.id(), img_size);
// Add image to content.
ui.vertical_centered(|ui| {
egui::Image::from_texture(sized_img)
// Setup to crop image at square.
.uv(Rect::from([
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
Pos2::new(1.0, 1.0)
]))
.max_height(ui.available_width())
.maintain_aspect_ratio(false)
.shrink_to_fit()
.ui(ui);
});
// Show UR scan progress. // Show UR scan progress.
let show_ur_progress = { self.ur_progress_ui(ui);
self.ur_data.clone().read().is_some() img_rect
};
let ur_progress = self.ur_progress();
if show_ur_progress && ur_progress != 0 {
ui.add_space(-52.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(format!("{}%", ur_progress))
.size(16.0)
.color(Colors::yellow()));
});
}
// Show button to switch cameras.
if cb.can_switch_camera() {
ui.add_space(-52.0);
let mut size = ui.available_size();
size.y = 48.0;
ui.allocate_ui_with_layout(size, Layout::right_to_left(Align::Max), |ui| {
ui.add_space(4.0);
View::button(ui, CAMERA_ROTATE.to_string(), Colors::white_or_black(false), || {
cb.switch_camera();
});
});
}
} else { } else {
self.loading_content_ui(ui); self.loading_ui(ui)
} }
} else { } else {
self.loading_content_ui(ui); self.loading_ui(ui)
} };
// Request redraw. // Show button to switch cameras.
ui.ctx().request_repaint(); if cb.can_switch_camera() {
let r = {
let mut r = rect.clone();
r.min.y = r.max.y - 52.0;
r.min.x = r.max.x - 52.0;
r
};
ui.allocate_new_ui(UiBuilder::new().max_rect(r), |ui| {
let rotate_img = CAMERA_ROTATE.to_string();
View::button(ui, rotate_img, Colors::white_or_black(false), || {
cb.switch_camera();
});
});
}
}
/// Draw camera image.
fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect {
// Setup image rotation.
img = match rotation {
90 => img.rotate90(),
180 => img.rotate180(),
270 => img.rotate270(),
_ => img
};
if View::is_desktop() {
img = img.fliph();
}
// Convert to ColorImage.
let color_img = match &img {
DynamicImage::ImageRgb8(image) => {
egui::ColorImage::from_rgb(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
)
},
other => {
let image = other.to_rgba8();
egui::ColorImage::from_rgba_unmultiplied(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
)
},
};
// Create image texture.
let texture = ui.ctx().load_texture("camera_image",
color_img.clone(),
TextureOptions::default());
let img_size = egui::emath::vec2(color_img.width() as f32,
color_img.height() as f32);
let sized_img = SizedTexture::new(texture.id(), img_size);
egui::Image::from_texture(sized_img)
// Setup to crop image at square.
.uv(Rect::from([
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
Pos2::new(1.0, 1.0)
]))
.max_height(ui.available_width())
.maintain_aspect_ratio(false)
.shrink_to_fit()
.ui(ui).rect
}
/// Draw animated QR code scanning progress.
fn ur_progress_ui(&self, ui: &mut egui::Ui) {
let show_ur_progress = {
self.ur_data.as_ref().read().is_some()
};
if show_ur_progress {
ui.centered_and_justified(|ui| {
ui.label(RichText::new(format!("{}%", self.ur_progress()))
.size(17.0)
.color(Colors::green()));
});
}
} }
/// Draw camera loading progress content. /// Draw camera loading progress content.
fn loading_content_ui(&self, ui: &mut egui::Ui) { fn loading_ui(&self, ui: &mut egui::Ui) -> Rect {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add_space(space); ui.add_space(space);
View::big_loading_spinner(ui); View::big_loading_spinner(ui);
ui.add_space(space); ui.add_space(space);
}); }).response.rect
} }
/// Check if image is processing to find QR code. /// Check if image is processing to find QR code.

View file

@ -52,6 +52,11 @@ pub struct Content {
allowed_modal_ids: Vec<&'static str> allowed_modal_ids: Vec<&'static str>
} }
/// Identifier for integrated node warning [`Modal`] on Android.
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
/// Identifier for crash report [`Modal`].
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
impl Default for Content { impl Default for Content {
fn default() -> Self { fn default() -> Self {
// Exit from eframe only for non-mobile platforms. // Exit from eframe only for non-mobile platforms.
@ -66,8 +71,8 @@ impl Default for Content {
allowed_modal_ids: vec![ allowed_modal_ids: vec![
Self::EXIT_CONFIRMATION_MODAL, Self::EXIT_CONFIRMATION_MODAL,
Self::SETTINGS_MODAL, Self::SETTINGS_MODAL,
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL, ANDROID_INTEGRATED_NODE_WARNING_MODAL,
Self::CRASH_REPORT_MODAL CRASH_REPORT_MODAL
], ],
} }
} }
@ -85,8 +90,8 @@ impl ModalContainer for Content {
match modal.id { match modal.id {
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb), Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal), Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal), ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb), CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
_ => {} _ => {}
} }
} }
@ -97,10 +102,6 @@ impl Content {
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal"; pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
/// Identifier for wallet opening [`Modal`]. /// Identifier for wallet opening [`Modal`].
pub const SETTINGS_MODAL: &'static str = "settings_modal"; pub const SETTINGS_MODAL: &'static str = "settings_modal";
/// Identifier for integrated node warning [`Modal`] on Android.
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
/// Identifier for crash report [`Modal`].
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
/// Default width of side panel at application UI. /// Default width of side panel at application UI.
pub const SIDE_PANEL_WIDTH: f32 = 400.0; pub const SIDE_PANEL_WIDTH: f32 = 400.0;
@ -110,11 +111,10 @@ impl Content {
pub const WINDOW_FRAME_MARGIN: f32 = 6.0; pub const WINDOW_FRAME_MARGIN: f32 = 6.0;
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container.
self.current_modal_ui(ui, cb); self.current_modal_ui(ui, cb);
let dual_panel = Self::is_dual_panel_mode(ui); let dual_panel = Self::is_dual_panel_mode(ui.ctx());
let (is_panel_open, panel_width) = Self::network_panel_state_width(ui, dual_panel); let (is_panel_open, panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
// Show network content. // Show network content.
egui::SidePanel::left("network_panel") egui::SidePanel::left("network_panel")
@ -137,48 +137,26 @@ impl Content {
}); });
if self.first_draw { if self.first_draw {
// Show crash report if needed. // Show crash report or integrated node Android warning.
if AppConfig::show_crash() { if Settings::crash_report_path().exists() {
Modal::new(Self::CRASH_REPORT_MODAL) Modal::new(CRASH_REPORT_MODAL)
.closeable(false) .closeable(false)
.position(ModalPosition::Center) .position(ModalPosition::Center)
.title(t!("crash_report")) .title(t!("crash_report"))
.show(); .show();
} else { } else if OperatingSystem::from_target_os() == OperatingSystem::Android &&
// Show integrated node warning on Android if needed.
if OperatingSystem::from_target_os() == OperatingSystem::Android &&
AppConfig::android_integrated_node_warning_needed() { AppConfig::android_integrated_node_warning_needed() {
Modal::new(Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL) Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL)
.title(t!("network.node")) .title(t!("network.node"))
.show(); .show();
}
} }
self.first_draw = false; self.first_draw = false;
} }
} }
/// Get [`NetworkContent`] panel state and width.
fn network_panel_state_width(ui: &mut egui::Ui, dual_panel: bool) -> (bool, f32) {
let is_panel_open = dual_panel || Self::is_network_panel_open();
let panel_width = if dual_panel {
Self::SIDE_PANEL_WIDTH + View::get_left_inset()
} else {
let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
View::window_size(ui).0 - if View::is_desktop() && !is_fullscreen &&
OperatingSystem::from_target_os() != OperatingSystem::Mac {
Self::WINDOW_FRAME_MARGIN * 2.0
} else {
0.0
}
};
(is_panel_open, panel_width)
}
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time. /// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
pub fn is_dual_panel_mode(ui: &egui::Ui) -> bool { pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool {
let (w, h) = View::window_size(ui); let (w, h) = View::window_size(ctx);
// Screen is wide if width is greater than height or just 20% smaller. // Screen is wide if width is greater than height or just 20% smaller.
let is_wide_screen = w > h || w + (w * 0.2) >= h; 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 // Dual panel mode is available when window is wide and its width is at least 2 times
@ -260,9 +238,9 @@ impl Content {
} }
/// Handle Back key event. /// Handle Back key event.
pub fn on_back(&mut self) { pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) {
if Modal::on_back() { if Modal::on_back() {
if self.wallets.on_back() { if self.wallets.on_back(cb) {
Self::show_exit_modal() Self::show_exit_modal()
} }
} }
@ -341,42 +319,40 @@ impl Content {
let item_rounding = View::item_rounding(index, len, false); let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke()); ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw button to select language.
// Draw button to select language. let is_current = if let Some(lang) = AppConfig::locale() {
let is_current = if let Some(lang) = AppConfig::locale() { lang == locale
lang == locale } else {
} else { rust_i18n::locale() == locale
rust_i18n::locale() == locale };
}; if !is_current {
if !is_current { View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || { rust_i18n::set_locale(locale);
rust_i18n::set_locale(locale); AppConfig::save_locale(locale);
AppConfig::save_locale(locale); modal.close();
modal.close(); });
}); } else {
} else { ui.add_space(14.0);
ui.add_space(14.0); ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); ui.add_space(14.0);
ui.add_space(14.0); }
}
let layout_size = ui.available_size(); let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
// Draw language name.
ui.add_space(12.0); ui.add_space(12.0);
ui.vertical(|ui| { let color = if is_current {
// Draw language name. Colors::title(false)
ui.add_space(12.0); } else {
let color = if is_current { Colors::gray()
Colors::title(false) };
} else { ui.label(RichText::new(t!("lang_name", locale = locale))
Colors::gray() .size(17.0)
}; .color(color));
ui.label(RichText::new(t!("lang_name", locale = locale)) ui.add_space(3.0);
.size(17.0)
.color(color));
ui.add_space(3.0);
});
}); });
}); });
}); });
@ -415,10 +391,10 @@ impl Content {
let text = format!("{} {}", FILE_X, t!("share")); let text = format!("{} {}", FILE_X, t!("share"));
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || { View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
if let Ok(data) = fs::read_to_string(Settings::crash_report_path()) { if let Ok(data) = fs::read_to_string(Settings::crash_report_path()) {
cb.share_data(Settings::CRASH_REPORT_FILE_NAME.to_string(), let name = Settings::CRASH_REPORT_FILE_NAME.to_string();
data.as_bytes().to_vec()).unwrap_or_default() let _ = cb.share_data(name, data.as_bytes().to_vec());
} }
AppConfig::set_show_crash(false); Settings::delete_crash_report();
modal.close(); modal.close();
}); });
}); });
@ -427,10 +403,29 @@ impl Content {
ui.add_space(8.0); ui.add_space(8.0);
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
AppConfig::set_show_crash(false); Settings::delete_crash_report();
modal.close(); modal.close();
}); });
}); });
ui.add_space(6.0); ui.add_space(6.0);
} }
}
/// Get [`NetworkContent`] panel state and width.
fn network_panel_state_width(ctx: &egui::Context, dual_panel: bool) -> (bool, f32) {
let is_panel_open = dual_panel || Content::is_network_panel_open();
let panel_width = if dual_panel {
Content::SIDE_PANEL_WIDTH + View::get_left_inset()
} else {
let is_fullscreen = ctx.input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
View::window_size(ctx).0 - if View::is_desktop() && !is_fullscreen &&
OperatingSystem::from_target_os() != OperatingSystem::Mac {
Content::WINDOW_FRAME_MARGIN * 2.0
} else {
0.0
}
};
(is_panel_open, panel_width)
} }

View file

@ -78,8 +78,8 @@ impl FilePickButton {
} }
} else { } else {
// Draw button to pick file. // Draw button to pick file.
let file_text = format!("{} {}", ARCHIVE_BOX, t!("choose_file")); let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || { View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
if let Some(path) = cb.pick_file() { if let Some(path) = cb.pick_file() {
self.on_file_pick(path); self.on_file_pick(path);
} }

View file

@ -16,7 +16,7 @@ use lazy_static::lazy_static;
use std::sync::Arc; use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use egui::{Align2, Rect, RichText, Rounding, Stroke, Vec2}; use egui::{Align2, RichText, Rounding, Stroke, UiBuilder, Vec2};
use egui::epaint::{RectShape, Shadow}; use egui::epaint::{RectShape, Shadow};
use egui::os::OperatingSystem; use egui::os::OperatingSystem;
@ -29,17 +29,17 @@ lazy_static! {
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default())); static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
} }
/// Stores data to draw modal [`egui::Window`] at ui. /// Modal [`egui::Window`] container.
#[derive(Clone)] #[derive(Clone)]
pub struct Modal { pub struct Modal {
/// Identifier for modal. /// Identifier for modal.
pub(crate) id: &'static str, pub(crate) id: &'static str,
/// Position on the screen. /// Position on the screen.
pub position: ModalPosition, pub position: ModalPosition,
/// To check if it can be closed. /// Flag to check if modal can be closed by keys.
closeable: Arc<AtomicBool>, closeable: Arc<AtomicBool>,
/// Title text /// Title text.
title: Option<String> title: Option<String>,
} }
impl Modal { impl Modal {
@ -54,7 +54,7 @@ impl Modal {
id, id,
position: ModalPosition::Center, position: ModalPosition::Center,
closeable: Arc::new(AtomicBool::new(true)), closeable: Arc::new(AtomicBool::new(true)),
title: None title: None,
} }
} }
@ -110,7 +110,7 @@ impl Modal {
} }
/// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed. /// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed.
/// Return `false` if Modal existed in [`ModalState`] before call. /// Return `false` if modal existed in state before call.
pub fn on_back() -> bool { pub fn on_back() -> bool {
let mut w_state = MODAL_STATE.write(); let mut w_state = MODAL_STATE.write();
@ -125,7 +125,7 @@ impl Modal {
true true
} }
/// Return id of opened [`Modal`]. /// Return identifier of opened [`Modal`].
pub fn opened() -> Option<&'static str> { pub fn opened() -> Option<&'static str> {
// Check if modal is showing. // Check if modal is showing.
{ {
@ -140,6 +140,19 @@ impl Modal {
Some(modal.id) Some(modal.id)
} }
/// Check if [`Modal`] is opened and can be closed.
pub fn opened_closeable() -> bool {
// Check if modal is showing.
{
if MODAL_STATE.read().modal.is_none() {
return false;
}
}
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
modal.closeable.load(Ordering::Relaxed)
}
/// Set title text for current opened [`Modal`]. /// Set title text for current opened [`Modal`].
pub fn set_title(title: String) { pub fn set_title(title: String) {
// Save state. // Save state.
@ -170,40 +183,42 @@ impl Modal {
let is_fullscreen = ctx.input(|i| { let is_fullscreen = ctx.input(|i| {
i.viewport().fullscreen.unwrap_or(false) i.viewport().fullscreen.unwrap_or(false)
}); });
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
let mut rect = ctx.screen_rect(); // Setup background rect.
if View::is_desktop() && !is_mac_os { let bg_rect = if View::is_desktop() {
let margin = if !is_fullscreen { let mut r = ctx.screen_rect();
Content::WINDOW_FRAME_MARGIN let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
} else { if !is_mac && !is_fullscreen {
0.0 r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
}; }
rect = rect.shrink(margin - 0.5); r.min.y += Content::WINDOW_TITLE_HEIGHT;
rect.min += egui::vec2(0.0, Content::WINDOW_TITLE_HEIGHT + 0.5); r
rect.max.x += 0.5; } else {
} ctx.screen_rect()
};
// Draw modal background.
egui::Window::new("modal_bg_window") egui::Window::new("modal_bg_window")
.title_bar(false) .title_bar(false)
.resizable(false) .resizable(false)
.collapsible(false) .collapsible(false)
.fixed_rect(rect) .fixed_rect(bg_rect)
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::semi_transparent(), fill: Colors::semi_transparent(),
..Default::default() ..Default::default()
}) })
.show(ctx, |ui| { .show(ctx, |ui| {
ui.set_min_size(rect.size()); ui.set_min_size(bg_rect.size());
}); });
// Setup width of modal content. // Setup width of modal content.
let side_insets = View::get_left_inset() + View::get_right_inset(); let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = rect.width() - (side_insets + Self::DEFAULT_MARGIN); let available_width = ctx.screen_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
let width = f32::min(available_width, Self::DEFAULT_WIDTH); let width = f32::min(available_width, Self::DEFAULT_WIDTH);
// Show main content Window at given position. // Show main content window at given position.
let (content_align, content_offset) = self.modal_position(is_fullscreen); let (content_align, content_offset) = self.modal_position();
let layer_id = egui::Window::new(format!("modal_window_{}", self.id)) let layer_id = egui::Window::new("modal_window")
.title_bar(false) .title_bar(false)
.resizable(false) .resizable(false)
.collapsible(false) .collapsible(false)
@ -218,33 +233,30 @@ impl Modal {
color: egui::Color32::from_black_alpha(32), color: egui::Color32::from_black_alpha(32),
}, },
rounding: Rounding::same(8.0), rounding: Rounding::same(8.0),
fill: Colors::fill(),
..Default::default() ..Default::default()
}) })
.show(ctx, |ui| { .show(ctx, |ui| {
if self.title.is_some() { if let Some(title) = &self.title {
self.title_ui(ui); title_ui(title, ui);
} }
self.content_ui(ui, add_content); self.content_ui(ui, add_content);
}).unwrap().response.layer_id; }).unwrap().response.layer_id;
// Always show main content Window above background Window. // Always show main content window above background window.
ctx.move_to_top(layer_id); ctx.move_to_top(layer_id);
} }
/// Get [`egui::Window`] position based on [`ModalPosition`]. /// Get [`egui::Window`] position based on [`ModalPosition`].
fn modal_position(&self, is_fullscreen: bool) -> (Align2, Vec2) { fn modal_position(&self) -> (Align2, Vec2) {
let align = match self.position { let align = match self.position {
ModalPosition::CenterTop => Align2::CENTER_TOP, ModalPosition::CenterTop => Align2::CENTER_TOP,
ModalPosition::Center => Align2::CENTER_CENTER ModalPosition::Center => Align2::CENTER_CENTER
}; };
let x_align = View::get_left_inset() - View::get_right_inset(); let x_align = View::get_left_inset() - View::get_right_inset();
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac; let extra_y = if View::is_desktop() {
let extra_y = if View::is_desktop() && !is_mac_os { Content::WINDOW_TITLE_HEIGHT + if !is_mac {
Content::WINDOW_TITLE_HEIGHT + if !is_fullscreen {
Content::WINDOW_FRAME_MARGIN Content::WINDOW_FRAME_MARGIN
} else { } else {
0.0 0.0
@ -252,7 +264,7 @@ impl Modal {
} else { } else {
0.0 0.0
}; };
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN + extra_y; let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y;
let offset = match self.position { let offset = match self.position {
ModalPosition::CenterTop => Vec2::new(x_align, y_align), ModalPosition::CenterTop => Vec2::new(x_align, y_align),
@ -264,80 +276,64 @@ impl Modal {
/// Draw provided content. /// Draw provided content.
fn content_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) { fn content_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
let mut rect = ui.available_rect_before_wrap(); 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. // Create background shape.
let rounding = if self.title.is_some() { let mut bg_shape = RectShape::new(rect, if self.title.is_none() {
Rounding::same(8.0)
} else {
Rounding { Rounding {
nw: 0.0, nw: 0.0,
ne: 0.0, ne: 0.0,
sw: 8.0, sw: 8.0,
se: 8.0, se: 8.0,
} }
} else { }, Colors::fill(), Stroke::NONE);
Rounding::same(8.0)
};
let mut bg_shape = RectShape {
rect,
rounding,
fill: Colors::fill(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
let bg_idx = ui.painter().add(bg_shape); let bg_idx = ui.painter().add(bg_shape);
// Draw main content. rect.min += egui::emath::vec2(6.0, 0.0);
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| { rect.max -= egui::emath::vec2(6.0, 0.0);
let resp = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
(add_content)(ui, self); (add_content)(ui, self);
}).response.rect;
// Setup background shape to be painted behind main content.
content_rect.min -= egui::emath::vec2(6.0, 0.0);
content_rect.max += egui::emath::vec2(6.0, 0.0);
bg_shape.rect = content_rect;
ui.painter().set(bg_idx, bg_shape);
}
/// Draw title content.
fn title_ui(&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: Colors::yellow(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
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(Self::DEFAULT_MARGIN + 1.0);
ui.label(RichText::new(self.title.as_ref().unwrap())
.size(19.0)
.color(Colors::title(true))
);
ui.add_space(Self::DEFAULT_MARGIN);
// Draw line below title.
View::horizontal_line(ui, Colors::item_stroke());
});
}).response; }).response;
// Setup background shape to be painted behind title content. // Setup background size.
bg_shape.rect = title_resp.rect; let bg_rect = {
let mut r = resp.rect.clone();
r.min -= egui::emath::vec2(6.0, 0.0);
r.max += egui::emath::vec2(6.0, 0.0);
r
};
bg_shape.rect = bg_rect;
ui.painter().set(bg_idx, bg_shape); ui.painter().set(bg_idx, bg_shape);
} }
}
/// Draw title content.
fn title_ui(title: &String, ui: &mut egui::Ui) {
let rect = ui.available_rect_before_wrap();
// Create background shape.
let mut bg_shape = RectShape::new(rect, Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
}, Colors::yellow(), Stroke::NONE);
let bg_idx = ui.painter().add(bg_shape);
// Draw title content.
let resp = ui.vertical_centered(|ui| {
ui.add_space(Modal::DEFAULT_MARGIN + 2.0);
ui.label(RichText::new(title)
.size(19.0)
.color(Colors::title(true))
);
ui.add_space(Modal::DEFAULT_MARGIN + 1.0);
// Draw line below title.
View::horizontal_line(ui, Colors::item_stroke());
}).response;
// Setup background size.
bg_shape.rect = resp.rect;
ui.painter().set(bg_idx, bg_shape);
} }

View file

@ -202,34 +202,32 @@ impl ConnectionsContent {
let item_rounding = View::item_rounding(index, len, false); let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke()); ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw provided buttons.
// Draw provided buttons. buttons_ui(ui);
buttons_ui(ui);
let layout_size = ui.available_size(); let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical(|ui| { ui.vertical(|ui| {
// Draw connections URL. // Draw connections URL.
ui.add_space(4.0); ui.add_space(4.0);
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url); let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false)); View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
ui.add_space(1.0); ui.add_space(1.0);
// Setup connection status text. // Setup connection status text.
let status_text = if let Some(available) = conn.available { let status_text = if let Some(available) = conn.available {
if available { if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available")) format!("{} {}", CHECK_CIRCLE, t!("network.available"))
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
} else { } else {
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check")) format!("{} {}", X_CIRCLE, t!("network.not_available"))
}; }
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray())); } else {
ui.add_space(3.0); format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
}); };
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
}); });
}); });
}); });

View file

@ -21,15 +21,15 @@ use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THRE
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, TitlePanel, View}; use crate::gui::views::{Content, TitlePanel, View};
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings}; use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
use crate::gui::views::network::types::{NetworkTab, NetworkTabType}; use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::types::{TitleContentType, TitleType}; use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
use crate::node::{Node, NodeConfig, NodeError}; use crate::node::{Node, NodeConfig, NodeError};
use crate::wallet::ExternalConnection; use crate::wallet::ExternalConnection;
/// Network content. /// Network content.
pub struct NetworkContent { pub struct NetworkContent {
/// Current integrated node tab content. /// Current integrated node tab content.
node_tab_content: Box<dyn NetworkTab>, node_tab_content: Box<dyn NodeTab>,
/// Connections content. /// Connections content.
connections: ConnectionsContent, connections: ConnectionsContent,
} }
@ -46,14 +46,14 @@ impl Default for NetworkContent {
impl NetworkContent { impl NetworkContent {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let show_connections = AppConfig::show_connections_network_panel(); let show_connections = AppConfig::show_connections_network_panel();
let dual_panel = Content::is_dual_panel_mode(ui); let dual_panel = Content::is_dual_panel_mode(ui.ctx());
// Show title panel. // Show title panel.
self.title_ui(ui, show_connections); self.title_ui(ui, dual_panel, show_connections);
// Show integrated node tabs content. // Show integrated node tabs content.
if !show_connections { if !show_connections {
egui::TopBottomPanel::bottom("node_tabs_content") egui::TopBottomPanel::bottom("node_tabs")
.min_height(0.5) .min_height(0.5)
.resizable(false) .resizable(false)
.frame(egui::Frame { .frame(egui::Frame {
@ -67,15 +67,23 @@ impl NetworkContent {
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
ui.vertical_centered(|ui| { let rect = ui.available_rect_before_wrap();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui); self.tabs_ui(ui);
});
}); });
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.x -= View::get_left_inset() + View::TAB_ITEMS_PADDING;
r.min.y -= View::TAB_ITEMS_PADDING;
r.max.x += View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING;
r
};
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
}); });
} }
// Show current node tab content. // Show integrated node tab content.
egui::SidePanel::right("node_tab_content") egui::SidePanel::right("node_tab_content")
.resizable(false) .resizable(false)
.exact_width(ui.available_width()) .exact_width(ui.available_width())
@ -85,8 +93,6 @@ impl NetworkContent {
.show_animated_inside(ui, !show_connections, |ui| { .show_animated_inside(ui, !show_connections, |ui| {
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::white_or_black(false),
stroke: View::item_stroke(),
inner_margin: Margin { inner_margin: Margin {
left: View::get_left_inset() + 4.0, left: View::get_left_inset() + 4.0,
right: View::far_right_inset_margin(ui) + 4.0, right: View::far_right_inset_margin(ui) + 4.0,
@ -96,14 +102,42 @@ impl NetworkContent {
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
self.node_tab_content.ui(ui, cb); let rect = ui.available_rect_before_wrap();
if self.node_tab_content.get_type() != NodeTabType::Settings {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let node_err = Node::get_error();
if let Some(err) = node_err {
node_error_ui(ui, err);
} else if !Node::is_running() {
disabled_node_ui(ui);
} else if Node::get_stats().is_none() || Node::is_restarting() ||
Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
} else {
self.node_tab_content.ui(ui, cb);
}
});
} else {
self.node_tab_content.ui(ui, cb);
}
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += 4.0;
r.max.y += 4.0;
r
};
if dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
}); });
}); });
// Show connections content. // Show connections content.
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
stroke: View::item_stroke(),
inner_margin: Margin { inner_margin: Margin {
left: if show_connections { left: if show_connections {
View::get_left_inset() + 4.0 View::get_left_inset() + 4.0
@ -116,18 +150,14 @@ impl NetworkContent {
0.0 0.0
}, },
top: 3.0, top: 3.0,
bottom: if View::is_desktop() && show_connections { bottom: 4.0 + View::get_bottom_inset(),
6.0
} else {
4.0
},
}, },
fill: Colors::button(),
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
ScrollArea::vertical() ScrollArea::vertical()
.id_source("connections_content") .id_salt("connections_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
@ -143,15 +173,26 @@ impl NetworkContent {
}); });
}); });
}); });
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += 4.0;
r.max.y += 4.0 + View::get_bottom_inset();
r
};
if show_connections && dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
}); });
// Redraw after delay. // Redraw after delay if node is running at non-dual-panel mode.
if Node::is_running() { if !dual_panel && Content::is_network_panel_open() && Node::is_running() {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY); ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
} }
} }
/// Draw tab buttons in the bottom of the screen. /// Draw tab buttons at bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui) { fn tabs_ui(&mut self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
// Setup spacing between tabs. // Setup spacing between tabs.
@ -163,22 +204,22 @@ impl NetworkContent {
let current_type = self.node_tab_content.get_type(); let current_type = self.node_tab_content.get_type();
ui.columns(4, |columns| { ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, DATABASE, current_type == NetworkTabType::Node, |_| { View::tab_button(ui, DATABASE, current_type == NodeTabType::Info, |_| {
self.node_tab_content = Box::new(NetworkNode::default()); self.node_tab_content = Box::new(NetworkNode::default());
}); });
}); });
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
View::tab_button(ui, GAUGE, current_type == NetworkTabType::Metrics, |_| { View::tab_button(ui, GAUGE, current_type == NodeTabType::Metrics, |_| {
self.node_tab_content = Box::new(NetworkMetrics::default()); self.node_tab_content = Box::new(NetworkMetrics::default());
}); });
}); });
columns[2].vertical_centered_justified(|ui| { columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, FACTORY, current_type == NetworkTabType::Mining, |_| { View::tab_button(ui, FACTORY, current_type == NodeTabType::Mining, |_| {
self.node_tab_content = Box::new(NetworkMining::default()); self.node_tab_content = Box::new(NetworkMining::default());
}); });
}); });
columns[3].vertical_centered_justified(|ui| { columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, FADERS, current_type == NetworkTabType::Settings, |_| { View::tab_button(ui, FADERS, current_type == NodeTabType::Settings, |_| {
self.node_tab_content = Box::new(NetworkSettings::default()); self.node_tab_content = Box::new(NetworkSettings::default());
}); });
}); });
@ -187,15 +228,15 @@ impl NetworkContent {
} }
/// Draw title content. /// Draw title content.
fn title_ui(&mut self, ui: &mut egui::Ui, show_connections: bool) { fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
// Setup values for title panel. // Setup values for title panel.
let title_text = self.node_tab_content.get_type().title().to_uppercase(); let title_text = self.node_tab_content.get_type().title();
let subtitle_text = Node::get_sync_status_text(); let subtitle_text = Node::get_sync_status_text();
let not_syncing = Node::not_syncing(); let not_syncing = Node::not_syncing();
let title_content = if !show_connections { let title_content = if !show_connections {
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing) TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
} else { } else {
TitleContentType::Title(t!("network.connections").to_uppercase()) TitleContentType::Title(t!("network.connections"))
}; };
// Draw title panel. // Draw title panel.
@ -209,7 +250,7 @@ impl NetworkContent {
}); });
} }
}, |ui| { }, |ui| {
if !Content::is_dual_panel_mode(ui) { if !dual_panel {
View::title_button_big(ui, BRIEFCASE, |_| { View::title_button_big(ui, BRIEFCASE, |_| {
Content::toggle_network_panel(); Content::toggle_network_panel();
}); });
@ -217,23 +258,6 @@ impl NetworkContent {
}, ui); }, ui);
} }
/// Content to draw when node is disabled.
pub fn disabled_node_ui(ui: &mut egui::Ui) {
View::center_content(ui, 156.0, |ui| {
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(8.0);
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
Node::start();
});
ui.add_space(2.0);
Self::autorun_node_ui(ui);
});
}
/// Content to draw on loading. /// Content to draw on loading.
pub fn loading_ui(ui: &mut egui::Ui, text: Option<String>) { pub fn loading_ui(ui: &mut egui::Ui, text: Option<String>) {
match text { match text {
@ -262,81 +286,98 @@ impl NetworkContent {
AppConfig::toggle_node_autostart(); AppConfig::toggle_node_autostart();
}); });
} }
}
/// Draw integrated node error content. /// Content to draw when node is disabled.
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) { fn disabled_node_ui(ui: &mut egui::Ui) {
match e { View::center_content(ui, 156.0, |ui| {
NodeError::Storage => { let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
View::center_content(ui, 156.0, |ui| { ui.label(RichText::new(text)
ui.label(RichText::new(t!("network_node.error_clean")) .size(16.0)
.size(16.0) .color(Colors::inactive_text())
.color(Colors::red()) );
); ui.add_space(8.0);
ui.add_space(8.0); View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
let btn_txt = format!("{} {}", Node::start();
ARROWS_COUNTER_CLOCKWISE, });
t!("network_node.resync")); ui.add_space(2.0);
View::action_button(ui, btn_txt, || { NetworkContent::autorun_node_ui(ui);
Node::clean_up_data(); });
Node::start(); }
});
ui.add_space(2.0); /// Draw integrated node error content.
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
match e {
NodeError::Storage => {
View::center_content(ui, 156.0, |ui| {
ui.label(RichText::new(t!("network_node.error_clean"))
.size(16.0)
.color(Colors::red())
);
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_node.resync"));
View::action_button(ui, btn_txt, || {
Node::clean_up_data();
Node::start();
}); });
return; ui.add_space(2.0);
} });
NodeError::P2P | NodeError::API => { return;
let msg_type = match e { }
NodeError::API => "API", NodeError::P2P | NodeError::API => {
_ => "P2P" let msg_type = match e {
}; NodeError::API => "API",
View::center_content(ui, 106.0, |ui| { _ => "P2P"
let text = t!( };
View::center_content(ui, 106.0, |ui| {
let text = t!(
"network_node.error_p2p_api", "network_node.error_p2p_api",
"p2p_api" => msg_type, "p2p_api" => msg_type,
"settings" => FADERS "settings" => FADERS
); );
ui.label(RichText::new(text) ui.label(RichText::new(text)
.size(16.0) .size(16.0)
.color(Colors::red()) .color(Colors::red())
); );
ui.add_space(2.0); ui.add_space(2.0);
});
return;
}
NodeError::Configuration => {
View::center_content(ui, 106.0, |ui| {
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS))
.size(16.0)
.color(Colors::red())
);
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_settings.reset"));
View::action_button(ui, btn_txt, || {
NodeConfig::reset_to_default();
Node::start();
}); });
return; ui.add_space(2.0);
} });
NodeError::Configuration => { }
View::center_content(ui, 106.0, |ui| { NodeError::Unknown => {
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS)) View::center_content(ui, 156.0, |ui| {
.size(16.0) ui.label(RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
.color(Colors::red()) .size(16.0)
); .color(Colors::red())
ui.add_space(8.0); );
let btn_txt = format!("{} {}", ui.add_space(8.0);
ARROWS_COUNTER_CLOCKWISE, let btn_txt = format!("{} {}",
t!("network_settings.reset")); ARROWS_COUNTER_CLOCKWISE,
View::action_button(ui, btn_txt, || { t!("network_node.resync"));
NodeConfig::reset_to_default(); View::action_button(ui, btn_txt, || {
Node::start(); Node::clean_up_data();
}); Node::start();
ui.add_space(2.0);
}); });
} ui.add_space(2.0);
NodeError::Unknown => { });
View::center_content(ui, 156.0, |ui| {
ui.label(RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
.size(16.0)
.color(Colors::red())
);
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_node.resync"));
View::action_button(ui, btn_txt, || {
Node::clean_up_data();
Node::start();
});
ui.add_space(2.0);
});
}
} }
} }
} }

View file

@ -14,6 +14,7 @@
use egui::{RichText, Rounding, ScrollArea, vec2}; use egui::{RichText, Rounding, ScrollArea, vec2};
use egui::scroll_area::ScrollBarVisibility; use egui::scroll_area::ScrollBarVisibility;
use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD};
use grin_servers::{DiffBlock, ServerStats}; use grin_servers::{DiffBlock, ServerStats};
use crate::gui::Colors; use crate::gui::Colors;
@ -21,90 +22,64 @@ use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HOURGLASS_LOW, HOURGLASS_ME
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View}; use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent; use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::types::{NetworkTab, NetworkTabType}; use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::node::Node; use crate::node::Node;
/// Chain metrics tab content. /// Chain metrics tab content.
#[derive(Default)] #[derive(Default)]
pub struct NetworkMetrics; pub struct NetworkMetrics;
const BLOCK_REWARD: f64 = 60.0; const BLOCK_REWARD: u64 = REWARD / GRIN_BASE;
// 1 year is calculated as 365 days and 6 hours (31557600). // 1 year as 365 days and 6 hours (31557600).
const YEARLY_SUPPLY: f64 = ((60 * 60 * 24 * 365) + 6 * 60 * 60) as f64; const YEARLY_SUPPLY: u64 = (BLOCK_REWARD * DAY_HEIGHT * 365) + 6 * HOUR_SEC;
impl NetworkTab for NetworkMetrics { impl NodeTab for NetworkMetrics {
fn get_type(&self) -> NetworkTabType { fn get_type(&self) -> NodeTabType {
NetworkTabType::Metrics NodeTabType::Metrics
} }
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
// Show an error content when available.
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show loading spinner when node is stopping.
if Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
return;
}
// Show message when metrics are not available.
let server_stats = Node::get_stats(); let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting() let stats = server_stats.as_ref().unwrap();
|| server_stats.as_ref().unwrap().diff_stats.height == 0 { if stats.diff_stats.height == 0 {
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading"))); NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
return; return;
} }
ui.add_space(1.0); ui.add_space(1.0);
ui.vertical_centered(|ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { // Show emission and difficulty info.
let stats = server_stats.as_ref().unwrap(); info_ui(ui, stats);
// Show emission and difficulty info. // Show difficulty adjustment window blocks.
info_ui(ui, stats); blocks_ui(ui, stats);
// Show difficulty adjustment window blocks.
blocks_ui(ui, stats);
});
}); });
} }
} }
const BLOCK_ITEM_HEIGHT: f32 = 78.0;
/// Draw emission and difficulty info. /// Draw emission and difficulty info.
fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) { fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
// Show emission info. // Show emission info.
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission"))); View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
ui.columns(3, |columns| { ui.columns(3, |columns| {
let supply = stats.header_stats.height as f64 * BLOCK_REWARD; let supply = stats.header_stats.height * BLOCK_REWARD;
let rate = (YEARLY_SUPPLY * 100.0) / supply; let rate = (YEARLY_SUPPLY * 100) / supply;
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
format!("{}", BLOCK_REWARD), format!("{}", BLOCK_REWARD),
t!("network_metrics.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::label_box(ui,
format!("{:.2}%", rate), format!("{:.2}%", rate),
t!("network_metrics.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::label_box(ui,
supply.to_string(), supply.to_string(),
t!("network_metrics.supply"), t!("network_metrics.supply"),
[false, true, false, true]); [false, true, false, true]);
}); });
}); });
ui.add_space(5.0); ui.add_space(5.0);
@ -117,32 +92,34 @@ fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title)); View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title));
ui.columns(3, |columns| { ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.diff_stats.height.to_string(), stats.diff_stats.height.to_string(),
t!("network_node.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::label_box(ui,
format!("{}s", stats.diff_stats.average_block_time), format!("{}s", stats.diff_stats.average_block_time),
t!("network_metrics.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::label_box(ui,
stats.diff_stats.average_difficulty.to_string(), stats.diff_stats.average_difficulty.to_string(),
t!("network_node.difficulty"), t!("network_node.difficulty"),
[false, true, false, true]); [false, true, false, true]);
}); });
}); });
} }
const BLOCK_ITEM_HEIGHT: f32 = 77.0;
/// Draw difficulty adjustment window blocks content. /// Draw difficulty adjustment window blocks content.
fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) { fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
let blocks_size = stats.diff_stats.last_blocks.len(); let blocks_size = stats.diff_stats.last_blocks.len();
ui.add_space(4.0); ui.add_space(4.0);
ScrollArea::vertical() ScrollArea::vertical()
.id_source("difficulty_scroll") .id_salt("mining_difficulty_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.stick_to_bottom(true) .stick_to_bottom(true)
@ -151,11 +128,8 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
BLOCK_ITEM_HEIGHT, BLOCK_ITEM_HEIGHT,
blocks_size, blocks_size,
|ui, row_range| { |ui, row_range| {
ui.add_space(4.0);
for index in row_range { for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let db = stats.diff_stats.last_blocks.get(index).unwrap(); let db = stats.diff_stats.last_blocks.get(index).unwrap();
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false)); block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
} }
@ -167,11 +141,11 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) { fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_height(BLOCK_ITEM_HEIGHT); rect.set_height(BLOCK_ITEM_HEIGHT);
ui.allocate_ui_at_rect(rect, |ui| { ui.allocate_ui(rect.size(), |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add_space(3.0); ui.add_space(4.0);
// Draw round background. // Draw round background.
rect.min += vec2(8.0, 0.0); rect.min += vec2(8.0, 0.0);
@ -180,24 +154,26 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
// Draw block hash. // Draw block hash.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(7.0); ui.add_space(8.0);
ui.label(RichText::new(db.block_hash.to_string()) ui.label(RichText::new(db.block_hash.to_string())
.color(Colors::white_or_black(true)) .color(Colors::white_or_black(true))
.size(17.0)); .size(17.0));
}); });
// Draw block difficulty and height. // Draw block difficulty and height.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(6.0); ui.add_space(7.0);
let diff_text = format!("{} {} {} {}", let diff_text = format!("{} {} {} {}",
CUBE_TRANSPARENT, CUBE_TRANSPARENT,
db.difficulty, db.difficulty,
AT, AT,
db.block_height); db.block_height);
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0)); ui.label(RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0));
}); });
// Draw block date. // Draw block date.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(6.0); ui.add_space(7.0);
let block_time = View::format_time(db.time as i64); let block_time = View::format_time(db.time as i64);
ui.label(RichText::new(format!("{} {}s {} {}", ui.label(RichText::new(format!("{} {}s {} {}",
TIMER, TIMER,
@ -205,7 +181,7 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
HOURGLASS_LOW, HOURGLASS_LOW,
block_time)) block_time))
.color(Colors::gray()) .color(Colors::gray())
.size(16.0)); .size(15.0));
}); });
ui.add_space(3.0); ui.add_space(3.0);
}); });

View file

@ -23,62 +23,30 @@ use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View}; use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent; use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::setup::StratumSetup; use crate::gui::views::network::setup::StratumSetup;
use crate::gui::views::network::types::{NetworkTab, NetworkTabType}; use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::node::{Node, NodeConfig}; use crate::node::{Node, NodeConfig};
use crate::wallet::WalletConfig;
/// Mining tab content. /// Mining tab content.
pub struct NetworkMining { pub struct NetworkMining {
/// Stratum server setup content. /// Stratum server setup content.
stratum_server_setup: StratumSetup, stratum_server_setup: StratumSetup,
/// Wallet name for rewards.
wallet_name: String,
} }
impl Default for NetworkMining { impl Default for NetworkMining {
fn default() -> Self { fn default() -> Self {
let wallet_name = if let Some(id) = NodeConfig::get_stratum_wallet_id() {
WalletConfig::name_by_id(id).unwrap_or("-".to_string())
} else {
"-".to_string()
};
Self { Self {
stratum_server_setup: StratumSetup::default(), stratum_server_setup: StratumSetup::default(),
wallet_name,
} }
} }
} }
impl NetworkTab for NetworkMining { impl NodeTab for NetworkMining {
fn get_type(&self) -> NetworkTabType { fn get_type(&self) -> NodeTabType {
NetworkTabType::Mining NodeTabType::Mining
} }
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Show an error content when available. if Node::is_stratum_starting() || Node::get_sync_status().unwrap() != SyncStatus::NoSync {
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show loading spinner when node is stopping or stratum server is starting.
if Node::is_stopping() || Node::is_stratum_starting() {
NetworkContent::loading_ui(ui, None);
return;
}
// Show message when mining is not available.
let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting()
|| Node::get_sync_status().unwrap() != SyncStatus::NoSync {
NetworkContent::loading_ui(ui, Some(t!("network_mining.loading"))); NetworkContent::loading_ui(ui, Some(t!("network_mining.loading")));
return; return;
} }
@ -87,15 +55,13 @@ impl NetworkTab for NetworkMining {
let stratum_stats = Node::get_stratum_stats(); let stratum_stats = Node::get_stratum_stats();
if !stratum_stats.is_running { if !stratum_stats.is_running {
ScrollArea::vertical() ScrollArea::vertical()
.id_source("stratum_setup_scroll") .id_salt("stratum_setup_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
ui.add_space(1.0); ui.add_space(1.0);
ui.vertical_centered(|ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { self.stratum_server_setup.ui(ui, cb);
self.stratum_server_setup.ui(ui, cb);
});
}); });
}); });
return; return;
@ -108,16 +74,19 @@ impl NetworkTab for NetworkMining {
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address(); let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
View::rounded_box(ui, View::label_box(ui,
format!("{}:{}", stratum_addr, stratum_port), format!("{}:{}", stratum_addr, stratum_port),
t!("network_mining.address"), t!("network_mining.address"),
[true, false, true, false]); [true, false, true, false]);
}); });
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
self.wallet_name.clone(), self.stratum_server_setup
t!("network_mining.rewards_wallet"), .wallet_name
[false, true, false, true]); .clone()
.unwrap_or("-".to_string()),
t!("network_mining.rewards_wallet"),
[false, true, false, true]);
}); });
}); });
ui.add_space(4.0); ui.add_space(4.0);
@ -131,10 +100,10 @@ impl NetworkTab for NetworkMining {
} else { } else {
"-".into() "-".into()
}; };
View::rounded_box(ui, View::label_box(ui,
difficulty, difficulty,
t!("network_node.difficulty"), t!("network_node.difficulty"),
[true, false, true, false]); [true, false, true, false]);
}); });
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
let block_height = if stratum_stats.block_height > 0 { let block_height = if stratum_stats.block_height > 0 {
@ -142,10 +111,10 @@ impl NetworkTab for NetworkMining {
} else { } else {
"-".into() "-".into()
}; };
View::rounded_box(ui, View::label_box(ui,
block_height, block_height,
t!("network_node.header"), t!("network_node.header"),
[false, false, false, false]); [false, false, false, false]);
}); });
columns[2].vertical_centered(|ui| { columns[2].vertical_centered(|ui| {
let hashrate = if stratum_stats.network_hashrate > 0.0 { let hashrate = if stratum_stats.network_hashrate > 0.0 {
@ -153,10 +122,10 @@ impl NetworkTab for NetworkMining {
} else { } else {
"-".into() "-".into()
}; };
View::rounded_box(ui, View::label_box(ui,
hashrate, hashrate,
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits), t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
[false, true, false, true]); [false, true, false, true]);
}); });
}); });
ui.add_space(4.0); ui.add_space(4.0);
@ -165,17 +134,17 @@ impl NetworkTab for NetworkMining {
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners"))); View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stratum_stats.num_workers.to_string(), stratum_stats.num_workers.to_string(),
t!("network_mining.devices"), t!("network_mining.devices"),
[true, false, true, false]); [true, false, true, false]);
}); });
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stratum_stats.blocks_found.to_string(), stratum_stats.blocks_found.to_string(),
t!("network_mining.blocks_found"), t!("network_mining.blocks_found"),
[false, true, false, true]); [false, true, false, true]);
}); });
}); });
ui.add_space(4.0); ui.add_space(4.0);
@ -187,7 +156,7 @@ impl NetworkTab for NetworkMining {
View::horizontal_line(ui, Colors::item_stroke()); View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(4.0); ui.add_space(4.0);
ScrollArea::vertical() ScrollArea::vertical()
.id_source("stratum_workers_scroll") .id_salt("stratum_workers_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show_rows( .show_rows(

View file

@ -20,51 +20,28 @@ use crate::gui::Colors;
use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK}; use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View}; use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent; use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::node::{Node, NodeConfig}; use crate::node::{Node, NodeConfig};
/// Integrated node tab content. /// Integrated node tab content.
#[derive(Default)] #[derive(Default)]
pub struct NetworkNode; pub struct NetworkNode;
impl NetworkTab for NetworkNode { impl NodeTab for NetworkNode {
fn get_type(&self) -> NetworkTabType { fn get_type(&self) -> NodeTabType {
NetworkTabType::Node NodeTabType::Info
} }
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
// Show an error content when available.
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show loading spinner when stats are not available.
let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting() || Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
return;
}
ScrollArea::vertical() ScrollArea::vertical()
.id_source("integrated_node") .id_salt("integrated_node_info_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
ui.add_space(2.0); ui.add_space(2.0);
ui.vertical_centered(|ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { // Show node stats content.
// Show node stats content. node_stats_ui(ui);
node_stats_ui(ui);
});
}); });
}); });
} }
@ -79,32 +56,32 @@ fn node_stats_ui(ui: &mut egui::Ui) {
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header"))); View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.header_stats.last_block_h.to_string(), stats.header_stats.last_block_h.to_string(),
t!("network_node.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::label_box(ui,
stats.header_stats.height.to_string(), stats.header_stats.height.to_string(),
t!("network_node.height"), t!("network_node.height"),
[false, true, false, false]); [false, true, false, false]);
}); });
}); });
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.header_stats.total_difficulty.to_string(), stats.header_stats.total_difficulty.to_string(),
t!("network_node.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.timestamp(); let h_ts = stats.header_stats.latest_timestamp.timestamp();
let h_time = View::format_time(h_ts); let h_time = View::format_time(h_ts);
View::rounded_box(ui, View::label_box(ui,
h_time, h_time,
t!("network_node.time"), t!("network_node.time"),
[false, false, false, true]); [false, false, false, true]);
}); });
}); });
ui.add_space(5.0); ui.add_space(5.0);
@ -113,32 +90,32 @@ fn node_stats_ui(ui: &mut egui::Ui) {
View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block"))); View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block")));
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.chain_stats.last_block_h.to_string(), stats.chain_stats.last_block_h.to_string(),
t!("network_node.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::label_box(ui,
stats.chain_stats.height.to_string(), stats.chain_stats.height.to_string(),
t!("network_node.height"), t!("network_node.height"),
[false, true, false, false]); [false, true, false, false]);
}); });
}); });
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.chain_stats.total_difficulty.to_string(), stats.chain_stats.total_difficulty.to_string(),
t!("network_node.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.timestamp(); let b_ts = stats.chain_stats.latest_timestamp.timestamp();
let b_time = View::format_time(b_ts); let b_time = View::format_time(b_ts);
View::rounded_box(ui, View::label_box(ui,
b_time, b_time,
t!("network_node.time"), t!("network_node.time"),
[false, false, false, true]); [false, false, false, true]);
}); });
}); });
ui.add_space(5.0); ui.add_space(5.0);
@ -151,10 +128,10 @@ fn node_stats_ui(ui: &mut egui::Ui) {
None => "0 (0)".to_string(), None => "0 (0)".to_string(),
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels) Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels)
}; };
View::rounded_box(ui, View::label_box(ui,
tx_stat, tx_stat,
t!("network_node.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| {
let stem_tx_stat = match &stats.tx_stats { let stem_tx_stat = match &stats.tx_stats {
@ -163,24 +140,24 @@ fn node_stats_ui(ui: &mut egui::Ui) {
stx.stem_pool_size, stx.stem_pool_size,
stx.stem_pool_kernels) stx.stem_pool_kernels)
}; };
View::rounded_box(ui, View::label_box(ui,
stem_tx_stat, stem_tx_stat,
t!("network_node.stem_pool"), t!("network_node.stem_pool"),
[false, true, false, false]); [false, true, false, false]);
}); });
}); });
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.disk_usage_gb.to_string(), stats.disk_usage_gb.to_string(),
t!("network_node.size"), t!("network_node.size"),
[false, false, true, false]); [false, false, true, false]);
}); });
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
let peers_txt = format!("{} ({})", let peers_txt = format!("{} ({})",
stats.peer_count, stats.peer_count,
NodeConfig::get_max_outbound_peers()); NodeConfig::get_max_outbound_peers());
View::rounded_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]); View::label_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]);
}); });
}); });
ui.add_space(5.0); ui.add_space(5.0);
@ -196,23 +173,27 @@ fn node_stats_ui(ui: &mut egui::Ui) {
} }
} }
const PEER_ITEM_HEIGHT: f32 = 77.0;
/// Draw connected peer info item. /// Draw connected peer info item.
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) { fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_height(79.0); rect.set_height(PEER_ITEM_HEIGHT);
ui.allocate_ui_at_rect(rect, |ui| { ui.allocate_ui(rect.size(), |ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add_space(4.0); ui.add_space(4.0);
// Draw round background. // Draw round background.
ui.painter().rect(rect, rounding, Colors::white_or_black(false), View::item_stroke()); ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
// Draw peer address // Draw IP address.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(7.0); ui.add_space(7.0);
ui.label(RichText::new(&peer.addr).color(Colors::white_or_black(true)).size(17.0)); ui.label(RichText::new(&peer.addr)
.color(Colors::white_or_black(true))
.size(17.0));
}); });
// Draw peer difficulty and height // Draw difficulty and height.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(6.0); ui.add_space(6.0);
let diff_text = format!("{} {} {} {}", let diff_text = format!("{} {} {} {}",
@ -220,13 +201,17 @@ fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
peer.total_difficulty, peer.total_difficulty,
AT, AT,
peer.height); peer.height);
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0)); ui.label(RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0));
}); });
// Draw peer user-agent // Draw user-agent.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(6.0); ui.add_space(6.0);
let agent_text = format!("{} {}", DEVICES, &peer.user_agent); let agent_text = format!("{} {}", DEVICES, &peer.user_agent);
ui.label(RichText::new(agent_text).color(Colors::gray()).size(16.0)); ui.label(RichText::new(agent_text)
.color(Colors::gray())
.size(15.0));
}); });
ui.add_space(3.0); ui.add_space(3.0);

View file

@ -20,7 +20,7 @@ use crate::gui::icons::ARROW_COUNTER_CLOCKWISE;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View}; use crate::gui::views::{Modal, Content, View};
use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup}; use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup};
use crate::gui::views::network::types::{NetworkTab, NetworkTabType}; use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::types::{ModalContainer, ModalPosition}; use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::node::{Node, NodeConfig}; use crate::node::{Node, NodeConfig};
@ -42,7 +42,7 @@ pub struct NetworkSettings {
} }
/// Identifier for settings reset confirmation [`Modal`]. /// Identifier for settings reset confirmation [`Modal`].
pub const RESET_SETTINGS_MODAL: &'static str = "reset_settings"; pub const RESET_SETTINGS_CONFIRMATION_MODAL: &'static str = "reset_settings_confirmation";
impl Default for NetworkSettings { impl Default for NetworkSettings {
fn default() -> Self { fn default() -> Self {
@ -53,7 +53,7 @@ impl Default for NetworkSettings {
pool: PoolSetup::default(), pool: PoolSetup::default(),
dandelion: DandelionSetup::default(), dandelion: DandelionSetup::default(),
modal_ids: vec![ modal_ids: vec![
RESET_SETTINGS_MODAL RESET_SETTINGS_CONFIRMATION_MODAL
] ]
} }
} }
@ -69,15 +69,15 @@ impl ModalContainer for NetworkSettings {
modal: &Modal, modal: &Modal,
_: &dyn PlatformCallbacks) { _: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
RESET_SETTINGS_MODAL => reset_settings_confirmation_modal(ui, modal), RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui, modal),
_ => {} _ => {}
} }
} }
} }
impl NetworkTab for NetworkSettings { impl NodeTab for NetworkSettings {
fn get_type(&self) -> NetworkTabType { fn get_type(&self) -> NodeTabType {
NetworkTabType::Settings NodeTabType::Settings
} }
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
@ -85,7 +85,7 @@ impl NetworkTab for NetworkSettings {
self.current_modal_ui(ui, cb); self.current_modal_ui(ui, cb);
ScrollArea::vertical() ScrollArea::vertical()
.id_source("network_settings") .id_salt("node_settings_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
@ -210,7 +210,7 @@ fn reset_settings_ui(ui: &mut egui::Ui) {
t!("network_settings.reset_settings")); t!("network_settings.reset_settings"));
View::action_button(ui, button_text, || { View::action_button(ui, button_text, || {
// Show modal to confirm settings reset. // Show modal to confirm settings reset.
Modal::new(RESET_SETTINGS_MODAL) Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL)
.position(ModalPosition::Center) .position(ModalPosition::Center)
.title(t!("confirmation")) .title(t!("confirmation"))
.show(); .show();

View file

@ -141,7 +141,7 @@ impl DandelionSetup {
ui.add_space(6.0); ui.add_space(6.0);
let epoch = NodeConfig::get_dandelion_epoch(); let epoch = NodeConfig::get_dandelion_epoch();
View::button(ui, format!("{} {}", WATCH, epoch.clone()), Colors::button(), || { View::button(ui, format!("{} {}", WATCH, &epoch), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.epoch_edit = epoch; self.epoch_edit = epoch;
// Show epoch setup modal. // Show epoch setup modal.
@ -218,8 +218,7 @@ impl DandelionSetup {
ui.add_space(6.0); ui.add_space(6.0);
let embargo = NodeConfig::get_dandelion_embargo(); let embargo = NodeConfig::get_dandelion_embargo();
View::button(ui, format!("{} {}", TIMER, embargo.clone()), Colors::button(), || { View::button(ui, format!("{} {}", TIMER, &embargo), Colors::white_or_black(false), || {
// Setup values for modal.
self.embargo_edit = embargo; self.embargo_edit = embargo;
// Show embargo setup modal. // Show embargo setup modal.
Modal::new(EMBARGO_MODAL) Modal::new(EMBARGO_MODAL)
@ -294,10 +293,10 @@ impl DandelionSetup {
); );
ui.add_space(6.0); ui.add_space(6.0);
let agg = NodeConfig::get_dandelion_aggregation(); let ag = NodeConfig::get_dandelion_aggregation();
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, agg.clone()), Colors::button(), || { View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, &ag), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.aggregation_edit = agg; self.aggregation_edit = ag;
// Show aggregation setup modal. // Show aggregation setup modal.
Modal::new(AGGREGATION_MODAL) Modal::new(AGGREGATION_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -372,7 +371,7 @@ impl DandelionSetup {
ui.add_space(6.0); ui.add_space(6.0);
let stem_prob = NodeConfig::get_stem_probability(); let stem_prob = NodeConfig::get_stem_probability();
View::button(ui, format!("{}%", stem_prob.clone()), Colors::button(), || { View::button(ui, format!("{}%", &stem_prob), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.stem_prob_edit = stem_prob; self.stem_prob_edit = stem_prob;
// Show stem probability setup modal. // Show stem probability setup modal.

View file

@ -255,7 +255,7 @@ impl NodeSetup {
ui.add_space(6.0); ui.add_space(6.0);
let (_, port) = NodeConfig::get_api_ip_port(); let (_, port) = NodeConfig::get_api_ip_port();
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || { View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.api_port_edit = port; self.api_port_edit = port;
self.api_port_available_edit = self.is_api_port_available; self.api_port_available_edit = self.is_api_port_available;
@ -283,7 +283,9 @@ impl NodeSetup {
fn api_port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { fn api_port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.api_port")).size(17.0).color(Colors::gray())); ui.label(RichText::new(t!("network_settings.api_port"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(6.0); ui.add_space(6.0);
// Draw API port text edit. // Draw API port text edit.
@ -356,8 +358,8 @@ impl NodeSetup {
ui.add_space(6.0); ui.add_space(6.0);
let secret_value = match modal_id { let secret_value = match modal_id {
API_SECRET_MODAL => NodeConfig::get_api_secret(), API_SECRET_MODAL => NodeConfig::get_api_secret(false),
_ => NodeConfig::get_foreign_api_secret() _ => NodeConfig::get_api_secret(true)
}; };
let secret_text = if secret_value.is_some() { let secret_text = if secret_value.is_some() {
@ -366,7 +368,7 @@ impl NodeSetup {
format!("{} {}", SHIELD_SLASH, t!("network_settings.disabled")) format!("{} {}", SHIELD_SLASH, t!("network_settings.disabled"))
}; };
View::button(ui, secret_text, Colors::button(), || { View::button(ui, secret_text, Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.secret_edit = secret_value.unwrap_or("".to_string()); self.secret_edit = secret_value.unwrap_or("".to_string());
// Show secret edit modal. // Show secret edit modal.
@ -449,7 +451,9 @@ impl NodeSetup {
ui.add_space(6.0); ui.add_space(6.0);
let ftl = NodeConfig::get_ftl(); let ftl = NodeConfig::get_ftl();
View::button(ui, format!("{} {}", CLOCK_CLOCKWISE, ftl.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", CLOCK_CLOCKWISE, &ftl),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.ftl_edit = ftl; self.ftl_edit = ftl;
// Show ftl value setup modal. // Show ftl value setup modal.

View file

@ -246,7 +246,9 @@ impl P2PSetup {
ui.add_space(6.0); ui.add_space(6.0);
let port = NodeConfig::get_p2p_port(); let port = NodeConfig::get_p2p_port();
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", PLUG, &port),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.port_edit = port; self.port_edit = port;
self.port_available_edit = self.is_port_available; self.port_available_edit = self.is_port_available;
@ -306,11 +308,9 @@ impl P2PSetup {
// Save port at config if it's available. // Save port at config if it's available.
if available { if available {
NodeConfig::save_p2p_port(self.port_edit.parse::<u16>().unwrap()); NodeConfig::save_p2p_port(self.port_edit.parse::<u16>().unwrap());
if Node::is_running() { if Node::is_running() {
Node::restart(); Node::restart();
} }
self.is_port_available = true; self.is_port_available = true;
cb.hide_keyboard(); cb.hide_keyboard();
modal.close(); modal.close();
@ -371,9 +371,7 @@ impl P2PSetup {
.size(16.0) .size(16.0)
.color(Colors::inactive_text())); .color(Colors::inactive_text()));
} }
if !peers.is_empty() { ui.add_space(12.0);
ui.add_space(12.0);
}
let add_text = if peer_type == &PeerType::CustomSeed { let add_text = if peer_type == &PeerType::CustomSeed {
format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_seed")) format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_seed"))
@ -381,7 +379,7 @@ impl P2PSetup {
format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_peer")) format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_peer"))
}; };
View::button(ui, add_text, Colors::button(), || { View::button(ui, add_text, Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.is_correct_address_edit = true; self.is_correct_address_edit = true;
self.peer_edit = "".to_string(); self.peer_edit = "".to_string();
@ -508,7 +506,9 @@ impl P2PSetup {
ui.add_space(6.0); ui.add_space(6.0);
let ban_window = NodeConfig::get_p2p_ban_window(); let ban_window = NodeConfig::get_p2p_ban_window();
View::button(ui, format!("{} {}", PROHIBIT_INSET, ban_window.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", PROHIBIT_INSET, &ban_window),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.ban_window_edit = ban_window; self.ban_window_edit = ban_window;
// Show ban window period setup modal. // Show ban window period setup modal.
@ -590,8 +590,9 @@ impl P2PSetup {
ui.add_space(6.0); ui.add_space(6.0);
let max_inbound = NodeConfig::get_max_inbound_peers(); let max_inbound = NodeConfig::get_max_inbound_peers();
let button_text = format!("{} {}", ARROW_FAT_LINES_DOWN, max_inbound.clone()); View::button(ui,
View::button(ui, button_text, Colors::button(), || { format!("{} {}", ARROW_FAT_LINES_DOWN, &max_inbound),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.max_inbound_count = max_inbound; self.max_inbound_count = max_inbound;
// Show maximum number of inbound peers setup modal. // Show maximum number of inbound peers setup modal.
@ -666,10 +667,10 @@ impl P2PSetup {
.color(Colors::gray()) .color(Colors::gray())
); );
ui.add_space(6.0); ui.add_space(6.0);
let max_outbound = NodeConfig::get_max_outbound_peers(); let max_outbound = NodeConfig::get_max_outbound_peers();
let button_text = format!("{} {}", ARROW_FAT_LINES_UP, max_outbound.clone()); View::button(ui,
View::button(ui, button_text, Colors::button(), || { format!("{} {}", ARROW_FAT_LINES_UP, &max_outbound),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.max_outbound_count = max_outbound; self.max_outbound_count = max_outbound;
// Show maximum number of outbound peers setup modal. // Show maximum number of outbound peers setup modal.
@ -740,9 +741,10 @@ impl P2PSetup {
/// Draw content to reset peers data. /// Draw content to reset peers data.
fn reset_peers_ui(&mut self, ui: &mut egui::Ui) { fn reset_peers_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(4.0); ui.add_space(4.0);
View::colored_text_button(ui,
let button_text = format!("{} {}", TRASH, t!("network_settings.reset_peers")); format!("{} {}", TRASH, t!("network_settings.reset_peers")),
View::colored_text_button(ui, button_text, Colors::red(), Colors::button(), || { Colors::red(),
Colors::white_or_black(false), || {
Node::reset_peers(false); Node::reset_peers(false);
self.peers_reset = true; self.peers_reset = true;
}); });

View file

@ -145,7 +145,7 @@ impl PoolSetup {
ui.add_space(6.0); ui.add_space(6.0);
let fee = NodeConfig::get_base_fee(); let fee = NodeConfig::get_base_fee();
View::button(ui, format!("{} {}", HAND_COINS, fee.clone()), Colors::button(), || { View::button(ui, format!("{} {}", HAND_COINS, &fee), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.fee_base_edit = fee; self.fee_base_edit = fee;
// Show fee setup modal. // Show fee setup modal.
@ -195,7 +195,6 @@ impl PoolSetup {
modal.close(); modal.close();
} }
}; };
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -220,9 +219,10 @@ impl PoolSetup {
.color(Colors::gray()) .color(Colors::gray())
); );
ui.add_space(6.0); ui.add_space(6.0);
let period = NodeConfig::get_reorg_cache_period(); let period = NodeConfig::get_reorg_cache_period();
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, period.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", CLOCK_COUNTDOWN, &period),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.reorg_period_edit = period; self.reorg_period_edit = period;
// Show reorg period setup modal. // Show reorg period setup modal.
@ -299,7 +299,7 @@ impl PoolSetup {
ui.add_space(6.0); ui.add_space(6.0);
let size = NodeConfig::get_max_pool_size(); let size = NodeConfig::get_max_pool_size();
View::button(ui, format!("{} {}", CIRCLES_THREE, size.clone()), Colors::button(), || { View::button(ui, format!("{} {}", CIRCLES_THREE, size), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.pool_size_edit = size; self.pool_size_edit = size;
// Show pool size setup modal. // Show pool size setup modal.
@ -376,7 +376,9 @@ impl PoolSetup {
ui.add_space(6.0); ui.add_space(6.0);
let size = NodeConfig::get_max_stempool_size(); let size = NodeConfig::get_max_stempool_size();
View::button(ui, format!("{} {}", BEZIER_CURVE, size.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", BEZIER_CURVE, &size),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.stempool_size_edit = size; self.stempool_size_edit = size;
// Show stempool size setup modal. // Show stempool size setup modal.
@ -453,7 +455,9 @@ impl PoolSetup {
ui.add_space(6.0); ui.add_space(6.0);
let weight = NodeConfig::get_mineable_max_weight(); let weight = NodeConfig::get_mineable_max_weight();
View::button(ui, format!("{} {}", BOUNDING_BOX, weight.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", BOUNDING_BOX, &weight),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.max_weight_edit = weight; self.max_weight_edit = weight;
// Show total tx weight setup modal. // Show total tx weight setup modal.

View file

@ -44,7 +44,7 @@ pub struct StratumSetup {
is_port_available: bool, is_port_available: bool,
/// Wallet name to receive rewards. /// Wallet name to receive rewards.
wallet_name: Option<String>, pub wallet_name: Option<String>,
/// Attempt time value in seconds to mine on a particular header. /// Attempt time value in seconds to mine on a particular header.
attempt_time_edit: String, attempt_time_edit: String,
@ -189,7 +189,9 @@ impl StratumSetup {
ui.add_space(8.0); ui.add_space(8.0);
// Show button to select wallet. // Show button to select wallet.
View::button(ui, t!("network_settings.choose_wallet"), Colors::button(), || { View::button(ui,
t!("network_settings.choose_wallet"),
Colors::white_or_black(false), || {
self.show_wallets_modal(); self.show_wallets_modal();
}); });
ui.add_space(12.0); ui.add_space(12.0);
@ -260,7 +262,7 @@ impl StratumSetup {
ui.add_space(6.0); ui.add_space(6.0);
let (_, port) = NodeConfig::get_stratum_address(); let (_, port) = NodeConfig::get_stratum_address();
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || { View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.stratum_port_edit = port; self.stratum_port_edit = port;
self.stratum_port_available_edit = self.is_port_available; self.stratum_port_available_edit = self.is_port_available;
@ -359,7 +361,7 @@ impl StratumSetup {
ui.add_space(6.0); ui.add_space(6.0);
let time = NodeConfig::get_stratum_attempt_time(); let time = NodeConfig::get_stratum_attempt_time();
View::button(ui, format!("{} {}", TIMER, time.clone()), Colors::button(), || { View::button(ui, format!("{} {}", TIMER, &time), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.attempt_time_edit = time; self.attempt_time_edit = time;
@ -442,7 +444,7 @@ impl StratumSetup {
ui.add_space(6.0); ui.add_space(6.0);
let diff = NodeConfig::get_stratum_min_share_diff(); let diff = NodeConfig::get_stratum_min_share_diff();
View::button(ui, format!("{} {}", BARBELL, diff.clone()), Colors::button(), || { View::button(ui, format!("{} {}", BARBELL, &diff), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.min_share_diff_edit = diff; self.min_share_diff_edit = diff;

View file

@ -14,28 +14,28 @@
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
/// Network tab content interface. /// Integrated node tab content interface.
pub trait NetworkTab { pub trait NodeTab {
fn get_type(&self) -> NetworkTabType; fn get_type(&self) -> NodeTabType;
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks); fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
} }
/// Type of [`NetworkTab`] content. /// Type of [`NodeTab`] content.
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum NetworkTabType { pub enum NodeTabType {
Node, Info,
Metrics, Metrics,
Mining, Mining,
Settings Settings
} }
impl NetworkTabType { impl NodeTabType {
pub fn title(&self) -> String { pub fn title(&self) -> String {
match *self { match *self {
NetworkTabType::Node => { t!("network.node") } NodeTabType::Info => { t!("network.node") }
NetworkTabType::Metrics => { t!("network.metrics") } NodeTabType::Metrics => { t!("network.metrics") }
NetworkTabType::Mining => { t!("network.mining") } NodeTabType::Mining => { t!("network.mining") }
NetworkTabType::Settings => { t!("network.settings") } NodeTabType::Settings => { t!("network.settings") }
} }
} }
} }

View file

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
use egui::scroll_area::ScrollAreaOutput; use egui::scroll_area::ScrollAreaOutput;
use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2}; use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2, UiBuilder};
use egui::epaint::{emath::lerp, vec2, Pos2, Shape, Stroke}; use egui::epaint::{emath::lerp, vec2, Pos2, Shape, Stroke};
/// A spinner widget used to indicate loading. /// A spinner widget used to indicate loading.
@ -195,7 +195,9 @@ impl PullToRefresh {
ui: &mut egui::Ui, ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> T, content: impl FnOnce(&mut egui::Ui) -> T,
) -> PullToRefreshResponse<T> { ) -> PullToRefreshResponse<T> {
let mut child = ui.child_ui(ui.available_rect_before_wrap(), *ui.layout(), None); let mut child = ui.new_child(UiBuilder::new()
.max_rect(ui.available_rect_before_wrap())
.layout(*ui.layout()));
let output = content(&mut child); let output = content(&mut child);

View file

@ -16,7 +16,7 @@ use std::mem::size_of;
use std::sync::Arc; use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::thread; use std::thread;
use egui::{SizeHint, TextureHandle}; use egui::{SizeHint, TextureHandle, UiBuilder};
use egui::epaint::RectShape; use egui::epaint::RectShape;
use image::{ExtendedColorType, ImageEncoder}; use image::{ExtendedColorType, ImageEncoder};
use image::codecs::png::{CompressionType, FilterType, PngEncoder}; use image::codecs::png::{CompressionType, FilterType, PngEncoder};
@ -235,26 +235,23 @@ impl QrCodeContent {
rect.max -= egui::emath::vec2(10.0, 0.0); rect.max -= egui::emath::vec2(10.0, 0.0);
// Create background shape. // Create background shape.
let mut bg_shape = RectShape { let mut bg_shape = RectShape::new(
rect, rect,
rounding: egui::Rounding::default(), egui::Rounding::default(),
fill: egui::Color32::WHITE, egui::Color32::WHITE,
stroke: egui::Stroke::NONE, egui::Stroke::NONE
blur_width: 0.0, );
fill_texture_id: Default::default(),
uv: egui::Rect::ZERO
};
let bg_idx = ui.painter().add(bg_shape); let bg_idx = ui.painter().add(bg_shape);
// Draw QR code image content. // Draw QR code image content.
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| { let mut content_rect = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
ui.add_space(10.0); ui.add_space(10.0);
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32); let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
self.texture_handle = Some(View::svg_image(ui, "qr_code", svg.as_slice(), Some(size))); self.texture_handle = Some(View::svg_image(ui, "qr_code", svg.as_slice(), Some(size)));
ui.add_space(10.0); ui.add_space(10.0);
}).response.rect; }).response.rect;
// Setup background shape to be painted behind content. // Setup background size.
content_rect.min -= egui::emath::vec2(10.0, 0.0); content_rect.min -= egui::emath::vec2(10.0, 0.0);
content_rect.max += egui::emath::vec2(10.0, 0.0); content_rect.max += egui::emath::vec2(10.0, 0.0);
bg_shape.rect = content_rect; bg_shape.rect = content_rect;

View file

@ -32,7 +32,7 @@ pub struct CameraScanModal {
impl Default for CameraScanModal { impl Default for CameraScanModal {
fn default() -> Self { fn default() -> Self {
Self { Self {
camera_content: None, camera_content: Some(CameraContent::default()),
qr_scan_result: None, qr_scan_result: None,
} }
} }
@ -51,7 +51,7 @@ impl CameraScanModal {
View::horizontal_line(ui, Colors::item_stroke()); View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0); ui.add_space(3.0);
ScrollArea::vertical() ScrollArea::vertical()
.id_source(Id::from("qr_scan_result_input")) .id_salt(Id::from("qr_scan_result_input"))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0) .max_height(128.0)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
@ -72,7 +72,7 @@ impl CameraScanModal {
// Show copy button. // Show copy button.
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
let copy_text = format!("{} {}", COPY, t!("copy")); let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::button(), || { View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(result_text.to_string()); cb.copy_string_to_buffer(result_text.to_string());
self.qr_scan_result = None; self.qr_scan_result = None;
modal.close(); modal.close();
@ -81,22 +81,7 @@ impl CameraScanModal {
ui.add_space(10.0); ui.add_space(10.0);
View::horizontal_line(ui, Colors::item_stroke()); View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0); ui.add_space(6.0);
} else if let Some(result) = self.camera_content.get_or_insert(CameraContent::default())
.qr_scan_result() {
cb.stop_camera();
self.camera_content = None;
on_result(&result);
// Set result and rename modal title.
self.qr_scan_result = Some(result);
Modal::set_title(t!("scan_result"));
} else {
ui.add_space(6.0);
self.camera_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(6.0);
}
if self.qr_scan_result.is_some() {
// Setup spacing between buttons. // Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
@ -117,14 +102,28 @@ impl CameraScanModal {
}); });
}); });
}); });
} else { } else if let Some(camera_content) = self.camera_content.as_mut() {
ui.vertical_centered_justified(|ui| { if let Some(result) = camera_content.qr_scan_result() {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { cb.stop_camera();
cb.stop_camera(); self.camera_content = None;
self.camera_content = None; on_result(&result);
modal.close();
// Set result and rename modal title.
self.qr_scan_result = Some(result);
Modal::set_title(t!("scan_result"));
} else {
// Draw camera content.
ui.add_space(6.0);
self.camera_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(12.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
cb.stop_camera();
self.camera_content = None;
modal.close();
});
}); });
}); }
} }
ui.add_space(6.0); ui.add_space(6.0);
} }

View file

@ -12,11 +12,11 @@
// 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 egui::{Margin, Id, Layout, Align}; use egui::{Margin, Id, Layout, Align, UiBuilder};
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::views::{Content, View}; use crate::gui::views::{Content, View};
use crate::gui::views::types::{TitleContentType, TitleType}; use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
/// Title panel with left/right action buttons and text in the middle. /// Title panel with left/right action buttons and text in the middle.
pub struct TitlePanel { pub struct TitlePanel {
@ -25,8 +25,8 @@ pub struct TitlePanel {
} }
impl TitlePanel { impl TitlePanel {
/// Default [`TitlePanel`] content height. /// Content height.
pub const DEFAULT_HEIGHT: f32 = 54.0; pub const HEIGHT: f32 = 54.0;
/// Create new title panel with provided identifier. /// Create new title panel with provided identifier.
pub fn new(id: Id) -> Self { pub fn new(id: Id) -> Self {
@ -43,7 +43,7 @@ impl TitlePanel {
// Draw title panel. // Draw title panel.
egui::TopBottomPanel::top(self.id) egui::TopBottomPanel::top(self.id)
.resizable(false) .resizable(false)
.exact_height(Self::DEFAULT_HEIGHT + View::get_top_inset()) .exact_height(Self::HEIGHT + View::get_top_inset())
.frame(egui::Frame { .frame(egui::Frame {
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui), left: View::far_left_inset_margin(ui),
@ -51,7 +51,6 @@ impl TitlePanel {
top: View::get_top_inset(), top: View::get_top_inset(),
bottom: 0.0, bottom: 0.0,
}, },
fill: Colors::yellow(),
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
@ -68,40 +67,51 @@ impl TitlePanel {
match title { match title {
TitleType::Single(content) => { TitleType::Single(content) => {
let content_rect = { let content_rect = {
let mut r = rect; let mut r = rect.clone();
r.min.x += Self::DEFAULT_HEIGHT; r.min.x += Self::HEIGHT;
r.max.x -= Self::DEFAULT_HEIGHT; r.max.x -= Self::HEIGHT;
r r
}; };
ui.allocate_ui_at_rect(content_rect, |ui| { ui.allocate_new_ui(UiBuilder::new().max_rect(content_rect), |ui| {
Self::title_text_content(ui, content); Self::title_text_content(ui, content);
}); });
} }
TitleType::Dual(first, second) => { TitleType::Dual(first, second) => {
let first_rect = { let first_rect = {
let mut r = rect; let mut r = rect.clone();
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::DEFAULT_HEIGHT; r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::HEIGHT;
r.min.x += Self::DEFAULT_HEIGHT; r.min.x += Self::HEIGHT;
r r
}; };
// Draw first title content. // Draw first title content.
ui.allocate_ui_at_rect(first_rect, |ui| { ui.allocate_new_ui(UiBuilder::new().max_rect(first_rect), |ui| {
Self::title_text_content(ui, first); Self::title_text_content(ui, first);
}); });
let second_rect = { let second_rect = {
let mut r = rect; let mut r = rect.clone();
r.min.x = first_rect.max.x + 2.0 * Self::DEFAULT_HEIGHT; r.min.x = first_rect.max.x + 2.0 * Self::HEIGHT;
r.max.x -= Self::DEFAULT_HEIGHT; r.max.x -= Self::HEIGHT;
r r
}; };
// Draw second title content. // Draw second title content.
ui.allocate_ui_at_rect(second_rect, |ui| { ui.allocate_new_ui(UiBuilder::new().max_rect(second_rect), |ui| {
Self::title_text_content(ui, second); Self::title_text_content(ui, second);
}); });
} }
} }
}); });
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.x -= View::far_left_inset_margin(ui);
r.max.x += View::far_right_inset_margin(ui);
r
};
if Content::is_dual_panel_mode(ui.ctx()) {
View::line(ui, LinePosition::BOTTOM, &r, Colors::stroke());
}
}); });
} }
@ -115,11 +125,11 @@ impl TitlePanel {
} else { } else {
0.0 0.0
}); });
View::ellipsize_text(ui, text, 19.0, Colors::title(true)); View::ellipsize_text(ui, text.to_uppercase(), 19.0, Colors::title(true));
} }
TitleContentType::WithSubTitle(text, subtitle, animate) => { TitleContentType::WithSubTitle(text, subtitle, animate) => {
ui.add_space(4.0); ui.add_space(4.0);
View::ellipsize_text(ui, text, 18.0, Colors::title(true)); View::ellipsize_text(ui, text.to_uppercase(), 18.0, Colors::title(true));
ui.add_space(-2.0); ui.add_space(-2.0);
View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate) View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate)
} }

View file

@ -32,6 +32,11 @@ pub enum TitleContentType {
WithSubTitle(String, String, bool) WithSubTitle(String, String, bool)
} }
/// Stroke position against content.
pub enum LinePosition {
TOP, LEFT, RIGHT, BOTTOM
}
/// Position of [`Modal`] on the screen. /// Position of [`Modal`] on the screen.
#[derive(Clone)] #[derive(Clone)]
pub enum ModalPosition { pub enum ModalPosition {

View file

@ -17,8 +17,8 @@ use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use egui::{Align, Button, CursorIcon, Layout, lerp, PointerState, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, TextBuffer, TextStyle, TextureHandle, TextureOptions, Widget}; use egui::{Align, Button, CursorIcon, Layout, lerp, PointerState, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, TextBuffer, TextStyle, TextureHandle, TextureOptions, Widget, UiBuilder};
use egui::epaint::{Color32, FontId, RectShape, Rounding, Stroke}; use egui::epaint::{Color32, FontId, PathShape, PathStroke, RectShape, Rounding, Stroke};
use egui::epaint::text::TextWrapping; use egui::epaint::text::TextWrapping;
use egui::load::SizedTexture; use egui::load::SizedTexture;
use egui::os::OperatingSystem; use egui::os::OperatingSystem;
@ -30,7 +30,7 @@ use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CHECK_SQUARE, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN, SQUARE}; use crate::gui::icons::{CHECK_SQUARE, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN, SQUARE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::TextEditOptions; use crate::gui::views::types::{LinePosition, TextEditOptions};
pub struct View; pub struct View;
@ -78,14 +78,16 @@ impl View {
rect.set_width(width); rect.set_width(width);
// Draw content. // Draw content.
ui.allocate_ui(rect.size(), |ui| { ui.vertical_centered(|ui| {
(add_content)(ui); ui.allocate_ui(rect.size(), |ui| {
(add_content)(ui);
});
}); });
} }
/// Get width and height of app window. /// Get width and height of app window.
pub fn window_size(ui: &egui::Ui) -> (f32, f32) { pub fn window_size(ctx: &egui::Context) -> (f32, f32) {
let rect = ui.ctx().screen_rect(); let rect = ctx.screen_rect();
(rect.width(), rect.height()) (rect.width(), rect.height())
} }
@ -108,7 +110,7 @@ impl View {
/// Calculate margin for far left view based on display insets (cutouts). /// Calculate margin for far left view based on display insets (cutouts).
pub fn far_right_inset_margin(ui: &mut egui::Ui) -> f32 { pub fn far_right_inset_margin(ui: &mut egui::Ui) -> f32 {
let container_width = ui.available_rect_before_wrap().max.x as i32; let container_width = ui.available_rect_before_wrap().max.x as i32;
let window_size = Self::window_size(ui); let window_size = Self::window_size(ui.ctx());
let display_width = window_size.0 as i32; let display_width = window_size.0 as i32;
// Means end of the screen. // Means end of the screen.
if container_width == display_width { if container_width == display_width {
@ -235,7 +237,7 @@ impl View {
ui.style_mut().visuals.widgets.active.expansion = 0.0; ui.style_mut().visuals.widgets.active.expansion = 0.0;
// Setup fill colors. // Setup fill colors.
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false); ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::button(); ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill(); ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
// Setup stroke colors. // Setup stroke colors.
ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke(); ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke();
@ -325,7 +327,7 @@ impl View {
action: impl FnOnce()) { action: impl FnOnce()) {
// Setup button size. // Setup button size.
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_width(32.0); rect.set_width(42.0);
let button_size = rect.size(); let button_size = rect.size();
ui.scope(|ui| { ui.scope(|ui| {
@ -336,12 +338,12 @@ impl View {
ui.style_mut().visuals.widgets.active.expansion = 0.0; ui.style_mut().visuals.widgets.active.expansion = 0.0;
// Setup fill colors. // Setup fill colors.
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false); ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::button(); ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill(); ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
// Setup stroke colors. // Disable strokes.
ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke(); ui.visuals_mut().widgets.inactive.bg_stroke = Stroke::NONE;
ui.visuals_mut().widgets.hovered.bg_stroke = Self::hover_stroke(); ui.visuals_mut().widgets.hovered.bg_stroke = Stroke::NONE;
ui.visuals_mut().widgets.active.bg_stroke = Self::item_stroke(); ui.visuals_mut().widgets.active.bg_stroke = Stroke::NONE;
// Setup button text color. // Setup button text color.
let text_color = if let Some(c) = color { c } else { Colors::item_button() }; let text_color = if let Some(c) = color { c } else { Colors::item_button() };
@ -353,9 +355,18 @@ impl View {
.ui(ui) .ui(ui)
.on_hover_cursor(CursorIcon::PointingHand); .on_hover_cursor(CursorIcon::PointingHand);
br.surrender_focus(); br.surrender_focus();
if Self::touched(ui, br) { if Self::touched(ui, br.clone()) {
(action)(); (action)();
} }
// Draw stroke.
let r = {
let mut r = ui.available_rect_before_wrap();
r.min = br.rect.min;
r.min.x += 0.5;
r
};
Self::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
}); });
} }
@ -529,28 +540,20 @@ impl View {
/// 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 label_box(ui: &mut egui::Ui, text: String, label: String, r: [bool; 4]) {
let rect = ui.available_rect_before_wrap(); let rect = ui.available_rect_before_wrap();
// Create background shape. // Create background shape.
let mut bg_shape = RectShape { let mut bg_shape = RectShape::new(rect, Rounding {
rect, nw: if r[0] { 8.0 } else { 0.0 },
rounding: Rounding { ne: if r[1] { 8.0 } else { 0.0 },
nw: if r[0] { 8.0 } else { 0.0 }, sw: if r[2] { 8.0 } else { 0.0 },
ne: if r[1] { 8.0 } else { 0.0 }, se: if r[3] { 8.0 } else { 0.0 },
sw: if r[2] { 8.0 } else { 0.0 }, }, Colors::fill_lite(), Self::item_stroke());
se: if r[3] { 8.0 } else { 0.0 },
},
fill: Colors::TRANSPARENT,
stroke: Self::item_stroke(),
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
let bg_idx = ui.painter().add(bg_shape); let bg_idx = ui.painter().add(bg_shape);
// Draw box content. // Draw box content.
let content_resp = ui.allocate_ui_at_rect(rect, |ui| { let content_resp = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
ui.add_space(4.0); ui.add_space(4.0);
ui.scope(|ui| { ui.scope(|ui| {
@ -558,7 +561,7 @@ impl View {
ui.style_mut().spacing.item_spacing.y = -3.0; ui.style_mut().spacing.item_spacing.y = -3.0;
// Draw box value. // Draw box value.
let mut job = LayoutJob::single_section(value, TextFormat { let mut job = LayoutJob::single_section(text, TextFormat {
font_id: FontId::proportional(17.0), font_id: FontId::proportional(17.0),
color: Colors::white_or_black(true), color: Colors::white_or_black(true),
..Default::default() ..Default::default()
@ -578,7 +581,7 @@ impl View {
}); });
}).response; }).response;
// Setup background shape to be painted behind box content. // Setup background shape size.
bg_shape.rect = content_resp.rect; bg_shape.rect = content_resp.rect;
ui.painter().set(bg_idx, bg_shape); ui.painter().set(bg_idx, bg_shape);
} }
@ -590,7 +593,7 @@ impl View {
let side_margin = 28.0; let side_margin = 28.0;
rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0); rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0);
rect.max -= egui::emath::vec2(side_margin, 0.0); rect.max -= egui::emath::vec2(side_margin, 0.0);
ui.allocate_ui_at_rect(rect, |ui| { ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
(content)(ui); (content)(ui);
}); });
}); });
@ -653,6 +656,47 @@ impl View {
Stroke { width: 1.0, color }); Stroke { width: 1.0, color });
} }
/// Draw line for panel content.
pub fn line(ui: &mut egui::Ui, pos: LinePosition, rect: &Rect, color: Color32) {
let points = match pos {
LinePosition::RIGHT => {
vec![{
let mut r = rect.clone();
r.min.x = r.max.x;
r.min
}, rect.max]
}
LinePosition::BOTTOM => {
vec![{
let mut r = rect.clone();
r.min.y = r.max.y;
r.min
}, rect.max]
}
LinePosition::LEFT => {
vec![rect.min, {
let mut r = rect.clone();
r.max.x = r.min.x;
r.max
}]
}
LinePosition::TOP => {
vec![rect.min, {
let mut r = rect.clone();
r.max.y = r.min.y;
r.max
}]
}
};
let stroke = PathShape {
points,
closed: false,
fill: Default::default(),
stroke: PathStroke::new(1.0, color),
};
ui.painter().add(stroke);
}
/// Draw SVG image from provided data with optional provided size. /// Draw SVG image from provided data with optional provided size.
pub fn svg_image(ui: &mut egui::Ui, pub fn svg_image(ui: &mut egui::Ui,
name: &str, name: &str,
@ -698,6 +742,19 @@ impl View {
); );
} }
/// Draw semi-transparent cover at specified area.
pub fn content_cover_ui(ui: &mut egui::Ui,
rect: Rect,
id: impl std::hash::Hash,
mut on_click: impl FnMut()) {
let resp = ui.interact(rect, egui::Id::new(id), Sense::click_and_drag());
if resp.clicked() || resp.dragged() {
on_click();
}
let shape = RectShape::filled(resp.rect, Rounding::ZERO, Colors::semi_transparent());
ui.painter().add(shape);
}
/// Get top display inset (cutout) size. /// Get top display inset (cutout) size.
pub fn get_top_inset() -> f32 { pub fn get_top_inset() -> f32 {
TOP_DISPLAY_INSET.load(Ordering::Relaxed) as f32 TOP_DISPLAY_INSET.load(Ordering::Relaxed) as f32

View file

@ -21,7 +21,7 @@ use crate::gui::Colors;
use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE}; use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, TitlePanel, View}; use crate::gui::views::{Modal, Content, TitlePanel, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, TitleContentType, TitleType}; use crate::gui::views::types::{ModalContainer, ModalPosition, LinePosition, TitleContentType, TitleType};
use crate::gui::views::wallets::creation::WalletCreation; use crate::gui::views::wallets::creation::WalletCreation;
use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletConnectionModal, WalletsModal}; use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletConnectionModal, WalletsModal};
use crate::gui::views::wallets::types::WalletTabType; use crate::gui::views::wallets::types::WalletTabType;
@ -149,14 +149,15 @@ impl WalletsContent {
let creating_wallet = self.creating_wallet(); let creating_wallet = self.creating_wallet();
let showing_wallet = self.showing_wallet() && !creating_wallet; let showing_wallet = self.showing_wallet() && !creating_wallet;
let dual_panel = is_dual_panel_mode(ui); let dual_panel = Self::is_dual_panel_mode(ui);
let content_width = ui.available_width(); let content_width = ui.available_width();
let list_hidden = creating_wallet || self.wallets.list().is_empty() let list_hidden = creating_wallet || self.wallets.list().is_empty()
|| (showing_wallet && self.wallet_content.as_ref().unwrap().qr_scan_content.is_some())
|| (dual_panel && showing_wallet && !self.show_wallets_at_dual_panel) || (dual_panel && showing_wallet && !self.show_wallets_at_dual_panel)
|| (!dual_panel && showing_wallet); || (!dual_panel && showing_wallet);
// Show title panel. // Show title panel.
self.title_ui(ui, dual_panel, showing_wallet); self.title_ui(ui, dual_panel, showing_wallet, cb);
if showing_wallet { if showing_wallet {
egui::SidePanel::right("wallet_panel") egui::SidePanel::right("wallet_panel")
@ -167,7 +168,6 @@ impl WalletsContent {
content_width - Content::SIDE_PANEL_WIDTH content_width - Content::SIDE_PANEL_WIDTH
}) })
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::fill_deep(),
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
@ -181,17 +181,19 @@ impl WalletsContent {
if !list_hidden { if !list_hidden {
egui::TopBottomPanel::bottom("wallets_bottom_panel") egui::TopBottomPanel::bottom("wallets_bottom_panel")
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::fill(),
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING, left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
right: View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING, right: View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING,
top: View::TAB_ITEMS_PADDING, top: View::TAB_ITEMS_PADDING,
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING, bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
}, },
fill: Colors::fill(),
..Default::default() ..Default::default()
}) })
.resizable(false) .resizable(false)
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
// Setup spacing between tabs. // Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
// Setup vertical padding inside buttons. // Setup vertical padding inside buttons.
@ -203,6 +205,16 @@ impl WalletsContent {
self.show_add_wallet_modal(cb); self.show_add_wallet_modal(cb);
}); });
}); });
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= View::TAB_ITEMS_PADDING;
r.min.x -= View::TAB_ITEMS_PADDING;
r.max.x += View::TAB_ITEMS_PADDING;
r
};
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
}); });
egui::SidePanel::left("wallet_list_panel") egui::SidePanel::left("wallet_list_panel")
@ -213,18 +225,17 @@ impl WalletsContent {
}) })
.resizable(false) .resizable(false)
.frame(egui::Frame { .frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::fill_deep(),
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0, left: View::far_left_inset_margin(ui) + 4.0,
right: View::far_right_inset_margin(ui) + 4.0, right: View::far_right_inset_margin(ui) + 4.0,
top: 3.0, top: 3.0,
bottom: 4.0, bottom: 4.0,
}, },
fill: Colors::fill_deep(),
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
if !list_hidden && !dual_panel && !showing_wallet && !creating_wallet { if !dual_panel && !showing_wallet {
ui.ctx().request_repaint_after(Duration::from_millis(1000)); ui.ctx().request_repaint_after(Duration::from_millis(1000));
} }
// Show wallet list. // Show wallet list.
@ -232,12 +243,10 @@ impl WalletsContent {
}); });
} }
// Show central panel with wallet creation.
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
stroke: View::item_stroke(), fill: if creating_wallet {
fill: if self.creation_content.is_some() { Colors::TRANSPARENT
Colors::white_or_black(false)
} else { } else {
Colors::fill_deep() Colors::fill_deep()
}, },
@ -276,6 +285,8 @@ impl WalletsContent {
self.show_add_wallet_modal(cb); self.show_add_wallet_modal(cb);
}); });
}); });
} else {
return;
} }
}); });
} }
@ -284,7 +295,8 @@ impl WalletsContent {
pub fn showing_wallet(&self) -> bool { pub fn showing_wallet(&self) -> bool {
if let Some(wallet_content) = &self.wallet_content { if let Some(wallet_content) = &self.wallet_content {
let w = &wallet_content.wallet; let w = &wallet_content.wallet;
return w.is_open() && w.get_config().chain_type == AppConfig::chain_type(); return w.is_open() && !w.is_deleted() &&
w.get_config().chain_type == AppConfig::chain_type();
} }
false false
} }
@ -301,26 +313,19 @@ impl WalletsContent {
return; return;
} }
// Close network panel on single panel mode. // Close network panel on single panel mode.
if !Content::is_dual_panel_mode(ui) && Content::is_network_panel_open() { if !Content::is_dual_panel_mode(ui.ctx()) && Content::is_network_panel_open() {
Content::toggle_network_panel(); Content::toggle_network_panel();
} }
// Pass data to opened selected wallet or show wallets selection. // Pass data to single wallet or show wallets selection.
if self.wallet_content.is_some() { if wallets_size == 1 {
if self.showing_wallet() { let w = self.wallets.list()[0].clone();
if wallets_size == 1 { if w.is_open() {
let wallet_content = self.wallet_content.as_mut().unwrap(); self.wallet_content = Some(WalletContent::new(w, data));
wallet_content.on_data(data);
} else {
self.show_wallet_selection_modal(data);
}
} else { } else {
if wallets_size == 1 { self.show_opening_modal(w, data, cb);
let wallet_content = self.wallet_content.as_ref().unwrap();
self.show_opening_modal(wallet_content.wallet.clone(), data, cb);
} else {
self.show_wallet_selection_modal(data);
}
} }
} else {
self.show_wallet_selection_modal(data);
} }
} }
@ -344,19 +349,26 @@ impl WalletsContent {
} }
/// Handle Back key event returning `false` when event was handled. /// Handle Back key event returning `false` when event was handled.
pub fn on_back(&mut self) -> bool { pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) -> bool {
if self.creation_content.is_some() { if self.creation_content.is_some() {
// Close wallet creation. // Close wallet creation.
let creation = self.creation_content.as_mut().unwrap(); let creation = self.creation_content.as_mut().unwrap();
if creation.on_back() { if creation.on_back() {
self.creation_content = None; self.creation_content = None;
} }
return false return false;
} else { } else {
// Close opened wallet.
if self.showing_wallet() { if self.showing_wallet() {
let content = self.wallet_content.as_mut().unwrap();
// Close opened QR code scanner.
if content.qr_scan_content.is_some() {
cb.stop_camera();
content.qr_scan_content = None;
return false;
}
// Close opened wallet.
self.wallet_content = None; self.wallet_content = None;
return false return false;
} }
} }
true true
@ -366,15 +378,27 @@ impl WalletsContent {
fn title_ui(&mut self, fn title_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
dual_panel: bool, dual_panel: bool,
show_wallet: bool) { show_wallet: bool,
cb: &dyn PlatformCallbacks) {
let show_list = self.show_wallets_at_dual_panel; let show_list = self.show_wallets_at_dual_panel;
let creating_wallet = self.creating_wallet(); let creating_wallet = self.creating_wallet();
let qr_scan = {
let mut scan = false;
if show_wallet {
scan = self.wallet_content.as_mut().unwrap().qr_scan_content.is_some();
}
scan
};
// Setup title. // Setup title.
let title_content = if show_wallet && (!dual_panel let title_content = if show_wallet && (!dual_panel
|| (dual_panel && !show_list)) && !creating_wallet { || (dual_panel && !show_list)) && !creating_wallet {
let wallet_content = self.wallet_content.as_ref().unwrap(); let wallet_content = self.wallet_content.as_ref().unwrap();
let wallet_tab_type = wallet_content.current_tab.get_type(); let wallet_tab_type = wallet_content.current_tab.get_type();
let title_text = wallet_tab_type.name().to_uppercase(); let title_text = if qr_scan {
t!("scan_qr")
} else {
wallet_tab_type.name()
};
if wallet_tab_type == WalletTabType::Settings { if wallet_tab_type == WalletTabType::Settings {
TitleType::Single(TitleContentType::Title(title_text)) TitleType::Single(TitleContentType::Title(title_text))
} else { } else {
@ -382,16 +406,18 @@ impl WalletsContent {
TitleType::Single(TitleContentType::WithSubTitle(title_text, subtitle_text, false)) TitleType::Single(TitleContentType::WithSubTitle(title_text, subtitle_text, false))
} }
} else { } else {
let title_text = if creating_wallet { let title_text = if qr_scan {
t!("scan_qr")
} else if creating_wallet {
t!("wallets.add") t!("wallets.add")
} else { } else {
t!("wallets.title") t!("wallets.title")
}.to_uppercase(); };
let dual_title = !creating_wallet && show_wallet && dual_panel; let dual_title = !qr_scan && !creating_wallet && show_wallet && dual_panel;
if dual_title { if dual_title {
let wallet_content = self.wallet_content.as_ref().unwrap(); let wallet_content = self.wallet_content.as_ref().unwrap();
let wallet_tab_type = wallet_content.current_tab.get_type(); let wallet_tab_type = wallet_content.current_tab.get_type();
let wallet_title_text = wallet_tab_type.name().to_uppercase(); let wallet_title_text = wallet_tab_type.name();
let wallet_title_content = if wallet_tab_type == WalletTabType::Settings { let wallet_title_content = if wallet_tab_type == WalletTabType::Settings {
TitleContentType::Title(wallet_title_text) TitleContentType::Title(wallet_title_text)
} else { } else {
@ -408,6 +434,16 @@ impl WalletsContent {
TitlePanel::new(Id::new("wallets_title_panel")).ui(title_content, |ui| { TitlePanel::new(Id::new("wallets_title_panel")).ui(title_content, |ui| {
if show_wallet && !dual_panel { if show_wallet && !dual_panel {
View::title_button_big(ui, ARROW_LEFT, |_| { View::title_button_big(ui, ARROW_LEFT, |_| {
let wallet_qr_scan = self.wallet_content
.as_ref()
.unwrap()
.qr_scan_content
.is_some();
if wallet_qr_scan {
cb.stop_camera();
self.wallet_content.as_mut().unwrap().qr_scan_content = None;
return;
}
self.wallet_content = None; self.wallet_content = None;
}); });
} else if self.creation_content.is_some() { } else if self.creation_content.is_some() {
@ -423,16 +459,23 @@ impl WalletsContent {
self.creation_content = None; self.creation_content = None;
} }
} else if show_wallet && dual_panel { } else if show_wallet && dual_panel {
let list_icon = if show_list { if qr_scan {
SIDEBAR_SIMPLE View::title_button_big(ui, ARROW_LEFT, |_| {
cb.stop_camera();
self.wallet_content.as_mut().unwrap().qr_scan_content = None;
});
} else { } else {
SUITCASE let list_icon = if show_list {
}; SIDEBAR_SIMPLE
View::title_button_big(ui, list_icon, |_| { } else {
self.show_wallets_at_dual_panel = !show_list; SUITCASE
AppConfig::toggle_show_wallets_at_dual_panel(); };
}); View::title_button_big(ui, list_icon, |_| {
} else if !Content::is_dual_panel_mode(ui) { self.show_wallets_at_dual_panel = !show_list;
AppConfig::toggle_show_wallets_at_dual_panel();
});
}
} else if !Content::is_dual_panel_mode(ui.ctx()) {
View::title_button_big(ui, GLOBE, |_| { View::title_button_big(ui, GLOBE, |_| {
Content::toggle_network_panel(); Content::toggle_network_panel();
}); });
@ -453,38 +496,32 @@ impl WalletsContent {
ui: &mut egui::Ui, ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
ScrollArea::vertical() ScrollArea::vertical()
.id_source("wallet_list") .id_salt("wallet_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
ui.vertical_centered(|ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { // Show application logo and name.
// Show application logo and name. View::app_logo_name_version(ui);
View::app_logo_name_version(ui); ui.add_space(15.0);
ui.add_space(15.0);
let mut list = self.wallets.list().clone(); let list = self.wallets.list().clone();
// Remove deleted wallet from the list. for w in &list {
list.retain(|w| { // Remove deleted.
let deleted = w.is_deleted(); if w.is_deleted() {
if deleted { self.wallet_content = None;
self.wallet_content = None; self.wallets.remove(w.get_config().id);
self.wallets.remove(w.get_config().id); ui.ctx().request_repaint();
ui.ctx().request_repaint(); continue;
}
!deleted
});
for wallet in &list {
// Check if wallet reopen is needed.
if wallet.reopen_needed() && !wallet.is_open() {
wallet.set_reopen(false);
self.show_opening_modal(wallet.clone(), None, cb);
}
// Draw wallet list item.
self.wallet_item_ui(ui, wallet, cb);
ui.add_space(5.0);
} }
}); // Check if wallet reopen is needed.
if w.reopen_needed() && !w.is_open() {
w.set_reopen(false);
self.show_opening_modal(w.clone(), None, cb);
}
self.wallet_item_ui(ui, w, cb);
ui.add_space(5.0);
}
}); });
}); });
} }
@ -603,11 +640,11 @@ impl WalletsContent {
.show(); .show();
cb.show_keyboard(); cb.show_keyboard();
} }
}
/// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time. /// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time.
fn is_dual_panel_mode(ui: &mut egui::Ui) -> bool { fn is_dual_panel_mode(ui: &mut egui::Ui) -> bool {
let dual_panel_root = Content::is_dual_panel_mode(ui); let dual_panel_root = Content::is_dual_panel_mode(ui.ctx());
let max_width = ui.available_width(); let max_width = ui.available_width();
dual_panel_root && max_width >= (Content::SIDE_PANEL_WIDTH * 2.0) + View::get_right_inset() dual_panel_root && max_width >= (Content::SIDE_PANEL_WIDTH * 2.0) + View::get_right_inset()
}
} }

View file

@ -20,7 +20,7 @@ use crate::gui::Colors;
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, SCAN}; use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, SCAN};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View, CameraScanModal}; use crate::gui::views::{Modal, Content, View, CameraScanModal};
use crate::gui::views::types::{ModalContainer, ModalPosition, QrScanResult}; use crate::gui::views::types::{LinePosition, ModalContainer, ModalPosition, QrScanResult};
use crate::gui::views::wallets::creation::MnemonicSetup; use crate::gui::views::wallets::creation::MnemonicSetup;
use crate::gui::views::wallets::creation::types::Step; use crate::gui::views::wallets::creation::types::Step;
use crate::gui::views::wallets::ConnectionSettings; use crate::gui::views::wallets::ConnectionSettings;
@ -112,27 +112,30 @@ impl WalletCreation {
on_create: impl FnMut(Wallet)) { on_create: impl FnMut(Wallet)) {
self.current_modal_ui(ui, cb); self.current_modal_ui(ui, cb);
// Show wallet creation step description and confirmation panel.
egui::TopBottomPanel::bottom("wallet_creation_step_panel") egui::TopBottomPanel::bottom("wallet_creation_step_panel")
.frame(egui::Frame { .frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::fill_deep(),
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 8.0, left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
right: View::get_right_inset() + 8.0, right: View::get_right_inset() + View::TAB_ITEMS_PADDING,
top: 4.0, top: View::TAB_ITEMS_PADDING,
bottom: View::get_bottom_inset(), bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
}, },
fill: Colors::fill_deep(),
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
ui.vertical_centered(|ui| { // Draw divider line.
ui.vertical_centered(|ui| { let rect = {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 2.0, |ui| { let mut r = ui.available_rect_before_wrap();
self.step_control_ui(ui, on_create, cb); r.min.y -= View::TAB_ITEMS_PADDING;
}); r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
}); r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
r
};
View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke());
// Show step control content.
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.step_control_ui(ui, on_create, cb);
}); });
}); });
@ -149,19 +152,17 @@ impl WalletCreation {
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
ScrollArea::vertical() ScrollArea::vertical()
.id_source(Id::from(format!("creation_step_scroll_{}", self.step.name()))) .id_salt(Id::from(format!("creation_step_scroll_{}", self.step.name())))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
ui.vertical_centered(|ui| { let max_width = if self.step == Step::SetupConnection {
let max_width = if self.step == Step::SetupConnection { Content::SIDE_PANEL_WIDTH * 1.3
Content::SIDE_PANEL_WIDTH * 1.3 } else {
} else { Content::SIDE_PANEL_WIDTH * 2.0
Content::SIDE_PANEL_WIDTH * 2.0 };
}; View::max_width_ui(ui, max_width, |ui| {
View::max_width_ui(ui, max_width, |ui| { self.step_content_ui(ui, cb);
self.step_content_ui(ui, cb);
});
}); });
}); });
}); });
@ -201,9 +202,8 @@ impl WalletCreation {
self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate; self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate;
if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) || if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) ||
generate_step { generate_step {
ui.add_space(2.0);
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray())); ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
ui.add_space(2.0); ui.add_space(6.0);
} else { } else {
next = false; next = false;
// Show error text. // Show error text.
@ -214,39 +214,35 @@ impl WalletCreation {
.color(Colors::red())); .color(Colors::red()));
ui.add_space(10.0); ui.add_space(10.0);
} else { } else {
ui.add_space(2.0);
ui.label(RichText::new(&t!("wallets.not_valid_phrase")) ui.label(RichText::new(&t!("wallets.not_valid_phrase"))
.size(16.0) .size(16.0)
.color(Colors::red())); .color(Colors::red()));
ui.add_space(2.0); ui.add_space(4.0);
}; };
} }
// Setup buttons. // Setup spacing between buttons.
ui.style_mut().spacing.item_spacing = egui::vec2(8.0, 0.0);
// Setup vertical padding inside button.
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 7.0);
match step { match step {
Step::EnterMnemonic => { Step::EnterMnemonic => {
ui.add_space(4.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| { ui.columns(2, |columns| {
// Show copy or paste button for mnemonic phrase step. // Show copy or paste button for mnemonic phrase step.
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
match self.mnemonic_setup.mnemonic.mode() { match self.mnemonic_setup.mnemonic.mode() {
PhraseMode::Generate => { PhraseMode::Generate => {
// Show copy button. let c_t = format!("{} {}",
let c_t = format!("{} {}", COPY, t!("copy").to_uppercase()); COPY,
View::button(ui, t!("copy").to_uppercase());
c_t.to_uppercase(), View::button(ui, c_t, Colors::white_or_black(false), || {
Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.mnemonic_setup cb.copy_string_to_buffer(self.mnemonic_setup
.mnemonic .mnemonic
.get_phrase()); .get_phrase());
}); });
} }
PhraseMode::Import => { PhraseMode::Import => {
// Show paste button.
let p_t = format!("{} {}", let p_t = format!("{} {}",
CLIPBOARD_TEXT, CLIPBOARD_TEXT,
t!("paste").to_uppercase()); t!("paste").to_uppercase());
@ -262,7 +258,9 @@ impl WalletCreation {
if next { if next {
self.next_step_button_ui(ui, on_create); self.next_step_button_ui(ui, on_create);
} else { } else {
let scan_text = format!("{} {}", SCAN, t!("scan").to_uppercase()); let scan_text = format!("{} {}",
SCAN,
t!("scan").to_uppercase());
View::button(ui, scan_text, Colors::white_or_black(false), || { View::button(ui, scan_text, Colors::white_or_black(false), || {
self.scan_modal_content = Some(CameraScanModal::default()); self.scan_modal_content = Some(CameraScanModal::default());
// Show QR code scan modal. // Show QR code scan modal.
@ -276,10 +274,8 @@ impl WalletCreation {
} }
}); });
}); });
ui.add_space(4.0);
} }
Step::ConfirmMnemonic => { Step::ConfirmMnemonic => {
ui.add_space(4.0);
// Show next step or paste button. // Show next step or paste button.
if next { if next {
self.next_step_button_ui(ui, on_create); self.next_step_button_ui(ui, on_create);
@ -290,17 +286,14 @@ impl WalletCreation {
self.mnemonic_setup.mnemonic.import(&data); self.mnemonic_setup.mnemonic.import(&data);
}); });
} }
ui.add_space(4.0);
} }
Step::SetupConnection => { Step::SetupConnection => {
if next { if next {
ui.add_space(4.0);
self.next_step_button_ui(ui, on_create); self.next_step_button_ui(ui, on_create);
ui.add_space(4.0); ui.add_space(2.0);
} }
} }
} }
ui.add_space(3.0);
} }
/// Draw button to go to next [`Step`]. /// Draw button to go to next [`Step`].
@ -311,7 +304,7 @@ impl WalletCreation {
let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection { let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection {
(format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold()) (format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold())
} else { } else {
(t!("continue"), Colors::text_button(), Colors::white_or_black(false)) (t!("continue"), Colors::green(), Colors::white_or_black(false))
}; };
// Show next step button. // Show next step button.
@ -361,7 +354,7 @@ impl WalletCreation {
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb), Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
Step::SetupConnection => { Step::SetupConnection => {
// Redraw if node is running. // Redraw if node is running.
if Node::is_running() && !Content::is_dual_panel_mode(ui) { if Node::is_running() && !Content::is_dual_panel_mode(ui.ctx()) {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY); ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
} }
self.network_setup.create_ui(ui, cb) self.network_setup.create_ui(ui, cb)

View file

@ -216,7 +216,7 @@ impl MnemonicSetup {
}; };
if edit { if edit {
ui.add_space(6.0); ui.add_space(6.0);
View::button(ui, PENCIL.to_string(), Colors::button(), || { View::button(ui, PENCIL.to_string(), Colors::white_or_black(false), || {
self.word_index_edit = num - 1; self.word_index_edit = num - 1;
self.word_edit = word.text.clone(); self.word_edit = word.text.clone();
self.valid_word_edit = word.valid; self.valid_word_edit = word.valid;

View file

@ -65,7 +65,7 @@ impl WalletConnectionModal {
} else { } else {
350.0 350.0
}) })
.id_source("integrated_node") .id_salt("connections_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2]) .auto_shrink([true; 2])
.show(ui, |ui| { .show(ui, |ui| {
@ -96,7 +96,7 @@ impl WalletConnectionModal {
ui.add_space(6.0); ui.add_space(6.0);
// Show button to add new external node connection. // Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node")); let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::button(), || { View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.ext_conn_content = Some(ExternalConnectionModal::new(None)); self.ext_conn_content = Some(ExternalConnectionModal::new(None));
}); });
}); });
@ -137,7 +137,7 @@ impl WalletConnectionModal {
}); });
ui.add_space(2.0); ui.add_space(2.0);
View::horizontal_line(ui, Colors::stroke()); View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0); ui.add_space(6.0);
// Show button to close modal. // Show button to close modal.

View file

@ -64,7 +64,7 @@ impl WalletsModal {
ui.add_space(4.0); ui.add_space(4.0);
ScrollArea::vertical() ScrollArea::vertical()
.max_height(373.0) .max_height(373.0)
.id_source("select_wallet_list") .id_salt("select_wallet_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2]) .auto_shrink([true; 2])
.show(ui, |ui| { .show(ui, |ui| {
@ -83,7 +83,7 @@ impl WalletsModal {
}); });
ui.add_space(2.0); ui.add_space(2.0);
View::horizontal_line(ui, Colors::stroke()); View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0); ui.add_space(6.0);
// Show button to close modal. // Show button to close modal.

View file

@ -13,16 +13,17 @@
// limitations under the License. // limitations under the License.
use std::time::Duration; use std::time::Duration;
use egui::{Align, Id, Layout, Margin, RichText}; use egui::{Align, Id, Layout, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use grin_chain::SyncStatus; use grin_chain::SyncStatus;
use grin_core::core::amount_to_hr_string; use grin_core::core::amount_to_hr_string;
use crate::AppConfig; use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE}; use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CAMERA_ROTATE, CHAT_CIRCLE_TEXT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View, CameraScanModal}; use crate::gui::views::{Modal, Content, View, CameraContent};
use crate::gui::views::types::{ModalPosition, QrScanResult}; use crate::gui::views::types::{LinePosition, ModalContainer, ModalPosition};
use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport}; use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport};
use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType}; use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::modals::WalletAccountsModal; use crate::gui::views::wallets::wallet::modals::WalletAccountsModal;
@ -35,21 +36,40 @@ use crate::wallet::types::{ConnectionMethod, WalletData};
pub struct WalletContent { pub struct WalletContent {
/// Selected and opened wallet. /// Selected and opened wallet.
pub wallet: Wallet, pub wallet: Wallet,
/// Current tab content to show.
pub current_tab: Box<dyn WalletTab>,
/// Wallet accounts [`Modal`] content. /// Wallet accounts [`Modal`] content.
accounts_modal_content: Option<WalletAccountsModal>, accounts_modal_content: Option<WalletAccountsModal>,
/// QR code scan [`Modal`] content.
scan_modal_content: Option<CameraScanModal>,
/// Current tab content to show. /// QR code scan content.
pub current_tab: Box<dyn WalletTab>, pub qr_scan_content: Option<CameraContent>,
/// List of allowed [`Modal`] ids for this [`ModalContainer`].
allowed_modal_ids: Vec<&'static str>
} }
/// Identifier for account list [`Modal`]. /// Identifier for account list [`Modal`].
const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal"; const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal";
/// Identifier for QR code scan [`Modal`]. impl ModalContainer for WalletContent {
const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal"; fn modal_ids(&self) -> &Vec<&'static str> {
&self.allowed_modal_ids
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
ACCOUNT_LIST_MODAL => {
if let Some(content) = self.accounts_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, &self.wallet, modal, cb);
});
}
}
_ => {}
}
}
}
impl WalletContent { impl WalletContent {
/// Create new instance with optional data. /// Create new instance with optional data.
@ -57,8 +77,11 @@ impl WalletContent {
let mut content = Self { let mut content = Self {
wallet, wallet,
accounts_modal_content: None, accounts_modal_content: None,
scan_modal_content: None, qr_scan_content: None,
current_tab: Box::new(WalletTransactions::default()), current_tab: Box::new(WalletTransactions::default()),
allowed_modal_ids: vec![
ACCOUNT_LIST_MODAL,
],
}; };
if data.is_some() { if data.is_some() {
content.on_data(data); content.on_data(data);
@ -68,114 +91,162 @@ impl WalletContent {
/// Handle data from deeplink or opened file. /// Handle data from deeplink or opened file.
pub fn on_data(&mut self, data: Option<String>) { pub fn on_data(&mut self, data: Option<String>) {
// Provide data to messages.
self.current_tab = Box::new(WalletMessages::new(data)); self.current_tab = Box::new(WalletMessages::new(data));
} }
/// Draw wallet content. /// Draw wallet content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.modal_content_ui(ui, cb); ui.ctx().request_repaint_after(Duration::from_millis(1000));
self.current_modal_ui(ui, cb);
let dual_panel = Content::is_dual_panel_mode(ui); let dual_panel = Content::is_dual_panel_mode(ui.ctx());
let show_wallets_dual = AppConfig::show_wallets_at_dual_panel();
let wallet = &self.wallet; let wallet = &self.wallet;
let wallet_id = wallet.identifier();
let data = wallet.get_data(); let data = wallet.get_data();
let data_empty = data.is_none(); let show_qr_scan = self.qr_scan_content.is_some();
let hide_tabs = Self::block_navigation_on_sync(wallet); let hide_tabs = Self::block_navigation_on_sync(wallet);
// Show wallet balance panel not on Settings tab with selected non-repairing // Show wallet account panel not on settings tab when navigation is not blocked and QR code
// wallet, when there is no error and data is not empty. // scanner is not showing and wallet data is not empty.
let mut show_balance = self.current_tab.get_type() != WalletTabType::Settings && !data_empty let mut show_account = self.current_tab.get_type() != WalletTabType::Settings && !hide_tabs
&& !wallet.sync_error() && !wallet.is_repairing() && !wallet.is_closing(); && !wallet.sync_error() && data.is_some();
if wallet.get_current_connection() == ConnectionMethod::Integrated && !Node::is_running() { if wallet.get_current_connection() == ConnectionMethod::Integrated && !Node::is_running() {
show_balance = false; show_account = false;
} }
egui::TopBottomPanel::top(Id::from("wallet_balance").with(wallet.identifier())) // Close scanner when balance got hidden.
if !show_account && show_qr_scan {
cb.stop_camera();
self.qr_scan_content = None;
}
egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier()))
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::fill(),
stroke: View::item_stroke(),
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0, left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0, right: View::get_right_inset() + 4.0,
top: 4.0, top: 4.0,
bottom: 0.0, bottom: 0.0,
}, },
outer_margin: Margin { fill: Colors::fill(),
left: if dual_panel {
-0.5
} else {
0.0
},
right: 0.0,
top: 0.0,
bottom: if dual_panel {
-1.0
} else {
-0.5
},
},
..Default::default() ..Default::default()
}) })
.show_animated_inside(ui, show_balance, |ui| { .show_animated_inside(ui, show_account, |ui| {
ui.vertical_centered(|ui| { let rect = ui.available_rect_before_wrap();
if !dual_panel { if show_qr_scan {
ui.add_space(1.0); View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| {
} self.qr_scan_content.as_mut().unwrap().ui(ui, cb);
// Draw account info. ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
self.qr_scan_content = None;
});
});
ui.add_space(6.0);
});
} else {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.account_ui(ui, data.unwrap(), cb); self.account_ui(ui, data.unwrap(), cb);
}); });
}); }
// Draw content divider lines.
let r = {
let mut r = rect.clone();
r.min.x -= 4.0 + View::far_left_inset_margin(ui);
r.min.y -= 4.0;
r.max.x += 4.0 + View::get_right_inset();
r
};
View::line(ui, LinePosition::BOTTOM, &r, Colors::item_stroke());
if dual_panel && show_wallets_dual && !show_qr_scan {
View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
}
}); });
// Show wallet tabs panel. // Show wallet tabs.
egui::TopBottomPanel::bottom("wallet_tabs_content") let show_tabs = !hide_tabs && self.qr_scan_content.is_none();
egui::TopBottomPanel::bottom("wallet_tabs")
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::fill(),
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING, left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
right: View::get_right_inset() + View::TAB_ITEMS_PADDING, right: View::get_right_inset() + View::TAB_ITEMS_PADDING,
top: View::TAB_ITEMS_PADDING, top: View::TAB_ITEMS_PADDING,
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING, bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
}, },
fill: Colors::fill(),
..Default::default() ..Default::default()
}) })
.show_animated_inside(ui, !hide_tabs, |ui| { .show_animated_inside(ui, show_tabs, |ui| {
ui.vertical_centered(|ui| { let rect = ui.available_rect_before_wrap();
// Draw wallet tabs. View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { self.tabs_ui(ui, cb);
self.tabs_ui(ui);
});
}); });
let rect = {
let mut r = rect.clone();
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
r.min.y -= View::TAB_ITEMS_PADDING;
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
r.max.y += View::get_bottom_inset() + View::TAB_ITEMS_PADDING;
r
};
// Draw content divider line.
View::line(ui, LinePosition::TOP, &rect, Colors::stroke());
}); });
// Show tab content panel. // Show tab content.
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
outer_margin: Margin { inner_margin: Margin {
left: if dual_panel { left: View::far_left_inset_margin(ui) + 4.0,
-0.5 right: View::get_right_inset() + 4.0,
} else {
0.0
},
right: 0.0,
top: 0.0, top: 0.0,
bottom: 0.0, bottom: 4.0,
}, },
stroke: View::item_stroke(),
fill: Colors::white_or_black(false),
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
self.current_tab.ui(ui, &self.wallet, cb); let rect = ui.available_rect_before_wrap();
let tab_type = self.current_tab.get_type();
let show_sync = (tab_type != WalletTabType::Settings || hide_tabs) &&
sync_ui(ui, &self.wallet);
if !show_sync {
if tab_type != WalletTabType::Txs {
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("wallet_scroll")
.with(tab_type.name())
.with(wallet_id))
.auto_shrink([false; 2])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.current_tab.ui(ui, &self.wallet, cb);
});
});
} else {
self.current_tab.ui(ui, &self.wallet, cb);
}
}
let rect = {
let mut r = rect.clone();
r.min.x -= View::far_left_inset_margin(ui) + 4.0;
r.max.x += View::get_right_inset() + 4.0;
r.max.y += 4.0;
r
};
// Draw cover when QR code scanner is active.
if show_qr_scan {
View::content_cover_ui(ui, rect, "wallet_tab", || {
cb.stop_camera();
self.qr_scan_content = None;
});
}
// Draw content divider line.
if dual_panel && show_wallets_dual {
View::line(ui, LinePosition::LEFT, &rect, Colors::item_stroke());
}
}); });
// Refresh content after 1 second for synced wallet.
if !data_empty {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
} else {
ui.ctx().request_repaint();
}
} }
/// Check when to block tabs navigation on sync progress. /// Check when to block tabs navigation on sync progress.
@ -191,64 +262,6 @@ impl WalletContent {
(!integrated_node || integrated_node_ready)) (!integrated_node || integrated_node_ready))
} }
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
ACCOUNT_LIST_MODAL => {
if let Some(content) = self.accounts_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, &self.wallet, modal, cb);
});
}
}
QR_CODE_SCAN_MODAL => {
let mut success = false;
if let Some(content) = self.scan_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, modal, cb, |result| {
match result {
QrScanResult::Slatepack(message) => {
success = true;
let msg = Some(message.to_string());
let messages = WalletMessages::new(msg);
self.current_tab = Box::new(messages);
return;
}
QrScanResult::Address(receiver) => {
success = true;
let balance = self.wallet.get_data()
.unwrap()
.info
.amount_currently_spendable;
if balance > 0 {
let mut transport = WalletTransport::default();
let rec = Some(receiver.to_string());
transport.show_send_tor_modal(cb, rec);
self.current_tab = Box::new(transport);
return;
}
}
_ => {}
}
if success {
modal.close();
}
});
});
}
if success {
self.scan_modal_content = None;
}
}
_ => {}
}
}
}
}
/// Draw wallet account content. /// Draw wallet account content.
fn account_ui(&mut self, fn account_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
@ -258,17 +271,12 @@ impl WalletContent {
rect.set_height(75.0); rect.set_height(75.0);
// Draw round background. // Draw round background.
let rounding = View::item_rounding(0, 2, false); let rounding = View::item_rounding(0, 2, false);
ui.painter().rect(rect, rounding, Colors::button(), View::hover_stroke()); ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to show QR code scanner. // Draw button to show QR code scanner.
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || { View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
self.scan_modal_content = Some(CameraScanModal::default()); self.qr_scan_content = Some(CameraContent::default());
Modal::new(QR_CODE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera(); cb.start_camera();
}); });
@ -346,15 +354,28 @@ impl WalletContent {
}); });
} }
/// Draw tab buttons in the bottom of the screen. /// Draw tab buttons at the bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui) { fn tabs_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.scope(|ui| { ui.scope(|ui| {
// Setup spacing between tabs. // Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
// Setup vertical padding inside tab button.
// Show camera switch button at QR code scan.
if self.qr_scan_content.is_some() && cb.can_switch_camera() {
// Setup vertical padding inside tab button.
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
ui.vertical_centered(|ui| {
View::tab_button(ui, CAMERA_ROTATE, false, |_| {
cb.switch_camera();
});
});
return;
}
// Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0); ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0);
// Draw tab buttons.
let current_type = self.current_tab.get_type(); let current_type = self.current_tab.get_type();
ui.columns(4, |columns| { ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
@ -384,101 +405,101 @@ impl WalletContent {
}); });
}); });
} }
}
/// Draw content when wallet is syncing and not ready to use, returns `true` at this case. /// Draw content when wallet is syncing and not ready to use, returns `true` at this case.
pub fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool { fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool {
if wallet.is_repairing() && !wallet.sync_error() { if wallet.is_repairing() && !wallet.sync_error() {
Self::sync_progress_ui(ui, wallet); sync_progress_ui(ui, wallet);
return true; return true;
} else if wallet.is_closing() { } else if wallet.is_closing() {
Self::sync_progress_ui(ui, wallet); sync_progress_ui(ui, wallet);
return true; return true;
} else if wallet.get_current_connection() == ConnectionMethod::Integrated { } else if wallet.get_current_connection() == ConnectionMethod::Integrated {
if !Node::is_running() || Node::is_stopping() { if !Node::is_running() || Node::is_stopping() {
View::center_content(ui, 108.0, |ui| { View::center_content(ui, 108.0, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.5, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let text = t!("wallets.enable_node", "settings" => GEAR_FINE); let text = t!("wallets.enable_node", "settings" => GEAR_FINE);
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text())); ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
ui.add_space(8.0); ui.add_space(8.0);
// Show button to enable integrated node at non-dual root panel mode // Show button to enable integrated node at non-dual root panel mode
// or when network connections are not showing and node is not stopping // or when network connections are not showing and node is not stopping
let dual_panel_root = Content::is_dual_panel_mode(ui); let dual_panel = Content::is_dual_panel_mode(ui.ctx());
if (!dual_panel_root || AppConfig::show_connections_network_panel()) if (!dual_panel || AppConfig::show_connections_network_panel())
&& !Node::is_stopping() { && !Node::is_stopping() {
let enable_text = format!("{} {}", POWER, t!("network.enable_node")); let enable_text = format!("{} {}", POWER, t!("network.enable_node"));
View::action_button(ui, enable_text, || { View::action_button(ui, enable_text, || {
Node::start(); Node::start();
}); });
} }
});
}); });
return true });
} else if wallet.sync_error() return true
&& Node::get_sync_status() == Some(SyncStatus::NoSync) { } else if wallet.sync_error()
Self::sync_error_ui(ui, wallet); && Node::get_sync_status() == Some(SyncStatus::NoSync) {
return true; sync_error_ui(ui, wallet);
} else if wallet.get_data().is_none() {
Self::sync_progress_ui(ui, wallet);
return true;
}
} else if wallet.sync_error() {
Self::sync_error_ui(ui, wallet);
return true; return true;
} else if wallet.get_data().is_none() { } else if wallet.get_data().is_none() {
Self::sync_progress_ui(ui, wallet); sync_progress_ui(ui, wallet);
return true; return true;
} }
false } else if wallet.sync_error() {
sync_error_ui(ui, wallet);
return true;
} else if wallet.get_data().is_none() {
sync_progress_ui(ui, wallet);
return true;
} }
false
}
/// Draw wallet sync error content. /// Draw wallet sync error content.
fn sync_error_ui(ui: &mut egui::Ui, wallet: &Wallet) { fn sync_error_ui(ui: &mut egui::Ui, wallet: &Wallet) {
View::center_content(ui, 108.0, |ui| { View::center_content(ui, 108.0, |ui| {
let text = t!("wallets.wallet_loading_err", "settings" => GEAR_FINE); let text = t!("wallets.wallet_loading_err", "settings" => GEAR_FINE);
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text())); ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
ui.add_space(8.0); ui.add_space(8.0);
let retry_text = format!("{} {}", ARROWS_CLOCKWISE, t!("retry")); let retry_text = format!("{} {}", ARROWS_CLOCKWISE, t!("retry"));
View::action_button(ui, retry_text, || { View::action_button(ui, retry_text, || {
wallet.set_sync_error(false); wallet.set_sync_error(false);
});
}); });
} });
}
/// Draw wallet sync progress content. /// Draw wallet sync progress content.
pub fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) { fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) {
View::center_content(ui, 162.0, |ui| { View::center_content(ui, 162.0, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.5, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
View::big_loading_spinner(ui); View::big_loading_spinner(ui);
ui.add_space(18.0); ui.add_space(18.0);
// Setup sync progress text. // Setup sync progress text.
let text = { let text = {
let int_node = wallet.get_current_connection() == ConnectionMethod::Integrated; let int_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let int_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); let int_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
let info_progress = wallet.info_sync_progress(); let info_progress = wallet.info_sync_progress();
if wallet.is_closing() { if wallet.is_closing() {
t!("wallets.wallet_closing") t!("wallets.wallet_closing")
} else if int_node && !int_ready { } else if int_node && !int_ready {
t!("wallets.node_loading", "settings" => GEAR_FINE) t!("wallets.node_loading", "settings" => GEAR_FINE)
} else if wallet.is_repairing() { } else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress(); let repair_progress = wallet.repairing_progress();
if repair_progress == 0 { if repair_progress == 0 {
t!("wallets.wallet_checking") t!("wallets.wallet_checking")
} else {
format!("{}: {}%", t!("wallets.wallet_checking"), repair_progress)
}
} else if info_progress != 100 {
if info_progress == 0 {
t!("wallets.wallet_loading")
} else {
format!("{}: {}%", t!("wallets.wallet_loading"), info_progress)
}
} else { } else {
t!("wallets.tx_loading") format!("{}: {}%", t!("wallets.wallet_checking"), repair_progress)
} }
}; } else if info_progress != 100 {
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text())); if info_progress == 0 {
}); t!("wallets.wallet_loading")
} else {
format!("{}: {}%", t!("wallets.wallet_loading"), info_progress)
}
} else {
t!("wallets.tx_loading")
}
};
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
}); });
} });
} }

View file

@ -14,7 +14,7 @@
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
use egui::{Id, Margin, RichText, ScrollArea}; use egui::{Id, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility; use egui::scroll_area::ScrollBarVisibility;
use grin_core::core::amount_to_hr_string; use grin_core::core::amount_to_hr_string;
use grin_wallet_libwallet::{Error, Slate, SlateState}; use grin_wallet_libwallet::{Error, Slate, SlateState};
@ -23,11 +23,11 @@ use parking_lot::RwLock;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, DOWNLOAD_SIMPLE, SCAN, UPLOAD_SIMPLE}; use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, DOWNLOAD_SIMPLE, SCAN, UPLOAD_SIMPLE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{FilePickButton, Modal, Content, View, CameraScanModal}; use crate::gui::views::{FilePickButton, Modal, View, CameraScanModal};
use crate::gui::views::types::{ModalPosition, QrScanResult}; use crate::gui::views::types::{ModalPosition, QrScanResult};
use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal; use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal;
use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::{WalletContent, WalletTransactionModal}; use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::wallet::types::WalletTransaction; use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet; use crate::wallet::Wallet;
@ -60,10 +60,8 @@ pub struct WalletMessages {
/// Identifier for amount input [`Modal`] to create invoice or sending request. /// Identifier for amount input [`Modal`] to create invoice or sending request.
const REQUEST_MODAL: &'static str = "messages_request_modal"; const REQUEST_MODAL: &'static str = "messages_request_modal";
/// Identifier for [`Modal`] modal to show transaction information. /// Identifier for [`Modal`] modal to show transaction information.
const TX_INFO_MODAL: &'static str = "messages_tx_info_modal"; const TX_INFO_MODAL: &'static str = "messages_tx_info_modal";
/// Identifier for [`Modal`] to scan Slatepack message from QR code. /// Identifier for [`Modal`] to scan Slatepack message from QR code.
const SCAN_QR_MODAL: &'static str = "messages_scan_qr_modal"; const SCAN_QR_MODAL: &'static str = "messages_scan_qr_modal";
@ -73,38 +71,8 @@ impl WalletTab for WalletMessages {
} }
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, wallet) {
return;
}
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb); self.modal_content_ui(ui, wallet, cb);
self.messages_ui(ui, wallet, cb);
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::white_or_black(false),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.id_source(Id::from("wallet_messages").with(wallet.get_config().id))
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.ui(ui, wallet, cb);
});
});
});
});
} }
} }
@ -124,8 +92,8 @@ impl WalletMessages {
} }
} }
/// Draw manual wallet transaction interaction content. /// Draw messages content.
pub fn ui(&mut self, fn messages_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &Wallet, wallet: &Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
@ -136,7 +104,6 @@ impl WalletMessages {
} }
self.first_draw = false; self.first_draw = false;
} }
ui.add_space(3.0); ui.add_space(3.0);
// Show creation of request to send or receive funds. // Show creation of request to send or receive funds.
@ -224,7 +191,10 @@ impl WalletMessages {
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
let send_text = format!("{} {}", UPLOAD_SIMPLE, t!("wallets.send")); let send_text = format!("{} {}", UPLOAD_SIMPLE, t!("wallets.send"));
View::colored_text_button(ui, send_text, Colors::red(), Colors::button(), || { View::colored_text_button(ui,
send_text,
Colors::red(),
Colors::white_or_black(false), || {
self.show_request_modal(false, cb); self.show_request_modal(false, cb);
}); });
}); });
@ -240,7 +210,10 @@ impl WalletMessages {
/// Draw invoice request creation button. /// Draw invoice request creation button.
fn receive_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { fn receive_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let receive_text = format!("{} {}", DOWNLOAD_SIMPLE, t!("wallets.receive")); let receive_text = format!("{} {}", DOWNLOAD_SIMPLE, t!("wallets.receive"));
View::colored_text_button(ui, receive_text, Colors::green(), Colors::button(), || { View::colored_text_button(ui,
receive_text,
Colors::green(),
Colors::white_or_black(false), || {
self.show_request_modal(true, cb); self.show_request_modal(true, cb);
}); });
} }
@ -253,7 +226,10 @@ impl WalletMessages {
} else { } else {
t!("wallets.send") t!("wallets.send")
}; };
Modal::new(REQUEST_MODAL).position(ModalPosition::CenterTop).title(title).show(); Modal::new(REQUEST_MODAL)
.position(ModalPosition::CenterTop)
.title(title)
.show();
cb.show_keyboard(); cb.show_keyboard();
} }
@ -264,7 +240,9 @@ impl WalletMessages {
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
// Setup description text. // Setup description text.
if !self.message_error.is_empty() { if !self.message_error.is_empty() {
ui.label(RichText::new(&self.message_error).size(16.0).color(Colors::red())); ui.label(RichText::new(&self.message_error)
.size(16.0)
.color(Colors::red()));
} else { } else {
ui.label(RichText::new(t!("wallets.input_slatepack_desc")) ui.label(RichText::new(t!("wallets.input_slatepack_desc"))
.size(16.0) .size(16.0)
@ -280,7 +258,7 @@ impl WalletMessages {
let scroll_id = Id::from("message_input_scroll").with(wallet.get_config().id); let scroll_id = Id::from("message_input_scroll").with(wallet.get_config().id);
ScrollArea::vertical() ScrollArea::vertical()
.id_source(scroll_id) .id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0) .max_height(128.0)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
@ -340,11 +318,9 @@ impl WalletMessages {
} }
Err(err) => { Err(err) => {
match err { match err {
// Set already canceled transaction error message.
Error::TransactionWasCancelled {..} => { Error::TransactionWasCancelled {..} => {
self.message_error = t!("wallets.resp_canceled_err"); self.message_error = t!("wallets.resp_canceled_err");
} }
// Set an error when there is not enough funds to pay.
Error::NotEnoughFunds {..} => { Error::NotEnoughFunds {..} => {
let m = t!( let m = t!(
"wallets.pay_balance_error", "wallets.pay_balance_error",
@ -352,15 +328,26 @@ impl WalletMessages {
); );
self.message_error = m; self.message_error = m;
} }
// Set default error message.
_ => { _ => {
let finalize = slate.state == SlateState::Standard2 || // Show tx modal or show default error message.
slate.state == SlateState::Invoice2; if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() {
self.message_error = if finalize { self.message_edit.clear();
t!("wallets.finalize_slatepack_err") self.tx_info_content = Some(
WalletTransactionModal::new(wallet, tx, false)
);
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
.show();
} else { } else {
t!("wallets.resp_slatepack_err") let finalize = slate.state == SlateState::Standard2 ||
}; slate.state == SlateState::Invoice2;
self.message_error = if finalize {
t!("wallets.finalize_slatepack_err")
} else {
t!("wallets.resp_slatepack_err")
};
}
} }
} }
} }
@ -377,7 +364,7 @@ impl WalletMessages {
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
let scan_text = format!("{} {}", SCAN, t!("scan")); let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::button(), || { View::button(ui, scan_text, Colors::white_or_black(false), || {
self.message_edit.clear(); self.message_edit.clear();
self.message_error.clear(); self.message_error.clear();
self.scan_modal_content = Some(CameraScanModal::default()); self.scan_modal_content = Some(CameraScanModal::default());
@ -393,7 +380,7 @@ impl WalletMessages {
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
// Draw button to paste text from clipboard. // Draw button to paste text from clipboard.
let paste = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); let paste = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste, Colors::button(), || { View::button(ui, paste, Colors::white_or_black(false), || {
let buf = cb.get_string_from_buffer(); let buf = cb.get_string_from_buffer();
let previous = self.message_edit.clone(); let previous = self.message_edit.clone();
self.message_edit = buf.clone().trim().to_string(); self.message_edit = buf.clone().trim().to_string();
@ -418,7 +405,7 @@ impl WalletMessages {
} else { } else {
// Draw button to clear message input. // Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear")); let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::button(), || { View::button(ui, clear_text, Colors::white_or_black(false), || {
self.message_edit.clear(); self.message_edit.clear();
self.message_error.clear(); self.message_error.clear();
}); });
@ -455,10 +442,8 @@ impl WalletMessages {
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
.title(t!("wallets.tx")) .title(t!("wallets.tx"))
.show(); .show();
} else { return;
self.message_error = t!("wallets.parse_slatepack_err");
} }
return;
} }
// Create response or finalize at separate thread. // Create response or finalize at separate thread.

View file

@ -130,7 +130,7 @@ impl WalletAccountsModal {
// Show list of accounts. // Show list of accounts.
let size = self.accounts.len(); let size = self.accounts.len();
ScrollArea::vertical() ScrollArea::vertical()
.id_source("account_list_modal_scroll") .id_salt("account_list_modal_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(266.0) .max_height(266.0)
.auto_shrink([true; 2]) .auto_shrink([true; 2])
@ -149,7 +149,7 @@ impl WalletAccountsModal {
}); });
ui.add_space(2.0); ui.add_space(2.0);
View::horizontal_line(ui, Colors::stroke()); View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0); ui.add_space(6.0);
// Setup spacing between buttons. // Setup spacing between buttons.

View file

@ -1,133 +0,0 @@
// Copyright 2024 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::scroll_area::ScrollBarVisibility;
use egui::{Id, ScrollArea};
use crate::gui::Colors;
use crate::gui::icons::COPY;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, View};
use crate::gui::views::types::QrScanResult;
use crate::wallet::Wallet;
/// QR code scan [`Modal`] content.
pub struct WalletScanModal {
/// Camera content for QR scan [`Modal`].
camera_content: Option<CameraContent>,
/// QR code scan result
qr_scan_result: Option<QrScanResult>,
}
impl Default for WalletScanModal {
fn default() -> Self {
Self {
camera_content: None,
qr_scan_result: None,
}
}
}
impl WalletScanModal {
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_result: impl FnMut(&QrScanResult)) {
// Show scan result if exists or show camera content while scanning.
if let Some(result) = &self.qr_scan_result {
let mut result_text = result.text();
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_source(Id::from("qr_scan_result_input").with(wallet.get_config().id))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
egui::TextEdit::multiline(&mut result_text)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(10.0);
// Show copy button.
ui.vertical_centered(|ui| {
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::button(), || {
cb.copy_string_to_buffer(result_text.to_string());
self.qr_scan_result = None;
modal.close();
});
});
ui.add_space(10.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
} else if let Some(result) = self.camera_content.get_or_insert(CameraContent::default())
.qr_scan_result() {
cb.stop_camera();
self.camera_content = None;
on_result(&result);
// Set result and rename modal title.
self.qr_scan_result = Some(result);
Modal::set_title(t!("scan_result"));
} else {
ui.add_space(6.0);
self.camera_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(6.0);
}
if self.qr_scan_result.is_some() {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_scan_result = None;
self.camera_content = None;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
Modal::set_title(t!("scan_qr"));
self.qr_scan_result = None;
self.camera_content = Some(CameraContent::default());
cb.start_camera();
});
});
});
} else {
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
cb.stop_camera();
self.camera_content = None;
modal.close();
});
});
}
ui.add_space(6.0);
}
}

View file

@ -62,7 +62,7 @@ impl Default for CommonSettings {
impl CommonSettings { impl CommonSettings {
/// Draw common wallet settings content. /// Draw common wallet settings content.
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Show modal content for this ui container. // Show modal content for this container.
self.modal_content_ui(ui, wallet, cb); self.modal_content_ui(ui, wallet, cb);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
@ -80,7 +80,7 @@ impl CommonSettings {
// Show wallet name setup. // Show wallet name setup.
let name_text = format!("{} {}", PENCIL, t!("change")); let name_text = format!("{} {}", PENCIL, t!("change"));
View::button(ui, name_text, Colors::button(), || { View::button(ui, name_text, Colors::white_or_black(false), || {
self.name_edit = config.name; self.name_edit = config.name;
// Show wallet name modal. // Show wallet name modal.
Modal::new(NAME_EDIT_MODAL) Modal::new(NAME_EDIT_MODAL)
@ -98,7 +98,7 @@ impl CommonSettings {
// Show wallet password setup. // Show wallet password setup.
let pass_text = format!("{} {}", PASSWORD, t!("change")); let pass_text = format!("{} {}", PASSWORD, t!("change"));
View::button(ui, pass_text, Colors::button(), || { View::button(ui, pass_text, Colors::white_or_black(false), || {
// Setup modal values. // Setup modal values.
self.first_edit_pass_opening = true; self.first_edit_pass_opening = true;
self.old_pass_edit = "".to_string(); self.old_pass_edit = "".to_string();
@ -120,7 +120,7 @@ impl CommonSettings {
// Show minimum amount of confirmations value setup. // Show minimum amount of confirmations value setup.
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations); let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations);
View::button(ui, min_conf_text, Colors::button(), || { View::button(ui, min_conf_text, Colors::white_or_black(false), || {
self.min_confirmations_edit = config.min_confirmations.to_string(); self.min_confirmations_edit = config.min_confirmations.to_string();
// Show minimum amount of confirmations value modal. // Show minimum amount of confirmations value modal.
Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL) Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL)

View file

@ -123,7 +123,7 @@ impl ConnectionSettings {
// Show button to add new external node connection. // Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node")); let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::button(), || { View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.ext_conn_modal = ExternalConnectionModal::new(None); self.ext_conn_modal = ExternalConnectionModal::new(None);
Modal::new(ExternalConnectionModal::WALLET_ID) Modal::new(ExternalConnectionModal::WALLET_ID)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)

View file

@ -12,15 +12,9 @@
// 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 egui::{Id, Margin, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View};
use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings}; use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings};
use crate::gui::views::wallets::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::WalletContent;
use crate::wallet::Wallet; use crate::wallet::Wallet;
/// Wallet settings tab content. /// Wallet settings tab content.
@ -52,42 +46,11 @@ impl WalletTab for WalletSettings {
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &Wallet, wallet: &Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
// Show loading progress if navigation is blocked. // Show common wallet setup.
if WalletContent::block_navigation_on_sync(wallet) { self.common_setup.ui(ui, wallet, cb);
WalletContent::sync_progress_ui(ui, wallet); // Show wallet connections setup.
return; self.conn_setup.wallet_ui(ui, wallet, cb);
} // Show wallet recovery setup.
self.recovery_setup.ui(ui, wallet, cb);
// Show settings content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::white_or_black(false),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.id_source(Id::from("wallet_settings_scroll").with(wallet.get_config().id))
.auto_shrink([false; 2])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show common wallet setup.
self.common_setup.ui(ui, wallet, cb);
// Show wallet connections setup.
self.conn_setup.wallet_ui(ui, wallet, cb);
// Show wallet recovery setup.
self.recovery_setup.ui(ui, wallet, cb);
});
});
});
});
} }
} }

View file

@ -92,9 +92,11 @@ impl RecoverySettings {
ui.add_space(6.0); ui.add_space(6.0);
// Draw button to restore the wallet. // Draw button to restore the wallet.
let recover_text = format!("{} {}", LIFEBUOY, t!("wallets.recover"));
ui.add_space(4.0); ui.add_space(4.0);
View::colored_text_button(ui, recover_text, Colors::green(), Colors::button(), || { View::colored_text_button(ui,
format!("{} {}", LIFEBUOY, t!("wallets.recover")),
Colors::green(),
Colors::white_or_black(false), || {
wallet.delete_db(true); wallet.delete_db(true);
}); });
ui.add_space(6.0); ui.add_space(6.0);
@ -112,7 +114,7 @@ impl RecoverySettings {
// Draw button to show recovery phrase. // Draw button to show recovery phrase.
let show_text = format!("{} {}", EYE, t!("show")); let show_text = format!("{} {}", EYE, t!("show"));
View::button(ui, show_text, Colors::button(), || { View::button(ui, show_text, Colors::white_or_black(false), || {
self.show_recovery_phrase_modal(cb); self.show_recovery_phrase_modal(cb);
}); });
@ -123,8 +125,10 @@ impl RecoverySettings {
ui.add_space(6.0); ui.add_space(6.0);
// Draw button to delete the wallet. // Draw button to delete the wallet.
let delete_text = format!("{} {}", TRASH, t!("wallets.delete")); View::colored_text_button(ui,
View::colored_text_button(ui, delete_text, Colors::red(), Colors::button(), || { format!("{} {}", TRASH, t!("wallets.delete")),
Colors::red(),
Colors::white_or_black(false), || {
Modal::new(DELETE_CONFIRMATION_MODAL) Modal::new(DELETE_CONFIRMATION_MODAL)
.position(ModalPosition::Center) .position(ModalPosition::Center)
.title(t!("confirmation")) .title(t!("confirmation"))

View file

@ -12,18 +12,16 @@
// 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 egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea}; use egui::{Align, Layout, RichText, Rounding};
use egui::scroll_area::ScrollBarVisibility;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE}; use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, QrCodeContent, Content, View}; use crate::gui::views::{Modal, QrCodeContent, View};
use crate::gui::views::types::ModalPosition; use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::wallet::transport::send::TransportSendModal; use crate::gui::views::wallets::wallet::transport::send::TransportSendModal;
use crate::gui::views::wallets::wallet::transport::settings::TransportSettingsModal; use crate::gui::views::wallets::wallet::transport::settings::TransportSettingsModal;
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::WalletContent;
use crate::tor::{Tor, TorConfig}; use crate::tor::{Tor, TorConfig};
use crate::wallet::types::WalletData; use crate::wallet::types::WalletData;
use crate::wallet::Wallet; use crate::wallet::Wallet;
@ -49,39 +47,8 @@ impl WalletTab for WalletTransport {
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &Wallet, wallet: &Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, wallet) {
return;
}
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb); self.modal_content_ui(ui, wallet, cb);
self.transport_ui(ui, wallet, cb);
// Show transport content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::white_or_black(false),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.id_source(Id::from("wallet_transport").with(wallet.get_config().id))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.ui(ui, wallet, cb);
});
});
});
});
} }
} }
@ -106,7 +73,7 @@ impl Default for WalletTransport {
impl WalletTransport { impl WalletTransport {
/// Draw wallet transport content. /// Draw wallet transport content.
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { fn transport_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(3.0); ui.add_space(3.0);
ui.label(RichText::new(t!("transport.desc")) ui.label(RichText::new(t!("transport.desc"))
.size(16.0) .size(16.0)
@ -180,7 +147,7 @@ impl WalletTransport {
// Draw round background. // Draw round background.
let bg_rect = rect.clone(); let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 2, false); let item_rounding = View::item_rounding(0, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke()); ui.painter().rect(bg_rect, item_rounding, Colors::fill_lite(), View::item_stroke());
ui.vertical(|ui| { ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
@ -286,7 +253,7 @@ impl WalletTransport {
} else { } else {
View::item_rounding(1, 2, false) View::item_rounding(1, 2, false)
}; };
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke()); ui.painter().rect(bg_rect, item_rounding, Colors::fill_lite(), View::item_stroke());
ui.vertical(|ui| { ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {

View file

@ -96,6 +96,7 @@ impl TransportSendModal {
/// Draw content to send. /// Draw content to send.
fn content_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, fn content_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code scanner content if requested. // Draw QR code scanner content if requested.
if let Some(scanner) = self.address_scan_content.as_mut() { if let Some(scanner) = self.address_scan_content.as_mut() {
let mut on_stop = || { let mut on_stop = || {
@ -249,7 +250,11 @@ impl TransportSendModal {
} }
/// Draw error content. /// Draw error content.
fn error_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) { fn error_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_send_error")) ui.label(RichText::new(t!("transport.tor_send_error"))
@ -342,7 +347,8 @@ impl TransportSendModal {
let res = self.send_result.read().clone().unwrap(); let res = self.send_result.read().clone().unwrap();
match res { match res {
Ok(tx) => { Ok(tx) => {
self.tx_info_content = Some(WalletTransactionModal::new(wallet, &tx, false)); self.tx_info_content =
Some(WalletTransactionModal::new(wallet, &tx, false));
} }
Err(_) => { Err(_) => {
self.error = true; self.error = true;

View file

@ -12,20 +12,23 @@
// 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::ops::Range;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use egui::{Align, Id, Layout, Margin, Rect, RichText, Rounding, ScrollArea}; use egui::{Align, Id, Layout, Rect, RichText, Rounding, ScrollArea};
use egui::epaint::RectShape;
use egui::scroll_area::ScrollBarVisibility; use egui::scroll_area::ScrollBarVisibility;
use grin_core::consensus::COINBASE_MATURITY;
use grin_core::core::amount_to_hr_string; use grin_core::core::amount_to_hr_string;
use grin_wallet_libwallet::TxLogEntryType; use grin_wallet_libwallet::TxLogEntryType;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE}; use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, PullToRefresh, Content, View}; use crate::gui::views::{Modal, PullToRefresh, Content, View};
use crate::gui::views::types::ModalPosition; use crate::gui::views::types::{LinePosition, ModalPosition};
use crate::gui::views::wallets::types::WalletTab; use crate::gui::views::wallets::types::WalletTab;
use crate::gui::views::wallets::wallet::types::{GRIN, WalletTabType}; use crate::gui::views::wallets::wallet::types::{GRIN, WalletTabType};
use crate::gui::views::wallets::wallet::{WalletContent, WalletTransactionModal}; use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::wallet::types::{WalletData, WalletTransaction}; use crate::wallet::types::{WalletData, WalletTransaction};
use crate::wallet::Wallet; use crate::wallet::Wallet;
@ -57,32 +60,8 @@ impl WalletTab for WalletTransactions {
} }
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, wallet) {
return;
}
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb); self.modal_content_ui(ui, wallet, cb);
self.txs_ui(ui, wallet, cb);
// Show wallet transactions content.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::button(),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 0.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
let data = wallet.get_data().unwrap();
self.txs_ui(ui, wallet, &data, cb);
});
});
} }
} }
@ -91,86 +70,46 @@ const TX_INFO_MODAL: &'static str = "tx_info_modal";
/// Identifier for transaction cancellation confirmation [`Modal`]. /// Identifier for transaction cancellation confirmation [`Modal`].
const CANCEL_TX_CONFIRMATION_MODAL: &'static str = "cancel_tx_conf_modal"; const CANCEL_TX_CONFIRMATION_MODAL: &'static str = "cancel_tx_conf_modal";
impl WalletTransactions { impl WalletTransactions {
/// Height of transaction list item. /// Height of transaction list item.
pub const TX_ITEM_HEIGHT: f32 = 76.0; pub const TX_ITEM_HEIGHT: f32 = 75.0;
/// Draw transactions content. /// Draw transactions content.
fn txs_ui(&mut self, fn txs_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &Wallet, wallet: &Wallet,
data: &WalletData,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
let amount_conf = data.info.amount_awaiting_confirmation; let data = wallet.get_data().unwrap();
let amount_fin = data.info.amount_awaiting_finalization;
let amount_locked = data.info.amount_locked;
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show non-zero awaiting confirmation amount.
if amount_conf != 0 {
let awaiting_conf = amount_to_hr_string(amount_conf, true);
let rounding = if amount_fin != 0 || amount_locked != 0 {
[false, false, false, false]
} else {
[false, false, true, true]
};
View::rounded_box(ui,
format!("{}", awaiting_conf),
t!("wallets.await_conf_amount"),
rounding);
}
// Show non-zero awaiting finalization amount.
if amount_fin != 0 {
let awaiting_conf = amount_to_hr_string(amount_fin, true);
let rounding = if amount_locked != 0 {
[false, false, false, false]
} else {
[false, false, true, true]
};
View::rounded_box(ui,
format!("{}", awaiting_conf),
t!("wallets.await_fin_amount"),
rounding);
}
// Show non-zero locked amount.
if amount_locked != 0 {
let awaiting_conf = amount_to_hr_string(amount_locked, true);
View::rounded_box(ui,
format!("{}", awaiting_conf),
t!("wallets.locked_amount"),
[false, false, true, true]);
}
// Show message when txs are empty.
if let Some(txs) = data.txs.as_ref() {
if txs.is_empty() {
View::center_content(ui, 96.0, |ui| {
let empty_text = t!(
"wallets.txs_empty",
"message" => CHAT_CIRCLE_TEXT,
"transport" => BRIDGE,
"settings" => GEAR_FINE
);
ui.label(RichText::new(empty_text).size(16.0).color(Colors::inactive_text()));
});
return;
}
}
});
// Show loader when txs are not loaded.
if data.txs.is_none() { if data.txs.is_none() {
ui.centered_and_justified(|ui| { ui.centered_and_justified(|ui| {
View::big_loading_spinner(ui); View::big_loading_spinner(ui);
}); });
return; return;
} }
let txs = data.txs.as_ref().unwrap();
let mut awaiting_amount = false;
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show message when txs are empty.
if txs.is_empty() {
View::center_content(ui, 96.0, |ui| {
let empty_text = t!(
"wallets.txs_empty",
"message" => CHAT_CIRCLE_TEXT,
"transport" => BRIDGE,
"settings" => GEAR_FINE
);
ui.label(RichText::new(empty_text)
.size(16.0)
.color(Colors::inactive_text()));
});
return;
}
// Draw awaiting amount info if exists.
awaiting_amount = self.awaiting_info_ui(ui, &data);
});
ui.add_space(4.0); ui.add_space(4.0);
// Show list of transactions. // Show list of transactions.
let txs = data.txs.as_ref().unwrap();
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
let refresh = self.manual_sync.unwrap_or(0) + 1600 > now; let refresh = self.manual_sync.unwrap_or(0) + 1600 > now;
let refresh_resp = PullToRefresh::new(refresh) let refresh_resp = PullToRefresh::new(refresh)
@ -178,57 +117,13 @@ impl WalletTransactions {
.min_refresh_distance(70.0) .min_refresh_distance(70.0)
.scroll_area_ui(ui, |ui| { .scroll_area_ui(ui, |ui| {
ScrollArea::vertical() ScrollArea::vertical()
.id_source(Id::from("txs_content").with(wallet.get_config().id)) .id_salt(Id::from("wallet_tx_list_scroll").with(wallet.get_config().id))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show_rows(ui, Self::TX_ITEM_HEIGHT, txs.len(), |ui, row_range| { .show_rows(ui, Self::TX_ITEM_HEIGHT, txs.len(), |ui, row_range| {
ui.add_space(1.0); ui.add_space(1.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let padding = amount_conf != 0 || amount_fin != 0 || amount_locked != 0; self.tx_list_ui(ui, awaiting_amount, row_range, wallet, txs, cb);
for index in row_range {
let tx = txs.get(index).unwrap();
let mut r = View::item_rounding(index, txs.len(), false);
let mut rect = ui.available_rect_before_wrap();
if padding {
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
}
rect.set_height(Self::TX_ITEM_HEIGHT);
Self::tx_item_ui(ui, tx, rect, r, &data, |ui| {
// Draw button to show transaction info.
if tx.data.tx_slate_id.is_some() {
r.nw = 0.0;
r.sw = 0.0;
View::item_button(ui, r, FILE_TEXT, None, || {
self.show_tx_info_modal(wallet, tx, false);
});
}
let wallet_loaded = wallet.foreign_api_port().is_some();
// Draw button to show transaction finalization.
if wallet_loaded && tx.can_finalize {
let (icon, color) = (CHECK, Some(Colors::green()));
View::item_button(ui, Rounding::default(), icon, color, || {
cb.hide_keyboard();
self.show_tx_info_modal(wallet, tx, true);
});
}
// Draw button to cancel transaction.
if wallet_loaded && tx.can_cancel() {
let (icon, color) = (PROHIBIT, Some(Colors::red()));
View::item_button(ui, Rounding::default(), icon, color, || {
self.confirm_cancel_tx_id = Some(tx.data.id);
// Show transaction cancellation confirmation modal.
Modal::new(CANCEL_TX_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
});
}
});
}
}); });
}) })
}); });
@ -242,6 +137,110 @@ impl WalletTransactions {
} }
} }
/// Draw transaction list content.
fn tx_list_ui(&mut self,
ui: &mut egui::Ui,
awaiting: bool,
row_range: Range<usize>,
wallet: &Wallet,
txs: &Vec<WalletTransaction>,
cb: &dyn PlatformCallbacks) {
for index in row_range {
let mut rect = if awaiting {
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);
rect
} else {
ui.available_rect_before_wrap()
};
rect.set_height(Self::TX_ITEM_HEIGHT);
// Draw tx item background.
let mut r = View::item_rounding(index, txs.len(), false);
let p = ui.painter();
p.rect(rect, r, Colors::fill_lite(), View::item_stroke());
let tx = txs.get(index).unwrap();
let data = wallet.get_data().unwrap();
Self::tx_item_ui(ui, tx, rect, &data, |ui| {
// Draw button to show transaction info.
if tx.data.tx_slate_id.is_some() {
r.nw = 0.0;
r.sw = 0.0;
View::item_button(ui, r, FILE_TEXT, None, || {
self.show_tx_info_modal(wallet, tx, false);
});
}
let wallet_loaded = wallet.foreign_api_port().is_some();
// Draw button to show transaction finalization.
if wallet_loaded && tx.can_finalize {
let (icon, color) = (CHECK, Some(Colors::green()));
View::item_button(ui, Rounding::default(), icon, color, || {
cb.hide_keyboard();
self.show_tx_info_modal(wallet, tx, true);
});
}
// Draw button to cancel transaction.
if wallet_loaded && tx.can_cancel() {
let (icon, color) = (PROHIBIT, Some(Colors::red()));
View::item_button(ui, Rounding::default(), icon, color, || {
self.confirm_cancel_tx_id = Some(tx.data.id);
// Show transaction cancellation confirmation modal.
Modal::new(CANCEL_TX_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
});
}
});
}
}
/// Draw information about locked, finalizing or confirming balance, return `true` if exists.
fn awaiting_info_ui(&mut self, ui: &mut egui::Ui, data: &WalletData) -> bool {
let amount_conf = data.info.amount_awaiting_confirmation;
let amount_fin = data.info.amount_awaiting_finalization;
let amount_locked = data.info.amount_locked;
if amount_conf == 0 && amount_fin == 0 && amount_locked == 0 {
return false;
}
let rect = ui.available_rect_before_wrap();
// Draw background.
let mut bg = RectShape::new(rect, Rounding {
nw: 0.0,
ne: 0.0,
sw: 8.0,
se: 8.0,
}, Colors::TRANSPARENT, View::item_stroke());
let bg_idx = ui.painter().add(bg);
let resp = ui.allocate_ui(rect.size(), |ui| {
ui.vertical_centered_justified(|ui| {
// Correct vertical spacing between items.
ui.style_mut().spacing.item_spacing.y = -3.0;
if amount_conf != 0 {
// Draw awaiting confirmation amount.
awaiting_item_ui(ui, amount_conf, t!("wallets.await_conf_amount"));
}
if amount_fin != 0 {
// Draw awaiting confirmation amount.
awaiting_item_ui(ui, amount_fin, t!("wallets.await_fin_amount"));
}
if amount_locked != 0 {
// Draw locked amount.
awaiting_item_ui(ui, amount_locked, t!("wallets.locked_amount"));
}
});
}).response;
// Setup background size.
bg.rect = resp.rect;
ui.painter().set(bg_idx, bg);
true
}
/// Draw [`Modal`] content for this ui container. /// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self, fn modal_content_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
@ -273,13 +272,8 @@ impl WalletTransactions {
pub fn tx_item_ui(ui: &mut egui::Ui, pub fn tx_item_ui(ui: &mut egui::Ui,
tx: &WalletTransaction, tx: &WalletTransaction,
rect: Rect, rect: Rect,
rounding: Rounding,
data: &WalletData, data: &WalletData,
buttons_ui: impl FnOnce(&mut egui::Ui)) { buttons_ui: impl FnOnce(&mut egui::Ui)) {
// Draw round background.
let bg_rect = rect.clone();
ui.painter().rect(bg_rect, rounding, Colors::TRANSPARENT, View::item_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| {
ui.horizontal_centered(|ui| { ui.horizontal_centered(|ui| {
// Draw buttons. // Draw buttons.
@ -322,6 +316,7 @@ impl WalletTransactions {
ui.add_space(-2.0); ui.add_space(-2.0);
// Setup transaction status text. // Setup transaction status text.
let height = data.info.last_confirmed_height;
let status_text = if !tx.data.confirmed { let status_text = if !tx.data.confirmed {
let is_canceled = tx.data.tx_type == TxLogEntryType::TxSentCancelled let is_canceled = tx.data.tx_type == TxLogEntryType::TxSentCancelled
|| tx.data.tx_type == TxLogEntryType::TxReceivedCancelled; || tx.data.tx_type == TxLogEntryType::TxReceivedCancelled;
@ -344,10 +339,34 @@ impl WalletTransactions {
DOTS_THREE_CIRCLE, DOTS_THREE_CIRCLE,
t!("wallets.tx_sending")) t!("wallets.tx_sending"))
}, },
TxLogEntryType::ConfirmedCoinbase => {
let tx_h = tx.height.unwrap_or(1) - 1;
if tx_h != 0 {
let left_conf = height - tx_h;
if height >= tx_h && left_conf < COINBASE_MATURITY {
let conf_info = format!("{}/{}",
left_conf,
COINBASE_MATURITY);
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
},
_ => { _ => {
format!("{} {}", format!("{} {}",
DOTS_THREE_CIRCLE, DOTS_THREE_CIRCLE,
t!("wallets.tx_confirmed")) t!("wallets.tx_confirming"))
} }
} }
} }
@ -355,10 +374,31 @@ impl WalletTransactions {
} else { } else {
match tx.data.tx_type { match tx.data.tx_type {
TxLogEntryType::ConfirmedCoinbase => { TxLogEntryType::ConfirmedCoinbase => {
format!("{} {}", CHECK_CIRCLE, t!("wallets.tx_confirmed")) let tx_h = tx.height.unwrap_or(1) - 1;
if tx_h != 0 {
let left_conf = height - tx_h;
if height >= tx_h && left_conf < COINBASE_MATURITY {
let conf_info = format!("{}/{}",
left_conf,
COINBASE_MATURITY);
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirmed"))
}
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirmed"))
}
}, },
TxLogEntryType::TxSent | TxLogEntryType::TxReceived => { TxLogEntryType::TxSent | TxLogEntryType::TxReceived => {
let height = data.info.last_confirmed_height;
let min_conf = data.info.minimum_confirmations; let min_conf = data.info.minimum_confirmations;
if tx.height.is_none() || (tx.height.unwrap() != 0 && if tx.height.is_none() || (tx.height.unwrap() != 0 &&
height - tx.height.unwrap() > min_conf - 1) { height - tx.height.unwrap() > min_conf - 1) {
@ -480,4 +520,19 @@ impl WalletTransactions {
ui.add_space(6.0); ui.add_space(6.0);
}); });
} }
}
/// Draw awaiting balance item content.
fn awaiting_item_ui(ui: &mut egui::Ui, amount: u64, label: String) {
let rect = ui.available_rect_before_wrap();
View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke());
ui.add_space(4.0);
let amount_format = amount_to_hr_string(amount, true);
ui.label(RichText::new(format!("{}", amount_format))
.color(Colors::white_or_black(true))
.size(17.0));
ui.label(RichText::new(label)
.color(Colors::gray())
.size(15.0));
ui.add_space(4.0);
} }

View file

@ -53,7 +53,7 @@ pub struct WalletTransactionModal {
qr_code_content: Option<QrCodeContent>, qr_code_content: Option<QrCodeContent>,
/// QR code scanner content. /// QR code scanner content.
qr_scan_content: Option<CameraContent>, scan_qr_content: Option<CameraContent>,
/// Button to parse picked file content. /// Button to parse picked file content.
file_pick_button: FilePickButton, file_pick_button: FilePickButton,
@ -93,7 +93,7 @@ impl WalletTransactionModal {
finalizing: false, finalizing: false,
final_result: Arc::new(RwLock::new(None)), final_result: Arc::new(RwLock::new(None)),
qr_code_content: None, qr_code_content: None,
qr_scan_content: None, scan_qr_content: None,
file_pick_button: FilePickButton::default(), file_pick_button: FilePickButton::default(),
} }
} }
@ -122,78 +122,9 @@ impl WalletTransactionModal {
} }
let tx = txs.get(0).unwrap(); let tx = txs.get(0).unwrap();
if self.qr_code_content.is_none() && self.qr_scan_content.is_none() { // Show transaction information.
ui.add_space(6.0); if self.qr_code_content.is_none() && self.scan_qr_content.is_none() {
self.info_ui(ui, tx, wallet, cb);
let r = View::item_rounding(0, 2, false);
let mut rect = ui.available_rect_before_wrap();
rect.set_height(WalletTransactions::TX_ITEM_HEIGHT);
// Show transaction amount status and time.
WalletTransactions::tx_item_ui(ui, tx, rect, r, &data, |ui| {
if self.finalizing {
return;
}
// Show block height or buttons.
if let Some(h) = tx.height {
if h != 0 {
ui.add_space(6.0);
let height = format!("{} {}", CUBE, h.to_string());
ui.with_layout(Layout::bottom_up(Align::Max), |ui| {
ui.add_space(3.0);
ui.label(RichText::new(height)
.size(15.0)
.color(Colors::text(false)));
});
}
return;
}
let wallet_loaded = wallet.foreign_api_port().is_some();
// Draw button to show transaction finalization or transaction info.
if wallet_loaded && tx.can_finalize {
let (icon, color) = if self.show_finalization {
(FILE_TEXT, None)
} else {
(CHECK, Some(Colors::green()))
};
let mut r = r.clone();
r.nw = 0.0;
r.sw = 0.0;
View::item_button(ui, r, icon, color, || {
cb.hide_keyboard();
if self.show_finalization {
self.show_finalization = false;
return;
}
self.show_finalization = true;
});
}
// Draw button to cancel transaction.
if wallet_loaded && tx.can_cancel() {
View::item_button(ui, Rounding::default(), PROHIBIT, Some(Colors::red()), || {
cb.hide_keyboard();
wallet.cancel(tx.data.id);
});
}
});
// Show identifier.
if let Some(id) = tx.data.tx_slate_id {
let label = format!("{} {}", HASH_STRAIGHT, t!("id"));
Self::info_item_ui(ui, id.to_string(), label, true, cb);
}
// Show kernel.
if let Some(kernel) = tx.data.kernel_excess {
let label = format!("{} {}", FILE_ARCHIVE, t!("kernel"));
Self::info_item_ui(ui, kernel.0.to_hex(), label, true, cb);
}
// Show receiver address.
if let Some(rec) = tx.receiver() {
let label = format!("{} {}", CUBE, t!("network_mining.address"));
Self::info_item_ui(ui, rec.to_string(), label, true, cb);
}
} }
// Show Slatepack message interaction. // Show Slatepack message interaction.
@ -220,21 +151,21 @@ impl WalletTransactionModal {
}); });
}); });
}); });
} else if self.qr_scan_content.is_some() { } else if self.scan_qr_content.is_some() {
ui.add_space(8.0); ui.add_space(8.0);
// Show buttons to close modal or scanner. // Show buttons to close modal or scanner.
ui.columns(2, |cols| { ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| { cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || { View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera(); cb.stop_camera();
self.qr_scan_content = None; self.scan_qr_content = None;
modal.close(); modal.close();
}); });
}); });
cols[1].vertical_centered_justified(|ui| { cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || { View::button(ui, t!("back"), Colors::white_or_black(false), || {
cb.stop_camera(); cb.stop_camera();
self.qr_scan_content = None; self.scan_qr_content = None;
modal.enable_closing(); modal.enable_closing();
}); });
}); });
@ -272,6 +203,7 @@ impl WalletTransactionModal {
if let Ok(_) = res { if let Ok(_) = res {
self.show_finalization = false; self.show_finalization = false;
self.finalize_edit = "".to_string(); self.finalize_edit = "".to_string();
self.response_edit = "".to_string();
} else { } else {
self.finalize_error = true; self.finalize_error = true;
} }
@ -286,44 +218,91 @@ impl WalletTransactionModal {
} }
} }
/// Draw transaction information item content. /// Draw transaction information content.
fn info_item_ui(ui: &mut egui::Ui, fn info_ui(&mut self,
value: String, ui: &mut egui::Ui,
label: String, tx: &WalletTransaction,
copy: bool, wallet: &Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
// Setup layout size. ui.add_space(6.0);
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_height(50.0); rect.set_height(WalletTransactions::TX_ITEM_HEIGHT);
// Draw round background. // Draw tx item background.
let bg_rect = rect.clone(); let p = ui.painter();
let mut rounding = View::item_rounding(1, 3, false); let r = View::item_rounding(0, 2, false);
p.rect(rect, r, Colors::TRANSPARENT, View::item_stroke());
ui.painter().rect(bg_rect, rounding, Colors::fill(), View::item_stroke()); // Show transaction amount status and time.
let data = wallet.get_data().unwrap();
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { WalletTransactions::tx_item_ui(ui, tx, rect, &data, |ui| {
// Draw button to copy transaction info value. if self.finalizing {
if copy { return;
rounding.nw = 0.0; }
rounding.sw = 0.0; // Show block height or buttons.
View::item_button(ui, rounding, COPY, None, || { if let Some(h) = tx.height {
cb.copy_string_to_buffer(value.clone()); if h != 0 {
}); ui.add_space(6.0);
let height = format!("{} {}", CUBE, h.to_string());
ui.with_layout(Layout::bottom_up(Align::Max), |ui| {
ui.add_space(3.0);
ui.label(RichText::new(height)
.size(15.0)
.color(Colors::text(false)));
});
}
return;
} }
// Draw value information. let wallet_loaded = wallet.foreign_api_port().is_some();
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { // Draw button to show transaction finalization or request info.
ui.add_space(6.0); if wallet_loaded && tx.can_finalize {
ui.vertical(|ui| { let (icon, color) = if self.show_finalization {
ui.add_space(3.0); (FILE_TEXT, None)
View::ellipsize_text(ui, value, 15.0, Colors::title(false)); } else {
ui.label(RichText::new(label).size(15.0).color(Colors::gray())); (CHECK, Some(Colors::green()))
ui.add_space(3.0); };
let r = View::item_rounding(0, 2, true);
View::item_button(ui, r, icon, color, || {
cb.hide_keyboard();
if self.show_finalization {
self.show_finalization = false;
return;
}
self.show_finalization = true;
}); });
}); }
// Draw button to cancel transaction.
if wallet_loaded && tx.can_cancel() {
let r = if tx.can_finalize {
Rounding::default()
} else {
View::item_rounding(0, 2, true)
};
View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || {
cb.hide_keyboard();
wallet.cancel(tx.data.id);
});
}
}); });
// Show identifier.
if let Some(id) = tx.data.tx_slate_id {
let label = format!("{} {}", HASH_STRAIGHT, t!("id"));
info_item_ui(ui, id.to_string(), label, true, cb);
}
// Show kernel.
if let Some(kernel) = tx.data.kernel_excess {
let label = format!("{} {}", FILE_ARCHIVE, t!("kernel"));
info_item_ui(ui, kernel.0.to_hex(), label, true, cb);
}
// Show receiver address.
if let Some(rec) = tx.receiver() {
let label = format!("{} {}", CUBE, t!("network_mining.address"));
info_item_ui(ui, rec.to_string(), label, true, cb);
}
} }
/// Draw Slatepack message content. /// Draw Slatepack message content.
@ -336,8 +315,8 @@ impl WalletTransactionModal {
ui.add_space(6.0); ui.add_space(6.0);
// Draw QR code scanner content if requested. // Draw QR code scanner content if requested.
if let Some(qr_scan_content) = self.qr_scan_content.as_mut() { if let Some(scan_content) = self.scan_qr_content.as_mut() {
if let Some(result) = qr_scan_content.qr_scan_result() { if let Some(result) = scan_content.qr_scan_result() {
cb.stop_camera(); cb.stop_camera();
// Setup value to finalization input field. // Setup value to finalization input field.
@ -345,9 +324,9 @@ impl WalletTransactionModal {
self.on_finalization_input_change(tx, wallet, modal, cb); self.on_finalization_input_change(tx, wallet, modal, cb);
modal.enable_closing(); modal.enable_closing();
self.qr_scan_content = None; self.scan_qr_content = None;
} else { } else {
qr_scan_content.ui(ui, cb); scan_content.ui(ui, cb);
} }
return; return;
} }
@ -415,7 +394,7 @@ impl WalletTransactionModal {
View::horizontal_line(ui, Colors::item_stroke()); View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0); ui.add_space(3.0);
ScrollArea::vertical() ScrollArea::vertical()
.id_source(scroll_id) .id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0) .max_height(128.0)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
@ -460,17 +439,17 @@ impl WalletTransactionModal {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
// Draw button to scan Slatepack message QR code. // Draw button to scan Slatepack message QR code.
let qr_text = format!("{} {}", SCAN, t!("scan")); let qr_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, qr_text, Colors::button(), || { View::button(ui, qr_text, Colors::fill_lite(), || {
cb.hide_keyboard(); cb.hide_keyboard();
modal.disable_closing(); modal.disable_closing();
cb.start_camera(); cb.start_camera();
self.qr_scan_content = Some(CameraContent::default()); self.scan_qr_content = Some(CameraContent::default());
}); });
}); });
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
// Draw button to paste data from clipboard. // Draw button to paste data from clipboard.
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::button(), || { View::button(ui, paste_text, Colors::fill_lite(), || {
self.finalize_edit = cb.get_string_from_buffer(); self.finalize_edit = cb.get_string_from_buffer();
}); });
}); });
@ -480,7 +459,7 @@ impl WalletTransactionModal {
if self.finalize_error { if self.finalize_error {
// Draw button to clear message input. // Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear")); let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::button(), || { View::button(ui, clear_text, Colors::fill_lite(), || {
self.finalize_edit.clear(); self.finalize_edit.clear();
self.finalize_error = false; self.finalize_error = false;
}); });
@ -501,7 +480,7 @@ impl WalletTransactionModal {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
// Draw button to show Slatepack message as QR code. // Draw button to show Slatepack message as QR code.
let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); let qr_text = format!("{} {}", QR_CODE, t!("qr_code"));
View::button(ui, qr_text.clone(), Colors::button(), || { View::button(ui, qr_text.clone(), Colors::white_or_black(false), || {
cb.hide_keyboard(); cb.hide_keyboard();
let text = self.response_edit.clone(); let text = self.response_edit.clone();
self.qr_code_content = Some(QrCodeContent::new(text, true)); self.qr_code_content = Some(QrCodeContent::new(text, true));
@ -510,7 +489,7 @@ impl WalletTransactionModal {
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
// Draw copy button. // Draw copy button.
let copy_text = format!("{} {}", COPY, t!("copy")); let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::button(), || { View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.response_edit.clone()); cb.copy_string_to_buffer(self.response_edit.clone());
self.finalize_edit = "".to_string(); self.finalize_edit = "".to_string();
if tx.can_finalize { if tx.can_finalize {
@ -578,4 +557,44 @@ impl WalletTransactionModal {
} }
} }
} }
}
/// Draw transaction information item content.
fn info_item_ui(ui: &mut egui::Ui,
value: String,
label: String,
copy: bool,
cb: &dyn PlatformCallbacks) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(50.0);
// Draw round background.
let bg_rect = rect.clone();
let mut rounding = View::item_rounding(1, 3, false);
ui.painter().rect(bg_rect, rounding, Colors::fill(), View::item_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to copy transaction info value.
if copy {
rounding.nw = 0.0;
rounding.sw = 0.0;
View::item_button(ui, rounding, COPY, None, || {
cb.copy_string_to_buffer(value.clone());
});
}
// Draw value information.
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
View::ellipsize_text(ui, value, 15.0, Colors::title(false));
ui.label(RichText::new(label).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
} }

View file

@ -16,7 +16,7 @@
extern crate rust_i18n; extern crate rust_i18n;
use eframe::NativeOptions; use eframe::NativeOptions;
use egui::{Context, Stroke}; use egui::{Context, Stroke, Theme};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::sync::Arc; use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
@ -77,6 +77,7 @@ fn android_main(app: AndroidApp) {
options.wgpu_options.device_descriptor = std::sync::Arc::new(|_| { options.wgpu_options.device_descriptor = std::sync::Arc::new(|_| {
let base_limits = wgpu::Limits::downlevel_webgl2_defaults(); let base_limits = wgpu::Limits::downlevel_webgl2_defaults();
wgpu::DeviceDescriptor { wgpu::DeviceDescriptor {
memory_hints: wgpu::MemoryHints::default(),
label: Some("egui wgpu device"), label: Some("egui wgpu device"),
required_features: wgpu::Features::default(), required_features: wgpu::Features::default(),
required_limits: wgpu::Limits { required_limits: wgpu::Limits {
@ -102,31 +103,22 @@ fn use_dark_theme(platform: &gui::platform::Android) -> bool {
} }
/// [`App`] setup for [`eframe`]. /// [`App`] setup for [`eframe`].
pub fn app_creator<T: 'static>(app: App<T>) -> eframe::AppCreator pub fn app_creator<T: 'static>(app: App<T>) -> eframe::AppCreator<'static>
where App<T>: eframe::App, T: PlatformCallbacks { where App<T>: eframe::App, T: PlatformCallbacks {
Box::new(|cc| { Box::new(|cc| {
setup_fonts(&cc.egui_ctx);
// Setup images support. // Setup images support.
egui_extras::install_image_loaders(&cc.egui_ctx); egui_extras::install_image_loaders(&cc.egui_ctx);
// Setup visuals.
setup_visuals(&cc.egui_ctx);
// Setup fonts.
setup_fonts(&cc.egui_ctx);
// Return app instance.
Ok(Box::new(app)) Ok(Box::new(app))
}) })
} }
/// Entry point to start ui with [`eframe`]. /// Entry point to start ui with [`eframe`].
pub fn start(mut options: NativeOptions, app_creator: eframe::AppCreator) -> eframe::Result<()> { pub fn start(options: NativeOptions, app_creator: eframe::AppCreator) -> eframe::Result<()> {
options.default_theme = if AppConfig::dark_theme().unwrap_or(false) {
eframe::Theme::Dark
} else {
eframe::Theme::Light
};
// Setup translations. // Setup translations.
setup_i18n(); setup_i18n();
// Start integrated node if needed. // Start integrated node if needed.
if Settings::app_config_to_read().auto_start_node { if AppConfig::autostart_node() {
Node::start(); Node::start();
} }
// Launch graphical interface. // Launch graphical interface.
@ -135,6 +127,10 @@ pub fn start(mut options: NativeOptions, app_creator: eframe::AppCreator) -> efr
/// Setup application [`egui::Style`] and [`egui::Visuals`]. /// Setup application [`egui::Style`] and [`egui::Visuals`].
pub fn setup_visuals(ctx: &Context) { pub fn setup_visuals(ctx: &Context) {
let use_dark = AppConfig::dark_theme().unwrap_or_else(|| {
ctx.system_theme().unwrap_or(Theme::Dark) == Theme::Dark
});
let mut style = (*ctx.style()).clone(); let mut style = (*ctx.style()).clone();
// Setup selection. // Setup selection.
style.interaction.selectable_labels = false; style.interaction.selectable_labels = false;
@ -155,7 +151,6 @@ pub fn setup_visuals(ctx: &Context) {
ctx.set_style(style); ctx.set_style(style);
// Setup visuals based on app color theme. // Setup visuals based on app color theme.
let use_dark = AppConfig::dark_theme().unwrap_or(false);
let mut visuals = if use_dark { let mut visuals = if use_dark {
egui::Visuals::dark() egui::Visuals::dark()
} else { } else {
@ -191,9 +186,9 @@ pub fn setup_fonts(ctx: &Context) {
"../fonts/phosphor.ttf" "../fonts/phosphor.ttf"
)).tweak(egui::FontTweak { )).tweak(egui::FontTweak {
scale: 1.0, scale: 1.0,
y_offset_factor: -0.30, y_offset_factor: -0.20,
y_offset: 0.0, y_offset: 0.0,
baseline_offset_factor: 0.50, baseline_offset_factor: 0.16,
}), }),
); );
fonts fonts

View file

@ -43,25 +43,39 @@ fn real_main() {
// Setup callback on panic crash. // Setup callback on panic crash.
std::panic::set_hook(Box::new(|info| { std::panic::set_hook(Box::new(|info| {
let backtrace = backtrace::Backtrace::new();
// Format error. // Format error.
let backtrace = backtrace::Backtrace::new();
let time = grim::gui::views::View::format_time(chrono::Utc::now().timestamp()); let time = grim::gui::views::View::format_time(chrono::Utc::now().timestamp());
let target = egui::os::OperatingSystem::from_target_os(); let os = egui::os::OperatingSystem::from_target_os();
let ver = grim::VERSION; let ver = grim::VERSION;
let msg = panic_info_message(info); let msg = panic_info_message(info);
let err = format!("{} - {:?} - v{}\n\n{}\n\n{:?}", time, target, ver, msg, backtrace); let loc = if let Some(location) = info.location() {
format!("{}:{}:{}", location.file(), location.line(), location.column())
} else {
"no location found.".parse().unwrap()
};
let err = format!("{} - {:?} - v{}\n{}\n{}\n\n{:?}", time, os, ver, msg, loc, backtrace);
// Save backtrace to file. // Save backtrace to file.
let log = grim::Settings::crash_report_path(); let log = grim::Settings::crash_report_path();
if log.exists() { if log.exists() {
let _ = std::fs::remove_file(log.clone()); use std::io::{Seek, SeekFrom, Write};
let mut file = std::fs::OpenOptions::new()
.write(true)
.append(true)
.open(log)
.unwrap();
if file.seek(SeekFrom::End(0)).is_ok() {
file.write(err.as_bytes()).unwrap_or_default();
}
} else {
std::fs::write(log, err.as_bytes()).unwrap_or_default();
} }
std::fs::write(log, err.as_bytes()).unwrap(); // Print message error.
// Setup flag to show crash after app restart. println!("{}\n{}", msg, loc);
grim::AppConfig::set_show_crash(true);
})); }));
// Start GUI. // Start GUI.
match std::panic::catch_unwind(|| { let _ = std::panic::catch_unwind(|| {
if is_app_running(&data) { if is_app_running(&data) {
return; return;
} else if let Some(data) = data { } else if let Some(data) = data {
@ -70,16 +84,13 @@ fn real_main() {
let platform = grim::gui::platform::Desktop::new(); let platform = grim::gui::platform::Desktop::new();
start_app_socket(platform.clone()); start_app_socket(platform.clone());
start_desktop_gui(platform); start_desktop_gui(platform);
}) { });
Ok(_) => {}
Err(e) => println!("{:?}", e)
}
} }
/// Get panic message from crash payload. /// Get panic message from crash payload.
#[allow(dead_code)] #[allow(dead_code)]
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
fn panic_info_message<'pi>(panic_info: &'pi std::panic::PanicInfo<'_>) -> &'pi str { fn panic_info_message<'pi>(panic_info: &'pi std::panic::PanicHookInfo<'_>) -> &'pi str {
let payload = panic_info.payload(); let payload = panic_info.payload();
// taken from: https://github.com/rust-lang/rust/blob/4b9f4b221b92193c7e95b1beb502c6eb32c3b613/library/std/src/panicking.rs#L194-L200 // taken from: https://github.com/rust-lang/rust/blob/4b9f4b221b92193c7e95b1beb502c6eb32c3b613/library/std/src/panicking.rs#L194-L200
match payload.downcast_ref::<&'static str>() { match payload.downcast_ref::<&'static str>() {
@ -97,24 +108,14 @@ fn panic_info_message<'pi>(panic_info: &'pi std::panic::PanicInfo<'_>) -> &'pi s
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
fn start_desktop_gui(platform: grim::gui::platform::Desktop) { fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
use grim::AppConfig; use grim::AppConfig;
use dark_light::Mode; let os = egui::os::OperatingSystem::from_target_os();
// Setup system theme if not set.
if let None = AppConfig::dark_theme() {
let dark = match dark_light::detect() {
Mode::Dark => true,
Mode::Light => false,
Mode::Default => false
};
AppConfig::set_dark_theme(dark);
}
let (width, height) = AppConfig::window_size(); let (width, height) = AppConfig::window_size();
let mut viewport = egui::ViewportBuilder::default() let mut viewport = egui::ViewportBuilder::default()
.with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT]) .with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT])
.with_inner_size([width, height]); .with_inner_size([width, height]);
// Setup an icon. // Setup icon.
if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) { if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) {
viewport = viewport.with_icon(std::sync::Arc::new(icon)); viewport = viewport.with_icon(std::sync::Arc::new(icon));
} }
@ -123,10 +124,10 @@ fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
viewport = viewport.with_position(egui::pos2(x, y)); viewport = viewport.with_position(egui::pos2(x, y));
} }
// Setup window decorations. // Setup window decorations.
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac; let is_mac = os == egui::os::OperatingSystem::Mac;
viewport = viewport viewport = viewport
.with_window_level(egui::WindowLevel::Normal)
.with_fullsize_content_view(true) .with_fullsize_content_view(true)
.with_window_level(egui::WindowLevel::Normal)
.with_title_shown(false) .with_title_shown(false)
.with_titlebar_buttons_shown(false) .with_titlebar_buttons_shown(false)
.with_titlebar_shown(false) .with_titlebar_shown(false)
@ -138,8 +139,8 @@ fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
..Default::default() ..Default::default()
}; };
// Use Glow renderer for Windows. // Use Glow renderer for Windows.
let win = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Windows; let is_win = os == egui::os::OperatingSystem::Windows;
options.renderer = if win { options.renderer = if is_win {
eframe::Renderer::Glow eframe::Renderer::Glow
} else { } else {
eframe::Renderer::Wgpu eframe::Renderer::Wgpu
@ -150,7 +151,7 @@ fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
match grim::start(options.clone(), grim::app_creator(app)) { match grim::start(options.clone(), grim::app_creator(app)) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
if win { if is_win {
panic!("{}", e); panic!("{}", e);
} }
// Start with another renderer on error. // Start with another renderer on error.
@ -176,19 +177,15 @@ fn is_app_running(data: &Option<String>) -> bool {
let res: Result<(), Box<dyn std::error::Error>> = runtime let res: Result<(), Box<dyn std::error::Error>> = runtime
.block_on(async { .block_on(async {
use interprocess::local_socket::{ use interprocess::local_socket::{
tokio::{prelude::*, Stream}, tokio::{prelude::*, Stream}
GenericFilePath, GenericNamespaced
}; };
use tokio::{ use tokio::{
io::AsyncWriteExt, io::AsyncWriteExt,
}; };
let socket_path = grim::Settings::socket_path(); let socket_path = grim::Settings::socket_path();
let name = if GenericNamespaced::is_supported() { let name = socket_name(&socket_path)?;
grim::Settings::SOCKET_NAME.to_ns_name::<GenericNamespaced>()?
} else {
socket_path.clone().to_fs_name::<GenericFilePath>()?
};
// Connect to running application socket. // Connect to running application socket.
let conn = Stream::connect(name).await?; let conn = Stream::connect(name).await?;
let data = data.clone().unwrap_or("".to_string()); let data = data.clone().unwrap_or("".to_string());
@ -203,7 +200,7 @@ fn is_app_running(data: &Option<String>) -> bool {
drop((rec, sen)); drop((rec, sen));
Ok(()) Ok(())
}); });
return match res { match res {
Ok(_) => true, Ok(_) => true,
Err(_) => false Err(_) => false
} }
@ -220,7 +217,7 @@ fn start_app_socket(platform: grim::gui::platform::Desktop) {
.block_on(async { .block_on(async {
use interprocess::local_socket::{ use interprocess::local_socket::{
tokio::{prelude::*, Stream}, tokio::{prelude::*, Stream},
GenericFilePath, GenericNamespaced, Listener, ListenerOptions, Listener, ListenerOptions,
}; };
use std::io; use std::io;
use tokio::{ use tokio::{
@ -238,15 +235,12 @@ fn start_app_socket(platform: grim::gui::platform::Desktop) {
Ok(buffer) Ok(buffer)
} }
// Setup socket name.
let socket_path = grim::Settings::socket_path(); let socket_path = grim::Settings::socket_path();
let name = if GenericNamespaced::is_supported() {
grim::Settings::SOCKET_NAME.to_ns_name::<GenericNamespaced>()?
} else {
socket_path.clone().to_fs_name::<GenericFilePath>()?
};
if socket_path.exists() { if socket_path.exists() {
let _ = std::fs::remove_file(socket_path); let _ = std::fs::remove_file(&socket_path);
} }
let name = socket_name(&socket_path)?;
// Create listener. // Create listener.
let opts = ListenerOptions::new().name(name); let opts = ListenerOptions::new().name(name);
@ -278,4 +272,18 @@ fn start_app_socket(platform: grim::gui::platform::Desktop) {
} }
}); });
}); });
}
/// Get application socket name from provided path.
#[allow(dead_code)]
#[cfg(not(target_os = "android"))]
fn socket_name(path: &std::path::PathBuf) -> std::io::Result<interprocess::local_socket::Name> {
use interprocess::local_socket::{NameType, ToFsName, ToNsName};
let name = if egui::os::OperatingSystem::Mac != egui::os::OperatingSystem::from_target_os() &&
interprocess::local_socket::GenericNamespaced::is_supported() {
grim::Settings::SOCKET_NAME.to_ns_name::<interprocess::local_socket::GenericNamespaced>()?
} else {
path.clone().to_fs_name::<interprocess::local_socket::GenericFilePath>()?
};
Ok(name)
} }

View file

@ -505,22 +505,29 @@ impl NodeConfig {
} }
/// Get API secret text. /// Get API secret text.
pub fn get_api_secret() -> Option<String> { pub fn get_api_secret(foreign: bool) -> Option<String> {
let r_config = Settings::node_config_to_read(); let r_config = Settings::node_config_to_read();
let api_secret_path = r_config let api_secret_path = if foreign {
.node &r_config
.server .node
.api_secret_path .server
.clone(); .foreign_api_secret_path
return if let Some(secret_path) = api_secret_path {
let api_secret_file = File::open(secret_path).unwrap();
let buf_reader = BufReader::new(api_secret_file);
let mut lines_iter = buf_reader.lines();
let first_line = lines_iter.next().unwrap();
Some(first_line.unwrap())
} else { } else {
None &r_config
}; .node
.server
.api_secret_path
}.clone();
if let Some(secret_path) = api_secret_path {
if let Ok(file) = File::open(secret_path) {
let buf_reader = BufReader::new(file);
let mut lines_iter = buf_reader.lines();
if let Some(Ok(line)) = lines_iter.next() {
return Some(line);
}
}
}
None
} }
/// Save API secret text. /// Save API secret text.
@ -528,25 +535,6 @@ impl NodeConfig {
Self::save_secret(api_secret, API_SECRET_FILE_NAME); Self::save_secret(api_secret, API_SECRET_FILE_NAME);
} }
/// Get Foreign API secret text.
pub fn get_foreign_api_secret() -> Option<String> {
let r_config = Settings::node_config_to_read();
let foreign_secret_path = r_config
.node
.server
.foreign_api_secret_path
.clone();
return if let Some(secret_path) = foreign_secret_path {
let foreign_secret_file = File::open(secret_path).unwrap();
let buf_reader = BufReader::new(foreign_secret_file);
let mut lines_iter = buf_reader.lines();
let first_line = lines_iter.next().unwrap();
Some(first_line.unwrap())
} else {
None
};
}
/// Update Foreign API secret. /// Update Foreign API secret.
pub fn save_foreign_api_secret(api_secret: &String) { pub fn save_foreign_api_secret(api_secret: &String) {
Self::save_secret(api_secret, FOREIGN_API_SECRET_FILE_NAME); Self::save_secret(api_secret, FOREIGN_API_SECRET_FILE_NAME);

View file

@ -15,7 +15,6 @@
//! Build a block to mine: gathers transactions from the pool, assembles //! Build a block to mine: gathers transactions from the pool, assembles
//! them into a block and returns it. //! them into a block and returns it.
use std::panic::panic_any;
use chrono::prelude::{DateTime, Utc}; use chrono::prelude::{DateTime, Utc};
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use serde_json::{json, Value}; use serde_json::{json, Value};
@ -77,7 +76,7 @@ pub fn get_block(
key_id: Option<Identifier>, key_id: Option<Identifier>,
wallet_listener_url: Option<String>, wallet_listener_url: Option<String>,
stop_state: &Arc<StratumStopState> stop_state: &Arc<StratumStopState>
) -> (core::Block, BlockFees) { ) -> Option<(core::Block, BlockFees)> {
let wallet_retry_interval = 5; let wallet_retry_interval = 5;
// get the latest chain state and build a block on top of it // get the latest chain state and build a block on top of it
let mut result = build_block(chain, tx_pool, key_id.clone(), wallet_listener_url.clone()); let mut result = build_block(chain, tx_pool, key_id.clone(), wallet_listener_url.clone());
@ -116,12 +115,11 @@ pub fn get_block(
// Stop attempts to build a block on stop. // Stop attempts to build a block on stop.
if stop_state.is_stopped() { if stop_state.is_stopped() {
panic_any("Stopped"); return None;
} }
result = build_block(chain, tx_pool, new_key_id, wallet_listener_url.clone()); result = build_block(chain, tx_pool, new_key_id, wallet_listener_url.clone());
} }
return result.unwrap(); Some(result.unwrap())
} }
/// Builds a new block with the chain head as previous and eligible /// Builds a new block with the chain head as previous and eligible

View file

@ -627,10 +627,6 @@ fn start_node_server() -> Result<Server, Error> {
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 server_result = Server::new(server_config, None, api_chan); let server_result = Server::new(server_config, None, api_chan);
// Delay after server start.
thread::sleep(Duration::from_millis(5000));
server_result server_result
} }

View file

@ -604,40 +604,43 @@ impl Handler {
let clear_blocks = current_hash != latest_hash; let clear_blocks = current_hash != latest_hash;
// Build the new block (version) // Build the new block (version)
let (new_block, block_fees) = get_block( if let Some((new_block, block_fees)) = get_block(
&self.chain, &self.chain,
tx_pool, tx_pool,
state.current_key_id.clone(), state.current_key_id.clone(),
wallet_listener_url, wallet_listener_url,
&stop_state &stop_state
); ) {
// scaled difficulty
state.current_difficulty =
(new_block.header.total_difficulty() - head.total_difficulty).to_num();
// scaled difficulty state.current_key_id = block_fees.key_id();
state.current_difficulty =
(new_block.header.total_difficulty() - head.total_difficulty).to_num();
state.current_key_id = block_fees.key_id(); current_hash = latest_hash;
// set the minimum acceptable share unscaled difficulty for this block
state.minimum_share_difficulty = config.minimum_share_difficulty;
current_hash = latest_hash; // set a new deadline for rebuilding with fresh transactions
// set the minimum acceptable share unscaled difficulty for this block deadline = Utc::now().timestamp() + config.attempt_time_per_block as i64;
state.minimum_share_difficulty = config.minimum_share_difficulty;
// set a new deadline for rebuilding with fresh transactions // If this is a new block we will clear the current_block version history
deadline = Utc::now().timestamp() + config.attempt_time_per_block as i64; if clear_blocks {
state.current_block_versions.clear();
}
// If this is a new block we will clear the current_block version history // Update the mining stats
if clear_blocks { self.workers.update_block_height(new_block.header.height);
state.current_block_versions.clear(); let difficulty = new_block.header.total_difficulty() - head.total_difficulty;
self.workers.update_network_difficulty(difficulty.to_num());
self.workers.update_network_hashrate();
// Add this new block candidate onto our list of block versions for height
state.current_block_versions.push(new_block);
} else {
thread::sleep(Duration::from_millis(1500));
break;
} }
// Update the mining stats
self.workers.update_block_height(new_block.header.height);
let difficulty = new_block.header.total_difficulty() - head.total_difficulty;
self.workers.update_network_difficulty(difficulty.to_num());
self.workers.update_network_hashrate();
// Add this new block candidate onto our list of block versions for this height
state.current_block_versions.push(new_block);
} }
// Send this job to all connected workers // Send this job to all connected workers
self.broadcast_job(); self.broadcast_job();
@ -716,7 +719,7 @@ fn accept_connections(listen_addr: SocketAddr,
let mut rt = Runtime::new().unwrap(); let mut rt = Runtime::new().unwrap();
let (task, handle) = abortable(task); let (task, handle) = abortable(task);
rt.spawn(check_stop_state(stop_state, handle)); rt.spawn(check_stop_state(stop_state, handle));
rt.block_on(task).unwrap(); rt.block_on(task).unwrap_or_default();
} }
async fn check_stop_state(stop_state: Arc<StratumStopState>, handle: AbortHandle) { async fn check_stop_state(stop_state: Arc<StratumStopState>, handle: AbortHandle) {

View file

@ -49,9 +49,6 @@ pub struct AppConfig {
/// Flag to check if dark theme should be used, use system settings if not set. /// Flag to check if dark theme should be used, use system settings if not set.
use_dark_theme: Option<bool>, use_dark_theme: Option<bool>,
/// Flag to show crash report when happened.
show_crash: Option<bool>
} }
impl Default for AppConfig { impl Default for AppConfig {
@ -68,7 +65,6 @@ impl Default for AppConfig {
y: None, y: None,
lang: None, lang: None,
use_dark_theme: None, use_dark_theme: None,
show_crash: None,
} }
} }
} }
@ -241,17 +237,4 @@ impl AppConfig {
w_config.use_dark_theme = Some(use_dark); w_config.use_dark_theme = Some(use_dark);
w_config.save(); w_config.save();
} }
/// Check if crash report should be shown on application start.
pub fn show_crash() -> bool {
let r_config = Settings::app_config_to_read();
r_config.show_crash.unwrap_or(false)
}
/// Setup flag to show crash report on application start.
pub fn set_show_crash(show: bool) {
let mut w_config = Settings::app_config_to_update();
w_config.show_crash = Some(show);
w_config.save();
}
} }

View file

@ -20,7 +20,6 @@ use lazy_static::lazy_static;
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use grin_config::ConfigError; use grin_config::ConfigError;
use crate::node::NodeConfig; use crate::node::NodeConfig;
@ -127,10 +126,7 @@ impl Settings {
/// Get base directory path for configuration. /// Get base directory path for configuration.
pub fn base_path(sub_dir: Option<String>) -> PathBuf { pub fn base_path(sub_dir: Option<String>) -> PathBuf {
// Check if dir exists. // Check if dir exists.
let mut path = match dirs::home_dir() { let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::new());
Some(p) => p,
None => PathBuf::new(),
};
path.push(Self::MAIN_DIR_NAME); path.push(Self::MAIN_DIR_NAME);
if sub_dir.is_some() { if sub_dir.is_some() {
path.push(sub_dir.unwrap()); path.push(sub_dir.unwrap());
@ -149,20 +145,28 @@ impl Settings {
socket_path socket_path
} }
/// Get configuration file path from provided name and sub-directory if needed. /// Get configuration file path from provided name and subdirectory if needed.
pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf { pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
let mut path = Self::base_path(sub_dir); let mut path = Self::base_path(sub_dir);
path.push(config_name); path.push(config_name);
path path
} }
/// Get configuration file path from provided name and sub-directory if needed. /// Get configuration file path from provided name and subdirectory if needed.
pub fn crash_report_path() -> PathBuf { pub fn crash_report_path() -> PathBuf {
let mut path = Self::base_path(None); let mut path = Self::base_path(None);
path.push(Self::CRASH_REPORT_FILE_NAME); path.push(Self::CRASH_REPORT_FILE_NAME);
path path
} }
/// Delete crash report file.
pub fn delete_crash_report() {
let log = Self::crash_report_path();
if log.exists() {
let _ = fs::remove_file(log.clone());
}
}
/// Read configuration from the file. /// Read configuration from the file.
pub fn read_from_file<T: DeserializeOwned>(config_path: PathBuf) -> Result<T, ConfigError> { pub fn read_from_file<T: DeserializeOwned>(config_path: PathBuf) -> Result<T, ConfigError> {
let file_content = fs::read_to_string(config_path.clone())?; let file_content = fs::read_to_string(config_path.clone())?;

258
src/tor/http.rs Normal file
View file

@ -0,0 +1,258 @@
// Copyright 2024 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::future::Future;
use std::io::Error;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use arti_client::{DataStream, IntoTorAddr, TorClient};
use hyper::client::connect::{Connected, Connection};
use hyper::http::uri::Scheme;
use hyper::http::Uri;
use hyper::service::Service;
use pin_project::pin_project;
use thiserror::Error;
use tls_api::TlsConnector as TlsConn; // This is different from tor_rtcompat::TlsConnector
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tor_config::deps::educe::Educe;
use tor_rtcompat::Runtime;
/// Error making or using http connection
///
/// This error ends up being passed to hyper and bundled up into a [`hyper::Error`]
#[derive(Error, Clone, Debug)]
#[non_exhaustive]
pub enum ConnectionError {
/// Unsupported URI scheme
#[error("unsupported URI scheme in {uri:?}")]
UnsupportedUriScheme {
/// URI
uri: Uri,
},
/// Missing hostname
#[error("Missing hostname in {uri:?}")]
MissingHostname {
/// URI
uri: Uri,
},
/// Tor connection failed
#[error("Tor connection failed")]
Arti(#[from] arti_client::Error),
/// TLS connection failed
#[error("TLS connection failed")]
TLS(#[source] Arc<anyhow::Error>),
}
/// We implement this for form's sake
impl tor_error::HasKind for ConnectionError {
#[rustfmt::skip]
fn kind(&self) -> tor_error::ErrorKind {
use ConnectionError as CE;
use tor_error::ErrorKind as EK;
match self {
CE::UnsupportedUriScheme{..} => EK::NotImplemented,
CE::MissingHostname{..} => EK::BadApiUsage,
CE::Arti(e) => e.kind(),
CE::TLS(_) => EK::RemoteProtocolViolation,
}
}
}
/// **Main entrypoint**: `hyper` connector to make HTTP\[S] connections via Tor, using Arti.
///
/// An `ArtiHttpConnector` combines an Arti Tor client, and a TLS implementation,
/// in a form that can be provided to hyper
/// (e.g. to [`hyper::client::Builder`]'s `build` method)
/// so that hyper can speak HTTP and HTTPS to origin servers via Tor.
///
/// TC is the TLS to used *across* Tor to connect to the origin server.
/// For example, it could be a [`tls_api_native_tls::TlsConnector`].
/// This is a different Rust type to the TLS used *by* Tor to connect to relays etc.
/// It might even be a different underlying TLS implementation
/// (although that is usually not a particularly good idea).
#[derive(Educe)]
#[educe(Clone)] // #[derive(Debug)] infers an unwanted bound TC: Clone
pub struct ArtiHttpConnector<R: Runtime, TC: TlsConn> {
/// The client
client: TorClient<R>,
/// TLS for using across Tor.
tls_conn: Arc<TC>,
}
// #[derive(Clone)] infers a TC: Clone bound
impl<R: Runtime, TC: TlsConn> ArtiHttpConnector<R, TC> {
/// Make a new `ArtiHttpConnector` using an Arti `TorClient` object.
pub fn new(client: TorClient<R>, tls_conn: TC) -> Self {
let tls_conn = tls_conn.into();
Self { client, tls_conn }
}
}
/// Wrapper type that makes an Arti `DataStream` implement necessary traits to be used as
/// a `hyper` connection object (mainly `Connection`).
///
/// This might represent a bare HTTP connection across Tor,
/// or it might represent an HTTPS connection through Tor to an origin server,
/// `TC::TlsStream` as the TLS layer.
///
/// An `ArtiHttpConnection` is constructed by hyper's use of the [`ArtiHttpConnector`]
/// implementation of [`hyper::service::Service`],
/// and then used by hyper as the transport for hyper's HTTP implementation.
#[pin_project]
pub struct ArtiHttpConnection<TC: TlsConn> {
/// The stream
#[pin]
inner: MaybeHttpsStream<TC>,
}
/// The actual stream; might be TLS, might not
#[pin_project(project = MaybeHttpsStreamProj)]
enum MaybeHttpsStream<TC: TlsConn> {
/// http
Http(Pin<Box<DataStream>>), // Tc:TlsStream is generally boxed; box this one too
/// https
Https(#[pin] TC::TlsStream),
}
impl<TC: TlsConn> Connection for ArtiHttpConnection<TC> {
fn connected(&self) -> Connected {
Connected::new()
}
}
// These trait implementations just defer to the inner `DataStream`; the wrapper type is just
// there to implement the `Connection` trait.
impl<TC: TlsConn> AsyncRead for ArtiHttpConnection<TC> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<Result<(), std::io::Error>> {
match self.project().inner.project() {
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_read(cx, buf),
MaybeHttpsStreamProj::Https(t) => t.poll_read(cx, buf),
}
}
}
impl<TC: TlsConn> AsyncWrite for ArtiHttpConnection<TC> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, Error>> {
match self.project().inner.project() {
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_write(cx, buf),
MaybeHttpsStreamProj::Https(t) => t.poll_write(cx, buf),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
match self.project().inner.project() {
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_flush(cx),
MaybeHttpsStreamProj::Https(t) => t.poll_flush(cx),
}
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
match self.project().inner.project() {
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_shutdown(cx),
MaybeHttpsStreamProj::Https(t) => t.poll_shutdown(cx),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
/// Are we doing TLS?
enum UseTls {
/// No
Bare,
/// Yes
Tls,
}
/// Convert uri to http\[s\] host and port, and whether to do tls
fn uri_to_host_port_tls(uri: Uri) -> Result<(String, u16, UseTls), ConnectionError> {
let use_tls = {
// Scheme doesn't derive PartialEq so can't be matched on
let scheme = uri.scheme();
if scheme == Some(&Scheme::HTTP) {
UseTls::Bare
} else if scheme == Some(&Scheme::HTTPS) {
UseTls::Tls
} else {
return Err(ConnectionError::UnsupportedUriScheme { uri });
}
};
let host = match uri.host() {
Some(h) => h,
_ => return Err(ConnectionError::MissingHostname { uri }),
};
let port = uri.port().map(|x| x.as_u16()).unwrap_or(match use_tls {
UseTls::Tls => 443,
UseTls::Bare => 80,
});
Ok((host.to_owned(), port, use_tls))
}
impl<R: Runtime, TC: TlsConn> Service<Uri> for ArtiHttpConnector<R, TC> {
type Response = ArtiHttpConnection<TC>;
type Error = ConnectionError;
#[allow(clippy::type_complexity)]
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Uri) -> Self::Future {
// `TorClient` objects can be cloned cheaply (the cloned objects refer to the same
// underlying handles required to make Tor connections internally).
// We use this to avoid the returned future having to borrow `self`.
let client = self.client.clone();
let tls_conn = self.tls_conn.clone();
Box::pin(async move {
// Extract the host and port to connect to from the URI.
let (host, port, use_tls) = uri_to_host_port_tls(req)?;
// Initiate a new Tor connection, producing a `DataStream` if successful.
let addr = (&host as &str, port)
.into_tor_addr()
.map_err(arti_client::Error::from)?;
let ds = client.connect(addr).await?;
let inner = match use_tls {
UseTls::Tls => {
let conn = tls_conn
.connect_impl_tls_stream(&host, ds)
.await
.map_err(|e| ConnectionError::TLS(e.into()))?;
MaybeHttpsStream::Https(conn)
}
UseTls::Bare => MaybeHttpsStream::Http(Box::new(ds).into()),
};
Ok(ArtiHttpConnection { inner })
})
}
}

View file

@ -19,4 +19,6 @@ mod tor;
pub use tor::Tor; pub use tor::Tor;
mod types; mod types;
pub use types::*; pub use types::*;
mod http;

View file

@ -24,7 +24,6 @@ use std::time::Duration;
use arti_client::config::{CfgPath, TorClientConfigBuilder}; use arti_client::config::{CfgPath, TorClientConfigBuilder};
use arti_client::{TorClient, TorClientConfig}; use arti_client::{TorClient, TorClientConfig};
use arti_hyper::ArtiHttpConnector;
use curve25519_dalek::digest::Digest; use curve25519_dalek::digest::Digest;
use ed25519_dalek::hazmat::ExpandedSecretKey; use ed25519_dalek::hazmat::ExpandedSecretKey;
use fs_mistrust::Mistrust; use fs_mistrust::Mistrust;
@ -32,6 +31,7 @@ use grin_util::secp::SecretKey;
use hyper::{Body, Uri}; use hyper::{Body, Uri};
use parking_lot::RwLock; use parking_lot::RwLock;
use sha2::Sha512; use sha2::Sha512;
use tls_api_native_tls::TlsConnector;
use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder}; use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder};
use tokio::time::sleep; use tokio::time::sleep;
use tor_hscrypto::pk::{HsIdKey, HsIdKeypair}; use tor_hscrypto::pk::{HsIdKey, HsIdKeypair};
@ -48,15 +48,7 @@ use tor_llcrypto::pk::ed25519::ExpandedKeypair;
use tor_rtcompat::tokio::TokioNativeTlsRuntime; use tor_rtcompat::tokio::TokioNativeTlsRuntime;
use tor_rtcompat::Runtime; use tor_rtcompat::Runtime;
// On aarch64-apple-darwin targets there is an issue with the native and rustls use crate::tor::http::ArtiHttpConnector;
// tls implementation so this makes it fall back to the openssl variant.
//
// https://gitlab.torproject.org/tpo/core/arti/-/issues/715
#[cfg(not(all(target_vendor = "apple", target_arch = "aarch64")))]
use tls_api_native_tls::TlsConnector;
#[cfg(all(target_vendor = "apple", target_arch = "aarch64"))]
use tls_api_openssl::TlsConnector;
use crate::tor::TorConfig; use crate::tor::TorConfig;
lazy_static! { lazy_static! {
@ -81,20 +73,16 @@ pub struct Tor {
impl Default for Tor { impl Default for Tor {
fn default() -> Self { fn default() -> Self {
// Cleanup keys, state and cache on start.
fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default();
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default();
// Create Tor client.
let runtime = TokioNativeTlsRuntime::create().unwrap(); let runtime = TokioNativeTlsRuntime::create().unwrap();
let config = Self::build_config(); let config = Self::build_config();
let client = if let Ok(c) = TorClient::with_runtime(runtime) let client = TorClient::with_runtime(runtime)
.config(config.clone()) .config(config.clone())
.create_unbootstrapped() { .create_unbootstrapped().unwrap();
c
} else {
fs::remove_dir_all(TorConfig::state_path()).unwrap();
fs::remove_dir_all(TorConfig::cache_path()).unwrap();
let runtime = TokioNativeTlsRuntime::create().unwrap();
TorClient::with_runtime(runtime)
.config(config.clone())
.create_unbootstrapped().unwrap()
};
Self { Self {
running_services: Arc::new(RwLock::new(BTreeMap::new())), running_services: Arc::new(RwLock::new(BTreeMap::new())),
starting_services: Arc::new(RwLock::new(BTreeSet::new())), starting_services: Arc::new(RwLock::new(BTreeSet::new())),
@ -314,8 +302,8 @@ impl Tor {
let mut w_services = let mut w_services =
TOR_SERVER_STATE.starting_services.write(); TOR_SERVER_STATE.starting_services.write();
w_services.remove(&service_id); w_services.remove(&service_id);
// Check again after 15 seconds. // Check again after 50 seconds.
Duration::from_millis(15000) Duration::from_millis(50000)
} }
Err(_) => { Err(_) => {
// Restart service on 3rd error. // Restart service on 3rd error.
@ -409,10 +397,10 @@ impl Tor {
hs_nickname: &HsNickname, hs_nickname: &HsNickname,
) -> tor_keymgr::Result<()> { ) -> tor_keymgr::Result<()> {
let arti_store = let arti_store =
ArtiNativeKeystore::from_path_and_mistrust(TorConfig::keystore_path(), &mistrust)?; ArtiNativeKeystore::from_path_and_mistrust(TorConfig::keystore_path(), mistrust)?;
let key_manager = KeyMgrBuilder::default() let key_manager = KeyMgrBuilder::default()
.default_store(Box::new(arti_store)) .primary_store(Box::new(arti_store))
.build() .build()
.unwrap(); .unwrap();
@ -427,13 +415,15 @@ impl Tor {
key_manager.insert( key_manager.insert(
HsIdKey::from(expanded_kp.public().clone()), HsIdKey::from(expanded_kp.public().clone()),
&HsIdPublicKeySpecifier::new(hs_nickname.clone()), &HsIdPublicKeySpecifier::new(hs_nickname.clone()),
KeystoreSelector::Default, KeystoreSelector::Primary,
true
)?; )?;
key_manager.insert( key_manager.insert(
HsIdKeypair::from(expanded_kp), HsIdKeypair::from(expanded_kp),
&HsIdKeypairSpecifier::new(hs_nickname.clone()), &HsIdKeypairSpecifier::new(hs_nickname.clone()),
KeystoreSelector::Default, KeystoreSelector::Primary,
true
)?; )?;
Ok(()) Ok(())
} }
@ -444,7 +434,7 @@ impl Tor {
builder.bridges().bridges().push(bridge); builder.bridges().bridges().push(bridge);
} }
// Now configure an snowflake transport. (Requires the "pt-client" feature) // Now configure a snowflake transport. (Requires the "pt-client" feature)
let mut transport = TransportConfigBuilder::default(); let mut transport = TransportConfigBuilder::default();
transport transport
.protocols(vec!["snowflake".parse().unwrap()]) .protocols(vec!["snowflake".parse().unwrap()])

View file

@ -80,7 +80,7 @@ impl WalletConfig {
}, },
min_confirmations: MIN_CONFIRMATIONS_DEFAULT, min_confirmations: MIN_CONFIRMATIONS_DEFAULT,
use_dandelion: Some(true), use_dandelion: Some(true),
enable_tor_listener: Some(true), enable_tor_listener: Some(false),
api_port: Some(rand::thread_rng().gen_range(10000..30000)), api_port: Some(rand::thread_rng().gen_range(10000..30000)),
}; };
Settings::write_to_file(&config, config_path); Settings::write_to_file(&config, config_path);

View file

@ -171,7 +171,7 @@ impl Wallet {
// Setup node client. // Setup node client.
let integrated = || { let integrated = || {
let api_url = format!("http://{}", NodeConfig::get_api_address()); let api_url = format!("http://{}", NodeConfig::get_api_address());
let api_secret = NodeConfig::get_foreign_api_secret(); let api_secret = NodeConfig::get_api_secret(true);
(api_url, api_secret) (api_url, api_secret)
}; };
let (node_api_url, node_secret) = if let Some(id) = config.ext_conn_id { let (node_api_url, node_secret) = if let Some(id) = config.ext_conn_id {
@ -617,7 +617,7 @@ impl Wallet {
let r_inst = self.instance.as_ref().read(); let r_inst = self.instance.as_ref().read();
let instance = r_inst.clone().unwrap(); let instance = r_inst.clone().unwrap();
let mut api = Owner::new(instance, None); let mut api = Owner::new(instance, None);
return match parse_slatepack(&mut api, None, None, Some(text.clone())) { match parse_slatepack(&mut api, None, None, Some(text.clone())) {
Ok(s) => Ok(s.0), Ok(s) => Ok(s.0),
Err(e) => Err(e) Err(e) => Err(e)
} }
@ -714,7 +714,7 @@ impl Wallet {
amount, amount,
minimum_confirmations: config.min_confirmations, minimum_confirmations: config.min_confirmations,
num_change_outputs: 1, num_change_outputs: 1,
selection_strategy_is_use_all: true, selection_strategy_is_use_all: false,
..Default::default() ..Default::default()
}; };
let r_inst = self.instance.as_ref().read(); let r_inst = self.instance.as_ref().read();
@ -857,7 +857,7 @@ impl Wallet {
src_acct_name: None, src_acct_name: None,
amount: slate.amount, amount: slate.amount,
minimum_confirmations: config.min_confirmations, minimum_confirmations: config.min_confirmations,
selection_strategy_is_use_all: true, selection_strategy_is_use_all: false,
..Default::default() ..Default::default()
}; };
let r_inst = self.instance.as_ref().read(); let r_inst = self.instance.as_ref().read();