75 Commits

Author SHA1 Message Date
ardocrat ee8841590a ci: recursive submodules 2026-06-23 15:42:43 +03:00
ardocrat 20db758bc2 wallet: update to last version, removing separate node module, added ability to finalize tx over tor 2026-06-23 15:35:17 +03:00
ardocrat 3981ebe3ed fix: repost pay tx over tor, do not try to send if tor is starting 2026-06-22 20:05:51 +03:00
ardocrat 161faba9a0 Merge remote-tracking branch 'refs/remotes/jwinterm/feat/auto-tor-reply-on-invoice-scan' 2026-06-22 18:44:01 +03:00
jwinterm 53bc6d3e3e wallet: auto-reply over Tor when scanning an Invoice1 slatepack
Mirrors the existing Standard1 + Send-task flow for the invoice
direction. When a customer scans an Invoice1 slatepack QR with an
embedded sender address (now standard for slatepacks produced by
grin-wallet's issue_invoice_tx), the patched pay() forwards the
sender address through create_slatepack_message, and the
OpenMessage handler — if the wallet's Tor service is running or
starting — pushes the signed Invoice2 to the sender's foreign-api
finalize_tx over Tor. The merchant's wallet finalizes + posts on
their side, so no local finalize/post is needed (cf. the existing
send_tor closure for Standard1 which does need that).

Backward-compatible: if the slatepack has no embedded sender
address (older clients) or the Tor service isn't up, the existing
write-slatepack-to-disk-for-paste-back fallback runs unchanged.
No protocol change, no new dependencies, no new failure modes —
the response slatepack file is always written before the Tor send
is attempted, so a Tor failure mid-flight is recoverable.

Closes the mobile-UX gap that required the customer to manually
copy the response slatepack from the wallet and paste it back to
the merchant's web/storefront interface. With this patch and a
foreign-api listener on the merchant side, scanning a Grin invoice
QR is now a single tap: scan → confirm → done.

send_tor() gains a `finalize: bool` parameter that selects between
the existing receive_tx body (for Standard1 sends) and a new
finalize_tx body (for the invoice-flow case). The same Tor SOCKS
plumbing handles both.

Real-world validation: end-to-end working today on a production
BTCPay deployment (Such Software's btcpayserver-plugin-grin v1.3.5)
— invoice QR scan with a patched build settles a merchant invoice
in ~1 confirmation window with zero customer interaction beyond
the scan.
2026-06-22 11:29:48 -04:00
ardocrat 8524084c47 wallet: ability to specify address for invoice to encrypt slatepack message 2026-06-20 15:12:43 +03:00
ardocrat a91d9016a8 node: ability to launch API, P2P and Stratum at all interfaces with IPv6 support 2026-06-19 14:46:49 +03:00
ardocrat 726a51bd0e tor: update to arti 0.43, do not store secret key, use new hyper to send requests 2026-06-15 14:53:52 +03:00
ardocrat 60d8dc7555 node + wallet: update to latest versions 2026-06-11 10:53:48 +03:00
ardocrat b51a46b943 build: update node and wallet to latest versions 2026-06-04 18:06:32 +03:00
ardocrat 3d1a721f29 node: optimize iterator 2026-05-31 15:57:22 +03:00
ardocrat 176df6f93e wallet: fix tx repost, delete txs with 0 amount, trim message to parse 2026-05-25 16:44:51 +03:00
ardocrat f7287bd9ad tor: create runtime only once 2026-05-25 16:32:13 +03:00
ardocrat 4c4b6cd5dc ui: better check for wallet data emptiness 2026-05-23 18:21:04 +03:00
ardocrat 4aeda9c9dc build: v0.3.6, format code 2026-05-21 00:56:28 +03:00
ardocrat a4eadebef2 wallet: handle iter result 2026-05-21 00:54:02 +03:00
ardocrat 15d1aa1a21 build: git format hook 2026-05-21 00:04:22 +03:00
ardocrat 242a3b9434 node: optimize lmdb iterator, pibd peers fix to use .zip fallback 2026-05-20 20:15:46 +03:00
ardocrat edc1a09b2c tor: remove delay after connection, immediately show service as started after bootstrap, remove unused features 2026-05-20 18:14:06 +03:00
ardocrat f31953f455 fix: recreate send/receive/message modals content on open 2026-05-20 17:43:46 +03:00
ardocrat d573ddedca tor: update to 0.42, add arti to logger 2026-05-18 22:53:11 +03:00
ardocrat 3be6925ff8 node: store blocked peers at memory, rust edition 2021 2026-05-14 23:01:37 +03:00
ardocrat c7abd9cbfa wallet: bigger scan window, include last height into scan batch 2026-05-14 21:40:57 +03:00
ardocrat 512d216fee log: filter, debug by default 2026-05-14 21:39:48 +03:00
ardocrat eaefc58c5a wallet: fix scan 2026-05-13 17:38:07 +03:00
ardocrat 2519e68dd5 ci: build checkout submodules 2026-05-13 17:37:45 +03:00
ardocrat 73c0884f95 ci: macos runner 2026-05-08 00:10:14 +03:00
ardocrat f7b2150228 wallet: parse slatepack message at background thread 2026-05-04 14:10:52 +03:00
ardocrat 03924b5300 build: remove unused cpp flags for android 2026-05-04 01:31:35 +03:00
ardocrat a479189135 tx: show message input after copy/share if finalization is needed 2026-05-03 23:34:45 +03:00
ardocrat e691a7b02d ci: fix changelog, update wix upgradecode on every build 2026-05-03 23:05:26 +03:00
ardocrat f2b79cd70d build: add rustfmt hook and config 2026-05-03 10:05:03 +03:00
ardocrat f20d1ee2c2 node: use git hash for user agent 2026-05-02 23:34:42 +03:00
ardocrat 534c4cc86a node: update user-agent, include last fixes for peers from PRs 2026-05-01 12:50:50 +03:00
ardocrat 558ac034b2 txs: fix save with new lmdb, sort to show new on top 2026-05-01 11:42:35 +03:00
ardocrat 13bf8e830c node: reset data from settings 2026-05-01 02:18:47 +03:00
ardocrat 57f319edfc android: include application mime type 2026-04-30 20:36:42 +03:00
ardocrat 748aebffb6 ci: release runner for version and forgejo release 2026-04-30 20:16:12 +03:00
ardocrat b32085a423 node + wallet: update lmdb 2026-04-30 18:27:27 +03:00
ardocrat a9c65546e3 node: update default dns seeds, setup seeds for testnet on launch 2026-04-30 14:14:51 +03:00
ardocrat b94241b82a wallet: select external connection by default on creation if integrated node is not running 2026-04-30 13:53:44 +03:00
ardocrat 8a1a69b739 node: update seeds 2026-04-24 01:14:16 +03:00
ardocrat 01d17e25ee tor: update webtunnel list 2026-04-24 00:12:22 +03:00
ardocrat cab38097fa build: v0.3.5 2026-04-21 13:13:28 +03:00
ardocrat 0026fc3717 build: fix android no_mangle attributes for rust 2024 2026-04-11 23:12:56 +03:00
ardocrat 0fd04f14a4 wallet: save last scanned block info to save progress on scan interruption 2026-04-11 22:52:43 +03:00
ardocrat 3338f51de5 tor: update to arti 0.41 2026-04-11 00:33:41 +03:00
ardocrat 0fa8963bd2 fix: wallet txs selection, wait starting tor service on send 2026-04-10 15:50:58 +03:00
ardocrat 70bba5d7ce pull_to_refresh: refresh when dragged far enough without release 2026-04-10 15:38:43 +03:00
ardocrat 0bb43e1e5d ui: show loader when fee is calculating 2026-04-10 15:18:23 +03:00
ardocrat fd52757549 build: version 0.3.4 2026-04-10 15:09:41 +03:00
ardocrat 6835bb1909 fix: do not send over tor when service not launched 2026-04-10 00:28:27 +03:00
ardocrat 31bc74529c build: update grin node 2026-04-09 20:56:45 +03:00
ardocrat 8d6943975b gui: glow renderer by default 2026-04-09 02:44:06 +03:00
ardocrat 4c5d8abe7b wix: update uuid 2026-04-09 02:44:01 +03:00
ardocrat 4dc42bce4a tor: fix multiline bridge connection 2026-03-30 01:37:36 +03:00
ardocrat e2d5d92f18 build: version 0.3.3 2026-03-30 01:36:45 +03:00
ardocrat b001eb4712 node: update to last git version (fix pibd stuck) 2026-03-29 21:18:36 +03:00
ardocrat f14bd902ea build: update win package guid 2026-03-24 14:52:20 +03:00
ardocrat 33ab11933a build: update wallet branch 2026-03-24 14:51:12 +03:00
ardocrat 6b05a2177e log: info level into file, crash report for android 2026-03-24 13:18:04 +03:00
ardocrat 7bbe637414 ui: do not show username at ext conn settings 2026-03-24 02:32:53 +03:00
ardocrat 9b6252de3a tor: fix connection with multiple bridges 2026-03-24 02:13:08 +03:00
ardocrat 26debcf51c ui: camera paddings, focus on password at wallet creation modal 2026-03-24 02:06:00 +03:00
ardocrat 497b967fd0 node: scan and share connection with qr code 2026-03-23 04:46:29 +03:00
ardocrat 05e18cf6c4 fix: show tx modal after message parse, cancel tx when slate not found 2026-03-23 02:49:23 +03:00
ardocrat 6e50b2b38a ui: make list items clickable, ability to delete tx 2026-03-23 01:21:09 +03:00
ardocrat 9bc96de398 ci: optimize release upload
- separate job telegram upload to avoid forgejo release upload repeat if failed
- upload artifacts to forgejo from another runner

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/55
2026-03-22 22:14:04 +00:00
ardocrat 5a525c50e1 img: update cover 2026-03-19 10:37:45 +03:00
ardocrat ba0af0968d tor: multiline bridges input, optimize tor connection check, add multiple default webtunnel bridges, fix tx cancel on finalization error 2026-03-18 15:44:32 +03:00
ardocrat a0947aa47c ci: fix pre-release check 2026-03-15 21:18:42 +00:00
ardocrat 06c6b8b4f5 android: fix text input on some devices 2026-03-15 23:26:57 +03:00
ardocrat b19335d0bc build: version 0.3.2 2026-03-15 23:25:46 +03:00
ardocrat 40eb30fb75 macos: fix version 2026-03-15 23:25:42 +03:00
ardocrat 8223e52570 build: version script 2026-03-15 23:25:08 +03:00
133 changed files with 26664 additions and 23864 deletions
+4 -2
View File
@@ -13,8 +13,10 @@ jobs:
runs-on: ubuntu
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Checkout submodules
run: |
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- name: Check commit
id: check
run: |
+131 -94
View File
@@ -10,15 +10,18 @@ on:
jobs:
version:
runs-on: ubuntu
runs-on: debian-release
outputs:
v: ${{ steps.version.outputs.v }}
pre: ${{ steps.version.outputs.pre }}
exists: ${{ steps.check.outputs.exists }}
last_tag: ${{ steps.check_prev.outputs.last_tag }}
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Checkout submodules
run: |
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- name: Get version
id: version
run: |
@@ -40,7 +43,7 @@ jobs:
echo "exists=${exists}" >> "$FORGEJO_OUTPUT"
echo ${exists}
mkdir release
- uses: ardocrat/forgejo-release@grim
- uses: actions/forgejo-release@v2.11.3
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
with:
direction: download
@@ -51,16 +54,6 @@ jobs:
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
working-directory: release
run: for f in *; do mv "$f" "$(echo "$f" | sed s/-dev-/-/)"; done
- uses: ardocrat/forgejo-release@grim
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
with:
direction: upload
token: ${{ secrets.RELEASE_TOKEN }}
tag: ${{ forgejo.ref_name }}
override: false
prerelease: false
release-dir: ./release
release-notes: "Full Changelog: [${{ steps.check_prev.outputs.last_tag }}...${{ steps.version.outputs.v }}](https://code.gri.mw/${{ forgejo.repository }}/compare/${{ steps.check_prev.outputs.last_tag }}...${{ steps.version.outputs.v }})"
- name: Delete dev release
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
uses: actions/delete-release@v1
@@ -70,17 +63,30 @@ jobs:
id: check_prev
run: |
git fetch --tags
last_tag=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1))
echo "last_tag=${last_tag}" >> "$FORGEJO_OUTPUT"
[[ ${{ forgejo.ref_type }} == 'tag' ]] && skip=0 || skip=1
last_tag=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=${skip} --max-count=1)) || true
echo "last_tag=${last_tag}" >> "$FORGEJO_OUTPUT"
- uses: actions/forgejo-release@v2.11.3
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
with:
direction: upload
token: ${{ secrets.RELEASE_TOKEN }}
tag: ${{ forgejo.ref_name }}
override: false
prerelease: false
release-dir: ./release
release-notes: "Full Changelog: [${{ steps.check_prev.outputs.last_tag }}...${{ steps.version.outputs.v }}](https://code.gri.mw/${{ forgejo.repository }}/compare/${{ steps.check_prev.outputs.last_tag }}...${{ steps.version.outputs.v }})"
android_libs:
android:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: ubuntu
runs-on: macos
needs: version
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Checkout submodules
run: |
sed -i -- 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- name: Restore cargo cache
id: cache-cargo-restore
uses: actions/cache/restore@v5
@@ -92,11 +98,9 @@ jobs:
~/.cargo/git/db/
target/
key: grim-android-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup registry
run: |
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
- name: Build libs
run: |
rustup -q update
chmod +x scripts/android.sh && ./scripts/android.sh lib ${{ needs.version.outputs.v }}
- name: Save cargo cache
uses: actions/cache/save@v5
@@ -108,20 +112,6 @@ jobs:
~/.cargo/git/db/
target/
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
- name: Upload artifacts
run: |
cd android/app/src/main
tar -czf jniLibs.tar.gz jniLibs
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file jniLibs.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/jniLibs.tar.gz
android_release:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: ubuntu-android
needs: [version, android_libs]
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Restore gradle cache
id: cache-gradle-restore
uses: actions/cache/restore@v5
@@ -132,23 +122,17 @@ jobs:
key: grim-android-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Download artifacts
run: |
cd android/app/src/main
curl -o jniLibs.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/jniLibs.tar.gz
tar -xzf jniLibs.tar.gz
rm jniLibs.tar.gz
- name: Setup build
run: |
chmod +x android/gradlew
echo "${{ secrets.ANDROID_KEYSTORE }}" > release.keystore.txt
base64 -d release.keystore.txt > android/keystore
base64 -d < release.keystore.txt -o android/keystore
echo "${{ secrets.ANDROID_KEYSTORE_PROPS }}" > release.keystore.props.txt
base64 -d release.keystore.props.txt > android/keystore.properties
base64 -d < release.keystore.props.txt -o android/keystore.properties
mkdir ~/.gradle && touch ~/.gradle/gradle.properties
printf "mavenHost=${{ secrets.MAVEN_HOST }}\n" >> ~/.gradle/gradle.properties
printf "mavenHost=${{ secrets.MAVEN_LOCAL_HOST }}\n" >> ~/.gradle/gradle.properties
printf "mavenUser=${{ secrets.MAVEN_USER }}\n" >> ~/.gradle/gradle.properties
printf "mavenPassword=${{ secrets.MAVEN_PASSWORD }}" >> ~/.gradle/gradle.properties
printf "mavenPassword=${{ secrets.MAVEN_PASSWORD }}" >> ~/.gradle/gradle.properties
- name: Release ARMv7+v8 APK
working-directory: android
run: |
@@ -193,16 +177,18 @@ jobs:
mkdir release
mv android/grim* release
tar -czf android.tar.gz release
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file android.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/android.tar.gz
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file android.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/android.tar.gz
linux_arm:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: ubuntu
needs: [version, android_libs]
runs-on: debian-rust-arm
needs: [version]
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Checkout submodules
run: |
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- name: Restore cargo cache
id: cache-cargo-restore
uses: actions/cache/restore@v5
@@ -216,9 +202,11 @@ jobs:
key: grim-linux-arm-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup registry
run: |
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
- name: Release Linux ARM
run: cargo zigbuild --release --target aarch64-unknown-linux-gnu
run: |
rustup -q update
cargo zigbuild --release --target aarch64-unknown-linux-gnu
- name: Save cargo cache
uses: actions/cache/save@v5
with:
@@ -229,28 +217,45 @@ jobs:
~/.cargo/git/db/
target/
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
- name: AppImage ARM
- name: Upload artifacts
run: |
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file target/aarch64-unknown-linux-gnu/release/grim ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/grim-linux-arm
linux_arm_appimage:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: debian-arm
needs: [version, linux_arm]
steps:
- uses: actions/checkout@v6
- name: Download Artifact
run: |
wget ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/grim-linux-arm
- name: AppImage
shell: bash
run: |
mkdir release
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
appimagetool linux/Grim.AppDir grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
cp grim-${{ needs.version.outputs.v }}-linux-arm.AppImage release/
chmod +x grim-linux-arm
mv grim-linux-arm linux/Grim.AppDir/AppRun
ARCH=aarch64 appimagetool linux/Grim.AppDir grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
mv grim-${{ needs.version.outputs.v }}-linux-arm.AppImage release/
- name: Checksum AppImage ARM
working-directory: release
run: sha256sum grim-${{ needs.version.outputs.v }}-linux-arm.AppImage > grim-${{ needs.version.outputs.v }}-linux-arm-appimage-sha256sum.txt
- name: Upload artifacts
run: |
tar -czf linux-arm.tar.gz release
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-arm.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-arm.tar.gz
tar -czf linux-arm.appimage.tar.gz release
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-arm.appimage.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-arm.appimage.tar.gz
linux_x86:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: ubuntu-linux-x86
runs-on: debian-rust-x86_64
needs: [version]
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Checkout submodules
run: |
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- name: Restore cargo cache
id: cache-cargo-restore
uses: actions/cache/restore@v5
@@ -261,12 +266,14 @@ jobs:
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: grim-linux-x86-cargo-${{ hashFiles('**/Cargo.lock') }}
key: grim-linux-arm-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup registry
run: |
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
- name: Release Linux x86
run: cargo zigbuild --release --target x86_64-unknown-linux-gnu
run: |
rustup -q update
cargo zigbuild --release --target x86_64-unknown-linux-gnu
- name: Save cargo cache
uses: actions/cache/save@v5
with:
@@ -280,25 +287,27 @@ jobs:
- name: AppImage x86
run: |
mkdir release
cp target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
mv target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
appimagetool linux/Grim.AppDir grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
cp grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage release/
mv grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage release/
- name: Checksum AppImage x86
working-directory: release
run: sha256sum grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage > grim-${{ needs.version.outputs.v }}-linux-x86_64-appimage-sha256sum.txt
- name: Upload artifacts
run: |
tar -czf linux-x86_64.tar.gz release
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-x86_64.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-x86_64.tar.gz
tar -czf linux-x86_64.appimage.tar.gz release
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-x86_64.appimage.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-x86_64.appimage.tar.gz
macos:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: ubuntu
needs: [version, android_libs, linux]
runs-on: macos
needs: [version, android]
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Checkout submodules
run: |
sed -i -- 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- run: mkdir release
- name: Restore cargo cache
id: cache-cargo-restore
@@ -311,13 +320,14 @@ jobs:
~/.cargo/git/db/
target/
key: grim-macos-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup registry
run: |
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
- name: Release MacOS Universal
run: |
cargo zigbuild --release --target universal2-apple-darwin
cp target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
rustup -q update
export MACOSX_DEPLOYMENT_TARGET=11.0
cargo build --release --target x86_64-apple-darwin
cargo build --release --target aarch64-apple-darwin
lipo -create -output grim "target/x86_64-apple-darwin/release/grim" "target/aarch64-apple-darwin/release/grim"
mv grim macos/Grim.app/Contents/MacOS
- name: Save cargo cache
uses: actions/cache/save@v5
with:
@@ -339,7 +349,7 @@ jobs:
- name: Upload artifacts
run: |
tar -czf macos.tar.gz release
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file macos.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/macos.tar.gz
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file macos.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/macos.tar.gz
windows:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
@@ -347,11 +357,22 @@ jobs:
needs: [version]
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Checkout submodules
run: |
(Get-content .gitmodules) | Foreach-Object {$_ -replace "https://code.gri.mw", "${{ secrets.REPO_HOST }}"} | Set-Content .gitmodules
git submodule update --init --recursive --remote
- name: Update UpgradeCode
shell: powershell
run: |
$guid = [guid]::NewGuid().ToString()
$wix = [xml](Get-Content wix/main.wxs)
$wix.Wix.Product.UpgradeCode = $guid
$wix.Save("wix/main.wxs")
Get-Content wix/main.wxs
- run: mkdir release
- name: Release Windows x86
run: |
rustup -q update
cargo wix -p grim -o grim-${{ needs.version.outputs.v }}-win-x86_64.msi --nocapture
mv grim-${{ needs.version.outputs.v }}-win-x86_64.msi release\
Compress-Archive -Path target\release\grim.exe -DestinationPath grim-${{ needs.version.outputs.v }}-win-x86_64.zip
@@ -365,32 +386,32 @@ jobs:
run: |
tar -czf windows.tar.gz release
Remove-Item alias:curl
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file windows.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/windows.tar.gz
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file windows.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/windows.tar.gz
release:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: debian-release
needs: [version, android_release, linux, linux_x86, macos, windows]
needs: [version, android, linux_x86, linux_arm_appimage, macos, windows]
steps:
- name: Download All Artifacts
run: |
curl -o android.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/android.tar.gz
curl -s -o android.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/android.tar.gz
tar -xzf android.tar.gz
rm android.tar.gz
curl -o linux-arm.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-arm.tar.gz
tar -xzf linux-arm.tar.gz
rm linux-arm.tar.gz
curl -o linux-x86_64.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-x86_64.tar.gz
tar -xzf linux-x86_64.tar.gz
rm linux-x86_64.tar.gz
curl -o macos.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/macos.tar.gz
curl -s -o linux-x86_64.appimage.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-x86_64.appimage.tar.gz
tar -xzf linux-x86_64.appimage.tar.gz
rm linux-x86_64.appimage.tar.gz
curl -s -o linux-arm.appimage.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-arm.appimage.tar.gz
tar -xzf linux-arm.appimage.tar.gz
rm linux-arm.appimage.tar.gz
curl -s -o macos.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/macos.tar.gz
tar -xzf macos.tar.gz
rm macos.tar.gz
curl -o windows.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/windows.tar.gz
curl -s -o windows.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/windows.tar.gz
tar -xzf windows.tar.gz
rm windows.tar.gz
- name: Upload release to Forgejo
uses: ardocrat/forgejo-release@grim
uses: actions/forgejo-release@v2.11.3
with:
direction: upload
token: ${{ secrets.RELEASE_TOKEN }}
@@ -417,6 +438,22 @@ jobs:
chat_id: ${{ secrets.TELEGRAM_GROUP_ID }}
status: ${{ job.status }}
notify_fields: "actor,repository,workflow,branch,commit"
release-telegram:
runs-on: debian-release
needs: [version, release]
steps:
- name: Download All Artifacts
run: |
mkdir release
cd release
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-android.apk
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-android-x86_64.apk
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-macos-universal.zip
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-win-x86_64.msi
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-win-x86_64.zip
- name: Upload files to Telegram
uses: actions/telegram-send-file@main
with:
+1 -4
View File
@@ -1,10 +1,7 @@
[submodule "node"]
path = node
url = https://code.gri.mw/ardocrat/node
[submodule "wallet"]
path = wallet
url = https://code.gri.mw/ardocrat/wallet
branch = grim
branch = grim-staging
[submodule "tor/webtunnel"]
path = tor/webtunnel
url = https://code.gri.mw/WEB/webtunnel
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Copyright 2026 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.
rustfmt --version &>/dev/null
if [ $? != 0 ]; then
printf "[pre_commit] \033[0;31merror\033[0m: \"rustfmt\" not available. \n"
printf "[pre_commit] \033[0;31merror\033[0m: rustfmt can be installed via - \n"
printf "[pre_commit] $ rustup component add rustfmt-preview \n"
exit 1
fi
problem_files=()
# first collect all the files that need reformatting
for file in $(git diff --name-only --cached); do
if [ ${file: -3} == ".rs" ]; then
rustfmt --check $file &>/dev/null
if [ $? != 0 ]; then
problem_files+=($file)
fi
fi
done
if [ ${#problem_files[@]} == 0 ]; then
# nothing to do
printf "[pre_commit] rustfmt \033[0;32mok\033[0m \n"
else
# reformat the files that need it and re-stage them.
printf "[pre_commit] the following files were rustfmt'd before commit: \n"
for file in ${problem_files[@]}; do
rustfmt $file
git add $file
printf "\033[0;32m $file\033[0m \n"
done
fi
exit 0
Generated
+968 -846
View File
File diff suppressed because it is too large Load Diff
+25 -28
View File
@@ -1,12 +1,12 @@
[package]
name = "grim"
version = "0.3.1"
version = "0.3.6"
authors = ["Ardocrat <ardocrat@gri.mw>"]
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
license = "Apache-2.0"
repository = "https://code.gri.mw/GUI/grim"
keywords = [ "crypto", "grin", "mimblewimble" ]
edition = "2021"
edition = "2024"
build = "build.rs"
[[bin]]
@@ -29,14 +29,14 @@ panic = "abort"
log = "0.4.27"
# node
grin_api = { path = "node/api" }
grin_chain = { path = "node/chain" }
grin_config = { path = "node/config" }
grin_core = { path = "node/core" }
grin_p2p = { path = "node/p2p" }
grin_servers = { path = "node/servers" }
grin_keychain = { path = "node/keychain" }
grin_util = { path = "node/util" }
grin_api = { path = "wallet/grin/api" }
grin_chain = { path = "wallet/grin/chain" }
grin_config = { path = "wallet/grin/config" }
grin_core = { path = "wallet/grin/core" }
grin_p2p = { path = "wallet/grin/p2p" }
grin_servers = { path = "wallet/grin/servers" }
grin_keychain = { path = "wallet/grin/keychain" }
grin_util = { path = "wallet/grin/util" }
# wallet
grin_wallet_impls = { path = "wallet/impls" }
@@ -52,10 +52,8 @@ egui-async = "0.3.4"
rust-i18n = "3.1.5"
## other
anyhow = "1.0.97"
pin-project = "1.1.10"
log4rs = "1.4.0"
backtrace = "0.3.76"
thiserror = "2.0.18"
futures = "0.3.31"
dirs = "6.0.0"
sys-locale = "0.3.2"
@@ -91,23 +89,19 @@ uuid = { version = "0.8.2", features = ["v4"] }
num-bigint = "0.4.6"
## tor
arti-client = { version = "0.38.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.38.0", features = ["static"] }
tor-config = "0.38.0"
fs-mistrust = "0.13.1"
tor-hsservice = "0.38.0"
tor-hsrproxy = "0.38.0"
tor-keymgr = "0.38.0"
tor-llcrypto = "0.38.0"
tor-hscrypto = "0.38.0"
tor-error = "0.38.0"
arti-client = { version = "0.43.0", features = ["static", "pt-client", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.43.0", features = ["static"] }
tor-config = "0.43.0"
fs-mistrust = "0.14.2"
tor-hsservice = "0.43.0"
tor-hsrproxy = "0.43.0"
tor-keymgr = "0.43.0"
tor-llcrypto = "0.43.0"
tor-hscrypto = "0.43.0"
sha2 = "0.10.8"
ed25519-dalek = "2.1.1"
curve25519-dalek = "4.1.3"
hyper-tor = { version = "0.14.32", features = ["full"], package = "hyper" }
tls-api = "0.12.0"
tls-api-native-tls = "0.12.1"
safelog = "0.7.0"
safelog = "0.8.1"
## stratum server
tokio-old = { version = "0.2", features = ["full"], package = "tokio" }
@@ -136,4 +130,7 @@ android_logger = "0.15.0"
jni = "0.21.1"
android-activity = { version = "0.6.0", features = ["game-activity"] }
winit = { version = "0.30.12", features = ["android-game-activity"] }
eframe = { version = "0.33.2", default-features = false, features = ["glow", "android-game-activity"] }
eframe = { version = "0.33.2", default-features = false, features = ["glow", "android-game-activity"] }
[build-dependencies]
built = "0.8.0"
+1 -1
View File
@@ -12,7 +12,7 @@ android {
minSdk 24
targetSdk 36
versionCode 5
versionName "0.3.1"
versionName "0.3.6"
}
lint {
+1
View File
@@ -54,6 +54,7 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:mimeType="application/*" />
<data android:pathPattern=".*\\.slatepack" />
</intent-filter>
@@ -331,6 +331,17 @@ public class MainActivity extends GameActivity {
onTextInput("9");
return false;
}
} else if (event.getAction() == KeyEvent.ACTION_MULTIPLE && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
if (!event.getCharacters().isEmpty()) {
onTextInput(event.getCharacters());
return false;
}
// Pass any other input values into native code.
} else if (event.getAction() == KeyEvent.ACTION_UP &&
event.getKeyCode() != KeyEvent.KEYCODE_ENTER &&
event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
onTextInput(String.valueOf((char)event.getUnicodeChar()));
return false;
}
return super.dispatchKeyEvent(event);
}
+94 -67
View File
@@ -1,74 +1,101 @@
use std::path::PathBuf;
use std::process::Command;
use std::{env, fs};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let tor_out_dir = format!("{}/tor", out_dir);
let mut webtunnel_file = format!("{}/webtunnel", tor_out_dir);
let exists = fs::exists(&webtunnel_file).unwrap();
if !exists {
// Create empty webtunnel file to allow build with include_bytes! macro.
fs::create_dir(&tor_out_dir).unwrap_or_default();
fs::File::create(&webtunnel_file).unwrap();
}
built::write_built_file().expect("Failed to acquire build-time information");
let target = env::var("TARGET").unwrap();
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
// Setting up git hooks in the project: rustfmt and so on.
let git_hooks = format!(
"git config core.hooksPath {}",
PathBuf::from("./.hooks").to_str().unwrap()
);
let is_android = target_os == "android";
if is_android {
// Set a path to Android Webtunnel binary.
let arch = if target.contains("aarch64") {
"arm64-v8a"
} else if target.contains("arm") {
"armeabi-v7a"
} else {
"x86_64"
};
let root = env::var("CARGO_MANIFEST_DIR").unwrap();
webtunnel_file = format!("{}/android/app/src/main/jniLibs/{}/libwebtunnel.so", root, arch);
}
if cfg!(target_os = "windows") {
Command::new("cmd")
.args(&["/C", &git_hooks])
.output()
.expect("failed to execute git config for hooks");
} else {
Command::new("sh")
.args(&["-c", &git_hooks])
.output()
.expect("failed to execute git config for hooks");
}
// Build if Webtunnel binary is empty or not exists.
let empty = match fs::File::open(&webtunnel_file) {
Ok(file) => file.metadata().unwrap().len() == 0,
Err(_) => true
};
let build = !exists || empty;
if build {
// Setup GOOS env variable.
let go_os = if target_os == "macos" {
"darwin"
} else {
target_os.as_str()
};
// Setup GOARCH env variable.
let go_arch = if target.contains("aarch64") {
"arm64"
} else if target.contains("arm") {
"arm"
} else {
"amd64"
};
// Run Webtunnel Go build.
let output = if env::consts::OS == "windows" {
Command::new("./scripts/webtunnel.bat")
.arg(go_os)
.arg(go_arch)
.arg(webtunnel_file)
.output()
} else {
Command::new("bash")
.arg("./scripts/webtunnel.sh")
.arg(go_os)
.arg(go_arch)
.arg(webtunnel_file)
.output()
};
if let Ok(out) = output {
if out.status.code().is_none() || out.status.code().unwrap() != 0 {
panic!("webtunnel go build failed:\n{:?}", out);
}
}
}
}
let out_dir = env::var("OUT_DIR").unwrap();
let tor_out_dir = format!("{}/tor", out_dir);
let mut webtunnel_file = format!("{}/webtunnel", tor_out_dir);
let exists = fs::exists(&webtunnel_file).unwrap();
if !exists {
// Create empty webtunnel file to allow build with include_bytes! macro.
fs::create_dir(&tor_out_dir).unwrap_or_default();
fs::File::create(&webtunnel_file).unwrap();
}
let target = env::var("TARGET").unwrap();
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
if target_os == "ios" {
return;
}
let is_android = target_os == "android";
if is_android {
// Set a path to Android Webtunnel binary.
let arch = if target.contains("aarch64") {
"arm64-v8a"
} else if target.contains("arm") {
"armeabi-v7a"
} else {
"x86_64"
};
let root = env::var("CARGO_MANIFEST_DIR").unwrap();
webtunnel_file = format!(
"{}/android/app/src/main/jniLibs/{}/libwebtunnel.so",
root, arch
);
}
// Build if Webtunnel binary is empty or not exists.
let empty = match fs::File::open(&webtunnel_file) {
Ok(file) => file.metadata().unwrap().len() == 0,
Err(_) => true,
};
let build = !exists || empty;
if build {
// Setup GOOS env variable.
let go_os = if target_os == "macos" {
"darwin"
} else {
target_os.as_str()
};
// Setup GOARCH env variable.
let go_arch = if target.contains("aarch64") {
"arm64"
} else if target.contains("arm") {
"arm"
} else {
"amd64"
};
// Run Webtunnel Go build.
let output = if env::consts::OS == "windows" {
Command::new("./scripts/webtunnel.bat")
.arg(go_os)
.arg(go_arch)
.arg(webtunnel_file)
.output()
} else {
Command::new("bash")
.arg("./scripts/webtunnel.sh")
.arg(go_os)
.arg(go_arch)
.arg(webtunnel_file)
.output()
};
if let Ok(out) = output {
if out.status.code().is_none() || out.status.code().unwrap() != 0 {
panic!("webtunnel go build failed:\n{:?}", out);
}
}
}
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 191 KiB

+5 -2
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: 'Geben Sie den erhaltenen Zahlungsnachweis ein, um die Transaktion zu verifizieren:'
payment_proof_valid: 'Der eingegebene Zahlungsnachweis ist gültig:'
payment_proof_error: 'Der eingetragene Zahlungsnachweis ist nicht gültig:'
tx_delete_confirmation: Bist du sicher, dass du die Transaktion aus dem Verlauf löschen möchtest?
transport:
desc: 'Transport verwenden, um Nachrichten synchron zu empfangen oder zu senden:'
tor_network: Tor Netzwek
@@ -151,6 +152,7 @@ transport:
conn_error: Verbindungsproblem
disconnected: Verbindung getrennt
receiver_address: 'Empfängeraddresse:'
sender_address: 'Absenderadresse:'
incorrect_addr_err: 'Eingegebene Addresse ist inkorrekt:'
tor_send_error: Beim Senden über Tor ist ein Fehler aufgetreten. Stellen Sie sicher, dass der Empfänger online ist. Die Transaktion wurde abgebrochen.
tor_autorun_desc: Gibt an, ob beim Öffnen des Wallets der Tor-Dienst gestartet werden soll, um Transaktionen synchron zu empfangen.
@@ -299,8 +301,9 @@ network_settings:
ban_window_desc: Die Entscheidung über das Verbot trifft der Knoten auf der Grundlage der Korrektheit der von der Gegenstelle erhaltenen Daten.
max_inbound_count: 'Maximale Anzahl der eingehenden Peer-Verbindungen:'
max_outbound_count: 'Maximale Anzahl von ausgehenden Peer-Verbindungen:'
reset_peers_desc: Peer-Daten zurücksetzen. Verwenden Sie diese Funktion nur, wenn es Probleme beim finden von Peers gibt.
reset_peers: Peers zurücksetzten
reset_data_desc: Reset-Knotendaten. Verwenden Sie diese Funktion nur, wenn es Probleme mit der Synchronisation gibt.
reset_data: Daten zurücksetzten
ip_listen_all: Hören Sie auf allen Schnittstellen
modal:
cancel: Abbrechen
save: Speichern
+5 -2
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: 'Enter received payment proof to verify transaction:'
payment_proof_valid: 'Entered payment proof is valid:'
payment_proof_error: 'Entered payment proof is not valid:'
tx_delete_confirmation: Are you sure you want to delete the transaction from history?
transport:
desc: 'Use transport to receive or send messages synchronously:'
tor_network: Tor network
@@ -151,6 +152,7 @@ transport:
conn_error: Connection error
disconnected: Disconnected
receiver_address: 'Address of the receiver:'
sender_address: 'Address of the sender:'
incorrect_addr_err: 'Entered address is incorrect:'
tor_send_error: An error occurred during sending over Tor, make sure receiver is online, transaction was canceled.
tor_autorun_desc: Whether to launch Tor service on wallet opening to receive transactions synchronously.
@@ -299,8 +301,9 @@ network_settings:
ban_window_desc: The decision to ban is made by node, based on the correctness of the data received from the peer.
max_inbound_count: 'Maximum number of inbound peer connections:'
max_outbound_count: 'Maximum number of outbound peer connections:'
reset_peers_desc: Reset peers data. Use it with a caution only if there are problems with finding peers.
reset_peers: Reset peers
reset_data_desc: Reset the node data. Use it with a caution only if there are problems with synchronization.
reset_data: Reset data
ip_listen_all: Listen on all interfaces
modal:
cancel: Cancel
save: Save
+5 -2
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: 'Saisissez la preuve de paiement reçue pour vérifier la transaction:'
payment_proof_valid: 'La preuve de paiement saisie est valide:'
payment_proof_error: "La preuve de paiement saisie n'est pas valide:"
tx_delete_confirmation: Êtes-vous sûr de vouloir supprimer la transaction de l'historique?
transport:
desc: 'Utilisez le transport pour recevoir ou envoyer des messages de manière synchronisée:'
tor_network: Réseau Tor
@@ -151,6 +152,7 @@ transport:
conn_error: Erreur de connexion
disconnected: Déconnecté
receiver_address: 'Adresse du destinataire:'
sender_address: "Adresse de l'expéditeur:"
incorrect_addr_err: 'Adresse entrée incorrecte:'
tor_send_error: "Une erreur s'est produite lors de l'envoi via Tor. Assurez-vous que le destinataire est en ligne, la transaction a été annulée."
tor_autorun_desc: "Lancer automatiquement le service Tor à l'ouverture du portefeuille pour recevoir les transactions de manière synchronisée."
@@ -299,8 +301,9 @@ network_settings:
ban_window_desc: La décision de bannir est prise par le noeud, en fonction de la validité des données reçues du pair.
max_inbound_count: 'Nombre maximum de connexions de pairs entrants :'
max_outbound_count: 'Nombre maximum de connexions de pairs sortants :'
reset_peers_desc: Réinitialiser les données des pairs. Utilisez-le avec précaution uniquement en cas de problèmes pour trouver des pairs.
reset_peers: Réinitialiser les pairs
reset_data_desc: Réinitialisez les données du noeud. Utilisez-le avec prudence uniquement en cas de problème de synchronisation.
reset_data: Réinitialisation des données
ip_listen_all: Écoutez sur toutes les interfaces
modal:
cancel: Annuler
save: Sauvegarder
+5 -2
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: 'Введите полученное подтверждение оплаты для проверки транзакции:'
payment_proof_valid: 'Введённое подтверждение оплаты действительно:'
payment_proof_error: 'Введённое подтверждение оплаты недействительно:'
tx_delete_confirmation: Вы уверены, что хотите удалить транзакцию из истории?
transport:
desc: 'Используйте транспорт для синхронных получения или отправки сообщений:'
tor_network: Сеть Tor
@@ -151,6 +152,7 @@ transport:
conn_error: Ошибка подключения
disconnected: Отключено
receiver_address: 'Адрес получателя:'
sender_address: 'Адрес отправителя:'
incorrect_addr_err: 'Введённый адрес неверен:'
tor_send_error: Во время отправки через Tor произошла ошибка, убедитесь, что получатель находится онлайн, транзакция была отменена.
tor_autorun_desc: Запускать ли Tor сервис при открытии кошелька для синхронного получения транзакций.
@@ -299,8 +301,9 @@ network_settings:
ban_window_desc: Решение о запрете принимается узлом, основываясь на корректности данных полученных от пира.
max_inbound_count: 'Максимальное количество входящих подключений пиров:'
max_outbound_count: 'Максимальное количество исходящих подключений к пирам:'
reset_peers_desc: Сбросить данные пиров. Используйте с осторожностью, только при наличии проблем с поиском пиров.
reset_peers: Сбросить пиры
reset_data_desc: Сбросить данные узла. Используйте с осторожностью, только при наличии проблем с синхронизацией.
reset_data: Сброс данных
ip_listen_all: Слушать на всех интерфейсах
modal:
cancel: Отмена
save: Сохранить
+5 -2
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: 'Islemi doğrulamak için alinan ödeme kanitini girin:'
payment_proof_valid: 'Girilen ödeme kaniti geçerlidir:'
payment_proof_error: 'Girilen ödeme kaniti geçerli değildir:'
tx_delete_confirmation: Islemi geçmişten silmek istediğinizden emin misiniz?
transport:
desc: 'Adresten senkronize GONDER veya AL:'
tor_network: Tor network
@@ -151,6 +152,7 @@ transport:
conn_error: Bagalanti hatasi
disconnected: Baglanti yok
receiver_address: 'Alicinin adresi:'
sender_address: 'Gönderici adresi:'
incorrect_addr_err: 'Girilen adres hatali:'
tor_send_error: Tor adresi uzerinden gonderimde aksaklik olustu, alici online olmasi gerek, islem iptal edildi.
tor_autorun_desc: Islemleri Tor adresi olarak AL,bunun için cuzdan acilisinda Tor hizmetinin baslatilip baslatilmayacagi.
@@ -299,8 +301,9 @@ network_settings:
ban_window_desc: Banlama karari, peerden alinan verilerin dogruluguna bagli olarak Node tarafindan verilir.
max_inbound_count: 'Maksimum gelen Peer baglanti sayisi:'
max_outbound_count: 'Maksimum giden Peer baglanti sayisi:'
reset_peers_desc: Peers verilerini sifirlayin. Yalnizca Peers bulma konusunda sorun yasiyorsaniz dikkatli kullanin.
reset_peers: Peers Resetle
reset_data_desc: Node verisini sifirlama. Sadece senkronizasyonda sorun varsa dikkatli kullanin.
reset_data: Verileri sifirlama
ip_listen_all: Tüm arayüzlerde dinle
modal:
cancel: Iptal
save: Kaydet
+5 -2
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: '輸入已收款證明以驗證交易:'
payment_proof_valid: '輸入的付款證明有效:'
payment_proof_error: '輸入的付款證明無效:'
tx_delete_confirmation: 你確定要從歷史紀錄中刪除這筆交易嗎?
transport:
desc: '使用传输同步接收或发送消息:'
tor_network: Tor 网络
@@ -151,6 +152,7 @@ transport:
conn_error: 连接错误
disconnected: 已断开连接
receiver_address: '接收者的地址:'
sender_address: '发件人地址:'
incorrect_addr_err: '输入的地址不正确:'
tor_send_error: 通过 Tor 发送时出错,请确保接收方在线, 交易已取消.
tor_autorun_desc: 是否在开钱包时启动 Tor 服务以同步接收交易.
@@ -299,8 +301,9 @@ network_settings:
ban_window_desc: 禁止的决定是由节点 根据从网络对点收到的数据的正确性做出的.
max_inbound_count: '入站网络对点连接的最大数量:'
max_outbound_count: '最大出站网络对点连接数:'
reset_peers_desc: 重置网络对点数据。仅当查找网络对点出现问题时,才请谨慎使用.
reset_peers: 重置网络对点
reset_data_desc: 重置点数据。只有在出现同步问题时才需谨慎使用.
reset_data: 重置数据
ip_listen_all: 在所有接口上监听
modal:
cancel: 取消
save: 保存
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.2.3</string>
<string>0.3.6</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
Submodule node deleted from 2ec7b4d5cd
+2
View File
@@ -0,0 +1,2 @@
hard_tabs = true
edition = "2024"
-3
View File
@@ -41,13 +41,10 @@ function build_lib() {
sed -i -e 's/"cdylib","rlib"]/"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
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
cargo ndk -t "${arch}" -o android/app/src/main/jniLibs build --profile release-apk
if [ $? -ne 0 ]; then
success=0
fi
unset CPPFLAGS && unset CFLAGS
sed -i -e 's/"cdylib","rlib"]/"rlib"]/g' Cargo.toml
rm -f Cargo.toml-e
+5 -2
View File
@@ -70,6 +70,9 @@ else
exit 1
fi
# Update MacOS version.
sed -i '' -e 's/'"$GIT_TAG_LATEST"'/'"$VERSION_NEXT"'/' macos/Grim.app/Contents/Info.plist
# Update version for Windows installer.
sed -i '' -e 's/" Version="[^\"]*"/" Version="'"$VERSION_NEXT"'"/g' wix/main.wxs
sed -i '' -e 's/<Package Id="[^\"]*"/<Package Id="'"$(uuidgen)"'"/g' wix/main.wxs
@@ -88,12 +91,12 @@ cargo update -p grim
# Commit the changes
git add .
git commit -m "release: v$VERSION_NEXT"
git commit -m "build: version $VERSION_NEXT"
# ==================================
# Create git tag for new version
# ==================================
# 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
+371 -359
View File
@@ -12,418 +12,430 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::atomic::{AtomicBool, Ordering};
use egui::epaint::RectShape;
use egui::{Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers, Order, ResizeDirection, Stroke, StrokeKind, UiBuilder, ViewportCommand};
use egui::{
Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers, Order, ResizeDirection,
Stroke, StrokeKind, UiBuilder, ViewportCommand,
};
use lazy_static::lazy_static;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View};
use crate::gui::Colors;
use crate::AppConfig;
lazy_static! {
/// State to check if platform Back button was pressed.
static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false);
/// State to check if platform Back button was pressed.
static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false);
}
/// Implements ui entry point and contains platform-specific callbacks.
pub struct App<Platform> {
/// Handles platform-specific functionality.
pub platform: Platform,
/// Handles platform-specific functionality.
pub platform: Platform,
/// Main content.
content: Content,
/// Main content.
content: Content,
/// Last window resize direction.
resize_direction: Option<ResizeDirection>,
/// Flag to check if it's first draw.
first_draw: bool
/// Last window resize direction.
resize_direction: Option<ResizeDirection>,
/// Flag to check if it's first draw.
first_draw: bool,
}
impl<Platform: PlatformCallbacks> App<Platform> {
pub fn new(platform: Platform) -> Self {
Self {
platform,
content: Content::default(),
resize_direction: None,
first_draw: true
}
}
pub fn new(platform: Platform) -> Self {
Self {
platform,
content: Content::default(),
resize_direction: None,
first_draw: true,
}
}
/// 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);
}
// Setup visuals.
crate::setup_fonts(ctx);
crate::setup_visuals(ctx);
}
/// 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);
}
// Setup visuals.
crate::setup_fonts(ctx);
crate::setup_visuals(ctx);
}
/// Draw application content.
pub fn ui(&mut self, ctx: &Context) {
if self.first_draw {
self.on_first_draw(ctx);
self.first_draw = false;
}
/// Draw application content.
pub fn ui(&mut self, ctx: &Context) {
if self.first_draw {
self.on_first_draw(ctx);
self.first_draw = false;
}
// Handle Esc keyboard key event and platform Back button key event.
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape) ||
i.consume_key(Modifiers::NONE, egui::Key::BrowserBack)) {
// Pass event to content.
self.content.on_back(ctx, &self.platform);
if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
}
// Request repaint to update previous content.
ctx.request_repaint();
}
// Handle Esc keyboard key event and platform Back button key event.
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
if back_pressed
|| ctx.input_mut(|i| {
i.consume_key(Modifiers::NONE, egui::Key::Escape)
|| i.consume_key(Modifiers::NONE, egui::Key::BrowserBack)
}) {
// Pass event to content.
self.content.on_back(ctx, &self.platform);
if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
}
// Request repaint to update previous content.
ctx.request_repaint();
}
// Handle Close event on desktop.
if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
if !self.content.exit_allowed {
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
Content::show_exit_modal();
} else {
let (w, h) = View::window_size(ctx);
AppConfig::save_window_size(w, h);
ctx.input(|i| {
if let Some(rect) = i.viewport().outer_rect {
AppConfig::save_window_pos(rect.left(), rect.top());
}
});
}
}
// Handle Close event on desktop.
if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
if !self.content.exit_allowed {
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
Content::show_exit_modal();
} else {
let (w, h) = View::window_size(ctx);
AppConfig::save_window_size(w, h);
ctx.input(|i| {
if let Some(rect) = i.viewport().outer_rect {
AppConfig::save_window_pos(rect.left(), rect.top());
}
});
}
}
// Show main content.
egui::CentralPanel::default()
.frame(egui::Frame {
..Default::default()
})
.show(ctx, |ui| {
if View::is_desktop() {
let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
let os = egui::os::OperatingSystem::from_target_os();
match os {
egui::os::OperatingSystem::Mac => {
self.window_title_ui(ui, is_fullscreen);
ui.add_space(-1.0);
Self::title_panel_bg(ui, true);
self.content.ui(ui, &self.platform);
}
egui::os::OperatingSystem::Windows => {
Self::title_panel_bg(ui, false);
self.content.ui(ui, &self.platform);
}
_ => {
self.custom_frame_ui(ui, is_fullscreen);
}
}
} else {
Self::title_panel_bg(ui, false);
self.content.ui(ui, &self.platform);
}
});
// Show main content.
egui::CentralPanel::default()
.frame(egui::Frame {
..Default::default()
})
.show(ctx, |ui| {
if View::is_desktop() {
let is_fullscreen =
ui.ctx().input(|i| i.viewport().fullscreen.unwrap_or(false));
let os = egui::os::OperatingSystem::from_target_os();
match os {
egui::os::OperatingSystem::Mac => {
self.window_title_ui(ui, is_fullscreen);
ui.add_space(-1.0);
Self::title_panel_bg(ui, true);
self.content.ui(ui, &self.platform);
}
egui::os::OperatingSystem::Windows => {
Self::title_panel_bg(ui, false);
self.content.ui(ui, &self.platform);
}
_ => {
self.custom_frame_ui(ui, is_fullscreen);
}
}
} else {
Self::title_panel_bg(ui, false);
self.content.ui(ui, &self.platform);
}
});
// Check if desktop window was focused after requested attention.
if self.platform.user_attention_required() &&
ctx.input(|i| i.viewport().focused.unwrap_or(true)) {
self.platform.clear_user_attention();
}
// Check if desktop window was focused after requested attention.
if self.platform.user_attention_required()
&& ctx.input(|i| i.viewport().focused.unwrap_or(true))
{
self.platform.clear_user_attention();
}
// Show modal or keyboard window above opened Modal.
if Modal::opened().is_some() {
ctx.move_to_top(LayerId::new(Order::Middle, egui::Id::new(Modal::WINDOW_ID)));
let keyboard_showing = if let Some(l) = ctx.top_layer_id() {
l.id == egui::Id::new(KeyboardContent::WINDOW_ID)
} else {
false
};
if keyboard_showing {
ctx.move_to_top(
LayerId::new(Order::Middle, egui::Id::new(KeyboardContent::WINDOW_ID))
);
}
}
// Reset keyboard state for newly opened modal.
if Modal::first_draw() {
KeyboardContent::reset_window_state();
}
}
// Show modal or keyboard window above opened Modal.
if Modal::opened().is_some() {
ctx.move_to_top(LayerId::new(Order::Middle, egui::Id::new(Modal::WINDOW_ID)));
let keyboard_showing = if let Some(l) = ctx.top_layer_id() {
l.id == egui::Id::new(KeyboardContent::WINDOW_ID)
} else {
false
};
if keyboard_showing {
ctx.move_to_top(LayerId::new(
Order::Middle,
egui::Id::new(KeyboardContent::WINDOW_ID),
));
}
}
// Reset keyboard state for newly opened modal.
if Modal::first_draw() {
KeyboardContent::reset_window_state();
}
}
/// Draw custom desktop window frame content.
fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
let content_bg_rect = {
let mut r = ui.max_rect();
if !is_fullscreen {
r = r.shrink(Content::WINDOW_FRAME_MARGIN);
}
r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
r
};
let content_bg = RectShape::new(content_bg_rect,
CornerRadius::ZERO,
Colors::fill_lite(),
View::default_stroke(),
StrokeKind::Outside);
// Draw content background.
ui.painter().add(content_bg);
/// Draw custom desktop window frame content.
fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
let content_bg_rect = {
let mut r = ui.max_rect();
if !is_fullscreen {
r = r.shrink(Content::WINDOW_FRAME_MARGIN);
}
r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
r
};
let content_bg = RectShape::new(
content_bg_rect,
CornerRadius::ZERO,
Colors::fill_lite(),
View::default_stroke(),
StrokeKind::Outside,
);
// Draw content background.
ui.painter().add(content_bg);
let mut content_rect = ui.max_rect();
if !is_fullscreen {
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
}
// Draw window content.
ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| {
// Draw window title.
self.window_title_ui(ui, is_fullscreen);
ui.add_space(-1.0);
let mut content_rect = ui.max_rect();
if !is_fullscreen {
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
}
// Draw window content.
ui.scope_builder(UiBuilder::new().max_rect(content_rect), |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, true);
// Draw title panel background.
Self::title_panel_bg(ui, true);
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);
});
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.
if !is_fullscreen {
self.resize_area_ui(ui, ResizeDirection::North);
self.resize_area_ui(ui, ResizeDirection::East);
self.resize_area_ui(ui, ResizeDirection::South);
self.resize_area_ui(ui, ResizeDirection::West);
self.resize_area_ui(ui, ResizeDirection::NorthWest);
self.resize_area_ui(ui, ResizeDirection::NorthEast);
self.resize_area_ui(ui, ResizeDirection::SouthEast);
self.resize_area_ui(ui, ResizeDirection::SouthWest);
}
}
// Setup resize areas.
if !is_fullscreen {
self.resize_area_ui(ui, ResizeDirection::North);
self.resize_area_ui(ui, ResizeDirection::East);
self.resize_area_ui(ui, ResizeDirection::South);
self.resize_area_ui(ui, ResizeDirection::West);
self.resize_area_ui(ui, ResizeDirection::NorthWest);
self.resize_area_ui(ui, ResizeDirection::NorthEast);
self.resize_area_ui(ui, ResizeDirection::SouthEast);
self.resize_area_ui(ui, ResizeDirection::SouthWest);
}
}
/// Draw title panel background.
fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) {
let title_rect = {
let mut rect = ui.max_rect();
if window_title {
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
}
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
rect
};
let title_bg = RectShape::filled(title_rect, CornerRadius::ZERO, Colors::yellow());
ui.painter().add(title_bg);
}
/// Draw title panel background.
fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) {
let title_rect = {
let mut rect = ui.max_rect();
if window_title {
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
}
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
rect
};
let title_bg = RectShape::filled(title_rect, CornerRadius::ZERO, Colors::yellow());
ui.painter().add(title_bg);
}
/// Draw custom window title content.
fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
let title_rect = {
let mut rect = ui.max_rect();
rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
rect
};
/// Draw custom window title content.
fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
let title_rect = {
let mut rect = ui.max_rect();
rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
rect
};
let title_bg_rect = {
let mut r = title_rect.clone();
r.max.y += TitlePanel::HEIGHT - 1.0;
r
};
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
let window_title_bg = RectShape::new(title_bg_rect, if is_fullscreen || is_mac {
CornerRadius::ZERO
} else {
CornerRadius {
nw: 8.0 as u8,
ne: 8.0 as u8,
sw: 0.0 as u8,
se: 0.0 as u8,
}
}, Colors::yellow_dark(), Stroke::new(1.0, Colors::STROKE), StrokeKind::Outside);
// Draw title background.
ui.painter().add(window_title_bg);
let title_bg_rect = {
let mut r = title_rect.clone();
r.max.y += TitlePanel::HEIGHT - 1.0;
r
};
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
let window_title_bg = RectShape::new(
title_bg_rect,
if is_fullscreen || is_mac {
CornerRadius::ZERO
} else {
CornerRadius {
nw: 8.0 as u8,
ne: 8.0 as u8,
sw: 0.0 as u8,
se: 0.0 as u8,
}
},
Colors::yellow_dark(),
Stroke::new(1.0, Colors::STROKE),
StrokeKind::Outside,
);
// Draw title background.
ui.painter().add(window_title_bg);
let painter = ui.painter();
let painter = ui.painter();
let interact_rect = {
let mut rect = title_rect.clone();
rect.max.x -= 128.0;
rect.min.x += 85.0;
if !is_fullscreen {
rect.min.y += Content::WINDOW_FRAME_MARGIN;
}
rect
};
let title_resp = ui.interact(
interact_rect,
egui::Id::new("window_title"),
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);
}
let interact_rect = {
let mut rect = title_rect.clone();
rect.max.x -= 128.0;
rect.min.x += 85.0;
if !is_fullscreen {
rect.min.y += Content::WINDOW_FRAME_MARGIN;
}
rect
};
let title_resp = ui.interact(
interact_rect,
egui::Id::new("window_title"),
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.
let title_text = format!("Grim {}", crate::VERSION);
painter.text(
title_rect.center(),
egui::Align2::CENTER_CENTER,
title_text,
egui::FontId::proportional(15.0),
Colors::title(true),
);
// Paint the title.
let title_text = format!("Grim {}", crate::VERSION);
painter.text(
title_rect.center(),
egui::Align2::CENTER_CENTER,
title_text,
egui::FontId::proportional(15.0),
Colors::title(true),
);
ui.scope_builder(UiBuilder::new().max_rect(title_rect), |ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
// Draw button to close window.
View::title_button_small(ui, X, |_| {
if Modal::opened().is_none() || Modal::opened_closeable() {
Content::show_exit_modal();
}
});
ui.scope_builder(UiBuilder::new().max_rect(title_rect), |ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
// Draw button to close window.
View::title_button_small(ui, X, |_| {
if Modal::opened().is_none() || Modal::opened_closeable() {
Content::show_exit_modal();
}
});
// Draw fullscreen button.
let fullscreen_icon = if is_fullscreen {
ARROWS_IN
} else {
ARROWS_OUT
};
View::title_button_small(ui, fullscreen_icon, |ui| {
ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
});
// Draw fullscreen button.
let fullscreen_icon = if is_fullscreen { ARROWS_IN } else { ARROWS_OUT };
View::title_button_small(ui, fullscreen_icon, |ui| {
ui.ctx()
.send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
});
// Draw button to minimize window.
View::title_button_small(ui, CARET_DOWN, |ui| {
ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true));
});
// Draw button to minimize window.
View::title_button_small(ui, CARET_DOWN, |ui| {
ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true));
});
// Draw application icon.
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
// Draw button to minimize window.
let use_dark = AppConfig::dark_theme().unwrap_or(false);
let theme_icon = if use_dark {
SUN
} else {
MOON
};
View::title_button_small(ui, theme_icon, |ui| {
AppConfig::set_dark_theme(!use_dark);
crate::setup_visuals(ui.ctx());
});
});
});
});
}
// Draw application icon.
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(
layout_size,
Layout::left_to_right(Align::Center),
|ui| {
// Draw button to minimize window.
let use_dark = AppConfig::dark_theme().unwrap_or(false);
let theme_icon = if use_dark { SUN } else { MOON };
View::title_button_small(ui, theme_icon, |ui| {
AppConfig::set_dark_theme(!use_dark);
crate::setup_visuals(ui.ctx());
});
},
);
});
});
}
/// Setup window resize area.
fn resize_area_ui(&mut self, ui: &egui::Ui, direction: ResizeDirection) {
let mut rect = ui.max_rect();
/// Setup window resize area.
fn resize_area_ui(&mut self, ui: &egui::Ui, direction: ResizeDirection) {
let mut rect = ui.max_rect();
// Setup area id, cursor and area rect based on direction.
let (id, cursor, rect) = match direction {
ResizeDirection::North => ("n", CursorIcon::ResizeNorth, {
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN;
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::East => ("e", CursorIcon::ResizeEast, {
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN;
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::South => ("s", CursorIcon::ResizeSouth, {
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN;
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::West => ("w", CursorIcon::ResizeWest, {
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN;
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::NorthWest => ("nw", CursorIcon::ResizeNorthWest, {
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.max.y + Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::NorthEast => ("ne", CursorIcon::ResizeNorthEast, {
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.y = Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::SouthEast => ("se", CursorIcon::ResizeSouthEast, {
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::SouthWest => ("sw", CursorIcon::ResizeSouthWest, {
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
};
// Setup area id, cursor and area rect based on direction.
let (id, cursor, rect) = match direction {
ResizeDirection::North => ("n", CursorIcon::ResizeNorth, {
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN;
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::East => ("e", CursorIcon::ResizeEast, {
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN;
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::South => ("s", CursorIcon::ResizeSouth, {
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN;
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::West => ("w", CursorIcon::ResizeWest, {
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN;
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::NorthWest => ("nw", CursorIcon::ResizeNorthWest, {
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.max.y + Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::NorthEast => ("ne", CursorIcon::ResizeNorthEast, {
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.y = Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::SouthEast => ("se", CursorIcon::ResizeSouthEast, {
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::SouthWest => ("sw", CursorIcon::ResizeSouthWest, {
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
};
// Setup resize area.
let id = egui::Id::new("window_resize").with(id);
let sense = egui::Sense::drag();
let area_resp = ui.interact(rect, id, sense).on_hover_cursor(cursor);
if area_resp.dragged() {
if self.resize_direction.is_none() {
self.resize_direction = Some(direction.clone());
ui.ctx().send_viewport_cmd(ViewportCommand::BeginResize(direction));
}
}
if area_resp.drag_stopped() {
self.resize_direction = None;
}
}
// Setup resize area.
let id = egui::Id::new("window_resize").with(id);
let sense = egui::Sense::drag();
let area_resp = ui.interact(rect, id, sense).on_hover_cursor(cursor);
if area_resp.dragged() {
if self.resize_direction.is_none() {
self.resize_direction = Some(direction.clone());
ui.ctx()
.send_viewport_cmd(ViewportCommand::BeginResize(direction));
}
}
if area_resp.drag_stopped() {
self.resize_direction = None;
}
}
}
/// To draw with egui`s eframe (for wgpu, glow backends and wasm target).
impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) {
ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
self.ui(ctx);
}
fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) {
ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
self.ui(ctx);
}
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
let os = egui::os::OperatingSystem::from_target_os();
let is_win = os == egui::os::OperatingSystem::Windows;
let is_mac = os == egui::os::OperatingSystem::Mac;
if !View::is_desktop() || is_win || is_mac {
return Colors::fill_lite().to_normalized_gamma_f32();
}
Colors::TRANSPARENT.to_normalized_gamma_f32()
}
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
let os = egui::os::OperatingSystem::from_target_os();
let is_win = os == egui::os::OperatingSystem::Windows;
let is_mac = os == egui::os::OperatingSystem::Mac;
if !View::is_desktop() || is_win || is_mac {
return Colors::fill_lite().to_normalized_gamma_f32();
}
Colors::TRANSPARENT.to_normalized_gamma_f32()
}
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
#[unsafe(no_mangle)]
/// Handle Back key code event from Android.
pub extern "C" fn Java_mw_gri_android_MainActivity_onBack(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) {
BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed);
}
BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed);
}
+132 -161
View File
@@ -38,8 +38,11 @@ const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 50, 30);
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
const BLUE_DARK: Color32 =
Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8);
const BLUE_DARK: Color32 = Color32::from_rgb(
0,
(0x66 as f32 * 1.3 + 0.5) as u8,
(0xE4 as f32 * 1.3 + 0.5) as u8,
);
const FILL: Color32 = Color32::from_gray(244);
const FILL_DARK: Color32 = Color32::from_gray(26);
@@ -80,187 +83,155 @@ const ITEM_HOVER_DARK: Color32 = Color32::from_gray(48);
/// Check if dark theme should be used.
fn use_dark() -> bool {
AppConfig::dark_theme().unwrap_or(false)
AppConfig::dark_theme().unwrap_or(false)
}
impl Colors {
pub const FILL_DEEP: Color32 = Color32::from_gray(238);
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
pub const STROKE: Color32 = Color32::from_gray(200);
pub const FILL_DEEP: Color32 = Color32::from_gray(238);
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 {
if use_dark() {
if black_in_white {
WHITE
} else {
BLACK
}
} else {
if black_in_white {
BLACK
} else {
WHITE
}
}
}
pub fn white_or_black(black_in_white: bool) -> Color32 {
if use_dark() {
if black_in_white { WHITE } else { BLACK }
} else {
if black_in_white { BLACK } else { WHITE }
}
}
pub fn semi_transparent() -> Color32 {
if use_dark() {
DARK_SEMI_TRANSPARENT
} else {
SEMI_TRANSPARENT
}
}
pub fn semi_transparent() -> Color32 {
if use_dark() {
DARK_SEMI_TRANSPARENT
} else {
SEMI_TRANSPARENT
}
}
pub fn gold() -> Color32 {
if use_dark() {
GOLD.gamma_multiply(0.9)
} else {
GOLD
}
}
pub fn gold() -> Color32 {
if use_dark() {
GOLD.gamma_multiply(0.9)
} else {
GOLD
}
}
pub fn gold_dark() -> Color32 {
if use_dark() {
GOLD_DARK.gamma_multiply(0.9)
} else {
GOLD_DARK
}
}
pub fn gold_dark() -> Color32 {
if use_dark() {
GOLD_DARK.gamma_multiply(0.9)
} else {
GOLD_DARK
}
}
pub fn yellow() -> Color32 {
YELLOW
}
pub fn yellow() -> Color32 {
YELLOW
}
pub fn yellow_dark() -> Color32 {
YELLOW_DARK
}
pub fn yellow_dark() -> Color32 {
YELLOW_DARK
}
pub fn green() -> Color32 {
if use_dark() {
GREEN_DARK
} else {
GREEN
}
}
pub fn green() -> Color32 {
if use_dark() { GREEN_DARK } else { GREEN }
}
pub fn red() -> Color32 {
if use_dark() {
RED_DARK
} else {
RED
}
}
pub fn red() -> Color32 {
if use_dark() { RED_DARK } else { RED }
}
pub fn blue() -> Color32 {
if use_dark() {
BLUE_DARK
} else {
BLUE
}
}
pub fn blue() -> Color32 {
if use_dark() { BLUE_DARK } else { BLUE }
}
pub fn fill() -> Color32 {
if use_dark() {
FILL_DARK
} else {
FILL
}
}
pub fn fill() -> Color32 {
if use_dark() { FILL_DARK } else { FILL }
}
pub fn fill_deep() -> Color32 {
if use_dark() {
FILL_DEEP_DARK
} else {
Self::FILL_DEEP
}
}
pub fn fill_deep() -> Color32 {
if use_dark() {
FILL_DEEP_DARK
} else {
Self::FILL_DEEP
}
}
pub fn fill_lite() -> Color32 {
if use_dark() {
FILL_LITE_DARK
} else {
FILL_LITE
}
}
pub fn fill_lite() -> Color32 {
if use_dark() {
FILL_LITE_DARK
} else {
FILL_LITE
}
}
pub fn checkbox() -> Color32 {
if use_dark() {
CHECKBOX_DARK
} else {
CHECKBOX
}
}
pub fn checkbox() -> Color32 {
if use_dark() { CHECKBOX_DARK } else { CHECKBOX }
}
pub fn text(always_light: bool) -> Color32 {
if use_dark() && !always_light {
TEXT_DARK
} else {
TEXT
}
}
pub fn text(always_light: bool) -> Color32 {
if use_dark() && !always_light {
TEXT_DARK
} else {
TEXT
}
}
pub fn text_button() -> Color32 {
if use_dark() {
TEXT_BUTTON_DARK
} else {
TEXT_BUTTON
}
}
pub fn text_button() -> Color32 {
if use_dark() {
TEXT_BUTTON_DARK
} else {
TEXT_BUTTON
}
}
pub fn title(always_light: bool) -> Color32 {
if use_dark() && !always_light {
TITLE_DARK
} else {
TITLE
}
}
pub fn title(always_light: bool) -> Color32 {
if use_dark() && !always_light {
TITLE_DARK
} else {
TITLE
}
}
pub fn gray() -> Color32 {
if use_dark() {
GRAY_DARK
} else {
GRAY
}
}
pub fn gray() -> Color32 {
if use_dark() { GRAY_DARK } else { GRAY }
}
pub fn stroke() -> Color32 {
if use_dark() {
STROKE_DARK
} else {
Self::STROKE
}
}
pub fn stroke() -> Color32 {
if use_dark() {
STROKE_DARK
} else {
Self::STROKE
}
}
pub fn inactive_text() -> Color32 {
if use_dark() {
INACTIVE_TEXT_DARK
} else {
INACTIVE_TEXT
}
}
pub fn inactive_text() -> Color32 {
if use_dark() {
INACTIVE_TEXT_DARK
} else {
INACTIVE_TEXT
}
}
pub fn item_button_text() -> Color32 {
if use_dark() {
ITEM_BUTTON_DARK
} else {
ITEM_BUTTON
}
}
pub fn item_button_text() -> Color32 {
if use_dark() {
ITEM_BUTTON_DARK
} else {
ITEM_BUTTON
}
}
pub fn item_stroke() -> Color32 {
if use_dark() {
ITEM_STROKE_DARK
} else {
ITEM_STROKE
}
}
pub fn item_stroke() -> Color32 {
if use_dark() {
ITEM_STROKE_DARK
} else {
ITEM_STROKE
}
}
pub fn item_hover() -> Color32 {
if use_dark() {
ITEM_HOVER_DARK
} else {
ITEM_HOVER
}
}
}
pub fn item_hover() -> Color32 {
if use_dark() {
ITEM_HOVER_DARK
} else {
ITEM_HOVER
}
}
}
+1 -2
View File
@@ -12,13 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod app;
pub use app::App;
mod colors;
pub use colors::Colors;
pub mod icons;
pub mod platform;
pub mod views;
pub mod icons;
+178 -171
View File
@@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::env;
use std::ffi::OsString;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use lazy_static::lazy_static;
use std::sync::Arc;
use parking_lot::RwLock;
use jni::JNIEnv;
use jni::objects::{JByteArray, JObject, JString, JValue};
@@ -30,210 +30,217 @@ use crate::gui::platform::PlatformCallbacks;
/// Android platform implementation.
#[derive(Clone)]
pub struct Android {
/// Android related state.
android_app: AndroidApp,
/// Android related state.
android_app: AndroidApp,
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
}
impl Android {
/// Create new Android platform instance from provided [`AndroidApp`].
pub fn new(app: AndroidApp) -> Self {
Self {
android_app: app,
ctx: Arc::new(RwLock::new(None)),
}
}
/// Create new Android platform instance from provided [`AndroidApp`].
pub fn new(app: AndroidApp) -> Self {
Self {
android_app: app,
ctx: Arc::new(RwLock::new(None)),
}
}
/// Call Android Activity method with JNI.
pub fn call_java_method(&self, name: &str, s: &str, a: &[JValue]) -> Option<jni::sys::jvalue> {
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
let activity = unsafe {
JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject)
};
if let Ok(result) = env.call_method(activity, name, s, a) {
return Some(result.as_jni().clone());
}
None
}
/// Call Android Activity method with JNI.
pub fn call_java_method(&self, name: &str, s: &str, a: &[JValue]) -> Option<jni::sys::jvalue> {
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
let activity =
unsafe { JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject) };
if let Ok(result) = env.call_method(activity, name, s, a) {
return Some(result.as_jni().clone());
}
None
}
}
impl PlatformCallbacks for Android {
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
fn exit(&self) {
let _ = self.call_java_method("exit", "()V", &[]);
}
fn exit(&self) {
let _ = self.call_java_method("exit", "()V", &[]);
}
fn copy_string_to_buffer(&self, data: String) {
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(data).unwrap();
let _ = self.call_java_method("copyText",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))]);
}
fn copy_string_to_buffer(&self, data: String) {
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(data).unwrap();
let _ = self.call_java_method(
"copyText",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))],
);
}
fn get_string_from_buffer(&self) -> String {
let result = self.call_java_method("pasteText", "()Ljava/lang/String;", &[]).unwrap();
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
let j_object: jni::sys::jobject = unsafe { result.l };
let paste_data: String = unsafe {
env.get_string(JString::from(JObject::from_raw(j_object)).as_ref()).unwrap().into()
};
paste_data
}
fn get_string_from_buffer(&self) -> String {
let result = self
.call_java_method("pasteText", "()Ljava/lang/String;", &[])
.unwrap();
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
let j_object: jni::sys::jobject = unsafe { result.l };
let paste_data: String = unsafe {
env.get_string(JString::from(JObject::from_raw(j_object)).as_ref())
.unwrap()
.into()
};
paste_data
}
fn start_camera(&self) {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
// Start camera.
let _ = self.call_java_method("startCamera", "()V", &[]);
}
fn start_camera(&self) {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
// Start camera.
let _ = self.call_java_method("startCamera", "()V", &[]);
}
fn stop_camera(&self) {
// Stop camera.
let _ = self.call_java_method("stopCamera", "()V", &[]);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
}
fn stop_camera(&self) {
// Stop camera.
let _ = self.call_java_method("stopCamera", "()V", &[]);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
}
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
let r_image = LAST_CAMERA_IMAGE.read();
if r_image.is_some() {
return Some(r_image.clone().unwrap());
}
None
}
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
let r_image = LAST_CAMERA_IMAGE.read();
if r_image.is_some() {
return Some(r_image.clone().unwrap());
}
None
}
fn can_switch_camera(&self) -> bool {
if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) {
let amount = unsafe { res.i };
return amount > 1;
}
false
}
fn can_switch_camera(&self) -> bool {
if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) {
let amount = unsafe { res.i };
return amount > 1;
}
false
}
fn switch_camera(&self) {
let _ = self.call_java_method("switchCamera", "()V", &[]);
}
fn switch_camera(&self) {
let _ = self.call_java_method("switchCamera", "()V", &[]);
}
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
let default_cache = OsString::from(dirs::cache_dir().unwrap());
let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
// File path for Android provider.
file.push("share");
if !file.exists() {
std::fs::create_dir(file.clone())?;
}
file.push(name);
if file.exists() {
std::fs::remove_file(file.clone())?;
}
let mut image = File::create_new(file.clone())?;
image.write_all(data.as_slice())?;
image.sync_all()?;
// Call share modal at system.
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(file.to_str().unwrap()).unwrap();
let _ = self.call_java_method("shareData",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))]);
Ok(())
}
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
let default_cache = OsString::from(dirs::cache_dir().unwrap());
let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
// File path for Android provider.
file.push("share");
if !file.exists() {
std::fs::create_dir(file.clone())?;
}
file.push(name);
if file.exists() {
std::fs::remove_file(file.clone())?;
}
let mut image = File::create_new(file.clone())?;
image.write_all(data.as_slice())?;
image.sync_all()?;
// Call share modal at system.
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(file.to_str().unwrap()).unwrap();
let _ = self.call_java_method(
"shareData",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))],
);
Ok(())
}
fn pick_file(&self) -> Option<String> {
// Clear previous result.
let mut w_path = PICKED_FILE_PATH.write();
*w_path = None;
// Launch file picker.
let _ = self.call_java_method("pickFile", "()V", &[]);
// Return empty string to identify async pick.
Some("".to_string())
}
fn pick_file(&self) -> Option<String> {
// Clear previous result.
let mut w_path = PICKED_FILE_PATH.write();
*w_path = None;
// Launch file picker.
let _ = self.call_java_method("pickFile", "()V", &[]);
// Return empty string to identify async pick.
Some("".to_string())
}
fn pick_folder(&self) -> Option<String> {
// Clear previous result.
let mut w_path = PICKED_FILE_PATH.write();
*w_path = None;
// Launch file picker.
let _ = self.call_java_method("pickFolder", "()V", &[]);
// Return empty string to identify async pick.
Some("".to_string())
}
fn pick_folder(&self) -> Option<String> {
// Clear previous result.
let mut w_path = PICKED_FILE_PATH.write();
*w_path = None;
// Launch file picker.
let _ = self.call_java_method("pickFolder", "()V", &[]);
// Return empty string to identify async pick.
Some("".to_string())
}
fn picked_file(&self) -> Option<String> {
let has_file = {
let r_path = PICKED_FILE_PATH.read();
r_path.is_some()
};
if has_file {
let mut w_path = PICKED_FILE_PATH.write();
let path = Some(w_path.clone().unwrap());
*w_path = None;
return path
}
None
}
fn picked_file(&self) -> Option<String> {
let has_file = {
let r_path = PICKED_FILE_PATH.read();
r_path.is_some()
};
if has_file {
let mut w_path = PICKED_FILE_PATH.write();
let path = Some(w_path.clone().unwrap());
*w_path = None;
return path;
}
None
}
fn request_user_attention(&self) {}
fn request_user_attention(&self) {}
fn user_attention_required(&self) -> bool {
false
}
fn user_attention_required(&self) -> bool {
false
}
fn clear_user_attention(&self) {}
fn clear_user_attention(&self) {}
}
lazy_static! {
/// Last image data from camera.
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
/// Picked file path.
static ref PICKED_FILE_PATH: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
/// Last image data from camera.
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
/// Picked file path.
static ref PICKED_FILE_PATH: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
}
/// Callback from Java code with last entered character from soft keyboard.
#[allow(non_snake_case)]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage(
env: JNIEnv,
_class: JObject,
buff: jni::sys::jbyteArray,
rotation: jni::sys::jint,
env: JNIEnv,
_class: JObject,
buff: jni::sys::jbyteArray,
rotation: jni::sys::jint,
) {
let arr = unsafe { JByteArray::from_raw(buff) };
let image : Vec<u8> = env.convert_byte_array(arr).unwrap();
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((image, rotation as u32));
let arr = unsafe { JByteArray::from_raw(buff) };
let image: Vec<u8> = env.convert_byte_array(arr).unwrap();
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((image, rotation as u32));
}
/// Callback from Java code with picked file path.
#[allow(non_snake_case)]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "C" fn Java_mw_gri_android_MainActivity_onFilePick(
_env: JNIEnv,
_class: JObject,
char: jni::sys::jstring
_env: JNIEnv,
_class: JObject,
char: jni::sys::jstring,
) {
use std::ops::Add;
unsafe {
let j_obj = JString::from_raw(char);
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
match j_str.to_str() {
Ok(str) => {
let mut w_path = PICKED_FILE_PATH.write();
*w_path = Some(w_path.clone().unwrap_or("".to_string()).add(str));
}
Err(_) => {}
}
}
}
use std::ops::Add;
unsafe {
let j_obj = JString::from_raw(char);
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
match j_str.to_str() {
Ok(str) => {
let mut w_path = PICKED_FILE_PATH.write();
*w_path = Some(w_path.clone().unwrap_or("".to_string()).add(str));
}
Err(_) => {}
}
}
}
+270 -263
View File
@@ -12,309 +12,316 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{UserAttentionType, ViewportCommand, WindowLevel};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use rfd::FileDialog;
use std::fs::File;
use std::io::Write;
use std::thread;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use parking_lot::RwLock;
use lazy_static::lazy_static;
use egui::{UserAttentionType, ViewportCommand, WindowLevel};
use rfd::FileDialog;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::thread;
use crate::gui::platform::PlatformCallbacks;
/// Desktop platform related actions.
#[derive(Clone)]
pub struct Desktop {
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
/// Context to repaint content and handle viewport commands.
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.
stop_camera: Arc<AtomicBool>,
/// Cameras amount.
cameras_amount: Arc<AtomicUsize>,
/// Camera index.
camera_index: Arc<AtomicUsize>,
/// Flag to check if camera stop is needed.
stop_camera: Arc<AtomicBool>,
/// Flag to check if attention required after window focusing.
attention_required: Arc<AtomicBool>,
/// Flag to check if attention required after window focusing.
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)),
}
}
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;
// #[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::ApiBackend;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
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;
}
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();
};
}
});
}
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 nokhwa::nokhwa_initialize;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
use nokhwa::utils::ApiBackend;
use nokhwa::query;
use nokhwa::CallbackCamera;
#[allow(dead_code)]
#[cfg(target_os = "macos")]
fn start_camera_capture(
cameras_amount: Arc<AtomicUsize>,
camera_index: Arc<AtomicUsize>,
stop_camera: Arc<AtomicBool>,
) {
use nokhwa::CallbackCamera;
use nokhwa::nokhwa_initialize;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::query;
use nokhwa::utils::ApiBackend;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
// Ask permission to open camera.
nokhwa_initialize(|_| {});
// Ask permission to open camera.
nokhwa_initialize(|_| {});
thread::spawn(move || {
let cameras = query(ApiBackend::Auto).unwrap();
cameras_amount.store(cameras.len(), Ordering::Relaxed);
let index = camera_index.load(Ordering::Relaxed);
if cameras.is_empty() || index >= cameras.len() {
return;
}
// Start camera.
let camera_index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
let camera_callback = CallbackCamera::new(
camera_index,
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate),
|_| {}
);
if let Ok(mut cb) = camera_callback {
if cb.open_stream().is_ok() {
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 image from camera.
if let Ok(frame) = cb.poll_frame() {
let image = frame.decode_image::<RgbFormat>().unwrap();
let mut bytes: Vec<u8> = Vec::new();
let format = image::ImageFormat::Jpeg;
// Convert image to Jpeg format.
image.write_to(&mut std::io::Cursor::new(&mut bytes), format).unwrap();
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((bytes, 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
}
}
}
});
}
thread::spawn(move || {
let cameras = query(ApiBackend::Auto).unwrap();
cameras_amount.store(cameras.len(), Ordering::Relaxed);
let index = camera_index.load(Ordering::Relaxed);
if cameras.is_empty() || index >= cameras.len() {
return;
}
// Start camera.
let camera_index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
let camera_callback = CallbackCamera::new(
camera_index,
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate),
|_| {},
);
if let Ok(mut cb) = camera_callback {
if cb.open_stream().is_ok() {
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 image from camera.
if let Ok(frame) = cb.poll_frame() {
let image = frame.decode_image::<RgbFormat>().unwrap();
let mut bytes: Vec<u8> = Vec::new();
let format = image::ImageFormat::Jpeg;
// Convert image to Jpeg format.
image
.write_to(&mut std::io::Cursor::new(&mut bytes), format)
.unwrap();
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((bytes, 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
}
}
}
});
}
}
impl PlatformCallbacks for Desktop {
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
fn exit(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(ViewportCommand::Close);
}
}
fn exit(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(ViewportCommand::Close);
}
}
fn copy_string_to_buffer(&self, data: String) {
let mut clipboard = arboard::Clipboard::new().unwrap();
clipboard.set_text(data).unwrap();
}
fn copy_string_to_buffer(&self, data: String) {
let mut clipboard = arboard::Clipboard::new().unwrap();
clipboard.set_text(data).unwrap();
}
fn get_string_from_buffer(&self) -> String {
let mut clipboard = arboard::Clipboard::new().unwrap();
clipboard.get_text().unwrap_or("".to_string())
}
fn get_string_from_buffer(&self) -> String {
let mut clipboard = arboard::Clipboard::new().unwrap();
clipboard.get_text().unwrap_or("".to_string())
}
fn start_camera(&self) {
// Clear image.
{
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
}
// Setup stop camera flag.
let stop_camera = self.stop_camera.clone();
stop_camera.store(false, Ordering::Relaxed);
fn start_camera(&self) {
// Clear image.
{
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
}
// Setup stop camera flag.
let stop_camera = self.stop_camera.clone();
stop_camera.store(false, Ordering::Relaxed);
Self::start_camera_capture(self.cameras_amount.clone(),
self.camera_index.clone(),
stop_camera);
}
Self::start_camera_capture(
self.cameras_amount.clone(),
self.camera_index.clone(),
stop_camera,
);
}
fn stop_camera(&self) {
// Stop camera.
self.stop_camera.store(true, Ordering::Relaxed);
}
fn stop_camera(&self) {
// Stop camera.
self.stop_camera.store(true, Ordering::Relaxed);
}
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
let r_image = LAST_CAMERA_IMAGE.read();
if r_image.is_some() {
return r_image.clone();
}
None
}
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
let r_image = LAST_CAMERA_IMAGE.read();
if r_image.is_some() {
return r_image.clone();
}
None
}
fn can_switch_camera(&self) -> bool {
let amount = self.cameras_amount.load(Ordering::Relaxed);
amount > 1
}
fn can_switch_camera(&self) -> bool {
let amount = self.cameras_amount.load(Ordering::Relaxed);
amount > 1
}
fn switch_camera(&self) {
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 switch_camera(&self) {
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> {
let folder = FileDialog::new()
.set_title(t!("share"))
.set_directory(dirs::home_dir().unwrap())
.set_file_name(name.clone())
.save_file();
if let Some(folder) = folder {
let mut image = File::create(folder)?;
image.write_all(data.as_slice())?;
image.sync_all()?;
}
Ok(())
}
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
let folder = FileDialog::new()
.set_title(t!("share"))
.set_directory(dirs::home_dir().unwrap())
.set_file_name(name.clone())
.save_file();
if let Some(folder) = folder {
let mut image = File::create(folder)?;
image.write_all(data.as_slice())?;
image.sync_all()?;
}
Ok(())
}
fn pick_file(&self) -> Option<String> {
let file = FileDialog::new()
.set_title(t!("choose_file"))
.set_directory(dirs::home_dir().unwrap())
.pick_file();
if let Some(file) = file {
return Some(file.to_str().unwrap_or_default().to_string());
}
None
}
fn pick_file(&self) -> Option<String> {
let file = FileDialog::new()
.set_title(t!("choose_file"))
.set_directory(dirs::home_dir().unwrap())
.pick_file();
if let Some(file) = file {
return Some(file.to_str().unwrap_or_default().to_string());
}
None
}
fn pick_folder(&self) -> Option<String> {
let file = FileDialog::new()
.set_title(t!("choose_folder"))
.set_directory(dirs::home_dir().unwrap())
.pick_folder();
if let Some(file) = file {
return Some(file.to_str().unwrap_or_default().to_string());
}
None
}
fn pick_folder(&self) -> Option<String> {
let file = FileDialog::new()
.set_title(t!("choose_folder"))
.set_directory(dirs::home_dir().unwrap())
.pick_folder();
if let Some(file) = file {
return Some(file.to_str().unwrap_or_default().to_string());
}
None
}
fn picked_file(&self) -> Option<String> {
None
}
fn picked_file(&self) -> Option<String> {
None
}
fn request_user_attention(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
// Request attention on taskbar.
ctx.send_viewport_cmd(
ViewportCommand::RequestUserAttention(UserAttentionType::Informational)
);
// Un-minimize window.
if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) {
ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
}
// Focus to window.
if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) {
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
ctx.send_viewport_cmd(ViewportCommand::Focus);
}
ctx.request_repaint();
}
self.attention_required.store(true, Ordering::Relaxed);
}
fn request_user_attention(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
// Request attention on taskbar.
ctx.send_viewport_cmd(ViewportCommand::RequestUserAttention(
UserAttentionType::Informational,
));
// Un-minimize window.
if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) {
ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
}
// Focus to window.
if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) {
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
ctx.send_viewport_cmd(ViewportCommand::Focus);
}
ctx.request_repaint();
}
self.attention_required.store(true, Ordering::Relaxed);
}
fn user_attention_required(&self) -> bool {
self.attention_required.load(Ordering::Relaxed)
}
fn user_attention_required(&self) -> bool {
self.attention_required.load(Ordering::Relaxed)
}
fn clear_user_attention(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(
ViewportCommand::RequestUserAttention(UserAttentionType::Reset)
);
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal));
}
self.attention_required.store(false, Ordering::Relaxed);
}
fn clear_user_attention(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(ViewportCommand::RequestUserAttention(
UserAttentionType::Reset,
));
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal));
}
self.attention_required.store(false, Ordering::Relaxed);
}
}
lazy_static! {
/// Last captured image from started camera.
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
/// Last captured image from started camera.
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
}
+17 -17
View File
@@ -22,20 +22,20 @@ pub mod platform;
pub mod platform;
pub trait PlatformCallbacks {
fn set_context(&mut self, ctx: &egui::Context);
fn exit(&self);
fn copy_string_to_buffer(&self, data: String);
fn get_string_from_buffer(&self) -> String;
fn start_camera(&self);
fn stop_camera(&self);
fn camera_image(&self) -> Option<(Vec<u8>, u32)>;
fn can_switch_camera(&self) -> bool;
fn switch_camera(&self);
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
fn pick_file(&self) -> Option<String>;
fn pick_folder(&self) -> Option<String>;
fn picked_file(&self) -> Option<String>;
fn request_user_attention(&self);
fn user_attention_required(&self) -> bool;
fn clear_user_attention(&self);
}
fn set_context(&mut self, ctx: &egui::Context);
fn exit(&self);
fn copy_string_to_buffer(&self, data: String);
fn get_string_from_buffer(&self) -> String;
fn start_camera(&self);
fn stop_camera(&self);
fn camera_image(&self) -> Option<(Vec<u8>, u32)>;
fn can_switch_camera(&self) -> bool;
fn switch_camera(&self);
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
fn pick_file(&self) -> Option<String>;
fn pick_folder(&self) -> Option<String>;
fn picked_file(&self) -> Option<String>;
fn request_user_attention(&self);
fn user_attention_required(&self) -> bool;
fn clear_user_attention(&self);
}
+429 -389
View File
@@ -22,425 +22,465 @@ use parking_lot::RwLock;
use std::sync::Arc;
use std::thread;
use crate::gui::Colors;
use crate::gui::icons::CAMERA_ROTATE;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{QrScanResult, QrScanState};
use crate::gui::views::View;
use crate::gui::Colors;
use crate::wallet::types::PhraseSize;
use crate::gui::views::{Modal, View};
use crate::wallet::WalletUtils;
use crate::wallet::types::PhraseSize;
/// Camera QR code scanner.
pub struct CameraContent {
/// QR code scanning progress and result.
qr_scan_state: Arc<RwLock<QrScanState>>,
/// Uniform Resources URIs collected from QR code scanning.
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>
/// QR code scanning progress and result.
qr_scan_state: Arc<RwLock<QrScanState>>,
/// Uniform Resources URIs collected from QR code scanning.
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>,
}
impl Default for CameraContent {
fn default() -> Self {
Self {
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
ur_data: Arc::new(RwLock::new(None))
}
}
fn default() -> Self {
Self {
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
ur_data: Arc::new(RwLock::new(None)),
}
}
}
impl CameraContent {
/// Draw camera content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let rect = if let Some(img_data) = cb.camera_image() {
if let Ok(img) = image::load_from_memory(&*img_data.0) {
// Process image to find QR code.
self.scan_qr(&img);
/// Draw camera content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let rect = if let Some(img_data) = cb.camera_image() {
if let Ok(img) = image::load_from_memory(&*img_data.0) {
// Process image to find QR code.
self.scan_qr(&img);
// Draw image.
let img_rect = self.image_ui(ui, img, img_data.1);
// Draw image.
let img_rect = self.image_ui(ui, img, img_data.1);
img_rect
} else {
self.loading_ui(ui)
}
} else {
self.loading_ui(ui)
};
img_rect
} else {
self.loading_ui(ui)
}
} else {
self.loading_ui(ui)
};
// Show UR scan progress.
self.ur_progress_ui(ui, &rect);
// Show UR scan progress.
self.ur_progress_ui(ui, &rect);
// Show button to switch cameras.
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.scope_builder(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();
});
});
}
ui.add_space(12.0);
ui.ctx().request_repaint();
}
// Show button to switch cameras.
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.scope_builder(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();
});
});
}
ui.add_space(6.0);
ui.ctx().request_repaint();
}
/// 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([
if img_size.y > img_size.x {
Pos2::new(0.0, 1.0 - (img_size.x / img_size.y))
} else {
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 modal camera content.
pub fn modal_ui(
&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
mut on_result: impl FnMut(Option<QrScanResult>),
) {
if let Some(result) = self.qr_scan_result() {
on_result(Some(result));
} else {
ui.add_space(6.0);
self.ui(ui, cb);
ui.add_space(6.0);
/// Draw animated QR code scanning progress.
fn ur_progress_ui(&self, ui: &mut egui::Ui, rect: &Rect) {
let show_ur_progress = {
self.ur_data.as_ref().read().is_some()
};
if show_ur_progress {
ui.scope_builder(UiBuilder::new().max_rect(rect.clone()), |ui| {
ui.centered_and_justified(|ui| {
ui.label(RichText::new(format!("{}%", self.ur_progress()))
.size(32.0)
.color(Colors::gold_dark()));
});
});
}
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
/// Draw camera loading progress content.
fn loading_ui(&self, ui: &mut egui::Ui) -> Rect {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
}).response.rect
}
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
on_result(None);
Modal::close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
on_result(None);
});
});
});
ui.add_space(6.0);
}
}
/// Check if image is processing to find QR code.
fn image_processing(&self) -> bool {
let r_scan = self.qr_scan_state.read();
r_scan.image_processing
}
/// 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([
if img_size.y > img_size.x {
Pos2::new(0.0, 1.0 - (img_size.x / img_size.y))
} else {
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
}
/// Get UR scanning progress in percents.
fn ur_progress(&self) -> i32 {
// Setup data.
let r_data = self.ur_data.read();
let (data, total) = r_data.clone().unwrap_or((vec![], 0));
if data.is_empty() {
return 0;
}
// Calculate progress.
let mut complete = 0;
for i in &data {
if !i.is_empty() {
complete += 1;
}
}
(100 * complete / total) as i32
}
/// Draw animated QR code scanning progress.
fn ur_progress_ui(&self, ui: &mut egui::Ui, rect: &Rect) {
let show_ur_progress = { self.ur_data.as_ref().read().is_some() };
if show_ur_progress {
ui.scope_builder(UiBuilder::new().max_rect(rect.clone()), |ui| {
ui.centered_and_justified(|ui| {
ui.label(
RichText::new(format!("{}%", self.ur_progress()))
.size(32.0)
.color(Colors::gold_dark()),
);
});
});
}
}
/// Parse QR code from provided image data.
fn scan_qr(&self, image_data: &DynamicImage) {
// Do not scan when another image is processing.
if self.image_processing() {
return;
}
// Setup scanning flag.
{
let mut w_scan = self.qr_scan_state.write();
w_scan.image_processing = true;
}
/// Draw camera loading progress content.
fn loading_ui(&self, ui: &mut egui::Ui) -> Rect {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
})
.response
.rect
}
let image_data = image_data.clone();
let qr_scan_state = self.qr_scan_state.clone();
let ur_data = self.ur_data.clone();
/// Check if image is processing to find QR code.
fn image_processing(&self) -> bool {
let r_scan = self.qr_scan_state.read();
r_scan.image_processing
}
let on_scan = async move {
// Prepare image data.
let img = image_data.to_luma8();
let mut img: rqrr::PreparedImage<image::GrayImage> = rqrr::PreparedImage::prepare(img);
// Scan and save results.
let grids = img.detect_grids();
if let Some(g) = grids.get(0) {
let mut qr_data = vec![];
if let Ok(_) = g.decode_to(&mut qr_data) {
// Setup scanned data into text.
let text = String::from_utf8(qr_data.clone()).unwrap_or("".to_string());
// Setup current text.
let cur_text = {
let r_scan = qr_scan_state.read();
let text = if let Some(res) = r_scan.qr_scan_result.clone() {
res.text()
} else {
"".to_string()
};
text
};
// Parse non-empty data if parsed text is different from saved.
if !qr_data.is_empty() && (cur_text.is_empty() || text != cur_text) {
let res = Self::parse_qr_code(qr_data);
match res {
QrScanResult::URPart(uri, index, total) => {
// Setup current UR data.
let mut cur_data = {
let r_data = ur_data.read();
let mut cur_data = vec!["".to_string(); total];
if let Some((d, _)) = r_data.clone() {
cur_data = d;
}
cur_data
};
if !cur_data.contains(&uri) {
// Save part of UR data.
{
cur_data.insert(index, uri);
let mut w_data = ur_data.write();
*w_data = Some((cur_data.clone(), total));
}
// Setup UR decoder.
let mut decoder = ur::Decoder::default();
for m in cur_data {
if !m.is_empty() {
if let Ok(_) = decoder.receive(m.as_str()) {
continue;
} else {
break;
}
}
}
// Check if UR data is complete.
if decoder.complete() {
if let Ok(data) = decoder.message() {
// Parse complete data.
let res = Self::parse_qr_code(data.unwrap_or(vec![]));
// Clean UR data.
let mut w_data = ur_data.write();
*w_data = None;
// Save scan result.
let mut w_scan = qr_scan_state.write();
w_scan.qr_scan_result = Some(res);
return;
}
}
}
}
_ => {
// Clean UR data.
let mut w_data = ur_data.write();
*w_data = None;
// Save scan result.
let mut w_scan = qr_scan_state.write();
w_scan.qr_scan_result = Some(res);
return;
}
}
}
}
}
// Reset scanning flag to process again.
{
let mut w_scan = qr_scan_state.write();
w_scan.image_processing = false;
}
};
/// Get UR scanning progress in percents.
fn ur_progress(&self) -> i32 {
// Setup data.
let r_data = self.ur_data.read();
let (data, total) = r_data.clone().unwrap_or((vec![], 0));
if data.is_empty() {
return 0;
}
// Calculate progress.
let mut complete = 0;
for i in &data {
if !i.is_empty() {
complete += 1;
}
}
(100 * complete / total) as i32
}
// Launch scanner at separate thread.
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(on_scan);
});
}
/// Parse QR code from provided image data.
fn scan_qr(&self, image_data: &DynamicImage) {
// Do not scan when another image is processing.
if self.image_processing() {
return;
}
// Setup scanning flag.
{
let mut w_scan = self.qr_scan_state.write();
w_scan.image_processing = true;
}
/// Parse QR code scan result.
fn parse_qr_code(data: Vec<u8>) -> QrScanResult {
// Check if string starts with Grin address prefix.
let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string());
let text = text_string.trim();
if text.starts_with("tgrin") || text.starts_with("grin") {
if SlatepackAddress::try_from(text).is_ok() {
return QrScanResult::Address(ZeroingString::from(text));
}
}
let image_data = image_data.clone();
let qr_scan_state = self.qr_scan_state.clone();
let ur_data = self.ur_data.clone();
// Check if string contains Slatepack message prefix and postfix.
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
return QrScanResult::Slatepack(text.to_string());
}
let on_scan = async move {
// Prepare image data.
let img = image_data.to_luma8();
let mut img: rqrr::PreparedImage<image::GrayImage> = rqrr::PreparedImage::prepare(img);
// Scan and save results.
let grids = img.detect_grids();
if let Some(g) = grids.get(0) {
let mut qr_data = vec![];
if let Ok(_) = g.decode_to(&mut qr_data) {
// Setup scanned data into text.
let text = String::from_utf8(qr_data.clone()).unwrap_or("".to_string());
// Setup current text.
let cur_text = {
let r_scan = qr_scan_state.read();
let text = if let Some(res) = r_scan.qr_scan_result.clone() {
res.text()
} else {
"".to_string()
};
text
};
// Parse non-empty data if parsed text is different from saved.
if !qr_data.is_empty() && (cur_text.is_empty() || text != cur_text) {
let res = Self::parse_qr_code(qr_data);
match res {
QrScanResult::URPart(uri, index, total) => {
// Setup current UR data.
let mut cur_data = {
let r_data = ur_data.read();
let mut cur_data = vec!["".to_string(); total];
if let Some((d, _)) = r_data.clone() {
cur_data = d;
}
cur_data
};
if !cur_data.contains(&uri) {
// Save part of UR data.
{
cur_data.insert(index, uri);
let mut w_data = ur_data.write();
*w_data = Some((cur_data.clone(), total));
}
// Setup UR decoder.
let mut decoder = ur::Decoder::default();
for m in cur_data {
if !m.is_empty() {
if let Ok(_) = decoder.receive(m.as_str()) {
continue;
} else {
break;
}
}
}
// Check if UR data is complete.
if decoder.complete() {
if let Ok(data) = decoder.message() {
// Parse complete data.
let res = Self::parse_qr_code(data.unwrap_or(vec![]));
// Clean UR data.
let mut w_data = ur_data.write();
*w_data = None;
// Save scan result.
let mut w_scan = qr_scan_state.write();
w_scan.qr_scan_result = Some(res);
return;
}
}
}
}
_ => {
// Clean UR data.
let mut w_data = ur_data.write();
*w_data = None;
// Save scan result.
let mut w_scan = qr_scan_state.write();
w_scan.qr_scan_result = Some(res);
return;
}
}
}
}
}
// Reset scanning flag to process again.
{
let mut w_scan = qr_scan_state.write();
w_scan.image_processing = false;
}
};
// Check Uniform Resource data.
// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
if text.starts_with("ur:bytes/") {
let split = text.split("/").collect::<Vec<_>>();
if let Some(index_total) = split.get(1) {
if let Some((index, total)) = index_total.split_once("-") {
let index = index.parse::<usize>();
let total = total.parse::<usize>();
if index.is_ok() && total.is_ok() {
let index = index.unwrap() - 1;
let total = total.unwrap();
return QrScanResult::URPart(text_string, index, total);
}
}
}
}
// Launch scanner at separate thread.
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(on_scan);
});
}
// Check Compact SeedQR format.
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification
if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 {
// Setup words amount.
let total_bits = data.len() * 8;
let checksum_bits = total_bits / 32;
let total_words = (total_bits + checksum_bits) / 11;
// Setup entropy.
let mut entropy = data.clone();
WalletUtils::setup_checksum(&mut entropy);
// Setup bits.
let mut bits = vec![false; entropy.len() * 8];
for i in 0..entropy.len() {
for j in 0..8 {
bits[(i * 8) + j] = (entropy[i] & (1 << (7 - j))) != 0;
}
}
// Extract word index.
let extract_index = |i: usize| -> usize {
let mut index = 0;
for j in 0..11 {
index = index << 1;
if bits[(i * 11) + j] {
index += 1;
}
}
return index;
};
// Setup words.
let mut words = "".to_string();
for n in 0..total_words {
// Setup word index.
let index = extract_index(n);
// Setup word.
let empty_word = "".to_string();
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
if word.is_empty() {
words = empty_word;
break;
}
words = if words.is_empty() {
format!("{}", word)
} else {
format!("{} {}", words, word)
};
}
if !words.is_empty() {
return QrScanResult::SeedQR(ZeroingString::from(words));
}
}
/// Parse QR code scan result.
fn parse_qr_code(data: Vec<u8>) -> QrScanResult {
// Check if string starts with Grin address prefix.
let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string());
let text = text_string.trim();
if text.starts_with("tgrin") || text.starts_with("grin") {
if SlatepackAddress::try_from(text).is_ok() {
return QrScanResult::Address(ZeroingString::from(text));
}
}
// Check Standard SeedQR format.
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification
let only_numbers = || {
for c in text.chars() {
if !c.is_numeric() {
return false;
}
}
true
};
if !text.is_empty() && data.len() <= 96 && data.len() % 4 == 0 && only_numbers() {
if let Some(_) = PhraseSize::type_for_value(text.len() / 4) {
let chars: Vec<char> = text.trim().chars().collect();
let split = &chars.chunks(4)
.map(|chunk| chunk.iter().collect::<String>()
.trim()
.trim_start_matches("0")
.to_string()
)
.collect::<Vec<_>>();
let mut words = "".to_string();
for i in split {
let index = if i.is_empty() {
0usize
} else {
i.parse::<usize>().unwrap_or(WORDS.len())
};
let empty_word = "".to_string();
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
// Return text result when BIP39 word was not found.
if word.is_empty() {
return QrScanResult::Text(ZeroingString::from(text));
}
words = if words.is_empty() {
format!("{}", word)
} else {
format!("{} {}", words, word)
};
}
return QrScanResult::SeedQR(ZeroingString::from(words));
}
}
// Check if string contains Slatepack message prefix and postfix.
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
return QrScanResult::Slatepack(text.to_string());
}
// Return default text result.
QrScanResult::Text(ZeroingString::from(text))
}
// Check Uniform Resource data.
// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
if text.starts_with("ur:bytes/") {
let split = text.split("/").collect::<Vec<_>>();
if let Some(index_total) = split.get(1) {
if let Some((index, total)) = index_total.split_once("-") {
let index = index.parse::<usize>();
let total = total.parse::<usize>();
if index.is_ok() && total.is_ok() {
let index = index.unwrap() - 1;
let total = total.unwrap();
return QrScanResult::URPart(text_string, index, total);
}
}
}
}
/// Get QR code scan result.
pub fn qr_scan_result(&self) -> Option<QrScanResult> {
let r_scan = self.qr_scan_state.read();
if r_scan.qr_scan_result.is_some() {
return Some(r_scan.qr_scan_result.clone().unwrap());
}
None
}
}
// Check Compact SeedQR format.
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification
if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 {
// Setup words amount.
let total_bits = data.len() * 8;
let checksum_bits = total_bits / 32;
let total_words = (total_bits + checksum_bits) / 11;
// Setup entropy.
let mut entropy = data.clone();
WalletUtils::setup_checksum(&mut entropy);
// Setup bits.
let mut bits = vec![false; entropy.len() * 8];
for i in 0..entropy.len() {
for j in 0..8 {
bits[(i * 8) + j] = (entropy[i] & (1 << (7 - j))) != 0;
}
}
// Extract word index.
let extract_index = |i: usize| -> usize {
let mut index = 0;
for j in 0..11 {
index = index << 1;
if bits[(i * 11) + j] {
index += 1;
}
}
return index;
};
// Setup words.
let mut words = "".to_string();
for n in 0..total_words {
// Setup word index.
let index = extract_index(n);
// Setup word.
let empty_word = "".to_string();
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
if word.is_empty() {
words = empty_word;
break;
}
words = if words.is_empty() {
format!("{}", word)
} else {
format!("{} {}", words, word)
};
}
if !words.is_empty() {
return QrScanResult::SeedQR(ZeroingString::from(words));
}
}
// Check Standard SeedQR format.
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification
let only_numbers = || {
for c in text.chars() {
if !c.is_numeric() {
return false;
}
}
true
};
if !text.is_empty() && data.len() <= 96 && data.len() % 4 == 0 && only_numbers() {
if let Some(_) = PhraseSize::type_for_value(text.len() / 4) {
let chars: Vec<char> = text.trim().chars().collect();
let split = &chars
.chunks(4)
.map(|chunk| {
chunk
.iter()
.collect::<String>()
.trim()
.trim_start_matches("0")
.to_string()
})
.collect::<Vec<_>>();
let mut words = "".to_string();
for i in split {
let index = if i.is_empty() {
0usize
} else {
i.parse::<usize>().unwrap_or(WORDS.len())
};
let empty_word = "".to_string();
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
// Return text result when BIP39 word was not found.
if word.is_empty() {
return QrScanResult::Text(ZeroingString::from(text));
}
words = if words.is_empty() {
format!("{}", word)
} else {
format!("{} {}", words, word)
};
}
return QrScanResult::SeedQR(ZeroingString::from(words));
}
}
// Return default text result.
QrScanResult::Text(ZeroingString::from(text))
}
/// Get QR code scan result.
pub fn qr_scan_result(&self) -> Option<QrScanResult> {
let r_scan = self.qr_scan_state.read();
if r_scan.qr_scan_result.is_some() {
return Some(r_scan.qr_scan_result.clone().unwrap());
}
None
}
}
+281 -250
View File
@@ -12,57 +12,57 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::os::OperatingSystem;
use egui::RichText;
use egui::os::OperatingSystem;
use lazy_static::lazy_static;
use std::fs;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::gui::Colors;
use crate::gui::icons::FILE_X;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::NetworkContent;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::wallets::WalletsContent;
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::node::Node;
use crate::{AppConfig, Settings};
lazy_static! {
/// Global state to check if [`NetworkContent`] panel is open.
static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false);
/// Global state to check if [`NetworkContent`] panel is open.
static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false);
}
/// Contains main ui content, handles side panel state.
pub struct Content {
/// Side panel [`NetworkContent`] content.
network: NetworkContent,
/// Side panel [`NetworkContent`] content.
network: NetworkContent,
/// Central panel [`WalletsContent`] content.
wallets: WalletsContent,
/// Central panel [`WalletsContent`] content.
wallets: WalletsContent,
/// Check if app exit is allowed on Desktop close event.
pub exit_allowed: bool,
/// Flag to show exit progress at [`Modal`].
show_exit_progress: bool,
/// Check if app exit is allowed on Desktop close event.
pub exit_allowed: bool,
/// Flag to show exit progress at [`Modal`].
show_exit_progress: bool,
/// Flag to check it's first draw of content.
first_draw: bool,
/// Flag to check it's first draw of content.
first_draw: bool,
}
impl Default for Content {
fn default() -> Self {
// Exit from eframe only for non-mobile platforms.
let os = OperatingSystem::from_target_os();
let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS;
Self {
network: NetworkContent::default(),
wallets: WalletsContent::default(),
exit_allowed,
show_exit_progress: false,
first_draw: true,
}
}
fn default() -> Self {
// Exit from eframe only for non-mobile platforms.
let os = OperatingSystem::from_target_os();
let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS;
Self {
network: NetworkContent::default(),
wallets: WalletsContent::default(),
exit_allowed,
show_exit_progress: false,
first_draw: true,
}
}
}
/// Identifier for integrated node warning [`Modal`] on Android.
@@ -71,250 +71,281 @@ const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warnin
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
impl ContentContainer for Content {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
Self::EXIT_CONFIRMATION_MODAL,
ANDROID_INTEGRATED_NODE_WARNING_MODAL,
CRASH_REPORT_MODAL
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![
Self::EXIT_CONFIRMATION_MODAL,
ANDROID_INTEGRATED_NODE_WARNING_MODAL,
CRASH_REPORT_MODAL,
]
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui),
CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, cb),
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui),
CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, cb),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let dual_panel = Self::is_dual_panel_mode(ui.ctx());
let (is_panel_open, mut panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
if self.network.showing_settings() {
panel_width = ui.available_width();
}
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let dual_panel = Self::is_dual_panel_mode(ui.ctx());
let (is_panel_open, mut panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
if self.network.showing_settings() {
panel_width = ui.available_width();
}
// Show network content.
egui::SidePanel::left("network_panel")
.resizable(false)
.exact_width(panel_width)
.frame(egui::Frame {
..Default::default()
})
.show_animated_inside(ui, is_panel_open, |ui| {
self.network.ui(ui, cb);
});
// Show network content.
egui::SidePanel::left("network_panel")
.resizable(false)
.exact_width(panel_width)
.frame(egui::Frame {
..Default::default()
})
.show_animated_inside(ui, is_panel_open, |ui| {
self.network.ui(ui, cb);
});
// Show wallets content.
egui::CentralPanel::default()
.frame(egui::Frame {
..Default::default()
})
.show_inside(ui, |ui| {
self.wallets.ui(ui, cb);
});
// Show wallets content.
egui::CentralPanel::default()
.frame(egui::Frame {
..Default::default()
})
.show_inside(ui, |ui| {
self.wallets.ui(ui, cb);
});
if self.first_draw {
// Show crash report or integrated node Android warning.
if Settings::crash_report_path().exists() {
Modal::new(CRASH_REPORT_MODAL)
.closeable(false)
.position(ModalPosition::Center)
.title(t!("crash_report"))
.show();
} else if OperatingSystem::from_target_os() == OperatingSystem::Android &&
AppConfig::android_integrated_node_warning_needed() {
Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL)
.title(t!("network.node"))
.show();
}
self.first_draw = false;
}
}
if self.first_draw {
// Show crash report or integrated node Android warning.
if Settings::crash_check_path().exists() {
Modal::new(CRASH_REPORT_MODAL)
.closeable(false)
.position(ModalPosition::Center)
.title(t!("crash_report"))
.show();
} else if OperatingSystem::from_target_os() == OperatingSystem::Android
&& AppConfig::android_integrated_node_warning_needed()
{
Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL)
.title(t!("network.node"))
.show();
}
self.first_draw = false;
}
}
}
impl Content {
/// Default width of side panel at application UI.
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
/// Desktop window title height.
pub const WINDOW_TITLE_HEIGHT: f32 = 38.0;
/// Margin of window frame at desktop.
pub const WINDOW_FRAME_MARGIN: f32 = 6.0;
/// Default width of side panel at application UI.
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
/// Desktop window title height.
pub const WINDOW_TITLE_HEIGHT: f32 = 38.0;
/// Margin of window frame at desktop.
pub const WINDOW_FRAME_MARGIN: f32 = 6.0;
/// Identifier for exit confirmation [`Modal`].
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
/// Identifier for exit confirmation [`Modal`].
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
/// Called to navigate back, return `true` if action was not consumed.
pub fn on_back(&mut self, ctx: &egui::Context, cb: &dyn PlatformCallbacks) -> bool {
if Modal::on_back() {
let dual_panel = Self::is_dual_panel_mode(ctx);
if !dual_panel && Self::is_network_panel_open() {
if self.network.on_back() {
Self::toggle_network_panel();
return false;
}
} else if self.wallets.on_back(cb) {
Self::show_exit_modal();
return false;
}
}
true
}
/// Called to navigate back, return `true` if action was not consumed.
pub fn on_back(&mut self, ctx: &egui::Context, cb: &dyn PlatformCallbacks) -> bool {
if Modal::on_back() {
let dual_panel = Self::is_dual_panel_mode(ctx);
if !dual_panel && Self::is_network_panel_open() {
if self.network.on_back() {
Self::toggle_network_panel();
return false;
}
} else if self.wallets.on_back(cb) {
Self::show_exit_modal();
return false;
}
}
true
}
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool {
let (w, h) = View::window_size(ctx);
// Screen is wide if width is greater than height or just 20% smaller.
let is_wide_screen = w > h || w + (w * 0.2) >= h;
// Dual panel mode is available when window is wide and its width is at least 2 times
// greater than minimal width of the side panel plus display insets from both sides.
let side_insets = View::get_left_inset() + View::get_right_inset();
is_wide_screen && w >= (Self::SIDE_PANEL_WIDTH * 2.0) + side_insets
}
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool {
let (w, h) = View::window_size(ctx);
// Screen is wide if width is greater than height or just 20% smaller.
let is_wide_screen = w > h || w + (w * 0.2) >= h;
// Dual panel mode is available when window is wide and its width is at least 2 times
// greater than minimal width of the side panel plus display insets from both sides.
let side_insets = View::get_left_inset() + View::get_right_inset();
is_wide_screen && w >= (Self::SIDE_PANEL_WIDTH * 2.0) + side_insets
}
/// Toggle [`NetworkContent`] panel state.
pub fn toggle_network_panel() {
let is_open = NETWORK_PANEL_OPEN.load(Ordering::Relaxed);
NETWORK_PANEL_OPEN.store(!is_open, Ordering::Relaxed);
}
/// Toggle [`NetworkContent`] panel state.
pub fn toggle_network_panel() {
let is_open = NETWORK_PANEL_OPEN.load(Ordering::Relaxed);
NETWORK_PANEL_OPEN.store(!is_open, Ordering::Relaxed);
}
/// Check if [`NetworkContent`] panel is open.
pub fn is_network_panel_open() -> bool {
NETWORK_PANEL_OPEN.load(Ordering::Relaxed)
}
/// Check if [`NetworkContent`] panel is open.
pub fn is_network_panel_open() -> bool {
NETWORK_PANEL_OPEN.load(Ordering::Relaxed)
}
/// Show exit confirmation [`Modal`].
pub fn show_exit_modal() {
Modal::new(Self::EXIT_CONFIRMATION_MODAL)
.title(t!("confirmation"))
.show();
}
/// Show exit confirmation [`Modal`].
pub fn show_exit_modal() {
Modal::new(Self::EXIT_CONFIRMATION_MODAL)
.title(t!("confirmation"))
.show();
}
/// Draw exit confirmation modal content.
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
if self.show_exit_progress {
if !Node::is_running() && !Node::data_dir_changing() {
self.exit_allowed = true;
cb.exit();
Modal::close();
}
ui.add_space(16.0);
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(12.0);
let exit_status_text = if Node::data_dir_changing() {
t!("moving_files")
} else {
t!("sync_status.shutdown")
};
ui.label(RichText::new(exit_status_text)
.size(17.0)
.color(Colors::text(false)));
});
ui.add_space(10.0);
} else {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("modal_exit.description"))
.size(17.0)
.color(Colors::text(false)));
});
ui.add_space(10.0);
/// Draw exit confirmation modal content.
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
if self.show_exit_progress {
if !Node::is_running() && !Node::data_dir_changing() {
self.exit_allowed = true;
cb.exit();
Modal::close();
}
ui.add_space(16.0);
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(12.0);
let exit_status_text = if Node::data_dir_changing() {
t!("moving_files")
} else {
t!("sync_status.shutdown")
};
ui.label(
RichText::new(exit_status_text)
.size(17.0)
.color(Colors::text(false)),
);
});
ui.add_space(10.0);
} else {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("modal_exit.description"))
.size(17.0)
.color(Colors::text(false)),
);
});
ui.add_space(10.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// 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!("modal.cancel"), Colors::white_or_black(false), || {
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| {
if !Node::is_running() && !Node::data_dir_changing() {
self.exit_allowed = true;
cb.exit();
Modal::close();
} else {
Node::stop(true);
modal.disable_closing();
Modal::set_title(t!("modal_exit.exit"));
self.show_exit_progress = true;
}
});
});
});
ui.add_space(6.0);
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button_ui(
ui,
t!("modal_exit.exit"),
Colors::white_or_black(false),
|_| {
if !Node::is_running() && !Node::data_dir_changing() {
self.exit_allowed = true;
cb.exit();
Modal::close();
} else {
Node::stop(true);
modal.disable_closing();
Modal::set_title(t!("modal_exit.exit"));
self.show_exit_progress = true;
}
},
);
});
});
ui.add_space(6.0);
}
}
/// Draw content for integrated node warning [`Modal`] on Android.
fn android_warning_modal_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network.android_warning"))
.size(16.0)
.color(Colors::text(false)));
});
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
AppConfig::show_android_integrated_node_warning();
Modal::close();
});
});
ui.add_space(6.0);
}
/// Draw content for integrated node warning [`Modal`] on Android.
fn android_warning_modal_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network.android_warning"))
.size(16.0)
.color(Colors::text(false)),
);
});
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
AppConfig::show_android_integrated_node_warning();
Modal::close();
});
});
ui.add_space(6.0);
}
/// Draw content for integrated node warning [`Modal`] on Android.
fn crash_report_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("crash_report_warning"))
.size(16.0)
.color(Colors::text(false)));
ui.add_space(6.0);
// Draw button to share crash report.
let text = format!("{} {}", FILE_X, t!("share"));
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()) {
let name = Settings::CRASH_REPORT_FILE_NAME.to_string();
let _ = cb.share_data(name, data.as_bytes().to_vec());
}
Settings::delete_crash_report();
Modal::close();
});
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
Settings::delete_crash_report();
Modal::close();
});
});
ui.add_space(6.0);
}
/// Draw content for integrated node warning [`Modal`] on Android.
fn crash_report_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("crash_report_warning"))
.size(16.0)
.color(Colors::text(false)),
);
ui.add_space(6.0);
// Draw button to share log file.
let text = format!("{} {}", FILE_X, t!("share"));
View::colored_text_button(
ui,
text,
Colors::blue(),
Colors::white_or_black(false),
|| {
if let Ok(data) = fs::read_to_string(Settings::log_path()) {
let name = Settings::LOG_FILE_NAME.to_string();
let _ = cb.share_data(name, data.as_bytes().to_vec());
}
Settings::delete_crash_check();
Modal::close();
},
);
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Settings::delete_crash_check();
Modal::close();
},
);
});
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)
}
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)
}
+154 -151
View File
@@ -14,179 +14,182 @@
use egui::CornerRadius;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{fs, thread};
use crate::gui::Colors;
use crate::gui::icons::ARCHIVE_BOX;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View;
use crate::gui::Colors;
/// Type of button.
pub enum FilePickContentType {
Button(String), ItemButton(CornerRadius), Tab
Button(String),
ItemButton(CornerRadius),
Tab,
}
/// Button to pick file and parse its data into text.
pub struct FilePickContent {
/// Content type.
content_type: FilePickContentType,
/// Content type.
content_type: FilePickContentType,
/// Flag to check if button is active.
active: bool,
/// Flag to check if button is active.
active: bool,
/// Flag to check if file is picking.
file_picking: Arc<AtomicBool>,
/// Flag to check if file is picking.
file_picking: Arc<AtomicBool>,
/// Flag to check if folder should be picked.
pick_folder: bool,
/// Flag to parse file content after pick.
parse_file: bool,
/// Flag to check if file is parsing.
file_parsing: Arc<AtomicBool>,
/// File parsing result.
file_parsing_result: Arc<RwLock<Option<String>>>,
/// Flag to check if folder should be picked.
pick_folder: bool,
/// Flag to parse file content after pick.
parse_file: bool,
/// Flag to check if file is parsing.
file_parsing: Arc<AtomicBool>,
/// File parsing result.
file_parsing_result: Arc<RwLock<Option<String>>>,
}
impl FilePickContent {
/// Create new content from provided type.
pub fn new(content_type: FilePickContentType) -> Self {
Self {
content_type,
active: false,
file_picking: Arc::new(AtomicBool::new(false)),
pick_folder: false,
parse_file: true,
file_parsing: Arc::new(AtomicBool::new(false)),
file_parsing_result: Arc::new(RwLock::new(None)),
}
}
/// Create new content from provided type.
pub fn new(content_type: FilePickContentType) -> Self {
Self {
content_type,
active: false,
file_picking: Arc::new(AtomicBool::new(false)),
pick_folder: false,
parse_file: true,
file_parsing: Arc::new(AtomicBool::new(false)),
file_parsing_result: Arc::new(RwLock::new(None)),
}
}
/// Pick folder.
pub fn pick_folder(mut self) -> Self {
self.pick_folder = true;
self
}
/// Pick folder.
pub fn pick_folder(mut self) -> Self {
self.pick_folder = true;
self
}
/// Do not parse file content.
pub fn no_parse(mut self) -> Self {
self.parse_file = false;
self
}
/// Do not parse file content.
pub fn no_parse(mut self) -> Self {
self.parse_file = false;
self
}
/// Enable or disable the button.
pub fn set_active(&mut self, active: bool) {
self.active = active;
}
/// Enable or disable the button.
pub fn set_active(&mut self, active: bool) {
self.active = active;
}
/// Draw content with provided callback to return path of the file.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks, pick: impl FnOnce(String)) {
if self.file_picking.load(Ordering::Relaxed) {
View::small_loading_spinner(ui);
// Check file pick result.
if let Some(path) = cb.picked_file() {
self.file_picking.store(false, Ordering::Relaxed);
if !path.is_empty() {
if self.parse_file {
self.parse_file(path);
} else {
pick(path);
}
}
}
} else if self.file_parsing.load(Ordering::Relaxed) {
View::small_loading_spinner(ui);
// Check file parsing result.
let has_result = {
let r_res = self.file_parsing_result.read();
r_res.is_some()
};
if has_result {
let text = {
let r_res = self.file_parsing_result.read();
r_res.clone().unwrap()
};
// Callback on result.
pick(text);
// Clear result.
let mut w_res = self.file_parsing_result.write();
*w_res = None;
self.file_parsing.store(false, Ordering::Relaxed);
}
} else {
// Draw button to pick file.
match &self.content_type {
FilePickContentType::Button(text) => {
let text = format!("{} {}", ARCHIVE_BOX, text);
let text_color = Colors::blue();
let fill = Colors::white_or_black(false);
View::colored_text_button(ui, text, text_color, fill, || {
self.on_file_pick(pick, cb);
});
}
FilePickContentType::ItemButton(r) => {
View::item_button(ui, r.clone(), ARCHIVE_BOX, Some(Colors::blue()), || {
self.on_file_pick(pick, cb);
});
}
FilePickContentType::Tab => {
let active = match self.active {
true => Some(self.file_parsing.load(Ordering::Relaxed) ||
self.file_picking.load(Ordering::Relaxed)),
false => None
};
View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), active, |_| {
self.on_file_pick(pick, cb);
});
}
}
}
}
/// Draw content with provided callback to return path of the file.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks, pick: impl FnOnce(String)) {
if self.file_picking.load(Ordering::Relaxed) {
View::small_loading_spinner(ui);
// Check file pick result.
if let Some(path) = cb.picked_file() {
self.file_picking.store(false, Ordering::Relaxed);
if !path.is_empty() {
if self.parse_file {
self.parse_file(path);
} else {
pick(path);
}
}
}
} else if self.file_parsing.load(Ordering::Relaxed) {
View::small_loading_spinner(ui);
// Check file parsing result.
let has_result = {
let r_res = self.file_parsing_result.read();
r_res.is_some()
};
if has_result {
let text = {
let r_res = self.file_parsing_result.read();
r_res.clone().unwrap()
};
// Callback on result.
pick(text);
// Clear result.
let mut w_res = self.file_parsing_result.write();
*w_res = None;
self.file_parsing.store(false, Ordering::Relaxed);
}
} else {
// Draw button to pick file.
match &self.content_type {
FilePickContentType::Button(text) => {
let text = format!("{} {}", ARCHIVE_BOX, text);
let text_color = Colors::blue();
let fill = Colors::white_or_black(false);
View::colored_text_button(ui, text, text_color, fill, || {
self.on_file_pick(pick, cb);
});
}
FilePickContentType::ItemButton(r) => {
View::item_button(ui, r.clone(), ARCHIVE_BOX, Some(Colors::blue()), || {
self.on_file_pick(pick, cb);
});
}
FilePickContentType::Tab => {
let active = match self.active {
true => Some(
self.file_parsing.load(Ordering::Relaxed)
|| self.file_picking.load(Ordering::Relaxed),
),
false => None,
};
View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), active, |_| {
self.on_file_pick(pick, cb);
});
}
}
}
}
/// Handle pick file request.
fn on_file_pick(&self, on_pick: impl FnOnce(String), cb: &dyn PlatformCallbacks) {
let path = if self.pick_folder {
cb.pick_folder()
} else {
cb.pick_file()
};
if path.is_none() {
return;
}
let path = path.unwrap();
// Wait for asynchronous file pick result if path is empty.
if path.is_empty() {
self.file_picking.store(true, Ordering::Relaxed);
return;
}
// Parse result if needed.
if self.parse_file {
self.parse_file(path);
} else {
on_pick(path);
}
}
/// Handle pick file request.
fn on_file_pick(&self, on_pick: impl FnOnce(String), cb: &dyn PlatformCallbacks) {
let path = if self.pick_folder {
cb.pick_folder()
} else {
cb.pick_file()
};
if path.is_none() {
return;
}
let path = path.unwrap();
// Wait for asynchronous file pick result if path is empty.
if path.is_empty() {
self.file_picking.store(true, Ordering::Relaxed);
return;
}
// Parse result if needed.
if self.parse_file {
self.parse_file(path);
} else {
on_pick(path);
}
}
/// Handle picked file path.
fn parse_file(&self, path: String) {
self.file_parsing.store(true, Ordering::Relaxed);
let result = self.file_parsing_result.clone();
thread::spawn(move || {
if path.ends_with(".gif") {
//TODO: Detect QR codes on GIF file.
} else if path.ends_with(".jpeg") || path.ends_with(".jpg") ||
path.ends_with(".png") {
//TODO: Detect QR codes on image files.
} else {
// Parse file as plain text.
let mut w_res = result.write();
if let Ok(text) = fs::read_to_string(path) {
*w_res = Some(text);
} else {
*w_res = Some("".to_string());
}
}
});
}
}
/// Handle picked file path.
fn parse_file(&self, path: String) {
self.file_parsing.store(true, Ordering::Relaxed);
let result = self.file_parsing_result.clone();
thread::spawn(move || {
if path.ends_with(".gif") {
//TODO: Detect QR codes on GIF file.
} else if path.ends_with(".jpeg") || path.ends_with(".jpg") || path.ends_with(".png") {
//TODO: Detect QR codes on image files.
} else {
// Parse file as plain text.
let mut w_res = result.write();
if let Ok(text) = fs::read_to_string(path) {
*w_res = Some(text);
} else {
*w_res = Some("".to_string());
}
}
});
}
}
+384 -362
View File
@@ -18,436 +18,458 @@ use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::sync::Arc;
use crate::gui::Colors;
use crate::gui::icons::{CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::input::keyboard::KeyboardContent;
use crate::gui::views::{KeyboardEvent, View};
use crate::gui::Colors;
/// Text input content.
pub struct TextEdit {
/// View identifier.
id: egui::Id,
/// Check if input is enabled or disabled.
enabled: bool,
/// Horizontal text centering is needed.
h_center: bool,
/// Focus is needed.
focus: bool,
/// Focus request was passed.
focus_request: bool,
/// Hide letters and draw button to show/hide letters.
password: bool,
/// Show copy button.
copy: bool,
/// Show paste button.
paste: bool,
/// Show button to scan QR code into text.
scan_qr: bool,
/// Scan button was pressed.
pub scan_pressed: bool,
/// Tab or Enter keys were pressed to focus on next line.
pub enter_pressed: bool,
/// Flag to enter only numbers.
numeric: bool,
/// Flag to not show soft keyboard.
no_soft_keyboard: bool,
/// View identifier.
id: egui::Id,
/// Check if input is enabled or disabled.
enabled: bool,
/// Horizontal text centering is needed.
h_center: bool,
/// Focus is needed.
focus: bool,
/// Focus request was passed.
focus_request: bool,
/// Hide letters and draw button to show/hide letters.
password: bool,
/// Show copy button.
copy: bool,
/// Show paste button.
paste: bool,
/// Show button to scan QR code into text.
scan_qr: bool,
/// Scan button was pressed.
pub scan_pressed: bool,
/// Tab or Enter keys were pressed to focus on next line.
pub enter_pressed: bool,
/// Flag to enter only numbers.
numeric: bool,
/// Flag to not show soft keyboard.
no_soft_keyboard: bool,
}
impl TextEdit {
/// Default height of [`egui::TextEdit`] view.
const TEXT_EDIT_HEIGHT: f32 = 42.0;
/// Default height of [`egui::TextEdit`] view.
const TEXT_EDIT_HEIGHT: f32 = 42.0;
pub fn new(id: egui::Id) -> Self {
Self {
id,
enabled: true,
h_center: false,
focus: true,
focus_request: false,
password: false,
copy: false,
paste: false,
scan_qr: false,
scan_pressed: false,
enter_pressed: false,
numeric: false,
no_soft_keyboard: is_android(),
}
}
pub fn new(id: egui::Id) -> Self {
Self {
id,
enabled: true,
h_center: false,
focus: true,
focus_request: false,
password: false,
copy: false,
paste: false,
scan_qr: false,
scan_pressed: false,
enter_pressed: false,
numeric: false,
no_soft_keyboard: is_android(),
}
}
/// Draw text input.
pub fn ui(&mut self, ui: &mut egui::Ui, input: &mut String, cb: &dyn PlatformCallbacks) {
self.input_ui(ui, input, |_| {}, cb);
}
/// Draw text input.
pub fn ui(&mut self, ui: &mut egui::Ui, input: &mut String, cb: &dyn PlatformCallbacks) {
self.input_ui(ui, input, |_| {}, cb);
}
/// Draw text input with additional buttons (right to left order).
pub fn custom_buttons_ui(&mut self,
ui: &mut egui::Ui,
input: &mut String,
cb: &dyn PlatformCallbacks,
buttons_content: impl FnOnce(&mut egui::Ui)) {
self.input_ui(ui, input, buttons_content, cb);
}
/// Draw text input with additional buttons (right to left order).
pub fn custom_buttons_ui(
&mut self,
ui: &mut egui::Ui,
input: &mut String,
cb: &dyn PlatformCallbacks,
buttons_content: impl FnOnce(&mut egui::Ui),
) {
self.input_ui(ui, input, buttons_content, cb);
}
/// Draw text input content.
fn input_ui(&mut self,
ui: &mut egui::Ui,
input: &mut String,
buttons_content: impl FnOnce(&mut egui::Ui),
cb: &dyn PlatformCallbacks) {
let mut layout_rect = ui.available_rect_before_wrap();
layout_rect.set_height(Self::TEXT_EDIT_HEIGHT);
ui.allocate_ui_with_layout(layout_rect.size(), Layout::right_to_left(Align::Max), |ui| {
let mut hide_input = false;
if self.password {
let show_pass_id = egui::Id::new(self.id).with("_show_pass");
hide_input = ui.data(|data| {
data.get_temp(show_pass_id)
}).unwrap_or(true);
// Draw button to show/hide current password.
let eye_icon = if hide_input { EYE } else { EYE_SLASH };
View::button_ui(ui, eye_icon.to_string(), Colors::white_or_black(false), |ui| {
hide_input = !hide_input;
ui.data_mut(|data| {
data.insert_temp(show_pass_id, hide_input);
});
});
ui.add_space(8.0);
}
/// Draw text input content.
fn input_ui(
&mut self,
ui: &mut egui::Ui,
input: &mut String,
buttons_content: impl FnOnce(&mut egui::Ui),
cb: &dyn PlatformCallbacks,
) {
let mut layout_rect = ui.available_rect_before_wrap();
layout_rect.set_height(Self::TEXT_EDIT_HEIGHT);
ui.allocate_ui_with_layout(
layout_rect.size(),
Layout::right_to_left(Align::Max),
|ui| {
let mut hide_input = false;
if self.password {
let show_pass_id = egui::Id::new(self.id).with("_show_pass");
hide_input = ui.data(|data| data.get_temp(show_pass_id)).unwrap_or(true);
// Draw button to show/hide current password.
let eye_icon = if hide_input { EYE } else { EYE_SLASH };
View::button_ui(
ui,
eye_icon.to_string(),
Colors::white_or_black(false),
|ui| {
hide_input = !hide_input;
ui.data_mut(|data| {
data.insert_temp(show_pass_id, hide_input);
});
},
);
ui.add_space(8.0);
}
// Extra buttons content.
(buttons_content)(ui);
// Extra buttons content.
(buttons_content)(ui);
// Setup copy button.
if self.copy {
let copy_icon = COPY.to_string();
View::button(ui, copy_icon, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(input.clone());
});
ui.add_space(8.0);
}
// Setup copy button.
if self.copy {
let copy_icon = COPY.to_string();
View::button(ui, copy_icon, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(input.clone());
});
ui.add_space(8.0);
}
// Setup paste button.
if self.paste {
let paste_icon = CLIPBOARD_TEXT.to_string();
View::button(ui, paste_icon, Colors::white_or_black(false), || {
*input = cb.get_string_from_buffer();
});
ui.add_space(8.0);
}
// Setup paste button.
if self.paste {
let paste_icon = CLIPBOARD_TEXT.to_string();
View::button(ui, paste_icon, Colors::white_or_black(false), || {
*input = cb.get_string_from_buffer();
});
ui.add_space(8.0);
}
// Setup scan QR code button.
if self.scan_qr {
let scan_icon = SCAN.to_string();
View::button(ui, scan_icon, Colors::white_or_black(false), || {
cb.start_camera();
self.scan_pressed = true;
});
ui.add_space(8.0);
}
// Setup scan QR code button.
if self.scan_qr {
let scan_icon = SCAN.to_string();
View::button(ui, scan_icon, Colors::white_or_black(false), || {
cb.start_camera();
self.scan_pressed = true;
});
ui.add_space(8.0);
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Min), |ui| {
// Setup text edit size.
let mut edit_rect = ui.available_rect_before_wrap();
edit_rect.set_height(Self::TEXT_EDIT_HEIGHT);
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Min), |ui| {
// Setup text edit size.
let mut edit_rect = ui.available_rect_before_wrap();
edit_rect.set_height(Self::TEXT_EDIT_HEIGHT);
// Setup focused input value to avoid dismiss when click on keyboard.
let focused_input_id = egui::Id::new("focused_input_id");
let focused = ui.data(|data| {
data.get_temp(focused_input_id)
}).unwrap_or(egui::Id::new("")) == self.id;
// Setup focused input value to avoid dismiss when click on keyboard.
let focused_input_id = egui::Id::new("focused_input_id");
let focused = ui
.data(|data| data.get_temp(focused_input_id))
.unwrap_or(egui::Id::new(""))
== self.id;
// Show text edit.
let text_edit_resp = egui::TextEdit::singleline(input)
.text_color(if self.enabled {
Colors::text(false)
} else {
Colors::inactive_text()
})
.interactive(self.enabled)
.id(self.id)
.font(TextStyle::Heading)
.min_size(edit_rect.size())
.margin(if View::is_desktop() {
egui::Margin::symmetric(4, 2)
} else {
egui::Margin::symmetric(8, 8)
})
.horizontal_align(if self.h_center { Align::Center } else { Align::Min })
.vertical_align(Align::Center)
.password(hide_input)
.cursor_at_end(true)
.ui(ui);
// Show text edit.
let text_edit_resp = egui::TextEdit::singleline(input)
.text_color(if self.enabled {
Colors::text(false)
} else {
Colors::inactive_text()
})
.interactive(self.enabled)
.id(self.id)
.font(TextStyle::Heading)
.min_size(edit_rect.size())
.margin(if View::is_desktop() {
egui::Margin::symmetric(4, 2)
} else {
egui::Margin::symmetric(8, 8)
})
.horizontal_align(if self.h_center {
Align::Center
} else {
Align::Min
})
.vertical_align(Align::Center)
.password(hide_input)
.cursor_at_end(true)
.ui(ui);
// Setup focus state.
let clicked = text_edit_resp.clicked();
if !text_edit_resp.has_focus() &&
(self.focus || self.focus_request || clicked || focused) {
text_edit_resp.request_focus();
}
// Setup focus state.
let clicked = text_edit_resp.clicked();
if !text_edit_resp.has_focus()
&& (self.focus || self.focus_request || clicked || focused)
{
text_edit_resp.request_focus();
}
// Reset keyboard state for newly focused.
if clicked || self.focus_request {
ui.ctx().send_viewport_cmd(ViewportCommand::IMEAllowed(true));
KeyboardContent::reset_window_state();
}
// Reset keyboard state for newly focused.
if clicked || self.focus_request {
ui.ctx()
.send_viewport_cmd(ViewportCommand::IMEAllowed(true));
KeyboardContent::reset_window_state();
}
// Apply text from software input.
if text_edit_resp.has_focus() {
ui.data_mut(|data| {
data.insert_temp(focused_input_id, self.id);
});
self.enter_pressed = self.on_soft_input(ui, self.id, false, input);
// Check Enter or Tab keys press.
if !self.focus_request {
if ui.ctx().input(|i| i.key_pressed(egui::Key::Enter) ||
i.key_pressed(egui::Key::Tab)) {
self.enter_pressed = true;
}
}
if self.enter_pressed {
KeyboardContent::unshift();
}
if !self.no_soft_keyboard {
KeyboardContent::default().window_ui(self.numeric, ui.ctx());
}
}
});
});
// Immediate repaint when input is open.
ui.ctx().request_repaint();
}
// Apply text from software input.
if text_edit_resp.has_focus() {
ui.data_mut(|data| {
data.insert_temp(focused_input_id, self.id);
});
self.enter_pressed = self.on_soft_input(ui, self.id, false, input);
// Check Enter or Tab keys press.
if !self.focus_request {
if ui.ctx().input(|i| {
i.key_pressed(egui::Key::Enter) || i.key_pressed(egui::Key::Tab)
}) {
self.enter_pressed = true;
}
}
if self.enter_pressed {
KeyboardContent::unshift();
}
if !self.no_soft_keyboard {
KeyboardContent::default().window_ui(self.numeric, ui.ctx());
}
}
});
},
);
// Immediate repaint when input is open.
ui.ctx().request_repaint();
}
/// Apply soft keyboard input data to provided String, returns `true` if Enter was pressed.
fn on_soft_input(&self, ui: &mut egui::Ui, id: egui::Id, multiline: bool, value: &mut String)
-> bool {
let event: Option<KeyboardEvent> = if is_android() {
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
w_input.take()
} else {
KeyboardContent::consume_event()
};
/// Apply soft keyboard input data to provided String, returns `true` if Enter was pressed.
fn on_soft_input(
&self,
ui: &mut egui::Ui,
id: egui::Id,
multiline: bool,
value: &mut String,
) -> bool {
let event: Option<KeyboardEvent> = if is_android() {
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
w_input.take()
} else {
KeyboardContent::consume_event()
};
// Handle keyboard input event.
if let Some(e) = event {
let mut enter_pressed = false;
let mut state = TextEditState::load(ui.ctx(), id).unwrap();
match state.cursor.char_range() {
None => {}
Some(range) => {
let mut r = range.clone();
let mut index = r.primary.index;
// Handle keyboard input event.
if let Some(e) = event {
let mut enter_pressed = false;
let mut state = TextEditState::load(ui.ctx(), id).unwrap();
match state.cursor.char_range() {
None => {}
Some(range) => {
let mut r = range.clone();
let mut index = r.primary.index;
let selected = r.primary.index != r.secondary.index;
let start_select = f32::min(r.primary.index as f32,
r.secondary.index as f32) as usize;
let end_select = f32::max(r.primary.index as f32,
r.secondary.index as f32) as usize;
match e {
KeyboardEvent::TEXT(text) => {
if selected {
*value = {
let part1: String = value.chars()
.skip(0)
.take(start_select)
.collect();
let part2: String = value.chars()
.skip(end_select)
.take(value.len() - end_select)
.collect();
format!("{}{}{}", part1, text, part2)
};
index = start_select + 1;
} else {
value.insert_text(text.as_str(), index);
index = index + 1;
}
}
KeyboardEvent::CLEAR => {
if selected {
*value = {
let part1: String = value.chars()
.skip(0)
.take(start_select)
.collect();
let part2: String = value.chars()
.skip(end_select)
.take(value.len() - end_select)
.collect();
format!("{}{}", part1, part2)
};
index = start_select;
} else if index != 0 {
*value = {
let part1: String = value.chars()
.skip(0)
.take(index - 1)
.collect();
let part2: String = value.chars()
.skip(index)
.take(value.len() - index)
.collect();
format!("{}{}", part1, part2)
};
index = index - 1;
}
}
KeyboardEvent::ENTER => {
if multiline {
value.insert_text("\n", index);
index = index + 1;
} else {
enter_pressed = true;
}
}
}
// Setup cursor index.
r.primary.index = index;
r.secondary.index = r.primary.index;
let selected = r.primary.index != r.secondary.index;
let start_select =
f32::min(r.primary.index as f32, r.secondary.index as f32) as usize;
let end_select =
f32::max(r.primary.index as f32, r.secondary.index as f32) as usize;
match e {
KeyboardEvent::TEXT(text) => {
if selected {
*value = {
let part1: String =
value.chars().skip(0).take(start_select).collect();
let part2: String = value
.chars()
.skip(end_select)
.take(value.len() - end_select)
.collect();
format!("{}{}{}", part1, text, part2)
};
index = start_select + 1;
} else {
value.insert_text(text.as_str(), index);
index = index + 1;
}
}
KeyboardEvent::CLEAR => {
if selected {
*value = {
let part1: String =
value.chars().skip(0).take(start_select).collect();
let part2: String = value
.chars()
.skip(end_select)
.take(value.len() - end_select)
.collect();
format!("{}{}", part1, part2)
};
index = start_select;
} else if index != 0 {
*value = {
let part1: String =
value.chars().skip(0).take(index - 1).collect();
let part2: String = value
.chars()
.skip(index)
.take(value.len() - index)
.collect();
format!("{}{}", part1, part2)
};
index = index - 1;
}
}
KeyboardEvent::ENTER => {
if multiline {
value.insert_text("\n", index);
index = index + 1;
} else {
enter_pressed = true;
}
}
}
// Setup cursor index.
r.primary.index = index;
r.secondary.index = r.primary.index;
state.cursor.set_char_range(Some(r));
TextEditState::store(state, ui.ctx(), id);
}
}
return enter_pressed;
}
false
}
state.cursor.set_char_range(Some(r));
TextEditState::store(state, ui.ctx(), id);
}
}
return enter_pressed;
}
false
}
/// Set cursor to the end of text.
pub fn cursor_to_end(&self, text_len: usize, ui: &mut egui::Ui) {
let mut state = TextEditState::load(ui.ctx(), self.id).unwrap();
match state.cursor.char_range() {
None => {}
Some(range) => {
let mut r = range.clone();
r.primary.index = text_len;
r.secondary.index = text_len;
state.cursor.set_char_range(Some(r));
TextEditState::store(state, ui.ctx(), self.id);
}
}
}
/// Set cursor to the end of text.
pub fn cursor_to_end(&self, text_len: usize, ui: &mut egui::Ui) {
let mut state = TextEditState::load(ui.ctx(), self.id).unwrap();
match state.cursor.char_range() {
None => {}
Some(range) => {
let mut r = range.clone();
r.primary.index = text_len;
r.secondary.index = text_len;
state.cursor.set_char_range(Some(r));
TextEditState::store(state, ui.ctx(), self.id);
}
}
}
/// Disable input.
pub fn disable(mut self) -> Self {
self.enabled = false;
self
}
/// Disable input.
pub fn disable(mut self) -> Self {
self.enabled = false;
self
}
/// Center text horizontally.
pub fn h_center(mut self) -> Self {
self.h_center = true;
self
}
/// Center text horizontally.
pub fn h_center(mut self) -> Self {
self.h_center = true;
self
}
/// Enable or disable constant focus.
pub fn focus(mut self, focus: bool) -> Self {
self.focus = focus;
self
}
/// Enable or disable constant focus.
pub fn focus(mut self, focus: bool) -> Self {
self.focus = focus;
self
}
/// Focus on field.
pub fn focus_request(&mut self) {
self.focus_request = true;
}
/// Focus on field.
pub fn focus_request(&mut self) {
self.focus_request = true;
}
/// Allow input of numbers only.
pub fn numeric(mut self) -> Self {
self.numeric = true;
self
}
/// Allow input of numbers only.
pub fn numeric(mut self) -> Self {
self.numeric = true;
self
}
/// Hide letters and draw button to show/hide letters.
pub fn password(mut self) -> Self {
self.password = true;
self
}
/// Hide letters and draw button to show/hide letters.
pub fn password(mut self) -> Self {
self.password = true;
self
}
/// Show button to copy text.
pub fn copy(mut self) -> Self {
self.copy = true;
self
}
/// Show button to copy text.
pub fn copy(mut self) -> Self {
self.copy = true;
self
}
/// Show button to paste text.
pub fn paste(mut self) -> Self {
self.paste = true;
self
}
/// Show button to paste text.
pub fn paste(mut self) -> Self {
self.paste = true;
self
}
/// Show button to scan QR code to text.
pub fn scan_qr(mut self) -> Self {
self.scan_qr = true;
self.scan_pressed = false;
self
}
/// Show button to scan QR code to text.
pub fn scan_qr(mut self) -> Self {
self.scan_qr = true;
self.scan_pressed = false;
self
}
/// Do not show soft keyboard for input.
pub fn no_soft_keyboard(mut self) -> Self {
self.no_soft_keyboard = true;
self
}
/// Do not show soft keyboard for input.
pub fn no_soft_keyboard(mut self) -> Self {
self.no_soft_keyboard = true;
self
}
}
/// Check if current system is Android.
fn is_android() -> bool {
egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Android
egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Android
}
lazy_static! {
static ref LAST_SOFT_KEYBOARD_EVENT: Arc<RwLock<Option<KeyboardEvent>>> = Arc::new(RwLock::new(None));
static ref LAST_SOFT_KEYBOARD_EVENT: Arc<RwLock<Option<KeyboardEvent>>> =
Arc::new(RwLock::new(None));
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
#[unsafe(no_mangle)]
/// Callback from Java code with last entered character from soft keyboard.
pub extern "C" fn Java_mw_gri_android_MainActivity_onTextInput(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
char: jni::sys::jstring
_env: jni::JNIEnv,
_class: jni::objects::JObject,
char: jni::sys::jstring,
) {
use jni::objects::JString;
use jni::objects::JString;
unsafe {
let j_obj = JString::from_raw(char);
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
match j_str.to_str() {
Ok(str) => {
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
*w_input = Some(KeyboardEvent::TEXT(str.to_string()));
}
Err(_) => {}
}
}
unsafe {
let j_obj = JString::from_raw(char);
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
match j_str.to_str() {
Ok(str) => {
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
*w_input = Some(KeyboardEvent::TEXT(str.to_string()));
}
Err(_) => {}
}
}
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
#[unsafe(no_mangle)]
/// Callback from Java code when Clear key was pressed at soft keyboard.
pub extern "C" fn Java_mw_gri_android_MainActivity_onClearInput(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_env: jni::JNIEnv,
_class: jni::objects::JObject,
) {
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
*w_input = Some(KeyboardEvent::CLEAR);
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
*w_input = Some(KeyboardEvent::CLEAR);
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
#[unsafe(no_mangle)]
/// Callback from Java code when Enter key was pressed at soft keyboard.
pub extern "C" fn Java_mw_gri_android_MainActivity_onEnterInput(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_env: jni::JNIEnv,
_class: jni::objects::JObject,
) {
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
*w_input = Some(KeyboardEvent::ENTER);
}
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
*w_input = Some(KeyboardEvent::ENTER);
}
+489 -449
View File
@@ -12,498 +12,538 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::string::ToString;
use egui::{Align, Align2, Button, Color32, CursorIcon, Layout, Margin, Rect, Response, RichText, Sense, Shadow, Vec2, Widget};
use egui::{
Align, Align2, Button, Color32, CursorIcon, Layout, Margin, Rect, Response, RichText, Sense,
Shadow, Vec2, Widget,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::sync::atomic::Ordering;
use std::string::ToString;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{ARROW_FAT_UP, BACKSPACE, GLOBE_SIMPLE, KEY_RETURN};
use crate::gui::views::{KeyboardEvent, KeyboardLayout, KeyboardState, View};
use crate::gui::Colors;
use crate::AppConfig;
lazy_static! {
/// Keyboard window state.
static ref WINDOW_STATE: Arc<RwLock<KeyboardState >> = Arc::new(
RwLock::new(KeyboardState::default())
);
/// Keyboard window state.
static ref WINDOW_STATE: Arc<RwLock<KeyboardState >> = Arc::new(
RwLock::new(KeyboardState::default())
);
}
/// Software keyboard content.
pub struct KeyboardContent {
/// Keyboard content state.
state: KeyboardState,
/// Keyboard content state.
state: KeyboardState,
}
impl Default for KeyboardContent {
fn default() -> Self {
Self {
state: KeyboardState::default(),
}
}
fn default() -> Self {
Self {
state: KeyboardState::default(),
}
}
}
impl KeyboardContent {
/// Maximum keyboard content width.
const MAX_WIDTH: f32 = 600.0;
/// Maximum numbers layout width.
const MAX_WIDTH_NUMBERS: f32 = 400.0;
/// Maximum keyboard content width.
const MAX_WIDTH: f32 = 600.0;
/// Maximum numbers layout width.
const MAX_WIDTH_NUMBERS: f32 = 400.0;
/// Keyboard window id.
pub const WINDOW_ID: &'static str = "soft_keyboard_window";
/// Keyboard window id.
pub const WINDOW_ID: &'static str = "soft_keyboard_window";
/// Draw keyboard content as separate [`Window`].
pub fn window_ui(&mut self, numeric: bool, ctx: &egui::Context) {
let width = ctx.content_rect().width();
let layer_id = egui::Window::new(Self::WINDOW_ID)
.title_bar(false)
.resizable(false)
.collapsible(false)
.min_width(width)
.default_width(width)
.anchor(Align2::CENTER_BOTTOM, Vec2::new(0.0, 0.0))
.frame(egui::Frame {
shadow: Shadow {
offset: Default::default(),
blur: 30.0 as u8,
spread: 3.0 as u8,
color: Color32::from_black_alpha(32),
},
inner_margin: Margin {
left: View::get_left_inset() as i8,
right: View::get_right_inset() as i8,
top: 1.0 as i8,
bottom: View::get_bottom_inset() as i8,
},
fill: Colors::fill(),
..Default::default()
})
.show(ctx, |ui| {
ui.set_min_width(width);
// Setup state.
{
let r_state = WINDOW_STATE.read();
self.state = (*r_state).clone();
}
// Calculate content width.
let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = width - side_insets;
let w = f32::min(available_width, if numeric {
Self::MAX_WIDTH_NUMBERS
} else {
Self::MAX_WIDTH
});
// Draw content.
View::max_width_ui(ui, w, |ui| {
self.ui(numeric, ui);
});
// Save state.
let mut w_state = WINDOW_STATE.write();
*w_state = self.state.clone();
}).unwrap().response.layer_id;
/// Draw keyboard content as separate [`Window`].
pub fn window_ui(&mut self, numeric: bool, ctx: &egui::Context) {
let width = ctx.content_rect().width();
let layer_id = egui::Window::new(Self::WINDOW_ID)
.title_bar(false)
.resizable(false)
.collapsible(false)
.min_width(width)
.default_width(width)
.anchor(Align2::CENTER_BOTTOM, Vec2::new(0.0, 0.0))
.frame(egui::Frame {
shadow: Shadow {
offset: Default::default(),
blur: 30.0 as u8,
spread: 3.0 as u8,
color: Color32::from_black_alpha(32),
},
inner_margin: Margin {
left: View::get_left_inset() as i8,
right: View::get_right_inset() as i8,
top: 1.0 as i8,
bottom: View::get_bottom_inset() as i8,
},
fill: Colors::fill(),
..Default::default()
})
.show(ctx, |ui| {
ui.set_min_width(width);
// Setup state.
{
let r_state = WINDOW_STATE.read();
self.state = (*r_state).clone();
}
// Calculate content width.
let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = width - side_insets;
let w = f32::min(
available_width,
if numeric {
Self::MAX_WIDTH_NUMBERS
} else {
Self::MAX_WIDTH
},
);
// Draw content.
View::max_width_ui(ui, w, |ui| {
self.ui(numeric, ui);
});
// Save state.
let mut w_state = WINDOW_STATE.write();
*w_state = self.state.clone();
})
.unwrap()
.response
.layer_id;
// Always show keyboard above others windows.
ctx.move_to_top(layer_id);
}
// Always show keyboard above others windows.
ctx.move_to_top(layer_id);
}
/// Draw keyboard content.
pub fn ui(&mut self, numeric: bool, ui: &mut egui::Ui) {
// Setup layout.
if numeric {
self.state.layout = Arc::new(KeyboardLayout::NUMBERS);
} else if *self.state.layout == KeyboardLayout::NUMBERS {
self.state.layout = Arc::new(KeyboardLayout::TEXT);
}
/// Draw keyboard content.
pub fn ui(&mut self, numeric: bool, ui: &mut egui::Ui) {
// Setup layout.
if numeric {
self.state.layout = Arc::new(KeyboardLayout::NUMBERS);
} else if *self.state.layout == KeyboardLayout::NUMBERS {
self.state.layout = Arc::new(KeyboardLayout::TEXT);
}
// Setup spacing between buttons.
ui.style_mut().spacing.item_spacing = egui::vec2(0.0, 0.0);
// Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(0.0, if numeric {
12.0
} else {
10.0
});
// Setup spacing between buttons.
ui.style_mut().spacing.item_spacing = egui::vec2(0.0, 0.0);
// Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(0.0, if numeric { 12.0 } else { 10.0 });
// Draw input buttons.
let button_rect = match *self.state.layout {
KeyboardLayout::TEXT => self.text_ui(ui),
KeyboardLayout::SYMBOLS => self.symbols_ui(ui),
KeyboardLayout::NUMBERS => self.numbers_ui(ui),
};
// Draw input buttons.
let button_rect = match *self.state.layout {
KeyboardLayout::TEXT => self.text_ui(ui),
KeyboardLayout::SYMBOLS => self.symbols_ui(ui),
KeyboardLayout::NUMBERS => self.numbers_ui(ui),
};
// Draw bottom keyboard buttons.
let bottom_size = {
let mut r = button_rect.clone();
r.set_width(ui.available_width());
r.size()
};
let button_width = ui.available_width() / match *self.state.layout {
KeyboardLayout::TEXT => 11.0,
KeyboardLayout::SYMBOLS => 10.0,
KeyboardLayout::NUMBERS => 4.0,
};
ui.allocate_ui_with_layout(bottom_size, Layout::right_to_left(Align::Center), |ui| {
match *self.state.layout {
KeyboardLayout::TEXT => {
// Enter key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 2.0);
self.custom_button_ui(KEY_RETURN.to_string(),
Colors::white_or_black(false),
Some(Colors::green()),
ui,
|_, c| {
c.state.last_event =
Arc::new(Some(KeyboardEvent::ENTER));
});
});
// Custom input key.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui("m3", true, ui);
});
// Space key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 5.0);
self.custom_button_ui(" ".to_string(),
Colors::inactive_text(),
None,
ui,
|l, c| {
c.state.last_event =
Arc::new(Some(KeyboardEvent::TEXT(l)));
});
});
// Switch to english and back.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.custom_button_ui(GLOBE_SIMPLE.to_string(),
Colors::text_button(),
Some(Colors::fill_lite()),
ui,
|_, _| {
AppConfig::toggle_english_keyboard()
});
});
// Switch to symbols layout.
self.custom_button_ui("!@ツ".to_string(),
Colors::text_button(),
Some(Colors::fill_lite()),
ui,
|_, c| {
c.state.layout = Arc::new(KeyboardLayout::SYMBOLS);
});
}
KeyboardLayout::SYMBOLS => {
// Enter key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 2.0);
self.custom_button_ui(KEY_RETURN.to_string(),
Colors::white_or_black(false),
Some(Colors::green()),
ui,
|_, c| {
c.state.last_event =
Arc::new(Some(KeyboardEvent::ENTER));
});
});
// Custom input key.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui("", false, ui);
});
// Space key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 4.0);
self.custom_button_ui(" ".to_string(),
Colors::inactive_text(),
None,
ui,
|l, c| {
c.state.last_event =
Arc::new(Some(KeyboardEvent::TEXT(l)));
});
});
// Switch to text layout.
let label = {
let q = t!("keyboard.q", locale = Self::input_locale().as_str());
let w = t!("keyboard.w", locale = Self::input_locale().as_str());
let e = t!("keyboard.e", locale = Self::input_locale().as_str());
format!("{}{}{}", q, w, e).to_uppercase()
};
self.custom_button_ui(label,
Colors::text_button(),
Some(Colors::fill_lite()),
ui,
|_, c| {
c.state.layout = Arc::new(KeyboardLayout::TEXT);
});
}
KeyboardLayout::NUMBERS => {
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 2.0);
self.custom_button_ui(KEY_RETURN.to_string(),
Colors::white_or_black(false),
Some(Colors::green()),
ui,
|_, c| {
c.state.last_event =
Arc::new(Some(KeyboardEvent::ENTER));
});
});
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui("0", true, ui);
});
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui(".", false, ui);
});
}
}
});
}
// Draw bottom keyboard buttons.
let bottom_size = {
let mut r = button_rect.clone();
r.set_width(ui.available_width());
r.size()
};
let button_width = ui.available_width()
/ match *self.state.layout {
KeyboardLayout::TEXT => 11.0,
KeyboardLayout::SYMBOLS => 10.0,
KeyboardLayout::NUMBERS => 4.0,
};
ui.allocate_ui_with_layout(bottom_size, Layout::right_to_left(Align::Center), |ui| {
match *self.state.layout {
KeyboardLayout::TEXT => {
// Enter key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 2.0);
self.custom_button_ui(
KEY_RETURN.to_string(),
Colors::white_or_black(false),
Some(Colors::green()),
ui,
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER));
},
);
});
// Custom input key.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui("m3", true, ui);
});
// Space key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 5.0);
self.custom_button_ui(
" ".to_string(),
Colors::inactive_text(),
None,
ui,
|l, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l)));
},
);
});
// Switch to english and back.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.custom_button_ui(
GLOBE_SIMPLE.to_string(),
Colors::text_button(),
Some(Colors::fill_lite()),
ui,
|_, _| AppConfig::toggle_english_keyboard(),
);
});
// Switch to symbols layout.
self.custom_button_ui(
"!@ツ".to_string(),
Colors::text_button(),
Some(Colors::fill_lite()),
ui,
|_, c| {
c.state.layout = Arc::new(KeyboardLayout::SYMBOLS);
},
);
}
KeyboardLayout::SYMBOLS => {
// Enter key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 2.0);
self.custom_button_ui(
KEY_RETURN.to_string(),
Colors::white_or_black(false),
Some(Colors::green()),
ui,
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER));
},
);
});
// Custom input key.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui("", false, ui);
});
// Space key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 4.0);
self.custom_button_ui(
" ".to_string(),
Colors::inactive_text(),
None,
ui,
|l, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l)));
},
);
});
// Switch to text layout.
let label = {
let q = t!("keyboard.q", locale = Self::input_locale().as_str());
let w = t!("keyboard.w", locale = Self::input_locale().as_str());
let e = t!("keyboard.e", locale = Self::input_locale().as_str());
format!("{}{}{}", q, w, e).to_uppercase()
};
self.custom_button_ui(
label,
Colors::text_button(),
Some(Colors::fill_lite()),
ui,
|_, c| {
c.state.layout = Arc::new(KeyboardLayout::TEXT);
},
);
}
KeyboardLayout::NUMBERS => {
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 2.0);
self.custom_button_ui(
KEY_RETURN.to_string(),
Colors::white_or_black(false),
Some(Colors::green()),
ui,
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER));
},
);
});
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui("0", true, ui);
});
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui(".", false, ui);
});
}
}
});
}
/// Draw numbers content returning button [`Rect`].
fn numbers_ui(&mut self, ui: &mut egui::Ui) -> Rect {
let mut button_rect = ui.available_rect_before_wrap();
let tl_0: Vec<&str> = vec!["1", "2", "3", "+"];
ui.columns(tl_0.len(), |columns| {
for (index, s) in tl_0.iter().enumerate() {
let last = index == tl_0.len() - 1;
button_rect = self.input_button_ui(s, !last, &mut columns[index]);
}
});
/// Draw numbers content returning button [`Rect`].
fn numbers_ui(&mut self, ui: &mut egui::Ui) -> Rect {
let mut button_rect = ui.available_rect_before_wrap();
let tl_0: Vec<&str> = vec!["1", "2", "3", "+"];
ui.columns(tl_0.len(), |columns| {
for (index, s) in tl_0.iter().enumerate() {
let last = index == tl_0.len() - 1;
button_rect = self.input_button_ui(s, !last, &mut columns[index]);
}
});
let tl_1: Vec<&str> = vec!["4", "5", "6", ","];
ui.columns(tl_1.len(), |columns| {
for (index, s) in tl_1.iter().enumerate() {
let last = index == tl_1.len() - 1;
self.input_button_ui(s, !last, &mut columns[index]);
}
});
let tl_1: Vec<&str> = vec!["4", "5", "6", ","];
ui.columns(tl_1.len(), |columns| {
for (index, s) in tl_1.iter().enumerate() {
let last = index == tl_1.len() - 1;
self.input_button_ui(s, !last, &mut columns[index]);
}
});
let tl_2: Vec<&str> = vec!["7", "8", "9", BACKSPACE];
ui.columns(tl_2.len(), |columns| {
for (index, s) in tl_2.iter().enumerate() {
if index == tl_2.len() - 1 {
self.custom_button_ui(BACKSPACE.to_string(),
Colors::red(),
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.last_event =
Arc::new(Some(KeyboardEvent::CLEAR));
});
} else {
self.input_button_ui(s, true, &mut columns[index]);
}
}
});
let tl_2: Vec<&str> = vec!["7", "8", "9", BACKSPACE];
ui.columns(tl_2.len(), |columns| {
for (index, s) in tl_2.iter().enumerate() {
if index == tl_2.len() - 1 {
self.custom_button_ui(
BACKSPACE.to_string(),
Colors::red(),
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR));
},
);
} else {
self.input_button_ui(s, true, &mut columns[index]);
}
}
});
button_rect
}
button_rect
}
/// Draw text content returning button [`Rect`].
fn text_ui(&mut self, ui: &mut egui::Ui) -> Rect {
let mut button_rect = ui.available_rect_before_wrap();
let tl_0: Vec<&str> = vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "01"];
ui.columns(tl_0.len(), |columns| {
for (index, s) in tl_0.iter().enumerate() {
button_rect = self.input_button_ui(s, true, &mut columns[index]);
}
});
/// Draw text content returning button [`Rect`].
fn text_ui(&mut self, ui: &mut egui::Ui) -> Rect {
let mut button_rect = ui.available_rect_before_wrap();
let tl_0: Vec<&str> = vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "01"];
ui.columns(tl_0.len(), |columns| {
for (index, s) in tl_0.iter().enumerate() {
button_rect = self.input_button_ui(s, true, &mut columns[index]);
}
});
let tl_1: Vec<&str> = vec!["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "p1"];
ui.columns(tl_1.len(), |columns| {
for (index, s) in tl_1.iter().enumerate() {
self.input_button_ui(s, true, &mut columns[index]);
}
});
let tl_1: Vec<&str> = vec!["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "p1"];
ui.columns(tl_1.len(), |columns| {
for (index, s) in tl_1.iter().enumerate() {
self.input_button_ui(s, true, &mut columns[index]);
}
});
let tl_2: Vec<&str> = vec!["a", "s", "d", "f", "g", "h", "j", "k", "l", "l1", "l2"];
ui.columns(tl_2.len(), |columns| {
for (index, s) in tl_2.iter().enumerate() {
self.input_button_ui(s, true, &mut columns[index]);
}
});
let tl_2: Vec<&str> = vec!["a", "s", "d", "f", "g", "h", "j", "k", "l", "l1", "l2"];
ui.columns(tl_2.len(), |columns| {
for (index, s) in tl_2.iter().enumerate() {
self.input_button_ui(s, true, &mut columns[index]);
}
});
let tl_3: Vec<&str> =
vec![ARROW_FAT_UP, "z", "x", "c", "v", "b", "n", "m", "m1", "m2", BACKSPACE];
ui.columns(tl_3.len(), |columns| {
for (index, s) in tl_3.iter().enumerate() {
if index == 0 {
let shift = self.state.shift.load(Ordering::Relaxed);
let color = if shift {
Colors::yellow_dark()
} else {
Colors::inactive_text()
};
self.custom_button_ui(ARROW_FAT_UP.to_string(),
color,
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.shift.store(!shift, Ordering::Relaxed);
});
} else if index == tl_3.len() - 1 {
self.custom_button_ui(BACKSPACE.to_string(),
Colors::red(),
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.last_event =
Arc::new(Some(KeyboardEvent::CLEAR));
});
} else {
self.input_button_ui(s, true, &mut columns[index]);
}
}
});
let tl_3: Vec<&str> = vec![
ARROW_FAT_UP,
"z",
"x",
"c",
"v",
"b",
"n",
"m",
"m1",
"m2",
BACKSPACE,
];
ui.columns(tl_3.len(), |columns| {
for (index, s) in tl_3.iter().enumerate() {
if index == 0 {
let shift = self.state.shift.load(Ordering::Relaxed);
let color = if shift {
Colors::yellow_dark()
} else {
Colors::inactive_text()
};
self.custom_button_ui(
ARROW_FAT_UP.to_string(),
color,
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.shift.store(!shift, Ordering::Relaxed);
},
);
} else if index == tl_3.len() - 1 {
self.custom_button_ui(
BACKSPACE.to_string(),
Colors::red(),
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR));
},
);
} else {
self.input_button_ui(s, true, &mut columns[index]);
}
}
});
button_rect
}
button_rect
}
/// Draw symbols content returning button [`Rect`].
fn symbols_ui(&mut self, ui: &mut egui::Ui) -> Rect {
let mut button_rect = ui.available_rect_before_wrap();
let tl_0: Vec<&str> = vec!["[", "]", "{", "}", "#", "%", "^", "*", "+", "="];
ui.columns(tl_0.len(), |columns| {
for (index, s) in tl_0.iter().enumerate() {
button_rect = self.input_button_ui(s, false, &mut columns[index]);
}
});
/// Draw symbols content returning button [`Rect`].
fn symbols_ui(&mut self, ui: &mut egui::Ui) -> Rect {
let mut button_rect = ui.available_rect_before_wrap();
let tl_0: Vec<&str> = vec!["[", "]", "{", "}", "#", "%", "^", "*", "+", "="];
ui.columns(tl_0.len(), |columns| {
for (index, s) in tl_0.iter().enumerate() {
button_rect = self.input_button_ui(s, false, &mut columns[index]);
}
});
let tl_1: Vec<&str> = vec!["_", "\\", "|", "~", "<", ">", "", "", "π", ""];
ui.columns(tl_1.len(), |columns| {
for (index, s) in tl_1.iter().enumerate() {
self.input_button_ui(s, false, &mut columns[index]);
}
});
let tl_1: Vec<&str> = vec!["_", "\\", "|", "~", "<", ">", "", "", "π", ""];
ui.columns(tl_1.len(), |columns| {
for (index, s) in tl_1.iter().enumerate() {
self.input_button_ui(s, false, &mut columns[index]);
}
});
let tl_2: Vec<&str> = vec!["-", "/", ":", ";", "(", ")", "`", "&", "@", "\""];
ui.columns(tl_2.len(), |columns| {
for (index, s) in tl_2.iter().enumerate() {
self.input_button_ui(s, false, &mut columns[index]);
}
});
let tl_2: Vec<&str> = vec!["-", "/", ":", ";", "(", ")", "`", "&", "@", "\""];
ui.columns(tl_2.len(), |columns| {
for (index, s) in tl_2.iter().enumerate() {
self.input_button_ui(s, false, &mut columns[index]);
}
});
let tl_3: Vec<&str> = vec![".", ",", "?", "!", "", "£", "¥", "$", "¢", BACKSPACE];
ui.columns(tl_3.len(), |columns| {
for (index, s) in tl_3.iter().enumerate() {
if index == tl_3.len() - 1 {
self.custom_button_ui(BACKSPACE.to_string(),
Colors::red(),
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.last_event =
Arc::new(Some(KeyboardEvent::CLEAR));
});
} else {
self.input_button_ui(s, false, &mut columns[index]);
}
}
});
let tl_3: Vec<&str> = vec![".", ",", "?", "!", "", "£", "¥", "$", "¢", BACKSPACE];
ui.columns(tl_3.len(), |columns| {
for (index, s) in tl_3.iter().enumerate() {
if index == tl_3.len() - 1 {
self.custom_button_ui(
BACKSPACE.to_string(),
Colors::red(),
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR));
},
);
} else {
self.input_button_ui(s, false, &mut columns[index]);
}
}
});
button_rect
}
button_rect
}
/// Draw custom keyboard button.
fn custom_button_ui(&mut self,
s: String,
color: Color32,
bg: Option<Color32>,
ui: &mut egui::Ui,
cb: impl FnOnce(String, &mut KeyboardContent)) -> Response {
ui.vertical_centered_justified(|ui| {
// Disable expansion on click/hover.
ui.style_mut().visuals.widgets.hovered.expansion = 0.0;
ui.style_mut().visuals.widgets.active.expansion = 0.0;
// Setup fill colors.
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
// Setup stroke colors.
ui.visuals_mut().widgets.inactive.bg_stroke = View::item_stroke();
ui.visuals_mut().widgets.hovered.bg_stroke = View::item_stroke();
ui.visuals_mut().widgets.active.bg_stroke = View::hover_stroke();
/// Draw custom keyboard button.
fn custom_button_ui(
&mut self,
s: String,
color: Color32,
bg: Option<Color32>,
ui: &mut egui::Ui,
cb: impl FnOnce(String, &mut KeyboardContent),
) -> Response {
ui.vertical_centered_justified(|ui| {
// Disable expansion on click/hover.
ui.style_mut().visuals.widgets.hovered.expansion = 0.0;
ui.style_mut().visuals.widgets.active.expansion = 0.0;
// Setup fill colors.
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
// Setup stroke colors.
ui.visuals_mut().widgets.inactive.bg_stroke = View::item_stroke();
ui.visuals_mut().widgets.hovered.bg_stroke = View::item_stroke();
ui.visuals_mut().widgets.active.bg_stroke = View::hover_stroke();
let shift = self.state.shift.load(Ordering::Relaxed);
let label = if shift {
s.to_uppercase()
} else {
s.to_string()
};
let mut button = Button::new(RichText::new(label.clone()).size(18.0).color(color))
.corner_radius(egui::CornerRadius::ZERO);
if let Some(bg) = bg {
button = button.fill(bg);
}
// Setup long press/touch.
let long_press = s == BACKSPACE;
if long_press {
button = button.sense(Sense::click_and_drag());
}
// Draw button.
let resp = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand);
if resp.clicked() || resp.long_touched() || resp.dragged() {
cb(label, self);
}
}).response
}
let shift = self.state.shift.load(Ordering::Relaxed);
let label = if shift {
s.to_uppercase()
} else {
s.to_string()
};
let mut button = Button::new(RichText::new(label.clone()).size(18.0).color(color))
.corner_radius(egui::CornerRadius::ZERO);
if let Some(bg) = bg {
button = button.fill(bg);
}
// Setup long press/touch.
let long_press = s == BACKSPACE;
if long_press {
button = button.sense(Sense::click_and_drag());
}
// Draw button.
let resp = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand);
if resp.clicked() || resp.long_touched() || resp.dragged() {
cb(label, self);
}
})
.response
}
/// Draw input button.
fn input_button_ui(&mut self, s: &str, translate: bool, ui: &mut egui::Ui) -> Rect {
let value = if translate {
t!(format!("keyboard.{}", s), locale = Self::input_locale().as_str()).into()
} else {
s.to_string()
};
let rect = self.custom_button_ui(value, Colors::text_button(), None, ui, |l, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l)));
c.state.shift.store(false, Ordering::Relaxed);
}).rect;
rect
}
/// Draw input button.
fn input_button_ui(&mut self, s: &str, translate: bool, ui: &mut egui::Ui) -> Rect {
let value = if translate {
t!(
format!("keyboard.{}", s),
locale = Self::input_locale().as_str()
)
.into()
} else {
s.to_string()
};
let rect = self
.custom_button_ui(value, Colors::text_button(), None, ui, |l, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l)));
c.state.shift.store(false, Ordering::Relaxed);
})
.rect;
rect
}
/// Get input locale.
fn input_locale() -> String {
let english = AppConfig::english_keyboard();
if english {
"en".to_string()
} else {
AppConfig::locale().unwrap_or("en".to_string())
}
}
/// Get input locale.
fn input_locale() -> String {
let english = AppConfig::english_keyboard();
if english {
"en".to_string()
} else {
AppConfig::locale().unwrap_or("en".to_string())
}
}
/// Check last keyboard input event.
pub fn consume_event() -> Option<KeyboardEvent> {
let empty = {
let r_state = WINDOW_STATE.read();
r_state.last_event.is_none()
};
if !empty {
let mut w_state = WINDOW_STATE.write();
let event = w_state.last_event.as_ref().clone().unwrap();
w_state.last_event = Arc::new(None);
return Some(event);
}
None
}
/// Check last keyboard input event.
pub fn consume_event() -> Option<KeyboardEvent> {
let empty = {
let r_state = WINDOW_STATE.read();
r_state.last_event.is_none()
};
if !empty {
let mut w_state = WINDOW_STATE.write();
let event = w_state.last_event.as_ref().clone().unwrap();
w_state.last_event = Arc::new(None);
return Some(event);
}
None
}
/// Emulate stop of Shift key press.
pub fn unshift() {
let r_state = WINDOW_STATE.read();
r_state.shift.store(false, Ordering::Relaxed);
}
/// Emulate stop of Shift key press.
pub fn unshift() {
let r_state = WINDOW_STATE.read();
r_state.shift.store(false, Ordering::Relaxed);
}
/// Reset keyboard window state.
pub fn reset_window_state() {
let mut w_state = WINDOW_STATE.write();
w_state.layout = Arc::new(KeyboardLayout::TEXT);
// *w_state = KeyboardState::default();
}
}
/// Reset keyboard window state.
pub fn reset_window_state() {
let mut w_state = WINDOW_STATE.write();
w_state.layout = Arc::new(KeyboardLayout::TEXT);
// *w_state = KeyboardState::default();
}
}
+1 -1
View File
@@ -19,4 +19,4 @@ mod edit;
pub use edit::*;
mod keyboard;
pub use keyboard::*;
pub use keyboard::*;
+20 -16
View File
@@ -18,32 +18,36 @@ use std::sync::atomic::AtomicBool;
/// Software keyboard input type.
#[derive(Clone, PartialOrd, PartialEq)]
pub enum KeyboardLayout {
TEXT, SYMBOLS, NUMBERS
TEXT,
SYMBOLS,
NUMBERS,
}
/// Software keyboard input event.
#[derive(Clone)]
pub enum KeyboardEvent {
TEXT(String), CLEAR, ENTER
TEXT(String),
CLEAR,
ENTER,
}
/// Software keyboard Window State.
#[derive(Clone)]
pub struct KeyboardState {
/// Last input event.
pub last_event: Arc<Option<KeyboardEvent>>,
/// Current layout.
pub layout: Arc<KeyboardLayout>,
/// Flag to enter uppercase symbol first.
pub shift: Arc<AtomicBool>,
/// Last input event.
pub last_event: Arc<Option<KeyboardEvent>>,
/// Current layout.
pub layout: Arc<KeyboardLayout>,
/// Flag to enter uppercase symbol first.
pub shift: Arc<AtomicBool>,
}
impl Default for KeyboardState {
fn default() -> Self {
Self {
last_event: Arc::new(None),
layout: Arc::new(KeyboardLayout::TEXT),
shift: Arc::new(AtomicBool::new(false)),
}
}
}
fn default() -> Self {
Self {
last_event: Arc::new(None),
layout: Arc::new(KeyboardLayout::TEXT),
shift: Arc::new(AtomicBool::new(false)),
}
}
}
+2 -2
View File
@@ -27,8 +27,8 @@ mod content;
pub use content::*;
pub mod network;
pub mod wallets;
pub mod settings;
pub mod wallets;
mod camera;
pub use camera::*;
@@ -46,4 +46,4 @@ mod scan;
pub use scan::*;
mod input;
pub use input::*;
pub use input::*;
+318 -302
View File
@@ -17,357 +17,373 @@ use egui::os::OperatingSystem;
use egui::{Align2, Color32, CornerRadius, RichText, Stroke, StrokeKind, UiBuilder, Vec2};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::gui::views::types::{ModalPosition, ModalState};
use crate::gui::views::{Content, View};
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ModalPosition, ModalState};
use crate::gui::views::{Content, View};
lazy_static! {
/// Showing [`Modal`] state to be accessible from different ui parts.
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
/// Showing [`Modal`] state to be accessible from different ui parts.
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
}
/// Modal [`egui::Window`] container.
#[derive(Clone)]
pub struct Modal {
/// Identifier for modal.
pub(crate) id: &'static str,
/// Position on the screen.
pub position: ModalPosition,
/// Flag to check if modal can be closed by keys.
closeable: Arc<AtomicBool>,
/// Title text.
title: Option<String>,
/// Flag to check first content render.
first_draw: Arc<AtomicBool>,
/// Background color.
fill: Color32,
/// Identifier for modal.
pub(crate) id: &'static str,
/// Position on the screen.
pub position: ModalPosition,
/// Flag to check if modal can be closed by keys.
closeable: Arc<AtomicBool>,
/// Title text.
title: Option<String>,
/// Flag to check first content render.
first_draw: Arc<AtomicBool>,
/// Background color.
fill: Option<Color32>,
}
impl Modal {
/// Margin from [`Modal`] window at top/left/right.
const DEFAULT_MARGIN: f32 = 8.0;
/// Maximum width of the content.
const DEFAULT_WIDTH: f32 = Content::SIDE_PANEL_WIDTH - (2.0 * Self::DEFAULT_MARGIN);
/// Modal content [`egui::Window`] id.
pub const WINDOW_ID: &'static str = "modal_window";
/// Margin from [`Modal`] window at top/left/right.
const DEFAULT_MARGIN: f32 = 8.0;
/// Maximum width of the content.
const DEFAULT_WIDTH: f32 = Content::SIDE_PANEL_WIDTH - (2.0 * Self::DEFAULT_MARGIN);
/// Modal content [`egui::Window`] id.
pub const WINDOW_ID: &'static str = "modal_window";
/// Create closeable [`Modal`] with center position.
pub fn new(id: &'static str) -> Self {
Self {
id,
position: ModalPosition::Center,
closeable: Arc::new(AtomicBool::new(true)),
title: None,
first_draw: Arc::new(AtomicBool::new(true)),
fill: Colors::fill(),
}
}
/// Create closeable [`Modal`] with center position.
pub fn new(id: &'static str) -> Self {
Self {
id,
position: ModalPosition::Center,
closeable: Arc::new(AtomicBool::new(true)),
title: None,
first_draw: Arc::new(AtomicBool::new(true)),
fill: None,
}
}
/// Setup position of [`Modal`] on the screen.
pub fn position(mut self, position: ModalPosition) -> Self {
self.position = position;
self
}
/// Setup position of [`Modal`] on the screen.
pub fn position(mut self, position: ModalPosition) -> Self {
self.position = position;
self
}
/// Change [`Modal`] position on the screen.
pub fn change_position(position: ModalPosition) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().position = position;
}
/// Change [`Modal`] position on the screen.
pub fn change_position(position: ModalPosition) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().position = position;
}
/// Close [`Modal`] by clearing its state.
pub fn close() {
let mut w_nav = MODAL_STATE.write();
w_nav.modal = None;
}
/// Close [`Modal`] by clearing its state.
pub fn close() {
let mut w_nav = MODAL_STATE.write();
w_nav.modal = None;
}
/// Setup possibility to close [`Modal`].
pub fn closeable(self, closeable: bool) -> Self {
self.closeable.store(closeable, Ordering::Relaxed);
self
}
/// Setup possibility to close [`Modal`].
pub fn closeable(self, closeable: bool) -> Self {
self.closeable.store(closeable, Ordering::Relaxed);
self
}
/// Disable possibility to close [`Modal`].
pub fn disable_closing(&self) {
self.closeable.store(false, Ordering::Relaxed);
}
/// Disable possibility to close [`Modal`].
pub fn disable_closing(&self) {
self.closeable.store(false, Ordering::Relaxed);
}
/// Enable possibility to close [`Modal`].
pub fn enable_closing(&self) {
self.closeable.store(true, Ordering::Relaxed);
}
/// Enable possibility to close [`Modal`].
pub fn enable_closing(&self) {
self.closeable.store(true, Ordering::Relaxed);
}
/// Check if [`Modal`] is closeable.
pub fn is_closeable(&self) -> bool {
self.closeable.load(Ordering::Relaxed)
}
/// Check if [`Modal`] is closeable.
pub fn is_closeable(&self) -> bool {
self.closeable.load(Ordering::Relaxed)
}
/// Set title text on [`Modal`] creation.
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into().to_uppercase());
self
}
/// Set title text on [`Modal`] creation.
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into().to_uppercase());
self
}
/// Set [`Modal`] instance into state to show at ui.
pub fn show(self) {
let mut w_nav = MODAL_STATE.write();
self.first_draw.store(true, Ordering::Relaxed);
w_nav.modal = Some(self);
}
/// Set [`Modal`] instance into state to show at ui.
pub fn show(self) {
let mut w_nav = MODAL_STATE.write();
self.first_draw.store(true, Ordering::Relaxed);
w_nav.modal = Some(self);
}
/// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed.
/// Return `false` if modal existed in state before call.
pub fn on_back() -> bool {
if Self::opened().is_some() {
if Self::opened_closeable() {
Self::close();
}
return false;
}
true
}
/// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed.
/// Return `false` if modal existed in state before call.
pub fn on_back() -> bool {
if Self::opened().is_some() {
if Self::opened_closeable() {
Self::close();
}
return false;
}
true
}
/// Return identifier of opened [`Modal`].
pub fn opened() -> Option<&'static str> {
// Check if modal is showing.
{
if MODAL_STATE.read().modal.is_none() {
return None;
}
}
/// Return identifier of opened [`Modal`].
pub fn opened() -> Option<&'static str> {
// Check if modal is showing.
{
if MODAL_STATE.read().modal.is_none() {
return None;
}
}
// Get identifier of opened modal.
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
Some(modal.id)
}
// Get identifier of opened modal.
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
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)
}
/// 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`].
pub fn set_title(title: impl Into<String>) {
let mut w_state = MODAL_STATE.write();
if w_state.modal.is_some() {
let mut modal = w_state.modal.clone().unwrap();
modal.title = Some(title.into().to_uppercase());
w_state.modal = Some(modal);
}
}
/// Set title text for current opened [`Modal`].
pub fn set_title(title: impl Into<String>) {
let mut w_state = MODAL_STATE.write();
if w_state.modal.is_some() {
let mut modal = w_state.modal.clone().unwrap();
modal.title = Some(title.into().to_uppercase());
w_state.modal = Some(modal);
}
}
/// Check for first [`Modal`] content rendering.
pub fn first_draw() -> bool {
if Self::opened().is_none() {
return false;
}
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
modal.first_draw.load(Ordering::Relaxed)
}
/// Check for first [`Modal`] content rendering.
pub fn first_draw() -> bool {
if Self::opened().is_none() {
return false;
}
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
modal.first_draw.load(Ordering::Relaxed)
}
pub fn ui(ctx: &egui::Context,
cb: &dyn PlatformCallbacks,
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks)) {
let has_modal = {
MODAL_STATE.read().modal.is_some()
};
if has_modal {
let modal = {
let r_state = MODAL_STATE.read();
r_state.modal.clone().unwrap()
};
modal.window_ui(ctx, cb, add_content);
}
}
pub fn ui(
ctx: &egui::Context,
cb: &dyn PlatformCallbacks,
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks),
) {
let has_modal = { MODAL_STATE.read().modal.is_some() };
if has_modal {
let modal = {
let r_state = MODAL_STATE.read();
r_state.modal.clone().unwrap()
};
modal.window_ui(ctx, cb, add_content);
}
}
/// Draw [`egui::Window`] with provided content.
fn window_ui(&self,
ctx: &egui::Context,
cb: &dyn PlatformCallbacks,
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks)) {
let is_fullscreen = ctx.input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
/// Draw [`egui::Window`] with provided content.
fn window_ui(
&self,
ctx: &egui::Context,
cb: &dyn PlatformCallbacks,
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks),
) {
let is_fullscreen = ctx.input(|i| i.viewport().fullscreen.unwrap_or(false));
// Setup background rect.
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
let bg_rect = if View::is_desktop() && !is_win {
let mut r = ctx.content_rect();
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
if !is_mac && !is_fullscreen {
r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
}
r.min.y += Content::WINDOW_TITLE_HEIGHT;
r
} else {
ctx.content_rect()
};
// Setup background rect.
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
let bg_rect = if View::is_desktop() && !is_win {
let mut r = ctx.content_rect();
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
if !is_mac && !is_fullscreen {
r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
}
r.min.y += Content::WINDOW_TITLE_HEIGHT;
r
} else {
ctx.content_rect()
};
// Draw modal background.
egui::Window::new("modal_bg_window")
.title_bar(false)
.resizable(false)
.collapsible(false)
.fixed_rect(bg_rect)
.frame(egui::Frame {
fill: Colors::semi_transparent(),
..Default::default()
})
.show(ctx, |ui| {
ui.set_min_size(bg_rect.size());
});
// Draw modal background.
egui::Window::new("modal_bg_window")
.title_bar(false)
.resizable(false)
.collapsible(false)
.fixed_rect(bg_rect)
.frame(egui::Frame {
fill: Colors::semi_transparent(),
..Default::default()
})
.show(ctx, |ui| {
ui.set_min_size(bg_rect.size());
});
// Setup width of modal content.
let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = ctx.content_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
let width = f32::min(available_width, Self::DEFAULT_WIDTH);
// Setup width of modal content.
let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = ctx.content_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
let width = f32::min(available_width, Self::DEFAULT_WIDTH);
// Show main content window at given position.
let (content_align, content_offset) = self.modal_position();
egui::Window::new(Self::WINDOW_ID)
.title_bar(false)
.resizable(false)
.collapsible(false)
.min_width(width)
.default_width(width)
.anchor(content_align, content_offset)
.frame(egui::Frame {
shadow: Shadow {
offset: Default::default(),
blur: 30.0 as u8,
spread: 3.0 as u8,
color: egui::Color32::from_black_alpha(32),
},
corner_radius: CornerRadius::same(8.0 as u8),
..Default::default()
})
.show(ctx, |ui| {
if let Some(title) = &self.title {
title_ui(title, ui);
}
self.content_ui(ui, cb, add_content);
});
// Show main content window at given position.
let (content_align, content_offset) = self.modal_position();
egui::Window::new(Self::WINDOW_ID)
.title_bar(false)
.resizable(false)
.collapsible(false)
.min_width(width)
.default_width(width)
.anchor(content_align, content_offset)
.frame(egui::Frame {
shadow: Shadow {
offset: Default::default(),
blur: 30.0 as u8,
spread: 3.0 as u8,
color: egui::Color32::from_black_alpha(32),
},
corner_radius: CornerRadius::same(8.0 as u8),
..Default::default()
})
.show(ctx, |ui| {
if let Some(title) = &self.title {
title_ui(title, ui);
}
self.content_ui(ui, cb, add_content);
});
// Setup first draw flag.
if Self::first_draw() {
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
modal.first_draw.store(false, Ordering::Relaxed);
}
}
// Setup first draw flag.
if Self::first_draw() {
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
modal.first_draw.store(false, Ordering::Relaxed);
}
}
/// Get [`egui::Window`] position based on [`ModalPosition`].
fn modal_position(&self) -> (Align2, Vec2) {
let align = match self.position {
ModalPosition::CenterTop => Align2::CENTER_TOP,
ModalPosition::Center => Align2::CENTER_CENTER
};
/// Get [`egui::Window`] position based on [`ModalPosition`].
fn modal_position(&self) -> (Align2, Vec2) {
let align = match self.position {
ModalPosition::CenterTop => Align2::CENTER_TOP,
ModalPosition::Center => Align2::CENTER_CENTER,
};
let x_align = View::get_left_inset() - View::get_right_inset();
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
let extra_y = if View::is_desktop() && !is_win {
Content::WINDOW_TITLE_HEIGHT + if !is_mac {
Content::WINDOW_FRAME_MARGIN
} else {
0.0
}
} else {
0.0
};
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y;
let x_align = View::get_left_inset() - View::get_right_inset();
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
let extra_y = if View::is_desktop() && !is_win {
Content::WINDOW_TITLE_HEIGHT
+ if !is_mac {
Content::WINDOW_FRAME_MARGIN
} else {
0.0
}
} else {
0.0
};
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y;
let offset = match self.position {
ModalPosition::CenterTop => Vec2::new(x_align, y_align),
ModalPosition::Center => Vec2::new(x_align, 0.0)
};
(align, offset)
}
let offset = match self.position {
ModalPosition::CenterTop => Vec2::new(x_align, y_align),
ModalPosition::Center => Vec2::new(x_align, 0.0),
};
(align, offset)
}
/// Set custom background color.
pub fn set_background_color(&self, color: Color32) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().fill = color;
}
/// Set custom background color.
pub fn set_background_color(&self, color: Color32) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().fill = Some(color);
}
/// Draw provided content.
fn content_ui(&self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks)) {
let mut rect = ui.available_rect_before_wrap();
/// Draw provided content.
fn content_ui(
&self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks),
) {
let mut rect = ui.available_rect_before_wrap();
// Create background shape.
let mut bg_shape = RectShape::new(rect, if self.title.is_none() {
CornerRadius::same(8.0 as u8)
} else {
CornerRadius {
nw: 0.0 as u8,
ne: 0.0 as u8,
sw: 8.0 as u8,
se: 8.0 as u8,
}
}, self.fill, Stroke::NONE, StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
// Create background shape.
let mut bg_shape = RectShape::new(
rect,
if self.title.is_none() {
CornerRadius::same(8.0 as u8)
} else {
CornerRadius {
nw: 0.0 as u8,
ne: 0.0 as u8,
sw: 8.0 as u8,
se: 8.0 as u8,
}
},
self.fill.unwrap_or(Colors::fill_lite()),
Stroke::NONE,
StrokeKind::Outside,
);
let bg_idx = ui.painter().add(bg_shape.clone());
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
let resp = ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
(add_content)(ui, self, cb);
}).response;
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
let resp = ui
.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
(add_content)(ui, self, cb);
})
.response;
// Setup background size.
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);
}
// Setup background size.
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);
}
}
/// Draw title content.
fn title_ui(title: &String, ui: &mut egui::Ui) {
let rect = ui.available_rect_before_wrap();
let rect = ui.available_rect_before_wrap();
// Create background shape.
let mut bg_shape = RectShape::new(rect, CornerRadius {
nw: 8.0 as u8,
ne: 8.0 as u8,
sw: 0.0 as u8,
se: 0.0 as u8,
}, Colors::yellow(), Stroke::NONE, StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
// Create background shape.
let mut bg_shape = RectShape::new(
rect,
CornerRadius {
nw: 8.0 as u8,
ne: 8.0 as u8,
sw: 0.0 as u8,
se: 0.0 as u8,
},
Colors::yellow(),
Stroke::NONE,
StrokeKind::Outside,
);
let bg_idx = ui.painter().add(bg_shape.clone());
// 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;
// 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);
}
// Setup background size.
bg_shape.rect = resp.rect;
ui.painter().set(bg_idx, bg_shape);
}
+342 -197
View File
@@ -12,240 +12,385 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Layout, RichText, CornerRadius, StrokeKind};
use eframe::epaint::RectShape;
use egui::{
Align, Color32, CornerRadius, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder,
};
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::icons::{
CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PLUS_CIRCLE, POWER, QR_CODE,
TRASH, WARNING_CIRCLE, X_CIRCLE,
};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::gui::views::network::NodeSetup;
use crate::gui::views::network::modals::{ExternalConnectionModal, ShareConnectionContent};
use crate::gui::views::network::types::ShareConnection;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, View};
use crate::node::{Node, NodeConfig};
use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Network connections content.
pub struct ConnectionsContent {
/// Flag to check connections state on first draw.
first_draw: bool,
/// External connection [`Modal`] content.
ext_conn_modal: ExternalConnectionModal,
/// Flag to check connections state on first draw.
first_draw: bool,
/// External connection [`Modal`] content.
ext_conn_modal_content: ExternalConnectionModal,
/// [`Modal`] content to share connection with QR code.
share_conn_modal_content: Option<ShareConnectionContent>,
}
impl Default for ConnectionsContent {
fn default() -> Self {
Self {
first_draw: true,
ext_conn_modal: ExternalConnectionModal::new(None),
}
}
fn default() -> Self {
Self {
first_draw: true,
ext_conn_modal_content: ExternalConnectionModal::new(None),
share_conn_modal_content: None,
}
}
}
/// Identifier for [`Modal`] to share connection.
const SHARE_CONN_QR_MODAL: &'static str = "share_conn_qr_modal";
impl ContentContainer for ConnectionsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
ExternalConnectionModal::NETWORK_ID
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![ExternalConnectionModal::NETWORK_ID, SHARE_CONN_QR_MODAL]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
ExternalConnectionModal::NETWORK_ID => {
self.ext_conn_modal.ui(ui, cb, modal, |_| {});
},
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
ExternalConnectionModal::NETWORK_ID => {
self.ext_conn_modal_content.ui(ui, cb, modal, |_| {});
}
SHARE_CONN_QR_MODAL => {
if let Some(c) = self.share_conn_modal_content.as_mut() {
c.ui(ui, modal, cb);
}
}
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
// Check connections state on first draw.
if self.first_draw {
ExternalConnection::check(None, ui.ctx());
self.first_draw = false;
}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
// Check connections state on first draw.
if self.first_draw {
ExternalConnection::check(None, ui.ctx());
self.first_draw = false;
}
ui.add_space(2.0);
ui.add_space(2.0);
// Show network type selection.
let saved_chain_type = AppConfig::chain_type();
NodeSetup::chain_type_ui(ui);
ui.add_space(6.0);
// Show network type selection.
let saved_chain_type = AppConfig::chain_type();
NodeSetup::chain_type_ui(ui);
ui.add_space(6.0);
// Check connections availability.
if saved_chain_type != AppConfig::chain_type() {
ExternalConnection::check(None, ui.ctx());
}
// Check connections availability.
if saved_chain_type != AppConfig::chain_type() {
ExternalConnection::check(None, ui.ctx());
}
// Show integrated node info content.
Self::integrated_node_item_ui(ui, |ui| {
// Draw button to show integrated node info.
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
AppConfig::toggle_show_connections_network_panel();
});
});
// Show integrated node info content.
Self::integrated_node_item_ui(
ui,
Colors::fill_lite(),
(true, || {
AppConfig::toggle_show_connections_network_panel();
}),
|ui| {
let r = View::item_rounding(0, 1, true);
View::item_button(ui, r, QR_CODE, None, || {
let (api_address, api_port) = NodeConfig::get_api_address();
if let Ok(c) = ShareConnectionContent::new(ShareConnection {
url: format!("http://{}:{}", api_address, api_port),
username: "grin".to_string(),
secret: NodeConfig::get_api_secret(true).unwrap_or("".to_string()),
}) {
self.share_conn_modal_content = Some(c);
// Show QR code to share integrated node connection.
Modal::new(SHARE_CONN_QR_MODAL)
.position(ModalPosition::Center)
.title(t!("network.node"))
.show();
}
});
true
},
);
// Show external connections.
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
// Show external connections.
ui.add_space(8.0);
ui.label(
RichText::new(t!("wallets.ext_conn"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.show_add_ext_conn_modal(None);
});
// Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.show_add_ext_conn_modal(None);
});
ui.add_space(4.0);
ui.add_space(4.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list();
let ext_conn_size = ext_conn_list.len();
if ext_conn_size != 0 {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
// Draw connection list item.
Self::ext_conn_item_ui(ui, conn, index, ext_conn_size, |ui| {
let button_rounding = View::item_rounding(index, ext_conn_size, true);
View::item_button(ui, button_rounding, TRASH, None, || {
ConnectionsConfig::remove_ext_conn(conn.id);
});
View::item_button(ui, CornerRadius::default(), PENCIL, None, || {
self.show_add_ext_conn_modal(Some(conn.clone()));
});
});
});
}
}
}
let ext_conn_list = ConnectionsConfig::ext_conn_list();
let len = ext_conn_list.len();
if len != 0 {
ui.add_space(8.0);
for (i, c) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
let mut show_qr_content: Option<ShareConnectionContent> = None;
// Draw external connection list item.
let bg = Colors::fill_lite();
Self::ext_conn_item_ui(
ui,
bg,
c,
i,
len,
(true, || {
self.show_add_ext_conn_modal(Some(c.clone()));
}),
|ui| {
// Draw button to delete connection.
let r = View::item_rounding(i, len, true);
View::item_button(ui, r, TRASH, Some(Colors::inactive_text()), || {
ConnectionsConfig::remove_ext_conn(c.id);
});
// Draw button to share connection
let r = CornerRadius::default();
View::item_button(ui, r, QR_CODE, None, || {
if let Ok(c) = ShareConnectionContent::new(ShareConnection {
url: c.url.clone(),
username: "grin".to_string(),
secret: c.secret.clone().unwrap_or("".to_string()),
}) {
show_qr_content = Some(c);
}
});
},
);
if let Some(c) = show_qr_content {
self.share_conn_modal_content = Some(c);
// Show QR code to share external connection.
Modal::new(SHARE_CONN_QR_MODAL)
.position(ModalPosition::Center)
.title(t!("wallets.ext_conn").replace(":", ""))
.show();
}
});
}
}
}
}
impl ConnectionsContent {
/// Draw integrated node connection item content.
pub fn integrated_node_item_ui(ui: &mut egui::Ui, custom_button: impl FnOnce(&mut egui::Ui)) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
let rounding = View::item_rounding(0, 1, false);
ui.painter().rect(rect, rounding, Colors::fill(), View::item_stroke(), StrokeKind::Outside);
/// Draw integrated node connection item content.
pub fn integrated_node_item_ui(
ui: &mut egui::Ui,
bg: Color32,
on_click: (bool, impl FnOnce()),
custom_button: impl FnOnce(&mut egui::Ui) -> bool,
) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
let r = View::item_rounding(0, 1, false);
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw custom button.
custom_button(ui);
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
// Draw custom button.
let extra_button = custom_button(ui);
// Draw buttons to start/stop node.
if Node::get_error().is_none() {
if !Node::is_running() {
View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::green()), || {
Node::start();
});
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::red()), || {
Node::stop(false);
});
}
}
// Draw buttons to start/stop node.
if Node::get_error().is_none() {
let rounding = if extra_button {
CornerRadius::default()
} else {
View::item_rounding(0, 1, true)
};
if !Node::is_running() {
View::item_button(ui, rounding, POWER, Some(Colors::green()), || {
Node::start();
});
} else if !Node::is_starting()
&& !Node::is_stopping()
&& !Node::is_restarting()
{
View::item_button(ui, rounding, POWER, Some(Colors::red()), || {
Node::stop(false);
});
}
}
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);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(t!("network.node"))
.size(18.0)
.color(Colors::title(false)));
});
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);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(
RichText::new(t!("network.node"))
.size(18.0)
.color(Colors::title(false)),
);
});
// Setup node status text.
let has_error = Node::get_error().is_some();
let status_icon = if has_error {
WARNING_CIRCLE
} else if !Node::is_running() {
X_CIRCLE
} else if Node::not_syncing() {
CHECK_CIRCLE
} else {
DOTS_THREE_CIRCLE
};
let status_text = format!("{} {}", status_icon, if has_error {
t!("error").into()
} else {
Node::get_sync_status_text()
});
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0);
// Setup node status text.
let has_error = Node::get_error().is_some();
let status_icon = if has_error {
WARNING_CIRCLE
} else if !Node::is_running() {
X_CIRCLE
} else if Node::not_syncing() {
CHECK_CIRCLE
} else {
DOTS_THREE_CIRCLE
};
let status_text = format!(
"{} {}",
status_icon,
if has_error {
t!("error").into()
} else {
Node::get_sync_status_text()
}
);
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0);
// Setup node API address text.
let api_address = NodeConfig::get_api_address();
let address_text = format!("{} http://{}", COMPUTER_TOWER, api_address);
ui.label(RichText::new(address_text).size(15.0).color(Colors::gray()));
})
});
});
}
// Setup node API address text.
let (api_address, api_port) = NodeConfig::get_api_address();
let address_text = format!(
"{} http://{}:{}",
COMPUTER_TOWER, api_address, api_port
);
ui.label(
RichText::new(address_text).size(15.0).color(Colors::gray()),
);
})
},
);
},
)
.response;
let (clickable, on_click) = on_click;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if clickable && res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked && clickable {
on_click();
}
}
/// Draw external connection item content.
pub fn ext_conn_item_ui(ui: &mut egui::Ui,
conn: &ExternalConnection,
index: usize,
len: usize,
buttons_ui: impl FnOnce(&mut egui::Ui)) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
/// Draw external connection item content.
pub fn ext_conn_item_ui(
ui: &mut egui::Ui,
bg: Color32,
conn: &ExternalConnection,
index: usize,
len: usize,
on_click: (bool, impl FnOnce()),
custom_button: impl FnOnce(&mut egui::Ui),
) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
let r = View::item_rounding(index, len, false);
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
// Draw custom button.
custom_button(ui);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw provided buttons.
buttons_ui(ui);
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| {
// Draw connections URL.
ui.add_space(4.0);
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
ui.add_space(1.0);
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| {
// Draw connections URL.
ui.add_space(4.0);
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
ui.add_space(1.0);
// Setup connection status text.
let status_text = if let Some(available) = conn.available {
if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
} else {
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);
});
},
);
},
)
.response;
let (clickable, on_click) = on_click;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if clickable && res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked && clickable {
on_click();
}
}
// Setup connection status text.
let status_text = if let Some(available) = conn.available {
if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
} else {
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);
});
});
});
}
/// Show [`Modal`] to add external connection.
pub fn show_add_ext_conn_modal(&mut self, conn: Option<ExternalConnection>) {
self.ext_conn_modal = ExternalConnectionModal::new(conn);
// Show modal.
Modal::new(ExternalConnectionModal::NETWORK_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add_node"))
.show();
}
}
/// Show [`Modal`] to add external connection.
pub fn show_add_ext_conn_modal(&mut self, conn: Option<ExternalConnection>) {
self.ext_conn_modal_content = ExternalConnectionModal::new(conn);
// Show modal.
Modal::new(ExternalConnectionModal::NETWORK_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add_node"))
.show();
}
}
+391 -382
View File
@@ -15,423 +15,432 @@
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, Margin, RichText, ScrollArea};
use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, ARROW_LEFT, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, GEAR, GLOBE, POWER};
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{
ARROW_LEFT, ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL,
FACTORY, FADERS, GAUGE, GEAR, GLOBE, POWER,
};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
use crate::gui::views::network::{
ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings,
};
use crate::gui::views::settings::SettingsContent;
use crate::gui::views::types::{ContentContainer, LinePosition, TitleContentType, TitleType};
use crate::gui::views::{Content, TitlePanel, View};
use crate::gui::Colors;
use crate::node::{Node, NodeConfig, NodeError};
use crate::AppConfig;
/// Network content.
pub struct NetworkContent {
/// Current integrated node tab content.
node_tab_content: Box<dyn NodeTab>,
/// Connections content.
connections: ConnectionsContent,
/// Current integrated node tab content.
node_tab_content: Box<dyn NodeTab>,
/// Connections content.
connections: ConnectionsContent,
/// Application settings content.
settings_content: Option<SettingsContent>,
/// Application settings content.
settings_content: Option<SettingsContent>,
}
impl Default for NetworkContent {
fn default() -> Self {
Self {
node_tab_content: Box::new(NetworkNode::default()),
connections: ConnectionsContent::default(),
settings_content: None,
}
}
fn default() -> Self {
Self {
node_tab_content: Box::new(NetworkNode::default()),
connections: ConnectionsContent::default(),
settings_content: None,
}
}
}
impl NetworkContent {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let show_settings = self.showing_settings();
let show_connections = AppConfig::show_connections_network_panel();
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let show_settings = self.showing_settings();
let show_connections = AppConfig::show_connections_network_panel();
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
// Show title panel.
self.title_ui(ui, dual_panel, show_connections);
// Show title panel.
self.title_ui(ui, dual_panel, show_connections);
// Show integrated node tabs content.
if !show_connections && !show_settings {
let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() {
0.0
} else {
4.0
};
let tabs_margin = Margin {
left: (View::get_left_inset() + side_padding) as i8,
right: (View::far_right_inset_margin(ui) + side_padding) as i8,
top: View::TAB_ITEMS_PADDING as i8,
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
};
egui::TopBottomPanel::bottom("network_tabs_content")
.min_height(0.5)
.resizable(false)
.frame(egui::Frame {
inner_margin: tabs_margin,
fill: Colors::fill(),
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui);
});
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.x -= tabs_margin.left as f32;
r.min.y -= tabs_margin.top as f32;
r.max.x += tabs_margin.right as f32;
r
};
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
});
}
// Show integrated node tabs content.
if !show_connections && !show_settings {
let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() { 0.0 } else { 4.0 };
let tabs_margin = Margin {
left: (View::get_left_inset() + side_padding) as i8,
right: (View::far_right_inset_margin(ui) + side_padding) as i8,
top: View::TAB_ITEMS_PADDING as i8,
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
};
egui::TopBottomPanel::bottom("network_tabs_content")
.min_height(0.5)
.resizable(false)
.frame(egui::Frame {
inner_margin: tabs_margin,
fill: Colors::fill(),
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui);
});
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.x -= tabs_margin.left as f32;
r.min.y -= tabs_margin.top as f32;
r.max.x += tabs_margin.right as f32;
r
};
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
});
}
// Show settings or integrated node content.
egui::SidePanel::right("network_side_content")
.resizable(false)
.exact_width(ui.available_width())
.frame(egui::Frame {
..Default::default()
})
.show_animated_inside(ui, show_settings || !show_connections, |ui| {
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
left: (View::get_left_inset() + View::content_padding()) as i8,
right: (View::far_right_inset_margin(ui) +
View::content_padding()) as i8,
top: 3.0 as i8,
bottom: 4.0 as i8,
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
if let Some(c) = &mut self.settings_content {
ScrollArea::vertical()
.id_salt("app_settings_network")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
// Show application settings content.
View::max_width_ui(ui,
Content::SIDE_PANEL_WIDTH * 1.3,
|ui| {
c.ui(ui, cb);
});
});
});
} else 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::<String>);
} else {
self.node_tab_content.tab_ui(ui, cb);
}
});
} else {
self.node_tab_content.tab_ui(ui, cb);
}
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += View::content_padding();
r.max.y += View::content_padding();
r
};
if dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
});
});
// Show settings or integrated node content.
egui::SidePanel::right("network_side_content")
.resizable(false)
.exact_width(ui.available_width())
.frame(egui::Frame {
..Default::default()
})
.show_animated_inside(ui, show_settings || !show_connections, |ui| {
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
left: (View::get_left_inset() + View::content_padding()) as i8,
right: (View::far_right_inset_margin(ui) + View::content_padding())
as i8,
top: 3.0 as i8,
bottom: 4.0 as i8,
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
if let Some(c) = &mut self.settings_content {
ScrollArea::vertical()
.id_salt("app_settings_network")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
// Show application settings content.
View::max_width_ui(
ui,
Content::SIDE_PANEL_WIDTH * 1.3,
|ui| {
c.ui(ui, cb);
},
);
});
});
} else 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::<String>);
} else {
self.node_tab_content.tab_ui(ui, cb);
}
});
} else {
self.node_tab_content.tab_ui(ui, cb);
}
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += View::content_padding();
r.max.y += View::content_padding();
r
};
if dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
});
});
// Show connections content.
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
left: if show_connections {
View::get_left_inset() + View::content_padding()
} else {
0.0
} as i8,
right: if show_connections {
View::far_right_inset_margin(ui) + View::content_padding()
} else {
0.0
} as i8,
top: 3.0 as i8,
bottom: 0.0 as i8,
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
ScrollArea::vertical()
.id_salt("connections_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
let max_width = if !dual_panel {
Content::SIDE_PANEL_WIDTH * 1.3
} else {
ui.available_width()
};
View::max_width_ui(ui, max_width, |ui| {
self.connections.ui(ui, cb);
});
});
ui.add_space(32.0);
});
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += View::content_padding();
r.max.y += View::content_padding() + View::get_bottom_inset();
r
};
if show_connections && dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
});
// Show connections content.
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
left: if show_connections {
View::get_left_inset() + View::content_padding()
} else {
0.0
} as i8,
right: if show_connections {
View::far_right_inset_margin(ui) + View::content_padding()
} else {
0.0
} as i8,
top: 3.0 as i8,
bottom: 0.0 as i8,
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
ScrollArea::vertical()
.id_salt("connections_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
let max_width = if !dual_panel {
Content::SIDE_PANEL_WIDTH * 1.3
} else {
ui.available_width()
};
View::max_width_ui(ui, max_width, |ui| {
self.connections.ui(ui, cb);
});
});
ui.add_space(32.0);
});
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += View::content_padding();
r.max.y += View::content_padding() + View::get_bottom_inset();
r
};
if show_connections && dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
});
// Redraw after delay if node is running at non-dual-panel mode.
if ((!dual_panel && Content::is_network_panel_open()) || dual_panel) && Node::is_running() {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
}
}
// Redraw after delay if node is running at non-dual-panel mode.
if ((!dual_panel && Content::is_network_panel_open()) || dual_panel) && Node::is_running() {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
}
}
/// Navigate back, return `true` if action was not consumed.
pub fn on_back(&mut self) -> bool {
if self.showing_settings() {
// Close settings.
self.settings_content = None;
return false;
}
true
}
/// Navigate back, return `true` if action was not consumed.
pub fn on_back(&mut self) -> bool {
if self.showing_settings() {
// Close settings.
self.settings_content = None;
return false;
}
true
}
/// Check if application settings content is showing.
pub fn showing_settings(&self) -> bool {
self.settings_content.is_some()
}
/// Check if application settings content is showing.
pub fn showing_settings(&self) -> bool {
self.settings_content.is_some()
}
/// Draw tab buttons at bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
/// Draw tab buttons at bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
// Draw tab buttons.
let current_type = self.node_tab_content.get_type();
ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Info);
View::tab_button(ui, DATABASE, None, active, |_| {
self.node_tab_content = Box::new(NetworkNode::default());
});
});
columns[1].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Metrics);
View::tab_button(ui, GAUGE, None, active, |_| {
self.node_tab_content = Box::new(NetworkMetrics::default());
});
});
columns[2].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Mining);
View::tab_button(ui, FACTORY, None, active, |_| {
self.node_tab_content = Box::new(NetworkMining::default());
});
});
columns[3].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Settings);
View::tab_button(ui, FADERS, None, active, |_| {
self.node_tab_content = Box::new(NetworkSettings::default());
});
});
});
});
}
// Draw tab buttons.
let current_type = self.node_tab_content.get_type();
ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Info);
View::tab_button(ui, DATABASE, None, active, |_| {
self.node_tab_content = Box::new(NetworkNode::default());
});
});
columns[1].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Metrics);
View::tab_button(ui, GAUGE, None, active, |_| {
self.node_tab_content = Box::new(NetworkMetrics::default());
});
});
columns[2].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Mining);
View::tab_button(ui, FACTORY, None, active, |_| {
self.node_tab_content = Box::new(NetworkMining::default());
});
});
columns[3].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Settings);
View::tab_button(ui, FADERS, None, active, |_| {
self.node_tab_content = Box::new(NetworkSettings::default());
});
});
});
});
}
/// Draw title content.
fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
let show_settings = self.showing_settings();
/// Draw title content.
fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
let show_settings = self.showing_settings();
// Setup values for title panel.
let title_text = self.node_tab_content.get_type().title();
let subtitle_text = Node::get_sync_status_text().into();
let not_syncing = Node::not_syncing() && !Node::data_dir_changing();
let title_content = if show_settings {
TitleContentType::Title(t!("settings").into())
} else if !show_connections {
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
} else {
TitleContentType::Title(t!("network.connections").into())
};
// Setup values for title panel.
let title_text = self.node_tab_content.get_type().title();
let subtitle_text = Node::get_sync_status_text().into();
let not_syncing = Node::not_syncing() && !Node::data_dir_changing();
let title_content = if show_settings {
TitleContentType::Title(t!("settings").into())
} else if !show_connections {
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
} else {
TitleContentType::Title(t!("network.connections").into())
};
// Draw title panel.
TitlePanel::new(Id::from("network_title_panel")).ui(TitleType::Single(title_content), |ui| {
if show_settings {
View::title_button_big(ui, ARROW_LEFT, |_| {
self.settings_content = None;
});
} else if !show_connections {
View::title_button_big(ui, GLOBE, |_| {
AppConfig::toggle_show_connections_network_panel();
});
} else if !dual_panel {
View::title_button_big(ui, GEAR, |_| {
self.settings_content = Some(SettingsContent::default());
});
}
}, |ui| {
if !dual_panel && !show_settings {
View::title_button_big(ui, BRIEFCASE, |_| {
Content::toggle_network_panel();
});
}
}, ui);
}
// Draw title panel.
TitlePanel::new(Id::from("network_title_panel")).ui(
TitleType::Single(title_content),
|ui| {
if show_settings {
View::title_button_big(ui, ARROW_LEFT, |_| {
self.settings_content = None;
});
} else if !show_connections {
View::title_button_big(ui, GLOBE, |_| {
AppConfig::toggle_show_connections_network_panel();
});
} else if !dual_panel {
View::title_button_big(ui, GEAR, |_| {
self.settings_content = Some(SettingsContent::default());
});
}
},
|ui| {
if !dual_panel && !show_settings {
View::title_button_big(ui, BRIEFCASE, |_| {
Content::toggle_network_panel();
});
}
},
ui,
);
}
/// Content to draw on loading.
pub fn loading_ui(ui: &mut egui::Ui, text: Option<impl Into<String>>) {
match text {
None => {
ui.centered_and_justified(|ui| {
View::big_loading_spinner(ui);
});
}
Some(t) => {
View::center_content(ui, 162.0, |ui| {
View::big_loading_spinner(ui);
ui.add_space(18.0);
ui.label(RichText::new(t)
.size(16.0)
.color(Colors::inactive_text())
);
});
}
}
}
/// Content to draw on loading.
pub fn loading_ui(ui: &mut egui::Ui, text: Option<impl Into<String>>) {
match text {
None => {
ui.centered_and_justified(|ui| {
View::big_loading_spinner(ui);
});
}
Some(t) => {
View::center_content(ui, 162.0, |ui| {
View::big_loading_spinner(ui);
ui.add_space(18.0);
ui.label(RichText::new(t).size(16.0).color(Colors::inactive_text()));
});
}
}
}
/// Draw checkbox to run integrated node on application launch.
pub fn autorun_node_ui(ui: &mut egui::Ui) {
let autostart = AppConfig::autostart_node();
View::checkbox(ui, autostart, t!("network.autorun"), || {
AppConfig::toggle_node_autostart();
});
}
/// Draw checkbox to run integrated node on application launch.
pub fn autorun_node_ui(ui: &mut egui::Ui) {
let autostart = AppConfig::autostart_node();
View::checkbox(ui, autostart, t!("network.autorun"), || {
AppConfig::toggle_node_autostart();
});
}
}
/// Content to draw when node is disabled.
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);
NetworkContent::autorun_node_ui(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);
NetworkContent::autorun_node_ui(ui);
});
}
/// 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();
});
ui.add_space(2.0);
});
return;
}
NodeError::P2P | NodeError::API => {
let msg_type = match e {
NodeError::API => "API",
_ => "P2P"
};
View::center_content(ui, 106.0, |ui| {
let text = t!(
"network_node.error_p2p_api",
"p2p_api" => msg_type,
"settings" => FADERS
);
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::red())
);
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();
});
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);
});
}
}
}
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();
});
ui.add_space(2.0);
});
return;
}
NodeError::P2P | NodeError::API => {
let msg_type = match e {
NodeError::API => "API",
_ => "P2P",
};
View::center_content(ui, 106.0, |ui| {
let text = t!(
"network_node.error_p2p_api",
"p2p_api" => msg_type,
"settings" => FADERS
);
ui.label(RichText::new(text).size(16.0).color(Colors::red()));
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();
});
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);
});
}
}
}
+155 -142
View File
@@ -12,17 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, CornerRadius, ScrollArea, vec2, StrokeKind};
use egui::scroll_area::ScrollBarVisibility;
use egui::{CornerRadius, RichText, ScrollArea, StrokeKind, vec2};
use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD};
use grin_servers::{DiffBlock, ServerStats};
use crate::gui::Colors;
use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HOURGLASS_LOW, HOURGLASS_MEDIUM, TIMER};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::{Content, View};
use crate::node::Node;
/// Chain metrics tab content.
@@ -34,162 +34,175 @@ const BLOCK_REWARD: u64 = REWARD / GRIN_BASE;
const YEARLY_SUPPLY: u64 = (BLOCK_REWARD * DAY_HEIGHT * 365) + 6 * HOUR_SEC;
impl NodeTab for NetworkMetrics {
fn get_type(&self) -> NodeTabType {
NodeTabType::Metrics
}
fn get_type(&self) -> NodeTabType {
NodeTabType::Metrics
}
fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
let server_stats = Node::get_stats();
let stats = server_stats.as_ref().unwrap();
if stats.diff_stats.height == 0 {
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
return;
}
ui.add_space(1.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show emission and difficulty info.
info_ui(ui, stats);
// Show difficulty adjustment window blocks.
blocks_ui(ui, stats);
});
}
fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
let server_stats = Node::get_stats();
let stats = server_stats.as_ref().unwrap();
if stats.diff_stats.height == 0 {
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
return;
}
ui.add_space(1.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show emission and difficulty info.
info_ui(ui, stats);
// Show difficulty adjustment window blocks.
blocks_ui(ui, stats);
});
}
}
/// Draw emission and difficulty info.
fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
// Show emission info.
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
ui.columns(3, |columns| {
let supply = stats.header_stats.height * BLOCK_REWARD;
let rate = (YEARLY_SUPPLY * 100) / supply;
// Show emission info.
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
ui.columns(3, |columns| {
let supply = stats.header_stats.height * BLOCK_REWARD;
let rate = (YEARLY_SUPPLY * 100) / supply;
columns[0].vertical_centered(|ui| {
View::label_box(ui,
format!("{}", BLOCK_REWARD),
t!("network_metrics.reward"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::label_box(ui,
format!("{:.2}%", rate),
t!("network_metrics.inflation"),
[false, false, false, false]);
});
columns[2].vertical_centered(|ui| {
View::label_box(ui,
supply.to_string(),
t!("network_metrics.supply"),
[false, true, false, true]);
});
});
ui.add_space(5.0);
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
format!("{}", BLOCK_REWARD),
t!("network_metrics.reward"),
[true, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
format!("{:.2}%", rate),
t!("network_metrics.inflation"),
[false, false, false, false],
);
});
columns[2].vertical_centered(|ui| {
View::label_box(
ui,
supply.to_string(),
t!("network_metrics.supply"),
[false, true, false, true],
);
});
});
ui.add_space(5.0);
// Show difficulty adjustment window info.
let difficulty_title = t!(
"network_metrics.difficulty_window",
"size" => stats.diff_stats.window_size
);
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title));
ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(ui,
stats.diff_stats.height.to_string(),
t!("network_node.height"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::label_box(ui,
format!("{}s", stats.diff_stats.average_block_time),
t!("network_metrics.block_time"),
[false, false, false, false]);
});
columns[2].vertical_centered(|ui| {
View::label_box(ui,
stats.diff_stats.average_difficulty.to_string(),
t!("network_node.difficulty"),
[false, true, false, true]);
});
});
// Show difficulty adjustment window info.
let difficulty_title = t!(
"network_metrics.difficulty_window",
"size" => stats.diff_stats.window_size
);
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title));
ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.diff_stats.height.to_string(),
t!("network_node.height"),
[true, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
format!("{}s", stats.diff_stats.average_block_time),
t!("network_metrics.block_time"),
[false, false, false, false],
);
});
columns[2].vertical_centered(|ui| {
View::label_box(
ui,
stats.diff_stats.average_difficulty.to_string(),
t!("network_node.difficulty"),
[false, true, false, true],
);
});
});
}
const BLOCK_ITEM_HEIGHT: f32 = 77.0;
/// Draw difficulty adjustment window blocks content.
fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
let blocks_size = stats.diff_stats.last_blocks.len();
ui.add_space(4.0);
ScrollArea::vertical()
.id_salt("mining_difficulty_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.stick_to_bottom(true)
.show_rows(
ui,
BLOCK_ITEM_HEIGHT,
blocks_size,
|ui, row_range| {
ui.add_space(4.0);
for index in row_range {
let db = stats.diff_stats.last_blocks.get(index).unwrap();
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
}
},
);
let blocks_size = stats.diff_stats.last_blocks.len();
ui.add_space(4.0);
ScrollArea::vertical()
.id_salt("mining_difficulty_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.stick_to_bottom(true)
.show_rows(ui, BLOCK_ITEM_HEIGHT, blocks_size, |ui, row_range| {
ui.add_space(4.0);
for index in row_range {
let db = stats.diff_stats.last_blocks.get(index).unwrap();
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
}
});
}
/// Draw block difficulty item.
fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: CornerRadius) {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(BLOCK_ITEM_HEIGHT);
ui.allocate_ui(rect.size(), |ui| {
ui.horizontal(|ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(4.0);
let mut rect = ui.available_rect_before_wrap();
rect.set_height(BLOCK_ITEM_HEIGHT);
ui.allocate_ui(rect.size(), |ui| {
ui.horizontal(|ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(4.0);
// Draw round background.
rect.min += vec2(8.0, 0.0);
rect.max -= vec2(8.0, 0.0);
ui.painter().rect(rect,
rounding,
Colors::white_or_black(false),
View::item_stroke(),
StrokeKind::Outside);
// Draw round background.
rect.min += vec2(8.0, 0.0);
rect.max -= vec2(8.0, 0.0);
ui.painter().rect(
rect,
rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside,
);
// Draw block hash.
ui.horizontal(|ui| {
ui.add_space(8.0);
ui.label(RichText::new(db.block_hash.to_string())
.color(Colors::white_or_black(true))
.size(17.0));
});
// Draw block difficulty and height.
ui.horizontal(|ui| {
ui.add_space(7.0);
let diff_text = format!("{} {} {} {}",
CUBE_TRANSPARENT,
db.difficulty,
AT,
db.block_height);
ui.label(RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0));
});
// Draw block date.
ui.horizontal(|ui| {
ui.add_space(7.0);
let block_time = View::format_time(db.time as i64);
ui.label(RichText::new(format!("{} {}s {} {}",
TIMER,
db.duration,
HOURGLASS_LOW,
block_time))
.color(Colors::gray())
.size(15.0));
});
ui.add_space(3.0);
});
ui.add_space(6.0);
});
});
}
// Draw block hash.
ui.horizontal(|ui| {
ui.add_space(8.0);
ui.label(
RichText::new(db.block_hash.to_string())
.color(Colors::white_or_black(true))
.size(17.0),
);
});
// Draw block difficulty and height.
ui.horizontal(|ui| {
ui.add_space(7.0);
let diff_text = format!(
"{} {} {} {}",
CUBE_TRANSPARENT, db.difficulty, AT, db.block_height
);
ui.label(
RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0),
);
});
// Draw block date.
ui.horizontal(|ui| {
ui.add_space(7.0);
let block_time = View::format_time(db.time as i64);
ui.label(
RichText::new(format!(
"{} {}s {} {}",
TIMER, db.duration, HOURGLASS_LOW, block_time
))
.color(Colors::gray())
.size(15.0),
);
});
ui.add_space(3.0);
});
ui.add_space(6.0);
});
});
}
+246 -218
View File
@@ -12,179 +12,195 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, CornerRadius, ScrollArea, StrokeKind};
use egui::scroll_area::ScrollBarVisibility;
use egui::{CornerRadius, RichText, ScrollArea, StrokeKind};
use grin_chain::SyncStatus;
use grin_servers::WorkerStats;
use crate::gui::Colors;
use crate::gui::icons::{BARBELL, CLOCK_AFTERNOON, CPU, CUBE, FADERS, FOLDER_DASHED, FOLDER_SIMPLE_MINUS, FOLDER_SIMPLE_PLUS, HARD_DRIVES, PLUGS, PLUGS_CONNECTED, POLYGON};
use crate::gui::icons::{
BARBELL, CLOCK_AFTERNOON, CPU, CUBE, FADERS, FOLDER_DASHED, FOLDER_SIMPLE_MINUS,
FOLDER_SIMPLE_PLUS, HARD_DRIVES, PLUGS, PLUGS_CONNECTED, POLYGON,
};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::setup::StratumSetup;
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Content, View};
use crate::node::{Node, NodeConfig};
/// Mining tab content.
pub struct NetworkMining {
/// Stratum server setup content.
stratum_server_setup: StratumSetup,
/// Stratum server setup content.
stratum_server_setup: StratumSetup,
}
impl Default for NetworkMining {
fn default() -> Self {
Self {
stratum_server_setup: StratumSetup::default(),
}
}
fn default() -> Self {
Self {
stratum_server_setup: StratumSetup::default(),
}
}
}
impl NodeTab for NetworkMining {
fn get_type(&self) -> NodeTabType {
NodeTabType::Mining
}
fn get_type(&self) -> NodeTabType {
NodeTabType::Mining
}
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if Node::is_stratum_starting() || Node::get_sync_status().unwrap() != SyncStatus::NoSync {
NetworkContent::loading_ui(ui, Some(t!("network_mining.loading")));
return;
}
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if Node::is_stratum_starting() || Node::get_sync_status().unwrap() != SyncStatus::NoSync {
NetworkContent::loading_ui(ui, Some(t!("network_mining.loading")));
return;
}
// Show stratum server setup when mining server is not running.
let stratum_stats = Node::get_stratum_stats();
if !stratum_stats.is_running {
ScrollArea::vertical()
.id_salt("stratum_setup_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.stratum_server_setup.ui(ui, cb);
});
});
return;
}
// Show stratum server setup when mining server is not running.
let stratum_stats = Node::get_stratum_stats();
if !stratum_stats.is_running {
ScrollArea::vertical()
.id_salt("stratum_setup_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.stratum_server_setup.ui(ui, cb);
});
});
return;
}
ui.add_space(1.0);
ui.add_space(1.0);
// Show stratum mining server info.
View::sub_title(ui, format!("{} {}", HARD_DRIVES, t!("network_mining.server")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
View::label_box(ui,
format!("{}:{}", stratum_addr, stratum_port),
t!("network_mining.address"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::label_box(ui,
self.stratum_server_setup
.wallet_name
.clone()
.unwrap_or("-".to_string()),
t!("network_mining.rewards_wallet"),
[false, true, false, true]);
});
});
ui.add_space(4.0);
// Show stratum mining server info.
View::sub_title(
ui,
format!("{} {}", HARD_DRIVES, t!("network_mining.server")),
);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
View::label_box(
ui,
format!("{}:{}", stratum_addr, stratum_port),
t!("network_mining.address"),
[true, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
self.stratum_server_setup
.wallet_name
.clone()
.unwrap_or("-".to_string()),
t!("network_mining.rewards_wallet"),
[false, true, false, true],
);
});
});
ui.add_space(4.0);
// Show network info.
View::sub_title(ui, format!("{} {}", POLYGON, t!("network.self")));
ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| {
let difficulty = if stratum_stats.network_difficulty > 0 {
stratum_stats.network_difficulty.to_string()
} else {
"-".into()
};
View::label_box(ui,
difficulty,
t!("network_node.difficulty"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let block_height = if stratum_stats.block_height > 0 {
stratum_stats.block_height.to_string()
} else {
"-".into()
};
View::label_box(ui,
block_height,
t!("network_node.header"),
[false, false, false, false]);
});
columns[2].vertical_centered(|ui| {
let hashrate = if stratum_stats.network_hashrate > 0.0 {
format!("{:.*}", 2, stratum_stats.network_hashrate)
} else {
"-".into()
};
View::label_box(ui,
hashrate,
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
[false, true, false, true]);
});
});
ui.add_space(4.0);
// Show network info.
View::sub_title(ui, format!("{} {}", POLYGON, t!("network.self")));
ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| {
let difficulty = if stratum_stats.network_difficulty > 0 {
stratum_stats.network_difficulty.to_string()
} else {
"-".into()
};
View::label_box(
ui,
difficulty,
t!("network_node.difficulty"),
[true, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
let block_height = if stratum_stats.block_height > 0 {
stratum_stats.block_height.to_string()
} else {
"-".into()
};
View::label_box(
ui,
block_height,
t!("network_node.header"),
[false, false, false, false],
);
});
columns[2].vertical_centered(|ui| {
let hashrate = if stratum_stats.network_hashrate > 0.0 {
format!("{:.*}", 2, stratum_stats.network_hashrate)
} else {
"-".into()
};
View::label_box(
ui,
hashrate,
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
[false, true, false, true],
);
});
});
ui.add_space(4.0);
// Show mining info.
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(ui,
stratum_stats.num_workers.to_string(),
t!("network_mining.devices"),
[true, false, true, false]);
});
// Show mining info.
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stratum_stats.num_workers.to_string(),
t!("network_mining.devices"),
[true, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
View::label_box(ui,
stratum_stats.blocks_found.to_string(),
t!("network_mining.blocks_found"),
[false, true, false, true]);
});
});
ui.add_space(4.0);
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
stratum_stats.blocks_found.to_string(),
t!("network_mining.blocks_found"),
[false, true, false, true],
);
});
});
ui.add_space(4.0);
// Show workers stats or info text when possible.
let workers_size = stratum_stats.worker_stats.len();
if workers_size != 0 && stratum_stats.num_workers > 0 {
ui.add_space(4.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(4.0);
ScrollArea::vertical()
.id_salt("stratum_workers_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show_rows(
ui,
WORKER_ITEM_HEIGHT,
workers_size,
|ui, row_range| {
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let worker = stratum_stats.worker_stats.get(index).unwrap();
let item_rounding = View::item_rounding(index, workers_size, false);
worker_item_ui(ui, worker, item_rounding);
}
},
);
} else if ui.available_height() > 142.0 {
View::center_content(ui, 142.0, |ui| {
ui.label(RichText::new(t!("network_mining.info", "settings" => FADERS))
.size(16.0)
.color(Colors::inactive_text())
);
});
}
}
// Show workers stats or info text when possible.
let workers_size = stratum_stats.worker_stats.len();
if workers_size != 0 && stratum_stats.num_workers > 0 {
ui.add_space(4.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(4.0);
ScrollArea::vertical()
.id_salt("stratum_workers_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show_rows(ui, WORKER_ITEM_HEIGHT, workers_size, |ui, row_range| {
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let worker = stratum_stats.worker_stats.get(index).unwrap();
let item_rounding = View::item_rounding(index, workers_size, false);
worker_item_ui(ui, worker, item_rounding);
}
});
} else if ui.available_height() > 142.0 {
View::center_content(ui, 142.0, |ui| {
ui.label(
RichText::new(t!("network_mining.info", "settings" => FADERS))
.size(16.0)
.color(Colors::inactive_text()),
);
});
}
}
}
/// Height of Stratum server worker list item.
@@ -192,84 +208,96 @@ const WORKER_ITEM_HEIGHT: f32 = 76.0;
/// Draw worker statistics item.
fn worker_item_ui(ui: &mut egui::Ui, ws: &WorkerStats, rounding: CornerRadius) {
ui.horizontal_wrapped(|ui| {
ui.vertical_centered_justified(|ui| {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(WORKER_ITEM_HEIGHT);
ui.painter().rect(rect,
rounding,
Colors::white_or_black(false),
View::item_stroke(),
StrokeKind::Outside);
ui.horizontal_wrapped(|ui| {
ui.vertical_centered_justified(|ui| {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(WORKER_ITEM_HEIGHT);
ui.painter().rect(
rect,
rounding,
Colors::white_or_black(false),
View::item_stroke(),
StrokeKind::Outside,
);
ui.add_space(2.0);
ui.horizontal(|ui| {
ui.add_space(5.0);
ui.add_space(2.0);
ui.horizontal(|ui| {
ui.add_space(5.0);
// Draw worker connection status.
let (status_text, status_icon, status_color) = match ws.is_connected {
true => (
t!("network_mining.connected"),
PLUGS_CONNECTED,
Colors::white_or_black(true)
),
false => (t!("network_mining.disconnected"), PLUGS, Colors::inactive_text())
};
let status_line_text = format!("{} {} {}", status_icon, ws.id, status_text);
ui.heading(RichText::new(status_line_text)
.color(status_color)
.size(17.0));
ui.add_space(2.0);
});
ui.horizontal(|ui| {
ui.add_space(6.0);
// Draw worker connection status.
let (status_text, status_icon, status_color) = match ws.is_connected {
true => (
t!("network_mining.connected"),
PLUGS_CONNECTED,
Colors::white_or_black(true),
),
false => (
t!("network_mining.disconnected"),
PLUGS,
Colors::inactive_text(),
),
};
let status_line_text = format!("{} {} {}", status_icon, ws.id, status_text);
ui.heading(
RichText::new(status_line_text)
.color(status_color)
.size(17.0),
);
ui.add_space(2.0);
});
ui.horizontal(|ui| {
ui.add_space(6.0);
// Draw difficulty.
let diff_text = format!("{} {}", BARBELL, ws.pow_difficulty);
ui.heading(RichText::new(diff_text)
.color(Colors::title(false))
.size(16.0));
ui.add_space(6.0);
// Draw difficulty.
let diff_text = format!("{} {}", BARBELL, ws.pow_difficulty);
ui.heading(
RichText::new(diff_text)
.color(Colors::title(false))
.size(16.0),
);
ui.add_space(6.0);
// Draw accepted shares.
let accepted_text = format!("{} {}", FOLDER_SIMPLE_PLUS, ws.num_accepted);
ui.heading(RichText::new(accepted_text)
.color(Colors::green())
.size(16.0));
ui.add_space(6.0);
// Draw accepted shares.
let accepted_text = format!("{} {}", FOLDER_SIMPLE_PLUS, ws.num_accepted);
ui.heading(
RichText::new(accepted_text)
.color(Colors::green())
.size(16.0),
);
ui.add_space(6.0);
// Draw rejected shares.
let rejected_text = format!("{} {}", FOLDER_SIMPLE_MINUS, ws.num_rejected);
ui.heading(RichText::new(rejected_text)
.color(Colors::red())
.size(16.0));
ui.add_space(6.0);
// Draw rejected shares.
let rejected_text = format!("{} {}", FOLDER_SIMPLE_MINUS, ws.num_rejected);
ui.heading(RichText::new(rejected_text).color(Colors::red()).size(16.0));
ui.add_space(6.0);
// Draw stale shares.
let stale_text = format!("{} {}", FOLDER_DASHED, ws.num_stale);
ui.heading(RichText::new(stale_text)
.color(Colors::gray())
.size(16.0));
ui.add_space(6.0);
// Draw stale shares.
let stale_text = format!("{} {}", FOLDER_DASHED, ws.num_stale);
ui.heading(RichText::new(stale_text).color(Colors::gray()).size(16.0));
ui.add_space(6.0);
// Draw blocks found.
let blocks_found_text = format!("{} {}", CUBE, ws.num_blocks_found);
ui.heading(RichText::new(blocks_found_text)
.color(Colors::title(false))
.size(16.0));
});
ui.horizontal(|ui| {
ui.add_space(6.0);
// Draw blocks found.
let blocks_found_text = format!("{} {}", CUBE, ws.num_blocks_found);
ui.heading(
RichText::new(blocks_found_text)
.color(Colors::title(false))
.size(16.0),
);
});
ui.horizontal(|ui| {
ui.add_space(6.0);
// Draw block time
let seen_ts = ws.last_seen.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
let seen_time = View::format_time(seen_ts as i64);
let seen_text = format!("{} {}", CLOCK_AFTERNOON, seen_time);
ui.heading(RichText::new(seen_text)
.color(Colors::gray())
.size(16.0));
});
});
});
}
// Draw block time
let seen_ts = ws
.last_seen
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let seen_time = View::format_time(seen_ts as i64);
let seen_text = format!("{} {}", CLOCK_AFTERNOON, seen_time);
ui.heading(RichText::new(seen_text).color(Colors::gray()).size(16.0));
});
});
});
}
+1 -1
View File
@@ -33,5 +33,5 @@ pub use content::*;
mod connections;
pub use connections::*;
pub mod modals;
pub mod types;
pub mod modals;
+234 -134
View File
@@ -16,158 +16,258 @@ use egui::{Id, RichText};
use url::Url;
use crate::gui::Colors;
use crate::gui::icons::SCAN;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::views::network::types::ShareConnection;
use crate::gui::views::{CameraContent, Modal, TextEdit, View};
use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Content to create or update external wallet connection.
pub struct ExternalConnectionModal {
/// Flag to check if content was just rendered.
first_draw: bool,
/// Flag to check if content was just rendered.
first_draw: bool,
/// External connection URL value for [`Modal`].
ext_node_url_edit: String,
/// External connection API secret value for [`Modal`].
ext_node_secret_edit: String,
/// Flag to show URL format error at [`Modal`].
ext_node_url_error: bool,
/// Editing external connection identifier for [`Modal`].
ext_conn_id: Option<i64>,
/// Editing external connection identifier.
id: Option<i64>,
/// External connection URL.
url_edit: String,
/// Flag to show URL format error.
url_error: bool,
// /// External connection username.
// username_edit: String,
/// External connection API secret.
secret_edit: String,
/// QR code scanner content.
scan_qr_content: Option<CameraContent>,
}
impl ExternalConnectionModal {
/// Network [`Modal`] identifier.
pub const NETWORK_ID: &'static str = "net_ext_conn_modal";
/// Wallet [`Modal`] identifier.
pub const WALLET_ID: &'static str = "wallet_ext_conn_modal";
/// Network [`Modal`] identifier.
pub const NETWORK_ID: &'static str = "net_ext_conn_modal";
/// Wallet [`Modal`] identifier.
pub const WALLET_ID: &'static str = "wallet_ext_conn_modal";
/// Create new instance from optional provided connection to update.
pub fn new(conn: Option<ExternalConnection>) -> Self {
let (ext_node_url_edit, ext_node_secret_edit, ext_conn_id) = if let Some(c) = conn {
(c.url, c.secret.unwrap_or("".to_string()), Some(c.id))
} else {
("".to_string(), "".to_string(), None)
};
Self {
first_draw: true,
ext_node_url_edit,
ext_node_secret_edit,
ext_node_url_error: false,
ext_conn_id,
}
}
/// Create new instance from optional provided connection to update.
pub fn new(conn: Option<ExternalConnection>) -> Self {
let (url_edit, secret_edit, id) = if let Some(c) = conn {
// let username = c.username.unwrap_or("grin".to_string());
let secret = c.secret.unwrap_or("".to_string());
(c.url, secret, Some(c.id))
} else {
("".to_string(), "".to_string(), None)
};
Self {
first_draw: true,
url_edit,
url_error: false,
secret_edit,
id,
scan_qr_content: None,
}
}
/// Draw external connection [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
modal: &Modal,
on_save: impl Fn(ExternalConnection)) {
// Add connection button callback.
let on_add = |ui: &mut egui::Ui, m: &mut ExternalConnectionModal| {
let url = if !m.ext_node_url_edit.starts_with("http") {
format!("https://{}", m.ext_node_url_edit)
} else {
m.ext_node_url_edit.clone()
};
let error = Url::parse(url.trim()).is_err();
m.ext_node_url_error = error;
if !error {
let secret = if m.ext_node_secret_edit.is_empty() {
None
} else {
Some(m.ext_node_secret_edit.clone())
};
/// Draw external connection [`Modal`] content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
modal: &Modal,
on_save: impl Fn(ExternalConnection),
) {
// Show QR code scanner content.
if let Some(scan_content) = self.scan_qr_content.as_mut() {
if let Some(result) = scan_content.qr_scan_result() {
cb.stop_camera();
modal.enable_closing();
self.scan_qr_content = None;
// Parse scan result.
if let Ok(c) = serde_json::from_str::<ShareConnection>(&result.text()) {
let ext_conn = ExternalConnection::new(c.url, Some(c.username), Some(c.secret));
ConnectionsConfig::add_ext_conn(ext_conn.clone());
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
Modal::close();
}
} else {
scan_content.ui(ui, cb);
}
ui.add_space(8.0);
// Update or create new connection.
let mut ext_conn = ExternalConnection::new(url, secret);
if let Some(id) = m.ext_conn_id {
ext_conn.id = id;
}
ConnectionsConfig::add_ext_conn(ext_conn.clone());
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
on_save(ext_conn);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Close modal.
m.ext_node_url_edit = "".to_string();
m.ext_node_secret_edit = "".to_string();
m.ext_node_url_error = false;
Modal::close();
}
};
// Show buttons to close modal or scanner.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scan_qr_content = None;
Modal::close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scan_qr_content = None;
modal.enable_closing();
});
});
});
ui.add_space(6.0);
return;
}
// Add connection button callback.
let on_add = |ui: &mut egui::Ui, m: &mut ExternalConnectionModal| {
let url = if !m.url_edit.starts_with("http") {
format!("https://{}", m.url_edit)
} else {
m.url_edit.clone()
};
let error = Url::parse(url.trim()).is_err();
m.url_error = error;
if !error {
let username = if m.secret_edit.is_empty() {
Some("grin".to_string())
} else {
Some(m.secret_edit.clone())
};
let secret = if m.secret_edit.is_empty() {
None
} else {
Some(m.secret_edit.clone())
};
// Update or create new connection.
let mut ext_conn = ExternalConnection::new(url, username, secret);
if let Some(id) = m.id {
ext_conn.id = id;
}
ConnectionsConfig::add_ext_conn(ext_conn.clone());
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
on_save(ext_conn);
ui.vertical_centered(|ui| {
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.node_url"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Close modal.
m.url_edit = "".to_string();
m.secret_edit = "".to_string();
m.url_error = false;
Modal::close();
}
};
// Draw node URL text edit.
let url_edit_id = Id::from(modal.id).with(self.ext_conn_id).with("node_url");
let mut url_edit = TextEdit::new(url_edit_id)
.paste()
.focus(self.first_draw);
url_edit.ui(ui, &mut self.ext_node_url_edit, cb);
ui.vertical_centered(|ui| {
ui.add_space(6.0);
ui.label(
RichText::new(t!("wallets.node_url"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.node_secret"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw node URL text edit.
let url_edit_id = Id::from(modal.id).with(self.id).with("node_url");
let mut url_edit = TextEdit::new(url_edit_id).paste().focus(self.first_draw);
let url_edit_before = self.url_edit.clone();
url_edit.ui(ui, &mut self.url_edit, cb);
if self.url_edit != url_edit_before {
self.url_error = false;
}
// Draw node API secret text edit.
let secret_edit_id = Id::from(modal.id).with(self.ext_conn_id).with("node_secret");
let mut secret_edit = TextEdit::new(secret_edit_id)
.password()
.paste()
.focus(false);
if url_edit.enter_pressed {
secret_edit.focus_request();
}
secret_edit.ui(ui, &mut self.ext_node_secret_edit, cb);
if secret_edit.enter_pressed {
(on_add)(ui, self);
}
// ui.add_space(8.0);
// ui.label(RichText::new(t!("wallets.name"))
// .size(17.0)
// .color(Colors::gray()));
// ui.add_space(8.0);
//
// // Draw node username text edit (disabled by default).
// let username_edit_id = Id::from(modal.id).with(self.id).with("node_username");
// let mut username_edit = TextEdit::new(username_edit_id).focus(false).disable();
// username_edit.ui(ui, &mut self.username_edit, cb);
// Show error when specified URL is not valid.
if self.ext_node_url_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.invalid_url"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
ui.add_space(8.0);
ui.label(
RichText::new(t!("wallets.node_secret"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Draw node API secret text edit.
let secret_edit_id = Id::from(modal.id).with(self.id).with("node_secret");
let mut secret_edit = TextEdit::new(secret_edit_id)
.h_center()
.password()
.paste()
.focus(false);
if url_edit.enter_pressed {
secret_edit.focus_request();
}
secret_edit.ui(ui, &mut self.secret_edit, cb);
if secret_edit.enter_pressed {
on_add(ui, self);
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
self.ext_node_url_edit = "".to_string();
self.ext_node_secret_edit = "".to_string();
self.ext_node_url_error = false;
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button_ui(ui, if self.ext_conn_id.is_some() {
t!("modal.save")
} else {
t!("modal.add")
}, Colors::white_or_black(false), |ui| {
(on_add)(ui, self);
});
});
});
ui.add_space(6.0);
});
// Show error when specified URL is not valid.
if self.url_error {
ui.add_space(12.0);
ui.label(
RichText::new(t!("wallets.invalid_url"))
.size(17.0)
.color(Colors::red()),
);
}
ui.add_space(12.0);
self.first_draw = false;
}
}
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::white_or_black(false), || {
modal.disable_closing();
self.scan_qr_content = Some(CameraContent::default());
cb.start_camera();
});
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
self.url_edit = "".to_string();
self.secret_edit = "".to_string();
self.url_error = false;
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button_ui(
ui,
if self.id.is_some() {
t!("modal.save")
} else {
t!("modal.add")
},
Colors::white_or_black(false),
|ui| {
(on_add)(ui, self);
},
);
});
});
ui.add_space(6.0);
});
self.first_draw = false;
}
}
+4 -1
View File
@@ -13,4 +13,7 @@
// limitations under the License.
mod ext_conn;
pub use ext_conn::*;
pub use ext_conn::*;
mod share_conn;
pub use share_conn::*;
@@ -0,0 +1,57 @@
// 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 crate::AppConfig;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::types::ShareConnection;
use crate::gui::views::{Modal, QrCodeContent, View};
/// [`Modal`] content to share connection with QR code.
pub struct ShareConnectionContent {
/// QR code content.
pub qr_details_content: QrCodeContent,
}
impl ShareConnectionContent {
/// Create new content instance from connection details.
pub fn new(details: ShareConnection) -> Result<Self, serde_json::Error> {
let details = serde_json::to_string_pretty(&details)?;
let c = Self {
qr_details_content: QrCodeContent::new(details, false).hide_text().no_copy(),
};
Ok(c)
}
/// Draw QR code content.
pub fn ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let dark_theme = AppConfig::dark_theme().unwrap_or(false);
// Set light theme for better scanning.
AppConfig::set_dark_theme(false);
modal.set_background_color(Colors::FILL_DEEP);
crate::setup_visuals(ui.ctx());
// Draw QR code content.
ui.add_space(6.0);
self.qr_details_content.ui(ui, cb);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
Modal::close();
});
});
ui.add_space(6.0);
// Set color theme back.
AppConfig::set_dark_theme(dark_theme);
crate::setup_visuals(ui.ctx());
}
}
+209 -175
View File
@@ -12,15 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, CornerRadius, ScrollArea, StrokeKind};
use egui::scroll_area::ScrollBarVisibility;
use egui::{CornerRadius, RichText, ScrollArea, StrokeKind};
use grin_servers::PeerStats;
use crate::gui::Colors;
use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View};
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::{Content, View};
use crate::node::{Node, NodeConfig};
/// Integrated node tab content.
@@ -28,193 +28,227 @@ use crate::node::{Node, NodeConfig};
pub struct NetworkNode;
impl NodeTab for NetworkNode {
fn get_type(&self) -> NodeTabType {
NodeTabType::Info
}
fn get_type(&self) -> NodeTabType {
NodeTabType::Info
}
fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
ScrollArea::vertical()
.id_salt("integrated_node_info_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(2.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show node stats content.
node_stats_ui(ui);
});
});
}
fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
ScrollArea::vertical()
.id_salt("integrated_node_info_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(2.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show node stats content.
node_stats_ui(ui);
});
});
}
}
/// Draw node statistics content.
fn node_stats_ui(ui: &mut egui::Ui) {
let server_stats = Node::get_stats();
let stats = server_stats.as_ref().unwrap();
let server_stats = Node::get_stats();
let stats = server_stats.as_ref().unwrap();
// Show header info.
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(ui,
stats.header_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
View::label_box(ui,
stats.header_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false]);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(ui,
stats.header_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let h_ts = stats.header_stats.latest_timestamp.timestamp();
let h_time = View::format_time(h_ts);
View::label_box(ui,
h_time,
t!("network_node.time"),
[false, false, false, true]);
});
});
ui.add_space(5.0);
// Show header info.
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.header_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false],
);
});
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
stats.header_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false],
);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.header_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
let h_ts = stats.header_stats.latest_timestamp.timestamp();
let h_time = View::format_time(h_ts);
View::label_box(
ui,
h_time,
t!("network_node.time"),
[false, false, false, true],
);
});
});
ui.add_space(5.0);
// Show block info.
View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(ui,
stats.chain_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
View::label_box(ui,
stats.chain_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false]);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(ui,
stats.chain_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let b_ts = stats.chain_stats.latest_timestamp.timestamp();
let b_time = View::format_time(b_ts);
View::label_box(ui,
b_time,
t!("network_node.time"),
[false, false, false, true]);
});
});
ui.add_space(5.0);
// Show block info.
View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.chain_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false],
);
});
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
stats.chain_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false],
);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.chain_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
let b_ts = stats.chain_stats.latest_timestamp.timestamp();
let b_time = View::format_time(b_ts);
View::label_box(
ui,
b_time,
t!("network_node.time"),
[false, false, false, true],
);
});
});
ui.add_space(5.0);
// Show data info.
View::sub_title(ui, format!("{} {}", SHARE_NETWORK, t!("network_node.data")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let tx_stat = match &stats.tx_stats {
None => "0 (0)".to_string(),
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels)
};
View::label_box(ui,
tx_stat,
t!("network_node.main_pool"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
let stem_tx_stat = match &stats.tx_stats {
None => "0 (0)".to_string(),
Some(stx) => format!("{} ({})",
stx.stem_pool_size,
stx.stem_pool_kernels)
};
View::label_box(ui,
stem_tx_stat,
t!("network_node.stem_pool"),
[false, true, false, false]);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(ui,
stats.disk_usage_gb.to_string(),
t!("network_node.size"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let peers_txt = format!("{} ({})",
stats.peer_count,
NodeConfig::get_max_outbound_peers());
View::label_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]);
});
});
ui.add_space(5.0);
// Show data info.
View::sub_title(ui, format!("{} {}", SHARE_NETWORK, t!("network_node.data")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let tx_stat = match &stats.tx_stats {
None => "0 (0)".to_string(),
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels),
};
View::label_box(
ui,
tx_stat,
t!("network_node.main_pool"),
[true, false, false, false],
);
});
columns[1].vertical_centered(|ui| {
let stem_tx_stat = match &stats.tx_stats {
None => "0 (0)".to_string(),
Some(stx) => format!("{} ({})", stx.stem_pool_size, stx.stem_pool_kernels),
};
View::label_box(
ui,
stem_tx_stat,
t!("network_node.stem_pool"),
[false, true, false, false],
);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.disk_usage_gb.to_string(),
t!("network_node.size"),
[false, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
let peers_txt = format!(
"{} ({})",
stats.peer_count,
NodeConfig::get_max_outbound_peers()
);
View::label_box(
ui,
peers_txt,
t!("network_node.peers"),
[false, false, false, true],
);
});
});
ui.add_space(5.0);
// Show peer stats when available.
if stats.peer_count > 0 {
View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_node.peers")));
let peers = &stats.peer_stats;
for (index, ps) in peers.iter().enumerate() {
peer_item_ui(ui, ps, View::item_rounding(index, peers.len(), false));
}
ui.add_space(5.0);
}
// Show peer stats when available.
if stats.peer_count > 0 {
View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_node.peers")));
let peers = &stats.peer_stats;
for (index, ps) in peers.iter().enumerate() {
peer_item_ui(ui, ps, View::item_rounding(index, peers.len(), false));
}
ui.add_space(5.0);
}
}
const PEER_ITEM_HEIGHT: f32 = 77.0;
/// Draw connected peer info item.
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: CornerRadius) {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(PEER_ITEM_HEIGHT);
ui.allocate_ui(rect.size(), |ui| {
ui.vertical(|ui| {
ui.add_space(4.0);
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, r: CornerRadius) {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(PEER_ITEM_HEIGHT);
ui.allocate_ui(rect.size(), |ui| {
ui.vertical(|ui| {
ui.add_space(4.0);
// Draw round background.
ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke(), StrokeKind::Outside);
// Draw round background.
ui.painter().rect(
rect,
r,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside,
);
// Draw IP address.
ui.horizontal(|ui| {
ui.add_space(7.0);
ui.label(RichText::new(&peer.addr)
.color(Colors::white_or_black(true))
.size(17.0));
});
// Draw difficulty and height.
ui.horizontal(|ui| {
ui.add_space(6.0);
let diff_text = format!("{} {} {} {}",
PACKAGE,
peer.total_difficulty,
AT,
peer.height);
ui.label(RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0));
});
// Draw user-agent.
ui.horizontal(|ui| {
ui.add_space(6.0);
let agent_text = format!("{} {}", DEVICES, &peer.user_agent);
ui.label(RichText::new(agent_text)
.color(Colors::gray())
.size(15.0));
});
// Draw IP address.
ui.horizontal(|ui| {
ui.add_space(7.0);
ui.label(
RichText::new(&peer.addr)
.color(Colors::white_or_black(true))
.size(17.0),
);
});
// Draw difficulty and height.
ui.horizontal(|ui| {
ui.add_space(6.0);
let diff_text = format!(
"{} {} {} {}",
PACKAGE, peer.total_difficulty, AT, peer.height
);
ui.label(
RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0),
);
});
// Draw user-agent.
ui.horizontal(|ui| {
ui.add_space(6.0);
let agent_text = format!("{} {}", DEVICES, &peer.user_agent);
ui.label(RichText::new(agent_text).color(Colors::gray()).size(15.0));
});
ui.add_space(3.0);
});
});
}
ui.add_space(3.0);
});
});
}
+282 -193
View File
@@ -12,249 +12,338 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use crate::gui::Colors;
use crate::gui::icons::ARROW_COUNTER_CLOCKWISE;
use crate::gui::icons::{ARROW_COUNTER_CLOCKWISE, TRASH};
use crate::gui::platform::PlatformCallbacks;
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::{NodeTab, NodeTabType};
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Content, Modal, View};
use crate::node::{Node, NodeConfig};
use egui::scroll_area::ScrollBarVisibility;
use egui::{RichText, ScrollArea};
/// Integrated node settings tab content.
pub struct NetworkSettings {
/// Integrated node general setup content.
node: NodeSetup,
/// P2P server setup content.
p2p: P2PSetup,
/// Stratum server setup content.
stratum: StratumSetup,
/// Pool setup content.
pool: PoolSetup,
/// Dandelion server setup content.
dandelion: DandelionSetup,
/// Integrated node general setup content.
node: NodeSetup,
/// P2P server setup content.
p2p: P2PSetup,
/// Stratum server setup content.
stratum: StratumSetup,
/// Pool setup content.
pool: PoolSetup,
/// Dandelion server setup content.
dandelion: DandelionSetup,
/// Flag to check if reset of data was called.
data_reset: bool,
}
/// Identifier for settings reset confirmation [`Modal`].
pub const RESET_SETTINGS_CONFIRMATION_MODAL: &'static str = "reset_settings_confirmation";
impl Default for NetworkSettings {
fn default() -> Self {
Self {
node: NodeSetup::default(),
p2p: P2PSetup::default(),
stratum: StratumSetup::default(),
pool: PoolSetup::default(),
dandelion: DandelionSetup::default(),
}
}
fn default() -> Self {
Self {
node: NodeSetup::default(),
p2p: P2PSetup::default(),
stratum: StratumSetup::default(),
pool: PoolSetup::default(),
dandelion: DandelionSetup::default(),
data_reset: false,
}
}
}
impl ContentContainer for NetworkSettings {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
RESET_SETTINGS_CONFIRMATION_MODAL
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![RESET_SETTINGS_CONFIRMATION_MODAL]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
_: &dyn PlatformCallbacks) {
match modal.id {
RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui),
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, _: &dyn PlatformCallbacks) {
match modal.id {
RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ScrollArea::vertical()
.id_salt("node_settings_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Draw node setup section.
self.node.ui(ui, cb);
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ScrollArea::vertical()
.id_salt("node_settings_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Draw node setup section.
self.node.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw P2P server setup section.
self.p2p.ui(ui, cb);
// Draw P2P server setup section.
self.p2p.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw Stratum server setup section.
self.stratum.ui(ui, cb);
// Draw Stratum server setup section.
self.stratum.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw pool setup section.
self.pool.ui(ui, cb);
// Draw pool setup section.
self.pool.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw Dandelion server setup section.
self.dandelion.ui(ui, cb);
// Draw Dandelion server setup section.
self.dandelion.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
// Draw content to reset the data.
if !Node::is_restarting() && !self.data_reset {
ui.add_space(4.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Draw reset settings content.
reset_settings_ui(ui);
});
});
});
}
self.reset_data_ui(ui);
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
// Draw reset settings content.
reset_settings_ui(ui);
});
});
});
}
}
impl NodeTab for NetworkSettings {
fn get_type(&self) -> NodeTabType {
NodeTabType::Settings
}
fn get_type(&self) -> NodeTabType {
NodeTabType::Settings
}
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.ui(ui, cb);
}
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.ui(ui, cb);
}
}
impl NetworkSettings {
/// Reminder to restart enabled node to show on edit setting at [`Modal`].
pub fn node_restart_required_ui(ui: &mut egui::Ui) {
if Node::is_running() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.restart_node_required"))
.size(16.0)
.color(Colors::green())
);
}
}
/// Reminder to restart enabled node to show on edit setting at [`Modal`].
pub fn node_restart_required_ui(ui: &mut egui::Ui) {
if Node::is_running() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.restart_node_required"))
.size(16.0)
.color(Colors::green()),
);
}
}
/// Draw IP addresses as radio buttons.
pub fn ip_addrs_ui(ui: &mut egui::Ui,
saved_ip: &String,
ips: &Vec<String>,
on_change: impl FnOnce(&String)) {
let mut selected_ip = saved_ip;
/// Draw IP addresses as radio buttons.
pub fn ip_addrs_ui(
ui: &mut egui::Ui,
saved_ip: &String,
ips: &Vec<String>,
on_change: impl FnOnce(&String),
) {
let mut all = NodeConfig::ALL_INTERFACES.to_string();
let all_ips = saved_ip == &all || saved_ip == &format!("[{}]", &all);
if all_ips {
all = saved_ip.clone();
}
// Set first IP address as current if saved is not present at system.
if !ips.contains(saved_ip) {
selected_ip = ips.get(0).unwrap();
}
let mut selected_ip = saved_ip.clone();
ui.add_space(2.0);
let mut listen_all_changed = false;
View::checkbox(ui, all_ips, t!("network_settings.ip_listen_all"), || {
listen_all_changed = true;
});
if listen_all_changed {
let new_ip = if all_ips {
ips.get(0).unwrap_or(&all).clone()
} else {
all.clone()
};
selected_ip = new_ip;
}
// Show available IP addresses on the system.
let _ = ips.chunks(2).map(|x| {
if x.len() == 2 {
ui.columns(2, |columns| {
let ip_left = x.get(0).unwrap();
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_ip, ip_left, ip_left.to_string());
});
let ip_right = x.get(1).unwrap();
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_ip, ip_right, ip_right.to_string());
})
});
} else {
let ip = x.get(0).unwrap();
View::radio_value(ui, &mut selected_ip, ip, ip.to_string());
}
ui.add_space(12.0);
}).collect::<Vec<_>>();
ui.add_space(8.0);
if saved_ip != selected_ip {
(on_change)(&selected_ip.to_string());
}
}
if selected_ip != all {
// Set first IP address as current if saved is not present at system.
if !ips.contains(&saved_ip) {
selected_ip = ips.get(0).unwrap().clone();
}
/// Show message when IP addresses are not available at system.
pub fn no_ip_address_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network.no_ips"))
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(6.0);
});
}
// Show available IP addresses on the system.
let _ = ips
.chunks(2)
.map(|x| {
if x.len() == 2 {
ui.columns(2, |columns| {
let ip_left = x.get(0).unwrap();
let val = if all_ips {
&mut ip_left.clone()
} else {
&mut selected_ip
};
columns[0].vertical_centered(|ui| {
View::radio_value(ui, val, ip_left.clone(), ip_left.to_string());
});
let ip_right = x.get(1).unwrap();
let val = if all_ips {
&mut ip_right.clone()
} else {
&mut selected_ip
};
columns[1].vertical_centered(|ui| {
View::radio_value(ui, val, ip_right.clone(), ip_right.to_string());
})
});
} else {
let ip = x.get(0).unwrap();
let val = if all_ips {
&mut ip.clone()
} else {
&mut selected_ip
};
View::radio_value(ui, val, ip.clone(), ip.to_string());
}
ui.add_space(12.0);
})
.collect::<Vec<_>>();
}
if saved_ip != &selected_ip {
on_change(&selected_ip.to_string());
}
}
/// Show message when IP addresses are not available at system.
pub fn no_ip_address_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network.no_ips"))
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(6.0);
});
}
/// Draw content to reset data.
fn reset_data_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(4.0);
View::colored_text_button(
ui,
format!("{} {}", TRASH, t!("network_settings.reset_data")),
Colors::red(),
Colors::white_or_black(false),
|| {
Node::reset_data(false);
self.data_reset = true;
},
);
ui.add_space(6.0);
ui.label(
RichText::new(t!("network_settings.reset_data_desc"))
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(4.0);
}
}
/// Draw button to reset integrated node settings to default values.
fn reset_settings_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.reset_settings_desc"))
.size(16.0)
.color(Colors::text(false)));
ui.add_space(8.0);
let button_text = format!("{} {}",
ARROW_COUNTER_CLOCKWISE,
t!("network_settings.reset_settings"));
View::action_button(ui, button_text, || {
// Show modal to confirm settings reset.
Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
});
// Show reminder to restart enabled node.
if Node::is_running() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.restart_node_required"))
.size(16.0)
.color(Colors::gray())
);
}
ui.add_space(12.0);
});
ui.label(
RichText::new(t!("network_settings.reset_settings_desc"))
.size(16.0)
.color(Colors::text(false)),
);
ui.add_space(8.0);
let button_text = format!(
"{} {}",
ARROW_COUNTER_CLOCKWISE,
t!("network_settings.reset_settings")
);
View::action_button(ui, button_text, || {
// Show modal to confirm settings reset.
Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
});
// Show reminder to restart enabled node.
if Node::is_running() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.restart_node_required"))
.size(16.0)
.color(Colors::gray()),
);
}
ui.add_space(10.0);
}
/// Confirmation to reset settings to default values.
fn reset_settings_confirmation_modal(ui: &mut egui::Ui) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let reset_text = format!("{}?", t!("network_settings.reset_settings_desc"));
ui.label(RichText::new(reset_text)
.size(17.0)
.color(Colors::text(false)));
ui.add_space(8.0);
});
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let reset_text = format!("{}?", t!("network_settings.reset_settings_desc"));
ui.label(
RichText::new(reset_text)
.size(17.0)
.color(Colors::text(false)),
);
ui.add_space(8.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("network_settings.reset"), Colors::white_or_black(false), || {
NodeConfig::reset_to_default();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
Modal::close();
});
});
});
ui.add_space(6.0);
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("network_settings.reset"),
Colors::white_or_black(false),
|| {
NodeConfig::reset_to_default();
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Modal::close();
},
);
});
});
ui.add_space(6.0);
});
}
+412 -355
View File
@@ -14,27 +14,27 @@
use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::icons::{CLOCK_COUNTDOWN, GRAPH, TIMER, WATCH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::NetworkSettings;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::Colors;
use crate::gui::views::network::NetworkSettings;
use crate::node::NodeConfig;
/// Dandelion server setup section content.
pub struct DandelionSetup {
/// Epoch duration value in seconds.
epoch_edit: String,
/// Epoch duration value in seconds.
epoch_edit: String,
/// Embargo expiration time value in seconds to fluff and broadcast if tx not seen on network.
embargo_edit: String,
/// Embargo expiration time value in seconds to fluff and broadcast if tx not seen on network.
embargo_edit: String,
/// Aggregation period value in seconds.
aggregation_edit: String,
/// Aggregation period value in seconds.
aggregation_edit: String,
/// Stem phase probability value (stem 90% of the time, fluff 10% of the time by default).
stem_prob_edit: String,
/// Stem phase probability value (stem 90% of the time, fluff 10% of the time by default).
stem_prob_edit: String,
}
/// Identifier epoch duration value [`Modal`].
@@ -47,396 +47,453 @@ pub const AGGREGATION_MODAL: &'static str = "aggregation_secs";
pub const STEM_PROBABILITY_MODAL: &'static str = "stem_probability";
impl Default for DandelionSetup {
fn default() -> Self {
Self {
epoch_edit: NodeConfig::get_dandelion_epoch(),
embargo_edit: NodeConfig::get_reorg_cache_period(),
aggregation_edit: NodeConfig::get_dandelion_aggregation(),
stem_prob_edit: NodeConfig::get_stem_probability(),
}
}
fn default() -> Self {
Self {
epoch_edit: NodeConfig::get_dandelion_epoch(),
embargo_edit: NodeConfig::get_reorg_cache_period(),
aggregation_edit: NodeConfig::get_dandelion_aggregation(),
stem_prob_edit: NodeConfig::get_stem_probability(),
}
}
}
impl ContentContainer for DandelionSetup {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
EPOCH_MODAL,
EMBARGO_MODAL,
AGGREGATION_MODAL,
STEM_PROBABILITY_MODAL
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![
EPOCH_MODAL,
EMBARGO_MODAL,
AGGREGATION_MODAL,
STEM_PROBABILITY_MODAL,
]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
EPOCH_MODAL => self.epoch_modal(ui, modal, cb),
EMBARGO_MODAL => self.embargo_modal(ui, modal, cb),
AGGREGATION_MODAL => self.aggregation_modal(ui, modal, cb),
STEM_PROBABILITY_MODAL => self.stem_prob_modal(ui, modal, cb),
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
EPOCH_MODAL => self.epoch_modal(ui, modal, cb),
EMBARGO_MODAL => self.embargo_modal(ui, modal, cb),
AGGREGATION_MODAL => self.aggregation_modal(ui, modal, cb),
STEM_PROBABILITY_MODAL => self.stem_prob_modal(ui, modal, cb),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
View::sub_title(ui, format!("{} {}", GRAPH, "Dandelion"));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
View::sub_title(ui, format!("{} {}", GRAPH, "Dandelion"));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Show epoch duration setup.
self.epoch_ui(ui);
ui.vertical_centered(|ui| {
// Show epoch duration setup.
self.epoch_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show embargo expiration time setup.
self.embargo_ui(ui);
// Show embargo expiration time setup.
self.embargo_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show aggregation period setup.
self.aggregation_ui(ui);
// Show aggregation period setup.
self.aggregation_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show Stem phase probability setup.
self.stem_prob_ui(ui);
// Show Stem phase probability setup.
self.stem_prob_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show setup to always stem our txs.
let always_stem = NodeConfig::always_stem_our_txs();
View::checkbox(ui, always_stem, t!("network_settings.stem_txs"), || {
NodeConfig::toggle_always_stem_our_txs();
});
ui.add_space(6.0);
});
}
// Show setup to always stem our txs.
let always_stem = NodeConfig::always_stem_our_txs();
View::checkbox(ui, always_stem, t!("network_settings.stem_txs"), || {
NodeConfig::toggle_always_stem_our_txs();
});
ui.add_space(6.0);
});
}
}
impl DandelionSetup {
/// Draw epoch duration setup content.
fn epoch_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.epoch_duration"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
/// Draw epoch duration setup content.
fn epoch_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.epoch_duration"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
let epoch = NodeConfig::get_dandelion_epoch();
View::button(ui, format!("{} {}", WATCH, &epoch), Colors::white_or_black(false), || {
// Setup values for modal.
self.epoch_edit = epoch;
// Show epoch setup modal.
Modal::new(EPOCH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(6.0);
}
let epoch = NodeConfig::get_dandelion_epoch();
View::button(
ui,
format!("{} {}", WATCH, &epoch),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.epoch_edit = epoch;
// Show epoch setup modal.
Modal::new(EPOCH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
}
/// Draw epoch duration [`Modal`] content.
fn epoch_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(epoch) = c.epoch_edit.parse::<u16>() {
NodeConfig::save_dandelion_epoch(epoch);
Modal::close();
}
};
/// Draw epoch duration [`Modal`] content.
fn epoch_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(epoch) = c.epoch_edit.parse::<u16>() {
NodeConfig::save_dandelion_epoch(epoch);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.epoch_duration"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.epoch_duration"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw epoch text edit.
let mut epoch_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
epoch_edit.ui(ui, &mut self.epoch_edit, cb);
if epoch_edit.enter_pressed {
on_save(self);
}
// Draw epoch text edit.
let mut epoch_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
epoch_edit.ui(ui, &mut self.epoch_edit, cb);
if epoch_edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.epoch_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show error when specified value is not valid or reminder to restart enabled node.
if self.epoch_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
/// Draw embargo expiration time setup content.
fn embargo_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.embargo_timer"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
/// Draw embargo expiration time setup content.
fn embargo_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.embargo_timer"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
let embargo = NodeConfig::get_dandelion_embargo();
View::button(ui, format!("{} {}", TIMER, &embargo), Colors::white_or_black(false), || {
self.embargo_edit = embargo;
// Show embargo setup modal.
Modal::new(EMBARGO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(6.0);
}
let embargo = NodeConfig::get_dandelion_embargo();
View::button(
ui,
format!("{} {}", TIMER, &embargo),
Colors::white_or_black(false),
|| {
self.embargo_edit = embargo;
// Show embargo setup modal.
Modal::new(EMBARGO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
}
/// Draw epoch duration [`Modal`] content.
fn embargo_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(embargo) = c.embargo_edit.parse::<u16>() {
NodeConfig::save_dandelion_embargo(embargo);
Modal::close();
}
};
/// Draw epoch duration [`Modal`] content.
fn embargo_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(embargo) = c.embargo_edit.parse::<u16>() {
NodeConfig::save_dandelion_embargo(embargo);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.embargo_timer"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.embargo_timer"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw embargo text edit.
let mut embargo_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
embargo_edit.ui(ui, &mut self.embargo_edit, cb);
if embargo_edit.enter_pressed {
on_save(self);
}
// Draw embargo text edit.
let mut embargo_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
embargo_edit.ui(ui, &mut self.embargo_edit, cb);
if embargo_edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.embargo_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show error when specified value is not valid or reminder to restart enabled node.
if self.embargo_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
/// Draw aggregation period setup content.
fn aggregation_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.aggregation_period"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
/// Draw aggregation period setup content.
fn aggregation_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.aggregation_period"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
let ag = NodeConfig::get_dandelion_aggregation();
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, &ag), Colors::white_or_black(false), || {
// Setup values for modal.
self.aggregation_edit = ag;
// Show aggregation setup modal.
Modal::new(AGGREGATION_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(6.0);
}
let ag = NodeConfig::get_dandelion_aggregation();
View::button(
ui,
format!("{} {}", CLOCK_COUNTDOWN, &ag),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.aggregation_edit = ag;
// Show aggregation setup modal.
Modal::new(AGGREGATION_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
}
/// Draw aggregation period [`Modal`] content.
fn aggregation_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(embargo) = c.aggregation_edit.parse::<u16>() {
NodeConfig::save_dandelion_aggregation(embargo);
Modal::close();
}
};
/// Draw aggregation period [`Modal`] content.
fn aggregation_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(embargo) = c.aggregation_edit.parse::<u16>() {
NodeConfig::save_dandelion_aggregation(embargo);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.aggregation_period"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.aggregation_period"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw aggregation period text edit.
let mut aggregation_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
aggregation_edit.ui(ui, &mut self.aggregation_edit, cb);
if aggregation_edit.enter_pressed {
on_save(self);
}
// Draw aggregation period text edit.
let mut aggregation_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
aggregation_edit.ui(ui, &mut self.aggregation_edit, cb);
if aggregation_edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.aggregation_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show error when specified value is not valid or reminder to restart enabled node.
if self.aggregation_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
/// Draw stem phase probability setup content.
fn stem_prob_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.stem_probability"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
/// Draw stem phase probability setup content.
fn stem_prob_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.stem_probability"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
let stem_prob = NodeConfig::get_stem_probability();
View::button(ui, format!("{}%", &stem_prob), Colors::white_or_black(false), || {
// Setup values for modal.
self.stem_prob_edit = stem_prob;
// Show stem probability setup modal.
Modal::new(STEM_PROBABILITY_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(6.0);
}
let stem_prob = NodeConfig::get_stem_probability();
View::button(
ui,
format!("{}%", &stem_prob),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.stem_prob_edit = stem_prob;
// Show stem probability setup modal.
Modal::new(STEM_PROBABILITY_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
}
/// Draw stem phase probability [`Modal`] content.
fn stem_prob_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(prob) = c.stem_prob_edit.parse::<u8>() {
NodeConfig::save_stem_probability(prob);
Modal::close();
}
};
/// Draw stem phase probability [`Modal`] content.
fn stem_prob_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(prob) = c.stem_prob_edit.parse::<u8>() {
NodeConfig::save_stem_probability(prob);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.stem_probability"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.stem_probability"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw stem phase probability text edit.
let mut stem_prob_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
stem_prob_edit.ui(ui, &mut self.stem_prob_edit, cb);
if stem_prob_edit.enter_pressed {
on_save(self);
}
// Draw stem phase probability text edit.
let mut stem_prob_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
stem_prob_edit.ui(ui, &mut self.stem_prob_edit, cb);
if stem_prob_edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.stem_prob_edit.parse::<u8>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show error when specified value is not valid or reminder to restart enabled node.
if self.stem_prob_edit.parse::<u8>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
}
+1 -1
View File
@@ -25,4 +25,4 @@ mod dandelion;
pub use dandelion::DandelionSetup;
mod stratum;
pub use stratum::StratumSetup;
pub use stratum::StratumSetup;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+474 -420
View File
@@ -18,39 +18,39 @@ use grin_chain::SyncStatus;
use crate::gui::Colors;
use crate::gui::icons::{BARBELL, HARD_DRIVES, PLUG, POWER, TIMER};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::views::network::settings::NetworkSettings;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::wallets::modals::WalletListModal;
use crate::gui::views::{Modal, TextEdit, View};
use crate::node::{Node, NodeConfig};
use crate::wallet::{WalletConfig, WalletList};
/// Stratum server setup section content.
pub struct StratumSetup {
/// Wallet list to select for mining rewards.
wallets: WalletList,
/// Wallets [`Modal`] content.
wallets_modal: WalletListModal,
/// Wallet list to select for mining rewards.
wallets: WalletList,
/// Wallets [`Modal`] content.
wallets_modal: WalletListModal,
/// IP Addresses available at system.
available_ips: Vec<String>,
/// IP Addresses available at system.
available_ips: Vec<String>,
/// Stratum port value.
stratum_port_edit: String,
/// Flag to check if stratum port is available.
stratum_port_available_edit: bool,
/// Stratum port value.
stratum_port_edit: String,
/// Flag to check if stratum port is available.
stratum_port_available_edit: bool,
/// Flag to check if stratum port from saved config value is available.
is_port_available: bool,
/// Flag to check if stratum port from saved config value is available.
is_port_available: bool,
/// Wallet name to receive rewards.
pub wallet_name: Option<String>,
/// Wallet name to receive rewards.
pub wallet_name: Option<String>,
/// Attempt time value in seconds to mine on a particular header.
attempt_time_edit: String,
/// Attempt time value in seconds to mine on a particular header.
attempt_time_edit: String,
/// Minimum share difficulty value to request from miners.
min_share_diff_edit: String,
/// Minimum share difficulty value to request from miners.
min_share_diff_edit: String,
}
/// Identifier for wallet selection [`Modal`].
@@ -63,460 +63,514 @@ const ATTEMPT_TIME_MODAL: &'static str = "stratum_attempt_time";
const MIN_SHARE_DIFF_MODAL: &'static str = "stratum_min_share_diff";
impl Default for StratumSetup {
fn default() -> Self {
let (ip, port) = NodeConfig::get_stratum_address();
let is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
fn default() -> Self {
let (ip, port) = NodeConfig::get_stratum_address();
let is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
// Setup mining rewards wallet name and identifier.
let mut wallet_id = NodeConfig::get_stratum_wallet_id();
let wallet_name = if let Some(id) = wallet_id {
WalletConfig::read_name_by_id(id)
} else {
None
};
if wallet_name.is_none() {
wallet_id = None;
}
// Setup mining rewards wallet name and identifier.
let mut wallet_id = NodeConfig::get_stratum_wallet_id();
let wallet_name = if let Some(id) = wallet_id {
WalletConfig::read_name_by_id(id)
} else {
None
};
if wallet_name.is_none() {
wallet_id = None;
}
Self {
wallets: WalletList::default(),
wallets_modal: WalletListModal::new(wallet_id, None, false),
available_ips: NodeConfig::get_ip_addrs(),
stratum_port_edit: port,
stratum_port_available_edit: is_port_available,
is_port_available,
wallet_name,
attempt_time_edit: NodeConfig::get_stratum_attempt_time(),
min_share_diff_edit: NodeConfig::get_stratum_min_share_diff(),
}
}
Self {
wallets: WalletList::default(),
wallets_modal: WalletListModal::new(wallet_id, None, false),
available_ips: NodeConfig::get_ip_addrs(),
stratum_port_edit: port,
stratum_port_available_edit: is_port_available,
is_port_available,
wallet_name,
attempt_time_edit: NodeConfig::get_stratum_attempt_time(),
min_share_diff_edit: NodeConfig::get_stratum_min_share_diff(),
}
}
}
impl ContentContainer for StratumSetup {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
WALLET_SELECTION_MODAL,
STRATUM_PORT_MODAL,
ATTEMPT_TIME_MODAL,
MIN_SHARE_DIFF_MODAL
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![
WALLET_SELECTION_MODAL,
STRATUM_PORT_MODAL,
ATTEMPT_TIME_MODAL,
MIN_SHARE_DIFF_MODAL,
]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
WALLET_SELECTION_MODAL => {
self.wallets_modal.ui(ui, &mut self.wallets, |wallet, _| {
let id = wallet.get_config().id;
NodeConfig::save_stratum_wallet_id(id);
self.wallet_name = WalletConfig::read_name_by_id(id);
})
},
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, &mut self.wallets, |wallet, _| {
let id = wallet.get_config().id;
NodeConfig::save_stratum_wallet_id(id);
self.wallet_name = WalletConfig::read_name_by_id(id);
}),
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
View::sub_title(ui, format!("{} {}", HARD_DRIVES, t!("network_mining.server")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
View::sub_title(
ui,
format!("{} {}", HARD_DRIVES, t!("network_mining.server")),
);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Show loading indicator or controls to start/stop stratum server.
if Node::get_sync_status().unwrap_or(SyncStatus::Initial) == SyncStatus::NoSync &&
self.is_port_available && self.wallet_name.is_some() {
if Node::is_stratum_starting() || Node::is_stratum_stopping() {
ui.vertical_centered(|ui| {
ui.add_space(8.0);
View::small_loading_spinner(ui);
ui.add_space(8.0);
});
} else if Node::get_stratum_stats().is_running {
ui.add_space(6.0);
let disable_text = format!("{} {}", POWER, t!("network_settings.disable"));
View::action_button(ui, disable_text, || {
Node::stop_stratum();
let (ip, port) = NodeConfig::get_stratum_address();
self.is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
});
ui.add_space(6.0);
} else {
ui.add_space(6.0);
let enable_text = format!("{} {}", POWER, t!("network_settings.enable"));
View::action_button(ui, enable_text, || {
Node::start_stratum();
});
ui.add_space(6.0);
}
}
ui.vertical_centered(|ui| {
// Show loading indicator or controls to start/stop stratum server.
if Node::get_sync_status().unwrap_or(SyncStatus::Initial) == SyncStatus::NoSync
&& self.is_port_available
&& self.wallet_name.is_some()
{
if Node::is_stratum_starting() || Node::is_stratum_stopping() {
ui.vertical_centered(|ui| {
ui.add_space(8.0);
View::small_loading_spinner(ui);
ui.add_space(8.0);
});
} else if Node::get_stratum_stats().is_running {
ui.add_space(6.0);
let disable_text = format!("{} {}", POWER, t!("network_settings.disable"));
View::action_button(ui, disable_text, || {
Node::stop_stratum();
let (ip, port) = NodeConfig::get_stratum_address();
self.is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
});
ui.add_space(6.0);
} else {
ui.add_space(6.0);
let enable_text = format!("{} {}", POWER, t!("network_settings.enable"));
View::action_button(ui, enable_text, || {
Node::start_stratum();
});
ui.add_space(6.0);
}
}
// Show stratum server autorun checkbox.
let stratum_enabled = NodeConfig::is_stratum_autorun_enabled();
View::checkbox(ui, stratum_enabled, t!("network.autorun"), || {
NodeConfig::toggle_stratum_autorun();
});
// Show stratum server autorun checkbox.
let stratum_enabled = NodeConfig::is_stratum_autorun_enabled();
View::checkbox(ui, stratum_enabled, t!("network.autorun"), || {
NodeConfig::toggle_stratum_autorun();
});
// Show reminder to restart running server.
if Node::get_stratum_stats().is_running {
ui.add_space(2.0);
ui.label(RichText::new(t!("network_mining.restart_server_required"))
.size(16.0)
.color(Colors::inactive_text())
);
}
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show reminder to restart running server.
if Node::get_stratum_stats().is_running {
ui.add_space(2.0);
ui.label(
RichText::new(t!("network_mining.restart_server_required"))
.size(16.0)
.color(Colors::inactive_text()),
);
}
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show wallet name.
ui.label(RichText::new(self.wallet_name.as_ref().unwrap_or(&"-".to_string()))
.size(16.0)
.color(Colors::white_or_black(true)));
ui.add_space(8.0);
// Show wallet name.
ui.label(
RichText::new(self.wallet_name.as_ref().unwrap_or(&"-".to_string()))
.size(16.0)
.color(Colors::white_or_black(true)),
);
ui.add_space(8.0);
// Show button to select wallet.
View::button(ui,
t!("network_settings.choose_wallet"),
Colors::white_or_black(false), || {
self.show_wallets_modal();
});
ui.add_space(12.0);
// Show button to select wallet.
View::button(
ui,
t!("network_settings.choose_wallet"),
Colors::white_or_black(false),
|| {
self.show_wallets_modal();
},
);
ui.add_space(12.0);
if self.wallet_name.is_some() {
ui.label(RichText::new(t!("network_settings.stratum_wallet_warning"))
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(12.0);
}
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
});
if self.wallet_name.is_some() {
ui.label(
RichText::new(t!("network_settings.stratum_wallet_warning"))
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(12.0);
}
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
});
// Show message when IP addresses are not available on the system.
if self.available_ips.is_empty() {
NetworkSettings::no_ip_address_ui(ui);
return;
}
// Show message when IP addresses are not available on the system.
if self.available_ips.is_empty() {
NetworkSettings::no_ip_address_ui(ui);
return;
}
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.stratum_ip"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
// Show stratum IP addresses to select.
let (ip, port) = NodeConfig::get_stratum_address();
NetworkSettings::ip_addrs_ui(ui, &ip, &self.available_ips, |selected_ip| {
NodeConfig::save_stratum_address(selected_ip, &port);
self.is_port_available = NodeConfig::is_stratum_port_available(selected_ip, &port);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.stratum_ip"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Show stratum IP addresses to select.
let (ip, port) = NodeConfig::get_stratum_address();
NetworkSettings::ip_addrs_ui(ui, &ip, &self.available_ips, |selected_ip| {
NodeConfig::save_stratum_address(selected_ip, &port);
self.is_port_available = NodeConfig::is_stratum_port_available(selected_ip, &port);
});
// Show stratum port setup.
self.port_setup_ui(ui);
});
// Show stratum port setup.
self.port_setup_ui(ui);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show attempt time setup.
self.attempt_time_ui(ui);
// Show attempt time setup.
self.attempt_time_ui(ui);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show minimum acceptable share difficulty setup.
self.min_diff_ui(ui);
});
}
// Show minimum acceptable share difficulty setup.
self.min_diff_ui(ui);
});
}
}
impl StratumSetup {
/// Show wallet selection [`Modal`].
fn show_wallets_modal(&mut self) {
self.wallets_modal = WalletListModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
// Show modal.
Modal::new(WALLET_SELECTION_MODAL)
.position(ModalPosition::Center)
.title(t!("network_settings.choose_wallet"))
.show();
}
/// Show wallet selection [`Modal`].
fn show_wallets_modal(&mut self) {
self.wallets_modal = WalletListModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
// Show modal.
Modal::new(WALLET_SELECTION_MODAL)
.position(ModalPosition::Center)
.title(t!("network_settings.choose_wallet"))
.show();
}
/// Draw stratum port value setup content.
fn port_setup_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.stratum_port"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
/// Draw stratum port value setup content.
fn port_setup_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.stratum_port"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
let (_, port) = NodeConfig::get_stratum_address();
View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || {
// Setup values for modal.
self.stratum_port_edit = port;
self.stratum_port_available_edit = self.is_port_available;
// Show stratum port modal.
Modal::new(STRATUM_PORT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(12.0);
let (_, port) = NodeConfig::get_stratum_address();
View::button(
ui,
format!("{} {}", PLUG, &port),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.stratum_port_edit = port;
self.stratum_port_available_edit = self.is_port_available;
// Show stratum port modal.
Modal::new(STRATUM_PORT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(12.0);
// Show error when stratum server port is unavailable.
if !self.is_port_available {
ui.add_space(6.0);
ui.label(RichText::new(t!("network_settings.port_unavailable"))
.size(16.0)
.color(Colors::red()));
ui.add_space(12.0);
}
}
// Show error when stratum server port is unavailable.
if !self.is_port_available {
ui.add_space(6.0);
ui.label(
RichText::new(t!("network_settings.port_unavailable"))
.size(16.0)
.color(Colors::red()),
);
ui.add_space(12.0);
}
}
/// Draw stratum port [`Modal`] content.
fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut StratumSetup| {
// Check if port is available.
let (stratum_ip, _) = NodeConfig::get_stratum_address();
let available = NodeConfig::is_stratum_port_available(
&stratum_ip,
&c.stratum_port_edit
);
c.stratum_port_available_edit = available;
/// Draw stratum port [`Modal`] content.
fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut StratumSetup| {
// Check if port is available.
let (stratum_ip, _) = NodeConfig::get_stratum_address();
let available =
NodeConfig::is_stratum_port_available(&stratum_ip, &c.stratum_port_edit);
c.stratum_port_available_edit = available;
// Save port at config if it's available.
if available {
NodeConfig::save_stratum_address(&stratum_ip, &c.stratum_port_edit);
// Save port at config if it's available.
if available {
NodeConfig::save_stratum_address(&stratum_ip, &c.stratum_port_edit);
c.is_port_available = true;
Modal::close();
}
};
c.is_port_available = true;
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.stratum_port"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.stratum_port"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw stratum port text edit.
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.stratum_port_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Draw stratum port text edit.
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.stratum_port_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified port is unavailable.
if !self.stratum_port_available_edit {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.port_unavailable"))
.size(17.0)
.color(Colors::red()));
} else {
server_restart_required_ui(ui);
}
// Show error when specified port is unavailable.
if !self.stratum_port_available_edit {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.port_unavailable"))
.size(17.0)
.color(Colors::red()),
);
} else {
server_restart_required_ui(ui);
}
ui.add_space(12.0);
ui.add_space(12.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
});
}
/// Draw attempt time value setup content.
fn attempt_time_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.attempt_time"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
/// Draw attempt time value setup content.
fn attempt_time_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.attempt_time"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
let time = NodeConfig::get_stratum_attempt_time();
View::button(ui, format!("{} {}", TIMER, &time), Colors::white_or_black(false), || {
// Setup values for modal.
self.attempt_time_edit = time;
let time = NodeConfig::get_stratum_attempt_time();
View::button(
ui,
format!("{} {}", TIMER, &time),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.attempt_time_edit = time;
// Show attempt time modal.
Modal::new(ATTEMPT_TIME_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(6.0);
ui.label(RichText::new(t!("network_settings.attempt_time_desc"))
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(6.0);
}
// Show attempt time modal.
Modal::new(ATTEMPT_TIME_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
ui.label(
RichText::new(t!("network_settings.attempt_time_desc"))
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(6.0);
}
/// Draw attempt time [`Modal`] content.
fn attempt_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut StratumSetup| {
if let Ok(time) = c.attempt_time_edit.parse::<u32>() {
NodeConfig::save_stratum_attempt_time(time);
Modal::close();
}
};
/// Draw attempt time [`Modal`] content.
fn attempt_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut StratumSetup| {
if let Ok(time) = c.attempt_time_edit.parse::<u32>() {
NodeConfig::save_stratum_attempt_time(time);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.attempt_time"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.attempt_time"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw attempt time text edit.
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.attempt_time_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Draw attempt time text edit.
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.attempt_time_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.attempt_time_edit.parse::<u32>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
server_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show error when specified value is not valid or reminder to restart enabled node.
if self.attempt_time_edit.parse::<u32>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
server_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
/// Draw minimum share difficulty value setup content.
fn min_diff_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.min_share_diff"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
/// Draw minimum share difficulty value setup content.
fn min_diff_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.min_share_diff"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
let diff = NodeConfig::get_stratum_min_share_diff();
View::button(ui, format!("{} {}", BARBELL, &diff), Colors::white_or_black(false), || {
// Setup values for modal.
self.min_share_diff_edit = diff;
let diff = NodeConfig::get_stratum_min_share_diff();
View::button(
ui,
format!("{} {}", BARBELL, &diff),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.min_share_diff_edit = diff;
// Show share difficulty setup modal.
Modal::new(MIN_SHARE_DIFF_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(6.0);
}
// Show share difficulty setup modal.
Modal::new(MIN_SHARE_DIFF_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
}
/// Draw minimum acceptable share difficulty [`Modal`] content.
fn min_diff_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut StratumSetup| {
if let Ok(diff) = c.min_share_diff_edit.parse::<u64>() {
NodeConfig::save_stratum_min_share_diff(diff);
Modal::close();
}
};
/// Draw minimum acceptable share difficulty [`Modal`] content.
fn min_diff_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut StratumSetup| {
if let Ok(diff) = c.min_share_diff_edit.parse::<u64>() {
NodeConfig::save_stratum_min_share_diff(diff);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.min_share_diff"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.min_share_diff"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw share difficulty text edit.
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.min_share_diff_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Draw share difficulty text edit.
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.min_share_diff_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.min_share_diff_edit.parse::<u64>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
server_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show error when specified value is not valid or reminder to restart enabled node.
if self.min_share_diff_edit.parse::<u64>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
server_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
}
/// Reminder to restart enabled node to show on edit setting at [`Modal`].
pub fn server_restart_required_ui(ui: &mut egui::Ui) {
if Node::get_stratum_stats().is_running {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_mining.restart_server_required"))
.size(16.0)
.color(Colors::green())
);
}
}
if Node::get_stratum_stats().is_running {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_mining.restart_server_required"))
.size(16.0)
.color(Colors::green()),
);
}
}
+25 -15
View File
@@ -13,29 +13,39 @@
// limitations under the License.
use crate::gui::platform::PlatformCallbacks;
use serde_derive::{Deserialize, Serialize};
/// Integrated node tab content interface.
pub trait NodeTab {
fn get_type(&self) -> NodeTabType;
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
fn get_type(&self) -> NodeTabType;
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
}
/// Type of [`NodeTab`] content.
#[derive(PartialEq)]
pub enum NodeTabType {
Info,
Metrics,
Mining,
Settings
Info,
Metrics,
Mining,
Settings,
}
impl NodeTabType {
pub fn title(&self) -> String {
match *self {
NodeTabType::Info => t!("network.node").into(),
NodeTabType::Metrics => t!("network.metrics").into(),
NodeTabType::Mining => t!("network.mining").into(),
NodeTabType::Settings => t!("network.settings").into()
}
}
}
pub fn title(&self) -> String {
match *self {
NodeTabType::Info => t!("network.node").into(),
NodeTabType::Metrics => t!("network.metrics").into(),
NodeTabType::Mining => t!("network.mining").into(),
NodeTabType::Settings => t!("network.settings").into(),
}
}
}
/// Connection details to share.
#[derive(Serialize, Deserialize, Clone)]
pub struct ShareConnection {
#[serde(rename(serialize = "ipPort", deserialize = "ipPort"))]
pub url: String,
pub username: String,
pub secret: String,
}
+297 -288
View File
@@ -12,361 +12,370 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::epaint::{Pos2, Shape, Stroke, emath::lerp, vec2};
use egui::scroll_area::ScrollAreaOutput;
use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2, UiBuilder};
use egui::epaint::{emath::lerp, vec2, Pos2, Shape, Stroke};
use egui::{Align2, Area, Color32, Id, Rect, Response, Sense, UiBuilder, Vec2, Widget};
/// A spinner widget used to indicate loading.
/// This was taken from egui and modified slightly to allow passing a progress value
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Default)]
pub struct ProgressSpinner {
/// Uses the style's `interact_size` if `None`.
size: Option<f32>,
color: Option<Color32>,
progress: Option<f64>,
/// Uses the style's `interact_size` if `None`.
size: Option<f32>,
color: Option<Color32>,
progress: Option<f64>,
}
impl ProgressSpinner {
/// Create a new spinner that uses the style's `interact_size` unless changed.
pub fn new() -> Self {
Self::default()
}
/// Create a new spinner that uses the style's `interact_size` unless changed.
pub fn new() -> Self {
Self::default()
}
/// Sets the spinner's size. The size sets both the height and width, as the spinner is always
/// square. If the size isn't set explicitly, the active style's `interact_size` is used.
#[allow(unused)]
pub fn size(mut self, size: f32) -> Self {
self.size = Some(size);
self
}
/// Sets the spinner's size. The size sets both the height and width, as the spinner is always
/// square. If the size isn't set explicitly, the active style's `interact_size` is used.
#[allow(unused)]
pub fn size(mut self, size: f32) -> Self {
self.size = Some(size);
self
}
/// Sets the spinner's color.
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.color = Some(color.into());
self
}
/// Sets the spinner's color.
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.color = Some(color.into());
self
}
/// Sets the spinner's progress.
/// Should be in the range `[0.0, 1.0]`.
pub fn progress(mut self, progress: impl Into<Option<f64>>) -> Self {
self.progress = progress.into();
self
}
/// Sets the spinner's progress.
/// Should be in the range `[0.0, 1.0]`.
pub fn progress(mut self, progress: impl Into<Option<f64>>) -> Self {
self.progress = progress.into();
self
}
/// Paint the spinner in the given rectangle.
pub fn paint_at(&self, ui: &egui::Ui, rect: Rect) {
if ui.is_rect_visible(rect) {
ui.ctx().request_repaint(); // because it is animated
/// Paint the spinner in the given rectangle.
pub fn paint_at(&self, ui: &egui::Ui, rect: Rect) {
if ui.is_rect_visible(rect) {
ui.ctx().request_repaint(); // because it is animated
let color = self
.color
.unwrap_or_else(|| ui.visuals().strong_text_color());
let radius = (rect.height() / 2.0) - 2.0;
let n_points = 20;
let color = self
.color
.unwrap_or_else(|| ui.visuals().strong_text_color());
let radius = (rect.height() / 2.0) - 2.0;
let n_points = 20;
let (start_angle, end_angle) = if let Some(progress) = self.progress {
let start_angle = 0f64.to_radians();
let end_angle = start_angle + 360f64.to_radians() * progress;
(start_angle, end_angle)
} else {
let time = ui.input(|i| i.time);
let start_angle = time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * time.sin();
(start_angle, end_angle)
};
let (start_angle, end_angle) = if let Some(progress) = self.progress {
let start_angle = 0f64.to_radians();
let end_angle = start_angle + 360f64.to_radians() * progress;
(start_angle, end_angle)
} else {
let time = ui.input(|i| i.time);
let start_angle = time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * time.sin();
(start_angle, end_angle)
};
let points: Vec<Pos2> = (0..=n_points)
.map(|i| {
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
let (sin, cos) = angle.sin_cos();
rect.center() + radius * vec2(cos as f32, sin as f32)
})
.collect();
ui.painter()
.add(Shape::line(points, Stroke::new(3.0, color)));
}
}
let points: Vec<Pos2> = (0..=n_points)
.map(|i| {
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
let (sin, cos) = angle.sin_cos();
rect.center() + radius * vec2(cos as f32, sin as f32)
})
.collect();
ui.painter()
.add(Shape::line(points, Stroke::new(3.0, color)));
}
}
}
impl Widget for ProgressSpinner {
fn ui(self, ui: &mut egui::Ui) -> Response {
let size = self
.size
.unwrap_or_else(|| ui.style().spacing.interact_size.y);
let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
self.paint_at(ui, rect);
fn ui(self, ui: &mut egui::Ui) -> Response {
let size = self
.size
.unwrap_or_else(|| ui.style().spacing.interact_size.y);
let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
self.paint_at(ui, rect);
response
}
response
}
}
/// The current state of the pull to refresh widget.
#[derive(Debug, Clone)]
pub enum PullToRefreshState {
/// The widget is idle, no refresh is happening.
Idle,
/// The user is dragging.
Dragging {
/// `distance` is the distance the user dragged.
distance: f32,
/// `far_enough` is true if the user dragged far enough to trigger a refresh.
far_enough: bool,
},
/// The user dragged far enough to trigger a refresh and released the pointer.
DoRefresh,
/// The refresh is currently happening.
Refreshing,
/// The widget is idle, no refresh is happening.
Idle,
/// The user is dragging.
Dragging {
/// `distance` is the distance the user dragged.
distance: f32,
/// `far_enough` is true if the user dragged far enough to trigger a refresh.
far_enough: bool,
},
/// The user dragged far enough to trigger a refresh.
DoRefresh,
/// The refresh is currently happening.
Refreshing,
}
impl PullToRefreshState {
fn progress(&self, min_distance: f32) -> Option<f64> {
match self {
PullToRefreshState::Idle => Some(0.0),
PullToRefreshState::Dragging { distance, .. } => {
Some((distance / min_distance).min(1.0).max(0.0) as f64)
}
PullToRefreshState::DoRefresh => Some(1.0),
PullToRefreshState::Refreshing => None,
}
}
fn progress(&self, min_distance: f32) -> Option<f64> {
match self {
PullToRefreshState::Idle => Some(0.0),
PullToRefreshState::Dragging { distance, .. } => {
Some((distance / min_distance).min(1.0).max(0.0) as f64)
}
PullToRefreshState::DoRefresh => Some(1.0),
PullToRefreshState::Refreshing => None,
}
}
}
/// The response of the pull to refresh widget.
#[derive(Debug, Clone)]
pub struct PullToRefreshResponse<T> {
/// Current state of the pull to refresh widget.
pub state: PullToRefreshState,
/// The inner response of the widget you wrapped in [`PullToRefresh::ui`] or [`PullToRefresh::scroll_area_ui`].
pub inner: T,
/// Current state of the pull to refresh widget.
pub state: PullToRefreshState,
/// The inner response of the widget you wrapped in [`PullToRefresh::ui`] or [`PullToRefresh::scroll_area_ui`].
pub inner: T,
}
impl<T> PullToRefreshResponse<T> {
/// Returns true if the user dragged far enough to trigger a refresh.
pub fn should_refresh(&self) -> bool {
matches!(self.state, PullToRefreshState::DoRefresh)
}
/// Returns true if the user dragged far enough to trigger a refresh.
pub fn should_refresh(&self) -> bool {
matches!(self.state, PullToRefreshState::DoRefresh)
}
}
/// A widget that allows the user to pull to refresh.
pub struct PullToRefresh {
id: Id,
loading: bool,
min_refresh_distance: f32,
can_refresh: bool,
id: Id,
loading: bool,
min_refresh_distance: f32,
can_refresh: bool,
}
impl PullToRefresh {
/// Creates a new pull to refresh widget.
/// If `loading` is true, the widget will show the loading indicator.
pub fn new(loading: bool) -> Self {
Self {
id: Id::new("pull_to_refresh"),
loading,
min_refresh_distance: 100.0,
can_refresh: true,
}
}
/// Creates a new pull to refresh widget.
/// If `loading` is true, the widget will show the loading indicator.
pub fn new(loading: bool) -> Self {
Self {
id: Id::new("pull_to_refresh"),
loading,
min_refresh_distance: 100.0,
can_refresh: true,
}
}
/// Sets the minimum distance the user needs to drag to trigger a refresh.
pub fn min_refresh_distance(mut self, min_refresh_distance: f32) -> Self {
self.min_refresh_distance = min_refresh_distance;
self
}
/// Sets the minimum distance the user needs to drag to trigger a refresh.
pub fn min_refresh_distance(mut self, min_refresh_distance: f32) -> Self {
self.min_refresh_distance = min_refresh_distance;
self
}
/// You need to provide a id if you use multiple pull to refresh widgets at once.
pub fn id(mut self, id: Id) -> Self {
self.id = id;
self
}
/// You need to provide a id if you use multiple pull to refresh widgets at once.
pub fn id(mut self, id: Id) -> Self {
self.id = id;
self
}
/// If `can_refresh` is false, pulling will not trigger a refresh.
pub fn can_refresh(mut self, can_refresh: bool) -> Self {
self.can_refresh = can_refresh;
self
}
/// If `can_refresh` is false, pulling will not trigger a refresh.
pub fn can_refresh(mut self, can_refresh: bool) -> Self {
self.can_refresh = can_refresh;
self
}
/// Shows the pull to refresh widget.
/// Note: If you want to use the pull to refresh widget in a scroll area, use [`Self::scroll_area_ui`].
/// You might want to disable text selection via [`egui::style::Interaction`]
/// to avoid conflicts with the drag gesture.
pub fn ui<T>(
self,
ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> T,
) -> PullToRefreshResponse<T> {
let mut child = ui.new_child(UiBuilder::new()
.max_rect(ui.available_rect_before_wrap())
.layout(*ui.layout()));
/// Shows the pull to refresh widget.
/// Note: If you want to use the pull to refresh widget in a scroll area, use [`Self::scroll_area_ui`].
/// You might want to disable text selection via [`egui::style::Interaction`]
/// to avoid conflicts with the drag gesture.
pub fn ui<T>(
self,
ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> T,
) -> PullToRefreshResponse<T> {
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);
let can_refresh = self.can_refresh;
let state = self.internal_ui(ui, can_refresh, None, child.min_rect());
let can_refresh = self.can_refresh;
let state = self.internal_ui(ui, can_refresh, None, child.min_rect());
PullToRefreshResponse {
state,
inner: output,
}
}
PullToRefreshResponse {
state,
inner: output,
}
}
/// Shows the pull to refresh widget, wrapping a [egui::ScrollArea].
/// Pass the output of the scroll area to the content function.
pub fn scroll_area_ui<T>(
self,
ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> ScrollAreaOutput<T>,
) -> PullToRefreshResponse<ScrollAreaOutput<T>> {
let scroll_output = content(ui);
let content_rect = scroll_output.inner_rect;
let can_refresh = scroll_output.state.offset.y == 0.0 && self.can_refresh;
// This is the id used in the Sense of the scroll area
// I hope this id is stable across egui patches...
let allow_dragged_id = scroll_output.id.with("area");
let state = self.internal_ui(ui, can_refresh, Some(allow_dragged_id), content_rect);
PullToRefreshResponse {
state,
inner: scroll_output,
}
}
/// Shows the pull to refresh widget, wrapping a [egui::ScrollArea].
/// Pass the output of the scroll area to the content function.
pub fn scroll_area_ui<T>(
self,
ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> ScrollAreaOutput<T>,
) -> PullToRefreshResponse<ScrollAreaOutput<T>> {
let scroll_output = content(ui);
let content_rect = scroll_output.inner_rect;
let can_refresh = scroll_output.state.offset.y == 0.0 && self.can_refresh;
// This is the id used in the Sense of the scroll area
// I hope this id is stable across egui patches...
let allow_dragged_id = scroll_output.id.with("area");
let state = self.internal_ui(ui, can_refresh, Some(allow_dragged_id), content_rect);
PullToRefreshResponse {
state,
inner: scroll_output,
}
}
fn internal_ui(
self,
ui: &mut egui::Ui,
can_refresh: bool,
allow_dragged_id: Option<Id>,
content_rect: Rect,
) -> PullToRefreshState {
let last_state = ui.data_mut(|data| {
data.get_temp_mut_or(self.id, PullToRefreshState::Idle)
.clone()
});
fn internal_ui(
self,
ui: &mut egui::Ui,
can_refresh: bool,
allow_dragged_id: Option<Id>,
content_rect: Rect,
) -> PullToRefreshState {
let last_state = ui.data_mut(|data| {
data.get_temp_mut_or(self.id, PullToRefreshState::Idle)
.clone()
});
let mut state = last_state;
if self.loading {
state = PullToRefreshState::Refreshing;
}
let mut state = last_state;
if self.loading {
state = PullToRefreshState::Refreshing;
}
if !self.loading && matches!(state, PullToRefreshState::Refreshing) {
state = PullToRefreshState::Idle;
}
if !self.loading && matches!(state, PullToRefreshState::Refreshing) {
state = PullToRefreshState::Idle;
}
if can_refresh && !self.loading {
let sense = ui.interact(content_rect, self.id, Sense::hover());
if can_refresh && !self.loading {
let sense = ui.interact(content_rect, self.id, Sense::hover());
let is_something_blocking_drag = ui.ctx().dragged_id().is_some()
&& !allow_dragged_id.map_or(false, |id| ui.ctx().is_being_dragged(id));
let is_something_blocking_drag = ui.ctx().dragged_id().is_some()
&& !allow_dragged_id.map_or(false, |id| ui.ctx().is_being_dragged(id));
if sense.contains_pointer() && !is_something_blocking_drag {
let (delta, any_released) = ui.input(|input| {
(
if input.pointer.is_decidedly_dragging() {
Some(input.pointer.delta())
} else {
None
},
input.pointer.any_released(),
)
});
if let Some(delta) = delta {
if matches!(state, PullToRefreshState::Idle) {
state = PullToRefreshState::Dragging {
distance: 0.0,
far_enough: false,
};
}
if let PullToRefreshState::Dragging { distance: drag, .. } = state.clone() {
let dist = drag + delta.y;
state = PullToRefreshState::Dragging {
distance: dist,
far_enough: dist > self.min_refresh_distance,
};
}
} else {
state = PullToRefreshState::Idle;
}
if any_released {
if let PullToRefreshState::Dragging {
far_enough: enough, ..
} = state.clone()
{
if enough {
state = PullToRefreshState::DoRefresh;
} else {
state = PullToRefreshState::Idle;
}
} else {
state = PullToRefreshState::Idle;
}
}
} else {
state = PullToRefreshState::Idle;
}
} else {
state = PullToRefreshState::Idle;
}
if sense.contains_pointer() && !is_something_blocking_drag {
let (delta, any_released) = ui.input(|input| {
(
if input.pointer.is_decidedly_dragging() {
Some(input.pointer.delta())
} else {
None
},
input.pointer.any_released(),
)
});
if let Some(delta) = delta {
if matches!(state, PullToRefreshState::Idle) {
state = PullToRefreshState::Dragging {
distance: 0.0,
far_enough: false,
};
}
if let PullToRefreshState::Dragging { distance: drag, .. } = state.clone() {
let dist = drag + delta.y;
state = PullToRefreshState::Dragging {
distance: dist,
far_enough: dist > self.min_refresh_distance,
};
}
} else {
state = PullToRefreshState::Idle;
}
if any_released {
if let PullToRefreshState::Dragging {
far_enough: enough, ..
} = state.clone()
{
if enough {
state = PullToRefreshState::DoRefresh;
} else {
state = PullToRefreshState::Idle;
}
} else {
state = PullToRefreshState::Idle;
}
} else if let PullToRefreshState::Dragging {
far_enough: enough, ..
} = state.clone()
{
if enough {
state = PullToRefreshState::DoRefresh;
}
}
} else {
state = PullToRefreshState::Idle;
}
} else {
state = PullToRefreshState::Idle;
}
if self.loading {
state = PullToRefreshState::Refreshing;
}
if self.loading {
state = PullToRefreshState::Refreshing;
}
let spinner_size = Vec2::splat(24.0);
let spinner_size = Vec2::splat(24.0);
let progress_for_offset = match &state {
PullToRefreshState::Idle => 0.0,
PullToRefreshState::Dragging { .. } => {
state.progress(self.min_refresh_distance).unwrap_or(1.0)
}
PullToRefreshState::DoRefresh => 1.0,
PullToRefreshState::Refreshing => 1.0,
} as f32;
let progress_for_offset = match &state {
PullToRefreshState::Idle => 0.0,
PullToRefreshState::Dragging { .. } => {
state.progress(self.min_refresh_distance).unwrap_or(1.0)
}
PullToRefreshState::DoRefresh => 1.0,
PullToRefreshState::Refreshing => 1.0,
} as f32;
let anim_progress = ui.ctx().animate_value_with_time(
self.id.with("offset_top"),
progress_for_offset,
ui.style().animation_time,
);
let anim_progress = ui.ctx().animate_value_with_time(
self.id.with("offset_top"),
progress_for_offset,
ui.style().animation_time,
);
let offset_top = -spinner_size.y + spinner_size.y * anim_progress * 2.0;
let offset_top = -spinner_size.y + spinner_size.y * anim_progress * 2.0;
if anim_progress > 0.0 {
Area::new(Id::new("Pull to refresh indicator"))
.fixed_pos(content_rect.center_top())
.pivot(Align2::CENTER_TOP)
.show(ui.ctx(), |ui| {
let (rect, _) = ui.allocate_exact_size(spinner_size, Sense::hover());
if anim_progress > 0.0 {
Area::new(Id::new("Pull to refresh indicator"))
.fixed_pos(content_rect.center_top())
.pivot(Align2::CENTER_TOP)
.show(ui.ctx(), |ui| {
let (rect, _) = ui.allocate_exact_size(spinner_size, Sense::hover());
ui.set_clip_rect(Rect::everything_below(rect.min.y));
ui.set_clip_rect(Rect::everything_below(rect.min.y));
let rect = rect.translate(Vec2::new(0.0, offset_top));
let rect = rect.translate(Vec2::new(0.0, offset_top));
ui.painter().circle(
rect.center(),
spinner_size.x / 1.5,
ui.style().visuals.widgets.inactive.bg_fill,
ui.visuals().widgets.inactive.bg_stroke,
);
ui.painter().circle(
rect.center(),
spinner_size.x / 1.5,
ui.style().visuals.widgets.inactive.bg_fill,
ui.visuals().widgets.inactive.bg_stroke,
);
let mut spinner_color = ui.style().visuals.widgets.inactive.fg_stroke.color;
if anim_progress < 1.0 {
spinner_color = Color32::from_rgba_unmultiplied(
spinner_color.r(),
spinner_color.g(),
spinner_color.b(),
(spinner_color.a() as f32 * 0.7).round() as u8,
);
}
ProgressSpinner::new()
.color(spinner_color)
.progress(state.progress(self.min_refresh_distance))
.paint_at(ui, rect);
});
}
let mut spinner_color = ui.style().visuals.widgets.inactive.fg_stroke.color;
if anim_progress < 1.0 {
spinner_color = Color32::from_rgba_unmultiplied(
spinner_color.r(),
spinner_color.g(),
spinner_color.b(),
(spinner_color.a() as f32 * 0.7).round() as u8,
);
}
ProgressSpinner::new()
.color(spinner_color)
.progress(state.progress(self.min_refresh_distance))
.paint_at(ui, rect);
});
}
ui.data_mut(|data| {
data.insert_temp(self.id, state.clone());
});
ui.data_mut(|data| {
data.insert_temp(self.id, state.clone());
});
state
}
}
state
}
}
+455 -397
View File
@@ -22,446 +22,504 @@ use std::mem::size_of;
use std::sync::Arc;
use std::thread;
use crate::gui::Colors;
use crate::gui::icons::{COPY, IMAGES_SQUARE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::QrImageState;
use crate::gui::views::View;
use crate::gui::Colors;
use crate::gui::views::types::QrImageState;
/// QR code image from text.
pub struct QrCodeContent {
/// QR code text.
pub text: String,
/// QR code text.
pub text: String,
/// Flag to show text below QR code.
show_text: bool,
/// Flag to copy text below QR code.
can_copy_text: bool,
/// Maximum QR code size.
max_size: f32,
/// Maximum QR code size.
max_size: f32,
/// Flag to draw animated QR with Uniform Resources
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
animated: bool,
/// Index of current image at animation.
animated_index: Option<usize>,
/// Time of last image draw.
animation_time: Option<i64>,
/// Flag to draw animated QR with Uniform Resources
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
animated: bool,
/// Index of current image at animation.
animated_index: Option<usize>,
/// Time of last image draw.
animation_time: Option<i64>,
/// Texture handle to show image when created.
texture_handle: Option<TextureHandle>,
/// QR code view data state.
qr_image_state: Arc<RwLock<QrImageState>>,
/// Texture handle to show image when created.
texture_handle: Option<TextureHandle>,
/// QR code view data state.
qr_image_state: Arc<RwLock<QrImageState>>,
}
const DEFAULT_QR_SIZE: u32 = 512;
impl QrCodeContent {
pub fn new(text: String, animated: bool) -> Self {
Self {
text,
max_size: DEFAULT_QR_SIZE as f32,
animated,
animated_index: None,
animation_time: None,
texture_handle: None,
qr_image_state: Arc::new(RwLock::new(QrImageState::default())),
}
}
pub fn new(text: String, animated: bool) -> Self {
Self {
text,
show_text: true,
can_copy_text: true,
max_size: DEFAULT_QR_SIZE as f32,
animated,
animated_index: None,
animation_time: None,
texture_handle: None,
qr_image_state: Arc::new(RwLock::new(QrImageState::default())),
}
}
/// Setup maximum QR code size.
pub fn with_max_size(mut self, max_size: f32) -> Self {
self.max_size = max_size;
self
}
/// Setup maximum QR code size.
pub fn with_max_size(mut self, max_size: f32) -> Self {
self.max_size = max_size;
self
}
/// Draw QR code.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if self.animated {
// Show animated QR code.
self.animated_ui(ui, cb);
} else {
// Show static QR code.
self.static_ui(ui, cb);
}
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
}
/// Hide text below QR code.
pub fn hide_text(mut self) -> Self {
self.show_text = false;
self
}
/// Draw animated QR code content.
fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
});
/// Do not show button to copy QR code text.
pub fn no_copy(mut self) -> Self {
self.can_copy_text = false;
self
}
// Create multiple vector images from text if not creating.
if !self.loading() {
self.create_svg_list();
}
} else {
let svg_list = {
let r_create = self.qr_image_state.read();
r_create.svg_list.clone().unwrap()
};
/// Draw QR code.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if self.animated {
// Show animated QR code.
self.animated_ui(ui, cb);
} else {
// Show static QR code.
self.static_ui(ui, cb);
}
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
}
// Setup animated index.
let now = chrono::Utc::now().timestamp_millis();
if now - *self.animation_time.get_or_insert(now) > 100 {
if let Some(i) = self.animated_index {
self.animated_index = Some(i + 1);
}
if *self.animated_index.get_or_insert(0) == svg_list.len() {
self.animated_index = Some(0);
}
self.animation_time = Some(now);
}
/// Draw animated QR code content.
fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
});
let svg = svg_list[self.animated_index.unwrap_or(0)].clone();
// Create multiple vector images from text if not creating.
if !self.loading() {
self.create_svg_list();
}
} else {
let svg_list = {
let r_create = self.qr_image_state.read();
r_create.svg_list.clone().unwrap()
};
// Create images from SVG data.
self.qr_image_ui(svg, ui);
// Setup animated index.
let now = chrono::Utc::now().timestamp_millis();
if now - *self.animation_time.get_or_insert(now) > 100 {
if let Some(i) = self.animated_index {
self.animated_index = Some(i + 1);
}
if *self.animated_index.get_or_insert(0) == svg_list.len() {
self.animated_index = Some(0);
}
self.animation_time = Some(now);
}
// Show QR code text.
self.text_ui(ui);
let svg = svg_list[self.animated_index.unwrap_or(0)].clone();
ui.vertical_centered(|ui| {
let sharing = {
let r_state = self.qr_image_state.read();
r_state.exporting || r_state.gif_creating
};
if !sharing {
ui.vertical_centered(|ui| {
// Show button to share QR.
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
View::colored_text_button(ui,
share_text,
Colors::blue(),
Colors::white_or_black(false), || {
{
let mut w_state = self.qr_image_state.write();
w_state.exporting = true;
}
// Create GIF to export.
self.create_qr_gif();
});
});
} else {
ui.vertical_centered(|ui| {
ui.add_space(2.0);
View::small_loading_spinner(ui);
ui.add_space(1.0);
});
}
// Create images from SVG data.
self.qr_image_ui(svg, ui);
// Check if GIF was created to share.
let has_gif = {
let r_state = self.qr_image_state.read();
r_state.gif_data.is_some()
};
if has_gif {
let data = {
let r_state = self.qr_image_state.read();
r_state.gif_data.clone().unwrap()
};
let name = format!("{}.gif", chrono::Utc::now().timestamp());
cb.share_data(name, data).unwrap_or_default();
// Clear GIF data and exporting flag.
{
let mut w_state = self.qr_image_state.write();
w_state.gif_data = None;
w_state.exporting = false;
}
}
});
// Show QR code text.
if self.show_text {
self.text_ui(ui);
}
ui.ctx().request_repaint();
}
}
ui.vertical_centered(|ui| {
let sharing = {
let r_state = self.qr_image_state.read();
r_state.exporting || r_state.gif_creating
};
if !sharing {
ui.vertical_centered(|ui| {
// Show button to share QR.
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
View::colored_text_button(
ui,
share_text,
Colors::blue(),
Colors::white_or_black(false),
|| {
{
let mut w_state = self.qr_image_state.write();
w_state.exporting = true;
}
// Create GIF to export.
self.create_qr_gif();
},
);
});
} else {
ui.vertical_centered(|ui| {
ui.add_space(2.0);
View::small_loading_spinner(ui);
ui.add_space(1.0);
});
}
/// Draw static QR code content.
fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
});
// Check if GIF was created to share.
let has_gif = {
let r_state = self.qr_image_state.read();
r_state.gif_data.is_some()
};
if has_gif {
let data = {
let r_state = self.qr_image_state.read();
r_state.gif_data.clone().unwrap()
};
let name = format!("{}.gif", chrono::Utc::now().timestamp());
cb.share_data(name, data).unwrap_or_default();
// Clear GIF data and exporting flag.
{
let mut w_state = self.qr_image_state.write();
w_state.gif_data = None;
w_state.exporting = false;
}
}
});
// Create vector image from text if not creating.
if !self.loading() {
self.create_svg();
}
} else {
// Create image from SVG data.
let svg = {
let r_state = self.qr_image_state.read();
r_state.svg.clone().unwrap()
};
self.qr_image_ui(svg, ui);
ui.ctx().request_repaint();
}
}
// Show QR code text.
self.text_ui(ui);
/// Draw static QR code content.
fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
});
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
// Create vector image from text if not creating.
if !self.loading() {
self.create_svg();
}
} else {
// Create image from SVG data.
let svg = {
let r_state = self.qr_image_state.read();
r_state.svg.clone().unwrap()
};
self.qr_image_ui(svg, ui);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw copy button.
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.text.clone());
});
});
columns[1].vertical_centered_justified(|ui| {
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
View::colored_text_button(ui,
share_text,
Colors::blue(),
Colors::white_or_black(false), || {
self.share_static(cb);
});
});
});
}
}
// Show QR code text.
if self.show_text {
self.text_ui(ui);
} else {
ui.add_space(8.0);
}
/// Share static QR code image.
fn share_static(&self, cb: &dyn PlatformCallbacks) {
let text = self.text.as_str();
if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) {
let size = DEFAULT_QR_SIZE as usize;
if let Some(data) = Self::qr_to_image_data(qr, size) {
let mut png = vec![];
let png_enc = PngEncoder::new_with_quality(&mut png,
CompressionType::Best,
FilterType::NoFilter);
if let Ok(()) = png_enc.write_image(data.as_slice(),
DEFAULT_QR_SIZE,
DEFAULT_QR_SIZE,
ExtendedColorType::L8) {
let name = format!("{}.png", chrono::Utc::now().timestamp());
cb.share_data(name, png).unwrap_or_default();
}
}
}
}
if self.can_copy_text {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
/// Draw QR code image content.
fn qr_image_ui(&mut self, svg: Vec<u8>, ui: &mut egui::Ui) {
View::max_width_ui(ui, self.max_size, |ui| {
let mut rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(10.0, 0.0);
rect.max -= egui::emath::vec2(10.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw copy button.
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.text.clone());
});
});
columns[1].vertical_centered_justified(|ui| {
self.share_static_button_ui(ui, cb);
});
});
} else {
ui.vertical_centered(|ui| {
self.share_static_button_ui(ui, cb);
});
}
}
}
// Create background shape.
let mut bg_shape = RectShape::new(
rect,
egui::CornerRadius::default(),
egui::Color32::WHITE,
egui::Stroke::NONE,
egui::StrokeKind::Outside
);
let bg_idx = ui.painter().add(bg_shape.clone());
/// Draw button to share static QR code.
fn share_static_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
View::colored_text_button(
ui,
share_text,
Colors::blue(),
Colors::white_or_black(false),
|| {
self.share_static(cb);
},
);
}
// Draw QR code image.
let mut content_rect = ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
ui.add_space(10.0);
let size = SizeHint::Size {
width: ui.available_width() as u32,
height: ui.available_width() as u32,
maintain_aspect_ratio: true,
};
self.texture_handle = Some(View::svg_image(ui, "qr", svg.as_slice(), size));
ui.add_space(10.0);
}).response.rect;
/// Share static QR code image.
fn share_static(&self, cb: &dyn PlatformCallbacks) {
let text = self.text.as_str();
if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) {
let size = DEFAULT_QR_SIZE as usize;
if let Some(data) = Self::qr_to_image_data(qr, size) {
let mut png = vec![];
let png_enc = PngEncoder::new_with_quality(
&mut png,
CompressionType::Best,
FilterType::NoFilter,
);
if let Ok(()) = png_enc.write_image(
data.as_slice(),
DEFAULT_QR_SIZE,
DEFAULT_QR_SIZE,
ExtendedColorType::L8,
) {
let name = format!("{}.png", chrono::Utc::now().timestamp());
cb.share_data(name, png).unwrap_or_default();
}
}
}
}
// Setup background size.
content_rect.min -= egui::emath::vec2(10.0, 0.0);
content_rect.max += egui::emath::vec2(10.0, 0.0);
bg_shape.rect = content_rect;
ui.painter().set(bg_idx, bg_shape);
});
}
/// Draw QR code image content.
fn qr_image_ui(&mut self, svg: Vec<u8>, ui: &mut egui::Ui) {
View::max_width_ui(ui, self.max_size, |ui| {
let mut rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(10.0, 0.0);
rect.max -= egui::emath::vec2(10.0, 0.0);
/// Draw QR code text.
fn text_ui(&self, ui: &mut egui::Ui) {
ui.add_space(6.0);
View::ellipsize_text(ui, self.text.clone(), 15.0, Colors::inactive_text());
ui.add_space(6.0);
}
// Create background shape.
let mut bg_shape = RectShape::new(
rect,
egui::CornerRadius::default(),
egui::Color32::WHITE,
egui::Stroke::NONE,
egui::StrokeKind::Outside,
);
let bg_idx = ui.painter().add(bg_shape.clone());
/// Check if QR code is loading.
fn loading(&self) -> bool {
let r_state = self.qr_image_state.read();
r_state.loading
}
// Draw QR code image.
let mut content_rect = ui
.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
ui.add_space(10.0);
let size = SizeHint::Size {
width: ui.available_width() as u32,
height: ui.available_width() as u32,
maintain_aspect_ratio: true,
};
self.texture_handle = Some(View::svg_image(ui, "qr", svg.as_slice(), size));
ui.add_space(10.0);
})
.response
.rect;
/// Create multiple vector QR code images at separate thread.
fn create_svg_list(&self) {
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 64).unwrap();
let mut data = Vec::with_capacity(encoder.fragment_count());
for _ in 0..encoder.fragment_count() {
let ur = encoder.next_part().unwrap();
if let Ok(qr) = QrCode::encode_text(ur.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
data.push(svg.into_bytes());
}
}
let mut w_state = qr_state.write();
if !data.is_empty() {
w_state.svg_list = Some(data);
}
w_state.loading = false;
});
}
// Setup background size.
content_rect.min -= egui::emath::vec2(10.0, 0.0);
content_rect.max += egui::emath::vec2(10.0, 0.0);
bg_shape.rect = content_rect;
ui.painter().set(bg_idx, bg_shape);
});
}
/// Check if image was created.
fn has_image(&self) -> bool {
let r_state = self.qr_image_state.read();
r_state.svg.is_some() || r_state.svg_list.is_some()
}
/// Draw QR code text.
fn text_ui(&self, ui: &mut egui::Ui) {
ui.add_space(6.0);
View::ellipsize_text(ui, self.text.clone(), 15.0, Colors::inactive_text());
ui.add_space(6.0);
}
/// Create vector QR code image at separate thread.
fn create_svg(&self) {
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
let mut w_state = qr_state.write();
w_state.loading = false;
w_state.svg = Some(svg.into_bytes());
}
});
}
/// Check if QR code is loading.
fn loading(&self) -> bool {
let r_state = self.qr_image_state.read();
r_state.loading
}
/// Convert QR code to SVG string.
fn qr_to_svg(qr: QrCode, border: i32) -> String {
let mut result = String::new();
let dimension = qr.size().checked_add(border.checked_mul(2).unwrap()).unwrap();
result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
result += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
result += &format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {0} {0}\" stroke=\"none\">\n", dimension);
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
result += "\t<path d=\"";
for y in 0 .. qr.size() {
for x in 0 .. qr.size() {
if qr.get_module(x, y) {
if x != 0 || y != 0 {
result += " ";
}
result += &format!("M{},{}h1v1h-1z", x + border, y + border);
}
}
}
result += "\" fill=\"#000000\"/>\n";
result += "</svg>\n";
result
}
/// Create multiple vector QR code images at separate thread.
fn create_svg_list(&self) {
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 64).unwrap();
let mut data = Vec::with_capacity(encoder.fragment_count());
for _ in 0..encoder.fragment_count() {
let ur = encoder.next_part().unwrap();
if let Ok(qr) = QrCode::encode_text(ur.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
data.push(svg.into_bytes());
}
}
let mut w_state = qr_state.write();
if !data.is_empty() {
w_state.svg_list = Some(data);
}
w_state.loading = false;
});
}
/// Create GIF image at separate thread.
fn create_qr_gif(&self) {
{
let mut w_state = self.qr_image_state.write();
w_state.gif_creating = true;
/// Check if image was created.
fn has_image(&self) -> bool {
let r_state = self.qr_image_state.read();
r_state.svg.is_some() || r_state.svg_list.is_some()
}
}
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
// Setup GIF image encoder.
let mut gif = vec![];
{
// Generate QR codes from text.
let mut qrs = vec![];
let mut ur_enc = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
for _ in 0..ur_enc.fragment_count() {
let ur = ur_enc.next_part().unwrap();
if let Ok(qr) = qrcode::QrCode::with_error_correction_level(
ur.as_bytes(),
qrcode::EcLevel::L
) {
// Create an image from QR data.
let image = qr.render()
.max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE)
.dark_color(image::Rgb([0, 0, 0]))
.light_color(image::Rgb([255, 255, 255]))
.build();
qrs.push(image);
}
}
/// Create vector QR code image at separate thread.
fn create_svg(&self) {
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
let mut w_state = qr_state.write();
w_state.loading = false;
w_state.svg = Some(svg.into_bytes());
}
});
}
if !qrs.is_empty() {
// Generate GIF data.
let color_map = &[0, 0, 0, 0xFF, 0xFF, 0xFF];
let mut gif_enc = gif::Encoder::new(&mut gif,
qrs[0].width() as u16,
qrs[0].height() as u16,
color_map).unwrap();
gif_enc.set_repeat(gif::Repeat::Infinite).unwrap();
for qr in qrs {
let mut frame = gif::Frame::from_rgb(qr.width() as u16,
qr.height() as u16,
qr.as_raw().as_slice());
frame.delay = 10;
// Write an image to GIF encoder.
if let Ok(_) = gif_enc.write_frame(&frame) {
continue;
}
// Exit on error.
let mut w_state = qr_state.write();
w_state.gif_creating = false;
return;
}
}
}
// Setup GIF image data.
let mut w_state = qr_state.write();
if !gif.is_empty() {
w_state.gif_data = Some(gif);
}
w_state.gif_creating = false;
});
}
/// Convert QR code to SVG string.
fn qr_to_svg(qr: QrCode, border: i32) -> String {
let mut result = String::new();
let dimension = qr
.size()
.checked_add(border.checked_mul(2).unwrap())
.unwrap();
result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
result += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
result += &format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {0} {0}\" stroke=\"none\">\n",
dimension
);
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
result += "\t<path d=\"";
for y in 0..qr.size() {
for x in 0..qr.size() {
if qr.get_module(x, y) {
if x != 0 || y != 0 {
result += " ";
}
result += &format!("M{},{}h1v1h-1z", x + border, y + border);
}
}
}
result += "\" fill=\"#000000\"/>\n";
result += "</svg>\n";
result
}
/// Convert QR code to image data.
fn qr_to_image_data(qr: QrCode, size: usize) -> Option<Vec<u8>> {
if size >= 2usize.pow((size_of::<usize>() * 4) as u32) {
return None;
}
let margin_size = 1;
let s = qr.size();
let data_length = s as usize;
let data_length_with_margin = data_length + 2 * margin_size;
let point_size = size / data_length_with_margin;
if point_size == 0 {
return None;
}
let margin = (size - (point_size * data_length)) / 2;
let length = size * size;
let mut img_raw: Vec<u8> = vec![255u8; length];
for i in 0..s {
for j in 0..s {
if qr.get_module(i, j) {
let x = i as usize * point_size + margin;
let y = j as usize * point_size + margin;
/// Create GIF image at separate thread.
fn create_qr_gif(&self) {
{
let mut w_state = self.qr_image_state.write();
w_state.gif_creating = true;
}
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
// Setup GIF image encoder.
let mut gif = vec![];
{
// Generate QR codes from text.
let mut qrs = vec![];
let mut ur_enc = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
for _ in 0..ur_enc.fragment_count() {
let ur = ur_enc.next_part().unwrap();
if let Ok(qr) = qrcode::QrCode::with_error_correction_level(
ur.as_bytes(),
qrcode::EcLevel::L,
) {
// Create an image from QR data.
let image = qr
.render()
.max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE)
.dark_color(image::Rgb([0, 0, 0]))
.light_color(image::Rgb([255, 255, 255]))
.build();
qrs.push(image);
}
}
for j in y..(y + point_size) {
let offset = j * size;
for i in x..(x + point_size) {
img_raw[offset + i] = 0;
}
}
}
}
}
Some(img_raw)
}
}
if !qrs.is_empty() {
// Generate GIF data.
let color_map = &[0, 0, 0, 0xFF, 0xFF, 0xFF];
let mut gif_enc = gif::Encoder::new(
&mut gif,
qrs[0].width() as u16,
qrs[0].height() as u16,
color_map,
)
.unwrap();
gif_enc.set_repeat(gif::Repeat::Infinite).unwrap();
for qr in qrs {
let mut frame = gif::Frame::from_rgb(
qr.width() as u16,
qr.height() as u16,
qr.as_raw().as_slice(),
);
frame.delay = 10;
// Write an image to GIF encoder.
if let Ok(_) = gif_enc.write_frame(&frame) {
continue;
}
// Exit on error.
let mut w_state = qr_state.write();
w_state.gif_creating = false;
return;
}
}
}
// Setup GIF image data.
let mut w_state = qr_state.write();
if !gif.is_empty() {
w_state.gif_data = Some(gif);
}
w_state.gif_creating = false;
});
}
/// Convert QR code to image data.
fn qr_to_image_data(qr: QrCode, size: usize) -> Option<Vec<u8>> {
if size >= 2usize.pow((size_of::<usize>() * 4) as u32) {
return None;
}
let margin_size = 1;
let s = qr.size();
let data_length = s as usize;
let data_length_with_margin = data_length + 2 * margin_size;
let point_size = size / data_length_with_margin;
if point_size == 0 {
return None;
}
let margin = (size - (point_size * data_length)) / 2;
let length = size * size;
let mut img_raw: Vec<u8> = vec![255u8; length];
for i in 0..s {
for j in 0..s {
if qr.get_module(i, j) {
let x = i as usize * point_size + margin;
let y = j as usize * point_size + margin;
for j in y..(y + point_size) {
let offset = j * size;
for i in x..(x + point_size) {
img_raw[offset + i] = 0;
}
}
}
}
}
Some(img_raw)
}
}
+117 -102
View File
@@ -18,120 +18,135 @@ 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::gui::views::{CameraContent, Modal, View};
/// QR code scanning content.
pub struct CameraScanContent {
/// Camera content.
camera_content: Option<CameraContent>,
/// Scan result.
qr_scan_result: Option<QrScanResult>,
/// Camera content.
camera_content: Option<CameraContent>,
/// Scan result.
qr_scan_result: Option<QrScanResult>,
}
impl Default for CameraScanContent {
fn default() -> Self {
Self {
camera_content: Some(CameraContent::default()),
qr_scan_result: None,
}
}
fn default() -> Self {
Self {
camera_content: Some(CameraContent::default()),
qr_scan_result: None,
}
}
}
impl CameraScanContent {
/// Draw [`Modal`] content.
pub fn modal_ui(&mut self,
ui: &mut egui::Ui,
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.clone() {
Self::result_ui(ui, result, cb, || {
Modal::close();
}, || {
self.qr_scan_result = None;
cb.start_camera();
Modal::set_title(t!("scan_qr"));
});
} else if let Some(camera_content) = self.camera_content.as_mut() {
if let Some(result) = camera_content.qr_scan_result() {
cb.stop_camera();
self.camera_content = None;
on_result(&result);
/// Draw [`Modal`] content.
pub fn modal_ui(
&mut self,
ui: &mut egui::Ui,
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.clone() {
Self::result_ui(
ui,
result,
cb,
|| {
Modal::close();
},
|| {
self.qr_scan_result = None;
cb.start_camera();
Modal::set_title(t!("scan_qr"));
},
);
} else if let Some(camera_content) = self.camera_content.as_mut() {
if let Some(result) = camera_content.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 {
// 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);
}
// 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);
}
/// Draw scan result content.
pub fn result_ui(ui: &mut egui::Ui,
result: &QrScanResult,
cb: &dyn PlatformCallbacks,
on_close: impl FnOnce(),
on_repeat: impl FnOnce()) {
let mut result_text = result.text();
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("qr_scan_result_input"))
.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);
/// Draw scan result content.
pub fn result_ui(
ui: &mut egui::Ui,
result: &QrScanResult,
cb: &dyn PlatformCallbacks,
on_close: impl FnOnce(),
on_repeat: impl FnOnce(),
) {
let mut result_text = result.text();
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("qr_scan_result_input"))
.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::white_or_black(false), || {
cb.copy_string_to_buffer(result_text.to_string());
});
});
ui.add_space(10.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show copy button.
ui.vertical_centered(|ui| {
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(result_text.to_string());
});
});
ui.add_space(10.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// 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), || {
on_close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
on_repeat();
});
});
});
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
on_repeat();
});
});
});
}
}
+50 -50
View File
@@ -13,71 +13,71 @@
// limitations under the License.
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::GLOBE_SIMPLE;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View;
use crate::gui::views::settings::{InterfaceSettingsContent, NetworkSettingsContent};
use crate::gui::views::types::ContentContainer;
use crate::gui::views::View;
use crate::gui::Colors;
/// Application settings content.
pub struct SettingsContent {
/// User interface settings.
interface_settings: InterfaceSettingsContent,
/// Network communication settings.
network_settings: NetworkSettingsContent,
// tor_settings: TorSettingsContent,
/// User interface settings.
interface_settings: InterfaceSettingsContent,
/// Network communication settings.
network_settings: NetworkSettingsContent,
// tor_settings: TorSettingsContent,
}
impl Default for SettingsContent {
fn default() -> Self {
Self {
interface_settings: InterfaceSettingsContent::default(),
network_settings: NetworkSettingsContent::default(),
//tor_settings: TorSettingsContent::default(),
}
}
fn default() -> Self {
Self {
interface_settings: InterfaceSettingsContent::default(),
network_settings: NetworkSettingsContent::default(),
//tor_settings: TorSettingsContent::default(),
}
}
}
impl SettingsContent {
/// Draw application settings content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.add_space(5.0);
View::checkbox(ui, AppConfig::check_updates(), t!("check_updates"), || {
AppConfig::toggle_check_updates();
});
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
/// Draw application settings content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.add_space(5.0);
View::checkbox(ui, AppConfig::check_updates(), t!("check_updates"), || {
AppConfig::toggle_check_updates();
});
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
// Show interface settings.
self.interface_settings.ui(ui, cb);
// Show interface settings.
self.interface_settings.ui(ui, cb);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
View::sub_title(ui, format!("{} {}", GLOBE_SIMPLE, t!("network.self")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
View::sub_title(ui, format!("{} {}", GLOBE_SIMPLE, t!("network.self")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
// Show network settings.
self.network_settings.ui(ui, cb);
ui.add_space(8.0);
// Show network settings.
self.network_settings.ui(ui, cb);
ui.add_space(8.0);
// Do not show Tor settings on Android.
// let os = OperatingSystem::from_target_os();
// let show_tor = os != OperatingSystem::Android;
// if show_tor {
// View::horizontal_line(ui, Colors::stroke());
// ui.add_space(6.0);
//
// View::sub_title(ui, format!("{} {}", CIRCLE_HALF, t!("transport.tor_network")));
// View::horizontal_line(ui, Colors::stroke());
// ui.add_space(6.0);
//
// // Show Tor settings.
// self.tor_settings.ui(ui, cb);
// ui.add_space(8.0);
// }
}
}
// Do not show Tor settings on Android.
// let os = OperatingSystem::from_target_os();
// let show_tor = os != OperatingSystem::Android;
// if show_tor {
// View::horizontal_line(ui, Colors::stroke());
// ui.add_space(6.0);
//
// View::sub_title(ui, format!("{} {}", CIRCLE_HALF, t!("transport.tor_network")));
// View::horizontal_line(ui, Colors::stroke());
// ui.add_space(6.0);
//
// // Show Tor settings.
// self.tor_settings.ui(ui, cb);
// ui.add_space(8.0);
// }
}
}
+114 -169
View File
@@ -12,193 +12,138 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
use eframe::epaint::RectShape;
use egui::{Align, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder};
use crate::gui::icons::{CHECK, PENCIL, TRANSLATE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Modal, View};
/// User interface settings content.
pub struct InterfaceSettingsContent {
/// Current locale.
locale: String,
/// Current locale.
locale: String,
}
/// Identifier for language selection [`Modal`].
const LANGUAGE_SELECTION_MODAL: &'static str = "language_selection_modal";
impl ContentContainer for InterfaceSettingsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
LANGUAGE_SELECTION_MODAL
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![]
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, _: &dyn PlatformCallbacks) {
match modal.id {
LANGUAGE_SELECTION_MODAL => self.language_selection_ui(ui),
_ => {}
}
}
fn modal_ui(&mut self, _: &mut egui::Ui, _: &Modal, _: &dyn PlatformCallbacks) {}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
ui.add_space(5.0);
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
ui.add_space(5.0);
// Draw theme selection.
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("theme")).size(16.0).color(Colors::gray()));
});
// Draw theme selection.
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("theme")).size(16.0).color(Colors::gray()));
});
let saved_use_dark = AppConfig::dark_theme().unwrap_or(false);
let mut selected_use_dark = saved_use_dark;
let saved_use_dark = AppConfig::dark_theme().unwrap_or(false);
let mut selected_use_dark = saved_use_dark;
ui.add_space(8.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, false, t!("light"));
});
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, true, t!("dark"));
})
});
ui.add_space(14.0);
if saved_use_dark != selected_use_dark {
AppConfig::set_dark_theme(selected_use_dark);
crate::setup_visuals(ui.ctx());
}
ui.add_space(8.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, false, t!("light"));
});
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, true, t!("dark"));
})
});
ui.add_space(14.0);
if saved_use_dark != selected_use_dark {
AppConfig::set_dark_theme(selected_use_dark);
crate::setup_visuals(ui.ctx());
}
// Draw language selection.
self.language_item_ui(self.locale.clone().as_str(), ui, true, 0, 1);
ui.add_space(4.0);
}
// Draw language selection.
let locales = rust_i18n::available_locales!();
for (index, locale) in locales.iter().enumerate() {
self.language_item_ui(locale, ui, index, locales.len());
}
ui.add_space(4.0);
}
}
impl Default for InterfaceSettingsContent {
fn default() -> Self {
let locale = if let Some(lang) = AppConfig::locale() {
lang
} else {
rust_i18n::locale().to_string()
};
Self {
locale,
}
}
fn default() -> Self {
let locale = if let Some(lang) = AppConfig::locale() {
lang
} else {
rust_i18n::locale().to_string()
};
Self { locale }
}
}
impl InterfaceSettingsContent {
/// Draw language selection content.
fn language_selection_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(4.0);
ScrollArea::vertical()
.max_height(373.0)
.id_salt("select_language_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2])
.show(ui, |ui| {
ui.add_space(2.0);
ui.vertical_centered(|ui| {
let locales = rust_i18n::available_locales!();
for (index, locale) in locales.iter().enumerate() {
self.language_item_ui(locale, ui, false, index, locales.len());
}
});
});
/// Draw language selection item content.
fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, index: usize, len: usize) {
let is_current = self.locale == locale;
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(50.0);
let r = View::item_rounding(index, len, false);
let bg = if is_current {
Colors::fill()
} else {
Colors::fill_lite()
};
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
Modal::close();
});
});
ui.add_space(6.0);
}
/// Draw language selection item content.
fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, edit: bool, index: usize, len: usize) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
if edit {
rect.set_height(56.0);
} else {
rect.set_height(50.0);
}
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if edit {
View::item_button(ui, View::item_rounding(index, len, true), PENCIL, None, || {
// Show language selection modal.
Modal::new(LANGUAGE_SELECTION_MODAL)
.position(ModalPosition::Center)
.title(t!("language"))
.show();
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
View::ellipsize_text(ui,
t!("lang_name", locale = locale),
18.0,
Colors::title(false));
ui.add_space(1.0);
let value = format!("{} {}",
TRANSLATE,
t!("language"));
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
} else {
// Draw button to select language.
let is_current = self.locale == locale;
if !is_current {
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
rust_i18n::set_locale(locale);
AppConfig::save_locale(locale);
self.locale = locale.to_string();
Modal::close();
});
} else {
View::selected_item_check(ui);
}
let layout_size = ui.available_size();
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);
let color = if is_current {
Colors::title(false)
} else {
Colors::gray()
};
ui.label(RichText::new(t!("lang_name", locale = locale))
.size(17.0)
.color(color));
ui.add_space(3.0);
});
});
}
});
}
}
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
if is_current {
View::selected_item_check(ui);
}
let layout_size = ui.available_size();
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);
let color = if is_current {
Colors::title(false)
} else {
Colors::gray()
};
ui.label(
RichText::new(t!("lang_name", locale = locale))
.size(17.0)
.color(color),
);
ui.add_space(14.0);
});
},
);
},
)
.response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() && !is_current {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked && !is_current {
rust_i18n::set_locale(locale);
AppConfig::save_locale(locale);
self.locale = locale.to_string();
}
}
}
+1 -1
View File
@@ -22,4 +22,4 @@ mod network;
pub use network::*;
mod tor;
pub use tor::*;
pub use tor::*;
+263 -234
View File
@@ -12,271 +12,300 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Id, Layout, RichText, StrokeKind};
use eframe::epaint::RectShape;
use egui::{Align, CursorIcon, Id, Layout, RichText, Sense, StrokeKind, UiBuilder};
use url::Url;
use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH, PENCIL};
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::Colors;
use crate::AppConfig;
/// Network communication settings content.
pub struct NetworkSettingsContent {
/// Proxy URL input value for [`Modal`].
proxy_url_edit: String,
/// Flag to check if entered proxy address was correct.
proxy_url_error: bool,
/// Proxy URL input value for [`Modal`].
proxy_url_edit: String,
/// Flag to check if entered proxy address was correct.
proxy_url_error: bool,
}
/// Identifier for proxy URL edit [`Modal`].
const PROXY_URL_EDIT_MODAL: &'static str = "settings_proxy_edit_modal";
impl ContentContainer for NetworkSettingsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
PROXY_URL_EDIT_MODAL
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![PROXY_URL_EDIT_MODAL]
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb),
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
let use_proxy = AppConfig::use_proxy();
View::checkbox(ui, use_proxy, t!("app_settings.proxy"), || {
// Show edit modal when both URLs are empty.
if AppConfig::http_proxy_url().is_none() && AppConfig::socks_proxy_url().is_none() &&
!use_proxy {
Modal::new(PROXY_URL_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("app_settings.proxy"))
.show();
} else {
AppConfig::toggle_use_proxy();
}
});
if !use_proxy {
ui.add_space(4.0);
ui.label(RichText::new(t!("app_settings.proxy_desc"))
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(8.0);
} else {
ui.add_space(8.0);
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
let use_proxy = AppConfig::use_proxy();
View::checkbox(ui, use_proxy, t!("app_settings.proxy"), || {
// Show edit modal when both URLs are empty.
if AppConfig::http_proxy_url().is_none()
&& AppConfig::socks_proxy_url().is_none()
&& !use_proxy
{
Modal::new(PROXY_URL_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("app_settings.proxy"))
.show();
} else {
AppConfig::toggle_use_proxy();
}
});
if !use_proxy {
ui.add_space(4.0);
ui.label(
RichText::new(t!("app_settings.proxy_desc"))
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(8.0);
} else {
ui.add_space(8.0);
// Draw proxy type selection.
Self::proxy_type_ui(ui);
// Draw proxy type selection.
Self::proxy_type_ui(ui);
// Draw proxy URL info.
self.proxy_item_ui(ui);
ui.add_space(6.0);
}
}
// Draw proxy URL info.
self.proxy_item_ui(ui);
ui.add_space(6.0);
}
}
}
impl Default for NetworkSettingsContent {
fn default() -> Self {
Self {
proxy_url_edit: "".to_string(),
proxy_url_error: false,
}
}
fn default() -> Self {
Self {
proxy_url_edit: "".to_string(),
proxy_url_error: false,
}
}
}
impl NetworkSettingsContent {
/// Draw proxy edit modal content.
fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut NetworkSettingsContent| {
let proxy = c.proxy_url_edit.trim().to_string();
let use_socks = AppConfig::use_socks_proxy();
// Clear value if empty.
if proxy.is_empty() {
if use_socks {
AppConfig::save_socks_proxy_url(None);
} else {
AppConfig::save_http_proxy_url(None);
}
Modal::close();
return;
}
// Format URL.
let http = "http://";
let socks = "socks5://";
let url = if use_socks {
let p = proxy.replace(http, "");
if !p.contains(socks) {
format!("{}{}", socks, p)
} else {
p
}
} else {
let p = proxy.replace(socks, "");
if !p.contains(http) {
format!("{}{}", http, p)
} else {
p
}
};
c.proxy_url_error = Url::parse(url.as_str()).is_err();
if !c.proxy_url_error {
// Save result when no error.
if !AppConfig::use_proxy() {
AppConfig::toggle_use_proxy();
}
if use_socks {
AppConfig::save_socks_proxy_url(Some(url))
} else {
AppConfig::save_http_proxy_url(Some(url));
}
Modal::close();
}
};
/// Draw proxy edit modal content.
fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut NetworkSettingsContent| {
let proxy = c.proxy_url_edit.trim().to_string();
let use_socks = AppConfig::use_socks_proxy();
// Clear value if empty.
if proxy.is_empty() {
if use_socks {
AppConfig::save_socks_proxy_url(None);
} else {
AppConfig::save_http_proxy_url(None);
}
Modal::close();
return;
}
// Format URL.
let http = "http://";
let socks = "socks5://";
let url = if use_socks {
let p = proxy.replace(http, "");
if !p.contains(socks) {
format!("{}{}", socks, p)
} else {
p
}
} else {
let p = proxy.replace(socks, "");
if !p.contains(http) {
format!("{}{}", http, p)
} else {
p
}
};
c.proxy_url_error = Url::parse(url.as_str()).is_err();
if !c.proxy_url_error {
// Save result when no error.
if !AppConfig::use_proxy() {
AppConfig::toggle_use_proxy();
}
if use_socks {
AppConfig::save_socks_proxy_url(Some(url))
} else {
AppConfig::save_http_proxy_url(Some(url));
}
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let label = format!("{}:", t!("enter_url"));
ui.label(RichText::new(label).size(17.0).color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let label = format!("{}:", t!("enter_url"));
ui.label(RichText::new(label).size(17.0).color(Colors::gray()));
ui.add_space(8.0);
// Draw proxy URL text edit.
let mut edit = TextEdit::new(
Id::from("proxy_url_edit")
.with(PROXY_URL_EDIT_MODAL)
.with(if AppConfig::use_proxy() {
"socks5"
} else {
"http"
})
).paste();
edit.ui(ui, &mut self.proxy_url_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Draw proxy URL text edit.
let mut edit =
TextEdit::new(Id::from("proxy_url_edit").with(PROXY_URL_EDIT_MODAL).with(
if AppConfig::use_proxy() {
"socks5"
} else {
"http"
},
))
.paste();
edit.ui(ui, &mut self.proxy_url_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified address is incorrect.
if self.proxy_url_error {
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.invalid_url"))
.size(16.0)
.color(Colors::red()));
}
ui.add_space(12.0);
// Show error when specified address is incorrect.
if self.proxy_url_error {
ui.add_space(10.0);
ui.label(
RichText::new(t!("wallets.invalid_url"))
.size(16.0)
.color(Colors::red()),
);
}
ui.add_space(12.0);
// Show type selection when both URLs are empty.
if AppConfig::socks_proxy_url().is_none() && AppConfig::http_proxy_url().is_none() {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
Self::proxy_type_ui(ui);
});
ui.add_space(4.0);
}
// Show type selection when both URLs are empty.
if AppConfig::socks_proxy_url().is_none() && AppConfig::http_proxy_url().is_none() {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
Self::proxy_type_ui(ui);
});
ui.add_space(4.0);
}
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
});
}
/// Draw proxy item content.
fn proxy_item_ui(&mut self, ui: &mut egui::Ui) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
/// Draw proxy item content.
fn proxy_item_ui(&mut self, ui: &mut egui::Ui) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let r = View::item_rounding(0, 1, false);
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 1, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(
layout_size,
Layout::left_to_right(Align::Center),
|ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
let use_socks = AppConfig::use_socks_proxy();
let proxy_url = if use_socks {
AppConfig::socks_proxy_url()
} else {
AppConfig::http_proxy_url()
};
let (url, color, icon, text) = if let Some(url) = proxy_url {
(
url,
Colors::title(false),
CLOUD_CHECK,
t!("network_settings.enabled"),
)
} else {
(
t!("enter_url").into(),
Colors::inactive_text(),
CLOUD_SLASH,
t!("network_settings.disabled"),
)
};
View::ellipsize_text(ui, url, 18.0, color);
ui.add_space(1.0);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
View::item_button(ui, View::item_rounding(0, 1, true), PENCIL, None, || {
let url = if AppConfig::use_socks_proxy() {
AppConfig::socks_proxy_url().unwrap_or("".to_string())
} else {
AppConfig::http_proxy_url().unwrap_or("".to_string())
};
self.proxy_url_edit = url;
// Show proxy URL edit modal.
Modal::new(PROXY_URL_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("app_settings.proxy"))
.show();
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
let use_socks = AppConfig::use_socks_proxy();
let proxy_url = if use_socks {
AppConfig::socks_proxy_url()
} else {
AppConfig::http_proxy_url()
};
let (url, color, icon, text) = if let Some(url) = proxy_url {
(url, Colors::title(false), CLOUD_CHECK, t!("network_settings.enabled"))
} else {
(
t!("enter_url").into(),
Colors::inactive_text(),
CLOUD_SLASH,
t!("network_settings.disabled")
)
};
View::ellipsize_text(ui, url, 18.0, color);
ui.add_space(1.0);
let value = format!("{} {}", icon, text);
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
},
);
},
)
.response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
let url = if AppConfig::use_socks_proxy() {
AppConfig::socks_proxy_url().unwrap_or("".to_string())
} else {
AppConfig::http_proxy_url().unwrap_or("".to_string())
};
self.proxy_url_edit = url;
// Show proxy URL edit modal.
Modal::new(PROXY_URL_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("app_settings.proxy"))
.show();
}
}
let value = format!("{} {}", icon, text);
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
}
/// Draw proxy type selection.
fn proxy_type_ui(ui: &mut egui::Ui) {
// Draw proxy type selection.
let saved_use_socks = AppConfig::use_socks_proxy();
let mut selected_use_socks = saved_use_socks;
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_socks, true, "SOCKS5".to_string());
});
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_socks, false, "HTTP".to_string());
})
});
ui.add_space(14.0);
if saved_use_socks != selected_use_socks {
AppConfig::toggle_use_socks_proxy();
}
}
}
/// Draw proxy type selection.
fn proxy_type_ui(ui: &mut egui::Ui) {
// Draw proxy type selection.
let saved_use_socks = AppConfig::use_socks_proxy();
let mut selected_use_socks = saved_use_socks;
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_socks, true, "SOCKS5".to_string());
});
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_socks, false, "HTTP".to_string());
})
});
ui.add_space(14.0);
if saved_use_socks != selected_use_socks {
AppConfig::toggle_use_socks_proxy();
}
}
}
File diff suppressed because it is too large Load Diff
+94 -100
View File
@@ -12,117 +12,111 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Margin, Id, Layout, Align, UiBuilder};
use egui::{Align, Id, Layout, Margin, UiBuilder};
use crate::gui::Colors;
use crate::gui::views::{Content, View};
use crate::gui::views::types::{TitleContentType, TitleType};
use crate::gui::views::{Content, View};
/// Title panel with left/right action buttons and text in the middle.
pub struct TitlePanel {
/// Widget identifier.
id: Id
/// Widget identifier.
id: Id,
}
impl TitlePanel {
/// Content height.
pub const HEIGHT: f32 = 54.0;
/// Content height.
pub const HEIGHT: f32 = 54.0;
/// Create new title panel with provided identifier.
pub fn new(id: Id) -> Self {
Self {
id,
}
}
/// Create new title panel with provided identifier.
pub fn new(id: Id) -> Self {
Self { id }
}
pub fn ui(&self,
title: TitleType,
mut left_content: impl FnMut(&mut egui::Ui),
mut right_content: impl FnMut(&mut egui::Ui),
ui: &mut egui::Ui) {
// Draw title panel.
egui::TopBottomPanel::top(self.id)
.resizable(false)
.exact_height(Self::HEIGHT + View::get_top_inset())
.frame(egui::Frame {
inner_margin: Margin {
left: View::far_left_inset_margin(ui) as i8,
right: View::far_right_inset_margin(ui) as i8,
top: View::get_top_inset() as i8,
bottom: 0.0 as i8,
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| {
ui.horizontal_centered(|ui| {
(right_content)(ui);
});
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.horizontal_centered(|ui| {
(left_content)(ui);
});
});
match title {
TitleType::Single(content) => {
let content_rect = {
let mut r = rect.clone();
r.min.x += Self::HEIGHT;
r.max.x -= Self::HEIGHT;
r
};
ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| {
Self::title_text_content(ui, content);
});
}
TitleType::Dual(first, second) => {
let first_rect = {
let mut r = rect.clone();
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::HEIGHT;
r.min.x += Self::HEIGHT;
r
};
// Draw first title content.
ui.scope_builder(UiBuilder::new().max_rect(first_rect), |ui| {
Self::title_text_content(ui, first);
});
pub fn ui(
&self,
title: TitleType,
mut left_content: impl FnMut(&mut egui::Ui),
mut right_content: impl FnMut(&mut egui::Ui),
ui: &mut egui::Ui,
) {
// Draw title panel.
egui::TopBottomPanel::top(self.id)
.resizable(false)
.exact_height(Self::HEIGHT + View::get_top_inset())
.frame(egui::Frame {
inner_margin: Margin {
left: View::far_left_inset_margin(ui) as i8,
right: View::far_right_inset_margin(ui) as i8,
top: View::get_top_inset() as i8,
bottom: 0.0 as i8,
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| {
ui.horizontal_centered(|ui| {
(right_content)(ui);
});
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.horizontal_centered(|ui| {
(left_content)(ui);
});
});
match title {
TitleType::Single(content) => {
let content_rect = {
let mut r = rect.clone();
r.min.x += Self::HEIGHT;
r.max.x -= Self::HEIGHT;
r
};
ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| {
Self::title_text_content(ui, content);
});
}
TitleType::Dual(first, second) => {
let first_rect = {
let mut r = rect.clone();
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::HEIGHT;
r.min.x += Self::HEIGHT;
r
};
// Draw first title content.
ui.scope_builder(UiBuilder::new().max_rect(first_rect), |ui| {
Self::title_text_content(ui, first);
});
let second_rect = {
let mut r = rect.clone();
r.min.x = first_rect.max.x + 2.0 * Self::HEIGHT;
r.max.x -= Self::HEIGHT;
r
};
// Draw second title content.
ui.scope_builder(UiBuilder::new().max_rect(second_rect), |ui| {
Self::title_text_content(ui, second);
});
}
}
});
});
}
let second_rect = {
let mut r = rect.clone();
r.min.x = first_rect.max.x + 2.0 * Self::HEIGHT;
r.max.x -= Self::HEIGHT;
r
};
// Draw second title content.
ui.scope_builder(UiBuilder::new().max_rect(second_rect), |ui| {
Self::title_text_content(ui, second);
});
}
}
});
});
}
/// Setup title text content.
fn title_text_content(ui: &mut egui::Ui, content: TitleContentType) {
ui.vertical_centered(|ui| {
match content {
TitleContentType::Title(text) => {
ui.add_space(13.0 + if !View::is_desktop() {
1.0
} else {
0.0
});
View::ellipsize_text(ui, text.to_uppercase(), 19.0, Colors::title(true));
}
TitleContentType::WithSubTitle(text, subtitle, animate) => {
ui.add_space(4.0);
View::ellipsize_text(ui, text.to_uppercase(), 18.0, Colors::title(true));
ui.add_space(-2.0);
View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate)
}
}
});
}
/// Setup title text content.
fn title_text_content(ui: &mut egui::Ui, content: TitleContentType) {
ui.vertical_centered(|ui| match content {
TitleContentType::Title(text) => {
ui.add_space(13.0 + if !View::is_desktop() { 1.0 } else { 0.0 });
View::ellipsize_text(ui, text.to_uppercase(), 19.0, Colors::title(true));
}
TitleContentType::WithSubTitle(text, subtitle, animate) => {
ui.add_space(4.0);
View::ellipsize_text(ui, text.to_uppercase(), 18.0, Colors::title(true));
ui.add_space(-2.0);
View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate)
}
});
}
}
+90 -87
View File
@@ -12,141 +12,144 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use grin_util::ZeroingString;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::Modal;
use grin_util::ZeroingString;
/// Title type, can be single or dual title in the row.
pub enum TitleType {
/// Single title content.
Single(TitleContentType),
/// Dual title content, will align first content for default panel size width.
Dual(TitleContentType, TitleContentType),
/// Single title content.
Single(TitleContentType),
/// Dual title content, will align first content for default panel size width.
Dual(TitleContentType, TitleContentType),
}
/// Title content type, can be single title or with animated subtitle.
pub enum TitleContentType {
/// Single text.
Title(String),
/// With optionally animated subtitle text.
WithSubTitle(String, String, bool)
/// Single text.
Title(String),
/// With optionally animated subtitle text.
WithSubTitle(String, String, bool),
}
/// Stroke position against content.
pub enum LinePosition {
TOP, LEFT, RIGHT, BOTTOM
TOP,
LEFT,
RIGHT,
BOTTOM,
}
/// Position of [`Modal`] on the screen.
#[derive(Clone)]
pub enum ModalPosition {
CenterTop,
Center
CenterTop,
Center,
}
/// Global [`Modal`] state.
#[derive(Default)]
pub struct ModalState {
/// Opened [`Modal`].
pub modal: Option<Modal>,
/// Opened [`Modal`].
pub modal: Option<Modal>,
}
/// Content container to simplify modals management and navigation.
pub trait ContentContainer {
/// List of allowed [`Modal`] identifiers.
fn modal_ids(&self) -> Vec<&'static str>;
/// Draw modal content.
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks);
/// Draw container content.
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
/// Draw content, to call by parent container.
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content.
if let Some(id) = Modal::opened() {
if self.modal_ids().contains(&id) {
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
self.modal_ui(ui, modal, cb);
});
}
}
self.container_ui(ui, cb);
}
/// List of allowed [`Modal`] identifiers.
fn modal_ids(&self) -> Vec<&'static str>;
/// Draw modal content.
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks);
/// Draw container content.
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
/// Draw content, to call by parent container.
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content.
if let Some(id) = Modal::opened() {
if self.modal_ids().contains(&id) {
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
self.modal_ui(ui, modal, cb);
});
}
}
self.container_ui(ui, cb);
}
}
/// QR code scan result.
#[derive(Clone)]
pub enum QrScanResult {
/// Slatepack message.
Slatepack(String),
/// Slatepack address.
Address(ZeroingString),
/// Parsed text.
Text(ZeroingString),
/// Recovery phrase in standard or compact SeedQR format.
/// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md
SeedQR(ZeroingString),
/// Part of Uniform Resources as URI with current index and total messages amount.
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
URPart(String, usize, usize),
/// Slatepack message.
Slatepack(String),
/// Slatepack address.
Address(ZeroingString),
/// Parsed text.
Text(ZeroingString),
/// Recovery phrase in standard or compact SeedQR format.
/// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md
SeedQR(ZeroingString),
/// Part of Uniform Resources as URI with current index and total messages amount.
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
URPart(String, usize, usize),
}
impl QrScanResult {
/// Get text scanning result.
pub fn text(&self) -> String {
match self {
QrScanResult::Slatepack(text) => text.to_string(),
QrScanResult::Address(text) => text.to_string(),
QrScanResult::Text(text) => text.to_string(),
QrScanResult::SeedQR(text) => text.to_string(),
QrScanResult::URPart(uri, _, _) => uri.to_string(),
}
}
/// Get text scanning result.
pub fn text(&self) -> String {
match self {
QrScanResult::Slatepack(text) => text.to_string(),
QrScanResult::Address(text) => text.to_string(),
QrScanResult::Text(text) => text.to_string(),
QrScanResult::SeedQR(text) => text.to_string(),
QrScanResult::URPart(uri, _, _) => uri.to_string(),
}
}
}
/// QR code scanning state.
pub struct QrScanState {
/// Flag to check if image is processing to find QR code.
pub image_processing: bool,
/// Processed QR code result.
pub qr_scan_result: Option<QrScanResult>
/// Flag to check if image is processing to find QR code.
pub image_processing: bool,
/// Processed QR code result.
pub qr_scan_result: Option<QrScanResult>,
}
impl Default for QrScanState {
fn default() -> Self {
Self {
image_processing: false,
qr_scan_result: None,
}
}
fn default() -> Self {
Self {
image_processing: false,
qr_scan_result: None,
}
}
}
/// QR code image data state.
pub struct QrImageState {
/// Flag to check if QR code image is loading.
pub loading: bool,
/// Flag to check if QR code image is exporting.
pub exporting: bool,
/// Flag to check if QR code image is loading.
pub loading: bool,
/// Flag to check if QR code image is exporting.
pub exporting: bool,
/// Created GIF data from animated QR code.
pub gif_data: Option<Vec<u8>>,
/// Flag to check if GIF is creating.
pub gif_creating: bool,
/// Created GIF data from animated QR code.
pub gif_data: Option<Vec<u8>>,
/// Flag to check if GIF is creating.
pub gif_creating: bool,
/// Vector image data.
pub svg: Option<Vec<u8>>,
/// Multiple vector image data for animated QR code.
pub svg_list: Option<Vec<Vec<u8>>>
/// Vector image data.
pub svg: Option<Vec<u8>>,
/// Multiple vector image data for animated QR code.
pub svg_list: Option<Vec<Vec<u8>>>,
}
impl Default for QrImageState {
fn default() -> Self {
Self {
loading: false,
exporting: false,
gif_data: None,
gif_creating: false,
svg: None,
svg_list: None,
}
}
}
fn default() -> Self {
Self {
loading: false,
exporting: false,
gif_data: None,
gif_creating: false,
svg: None,
svg_list: None,
}
}
}
+752 -689
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+323 -326
View File
@@ -12,368 +12,365 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, Margin, RichText, ScrollArea};
use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View, CameraScanContent};
use crate::gui::views::types::{LinePosition, ContentContainer, ModalPosition, QrScanResult};
use crate::gui::views::types::{ContentContainer, LinePosition, ModalPosition, QrScanResult};
use crate::gui::views::wallets::ConnectionSettings;
use crate::gui::views::wallets::creation::MnemonicSetup;
use crate::gui::views::wallets::creation::types::Step;
use crate::gui::views::wallets::ConnectionSettings;
use crate::gui::views::{CameraScanContent, Content, Modal, View};
use crate::node::Node;
use crate::wallet::{ExternalConnection, Wallet};
use crate::wallet::types::PhraseMode;
use crate::wallet::{ExternalConnection, Wallet};
/// Wallet creation content.
pub struct WalletCreationContent {
/// Wallet name.
pub name: String,
/// Wallet password.
pub pass: ZeroingString,
/// Wallet name.
pub name: String,
/// Wallet password.
pub pass: ZeroingString,
/// Wallet creation step.
step: Step,
/// Wallet creation step.
step: Step,
/// QR code scanning [`Modal`] content.
scan_modal_content: Option<CameraScanContent>,
/// QR code scanning [`Modal`] content.
scan_modal_content: Option<CameraScanContent>,
/// Mnemonic phrase setup content.
mnemonic_setup: MnemonicSetup,
/// Network setup content.
network_setup: ConnectionSettings,
/// Mnemonic phrase setup content.
mnemonic_setup: MnemonicSetup,
/// Network setup content.
network_setup: ConnectionSettings,
/// Flag to check if an error occurred during wallet creation.
creation_error: Option<String>,
/// Flag to check if an error occurred during wallet creation.
creation_error: Option<String>,
}
const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
impl ContentContainer for WalletCreationContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
QR_CODE_PHRASE_SCAN_MODAL
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![QR_CODE_PHRASE_SCAN_MODAL]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
QR_CODE_PHRASE_SCAN_MODAL => {
if let Some(content) = self.scan_modal_content.as_mut() {
content.modal_ui(ui, cb, |result| {
match result {
QrScanResult::Text(text) => {
self.mnemonic_setup.mnemonic.import(&text);
Modal::close();
}
QrScanResult::SeedQR(text) => {
self.mnemonic_setup.mnemonic.import(&text);
Modal::close();
}
_ => {}
}
});
}
},
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
QR_CODE_PHRASE_SCAN_MODAL => {
if let Some(content) = self.scan_modal_content.as_mut() {
content.modal_ui(ui, cb, |result| match result {
QrScanResult::Text(text) => {
self.mnemonic_setup.mnemonic.import(&text);
Modal::close();
}
QrScanResult::SeedQR(text) => {
self.mnemonic_setup.mnemonic.import(&text);
Modal::close();
}
_ => {}
});
}
}
_ => {}
}
}
_ => {}
}
}
fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {
}
fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {}
}
impl WalletCreationContent {
/// Create new wallet creation content from name and password.
pub fn new(name: String, pass: ZeroingString) -> Self {
Self {
name,
pass,
step: Step::EnterMnemonic,
scan_modal_content: None,
mnemonic_setup: MnemonicSetup::default(),
network_setup: ConnectionSettings::default(),
creation_error: None,
}
}
/// Create new wallet creation content from name and password.
pub fn new(name: String, pass: ZeroingString) -> Self {
Self {
name,
pass,
step: Step::EnterMnemonic,
scan_modal_content: None,
mnemonic_setup: MnemonicSetup::default(),
network_setup: ConnectionSettings::default(),
creation_error: None,
}
}
/// Draw wallet creation content.
pub fn content_ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
on_create: impl FnMut(Wallet)) {
self.ui(ui, cb);
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8,
right: (View::get_right_inset() + View::TAB_ITEMS_PADDING) as i8,
top: View::TAB_ITEMS_PADDING as i8,
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
},
fill: Colors::fill(),
..Default::default()
})
.show_inside(ui, |ui| {
// Draw divider line.
let rect = {
let mut r = ui.available_rect_before_wrap();
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);
});
});
/// Draw wallet creation content.
pub fn content_ui(
&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
on_create: impl FnMut(Wallet),
) {
self.ui(ui, cb);
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8,
right: (View::get_right_inset() + View::TAB_ITEMS_PADDING) as i8,
top: View::TAB_ITEMS_PADDING as i8,
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
},
fill: Colors::fill(),
..Default::default()
})
.show_inside(ui, |ui| {
// Draw divider line.
let rect = {
let mut r = ui.available_rect_before_wrap();
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);
});
});
// Show wallet creation step content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + 4.0) as i8,
right: (View::get_right_inset() + 4.0) as i8,
top: 3.0 as i8,
bottom: 4.0 as i8,
},
fill: Colors::fill_lite(),
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.id_salt(Id::from(format!("creation_step_scroll_{}", self.step.name())))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
let max_width = if self.step == Step::SetupConnection {
Content::SIDE_PANEL_WIDTH * 1.3
} else {
Content::SIDE_PANEL_WIDTH * 2.0
};
View::max_width_ui(ui, max_width, |ui| {
self.step_content_ui(ui, cb);
});
});
});
}
// Show wallet creation step content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + 4.0) as i8,
right: (View::get_right_inset() + 4.0) as i8,
top: 3.0 as i8,
bottom: 4.0 as i8,
},
fill: Colors::fill_lite(),
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.id_salt(Id::from(format!(
"creation_step_scroll_{}",
self.step.name()
)))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
let max_width = if self.step == Step::SetupConnection {
Content::SIDE_PANEL_WIDTH * 1.3
} else {
Content::SIDE_PANEL_WIDTH * 2.0
};
View::max_width_ui(ui, max_width, |ui| {
self.step_content_ui(ui, cb);
});
});
});
}
/// Draw [`Step`] description and confirmation control.
fn step_control_ui(&mut self,
ui: &mut egui::Ui,
on_create: impl FnOnce(Wallet),
cb: &dyn PlatformCallbacks) {
let step = &self.step;
// Setup description and next step availability.
let (step_text, mut next) = match step {
Step::EnterMnemonic => {
let mode = &self.mnemonic_setup.mnemonic.mode();
let (text, available) = match mode {
PhraseMode::Generate => (t!("wallets.create_phrase_desc"), true),
PhraseMode::Import => {
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
(t!("wallets.restore_phrase_desc"), available)
}
};
(text, available)
}
Step::ConfirmMnemonic => {
let text = t!("wallets.restore_phrase_desc");
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
(text, available)
}
Step::SetupConnection => {
(t!("wallets.setup_conn_desc"), self.creation_error.is_none())
}
};
/// Draw [`Step`] description and confirmation control.
fn step_control_ui(
&mut self,
ui: &mut egui::Ui,
on_create: impl FnOnce(Wallet),
cb: &dyn PlatformCallbacks,
) {
let step = &self.step;
// Setup description and next step availability.
let (step_text, mut next) = match step {
Step::EnterMnemonic => {
let mode = &self.mnemonic_setup.mnemonic.mode();
let (text, available) = match mode {
PhraseMode::Generate => (t!("wallets.create_phrase_desc"), true),
PhraseMode::Import => {
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
(t!("wallets.restore_phrase_desc"), available)
}
};
(text, available)
}
Step::ConfirmMnemonic => {
let text = t!("wallets.restore_phrase_desc");
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
(text, available)
}
Step::SetupConnection => (t!("wallets.setup_conn_desc"), self.creation_error.is_none()),
};
// Show step description or error.
let generate_step = step == &Step::EnterMnemonic &&
self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate;
if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) ||
generate_step {
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
} else {
next = false;
// Show error text.
if let Some(err) = &self.creation_error {
ui.add_space(10.0);
ui.label(RichText::new(err)
.size(16.0)
.color(Colors::red()));
ui.add_space(10.0);
} else {
ui.label(RichText::new(t!("wallets.not_valid_phrase"))
.size(16.0)
.color(Colors::red()));
ui.add_space(4.0);
};
}
// Show step description or error.
let generate_step = step == &Step::EnterMnemonic
&& self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate;
if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) || generate_step
{
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
} else {
next = false;
// Show error text.
if let Some(err) = &self.creation_error {
ui.add_space(10.0);
ui.label(RichText::new(err).size(16.0).color(Colors::red()));
ui.add_space(10.0);
} else {
ui.label(
RichText::new(t!("wallets.not_valid_phrase"))
.size(16.0)
.color(Colors::red()),
);
ui.add_space(4.0);
};
}
// 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);
// 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 {
Step::EnterMnemonic => {
ui.columns(2, |columns| {
// Show copy or paste button for mnemonic phrase step.
columns[0].vertical_centered_justified(|ui| {
match self.mnemonic_setup.mnemonic.mode() {
PhraseMode::Generate => {
let c_t = format!("{} {}",
COPY,
t!("copy").to_uppercase());
View::button(ui, c_t, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.mnemonic_setup
.mnemonic
.get_phrase());
});
}
PhraseMode::Import => {
let p_t = format!("{} {}",
CLIPBOARD_TEXT,
t!("paste").to_uppercase());
View::button(ui, p_t, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer());
self.mnemonic_setup.mnemonic.import(&data);
});
}
}
});
// Show next step or QR code scan button.
columns[1].vertical_centered_justified(|ui| {
if next {
self.next_step_button_ui(ui, on_create);
} else {
let scan_text = format!("{} {}",
SCAN,
t!("scan").to_uppercase());
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.scan_modal_content = Some(CameraScanContent::default());
// Show QR code scan modal.
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
});
}
});
});
}
Step::ConfirmMnemonic => {
// Show next step or paste button.
if next {
self.next_step_button_ui(ui, on_create);
} else {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
View::button(ui, paste_text, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer());
self.mnemonic_setup.mnemonic.import(&data);
});
}
}
Step::SetupConnection => {
if next {
self.next_step_button_ui(ui, on_create);
ui.add_space(2.0);
}
}
}
}
match step {
Step::EnterMnemonic => {
ui.columns(2, |columns| {
// Show copy or paste button for mnemonic phrase step.
columns[0].vertical_centered_justified(|ui| {
match self.mnemonic_setup.mnemonic.mode() {
PhraseMode::Generate => {
let c_t = format!("{} {}", COPY, t!("copy"));
View::button(ui, c_t, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(
self.mnemonic_setup.mnemonic.get_phrase(),
);
});
}
PhraseMode::Import => {
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, p_t, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer());
self.mnemonic_setup.mnemonic.import(&data);
});
}
}
});
// Show next step or QR code scan button.
columns[1].vertical_centered_justified(|ui| {
if next {
self.next_step_button_ui(ui, on_create);
} else {
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.scan_modal_content = Some(CameraScanContent::default());
// Show QR code scan modal.
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
});
}
});
});
}
Step::ConfirmMnemonic => {
// Show next step or paste button.
if next {
self.next_step_button_ui(ui, on_create);
} else {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer());
self.mnemonic_setup.mnemonic.import(&data);
});
}
}
Step::SetupConnection => {
if next {
self.next_step_button_ui(ui, on_create);
ui.add_space(2.0);
}
}
}
}
/// Draw button to go to next [`Step`].
fn next_step_button_ui(&mut self,
ui: &mut egui::Ui,
on_create: impl FnOnce(Wallet)) {
// Setup button text.
let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection {
(format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold())
} else {
(t!("continue").into(), Colors::green(), Colors::white_or_black(false))
};
/// Draw button to go to next [`Step`].
fn next_step_button_ui(&mut self, ui: &mut egui::Ui, on_create: impl FnOnce(Wallet)) {
// Setup button text.
let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection {
(
format!("{} {}", CHECK, t!("complete")),
Colors::title(true),
Colors::gold(),
)
} else {
(
t!("continue").into(),
Colors::green(),
Colors::white_or_black(false),
)
};
// Show next step button.
View::colored_text_button_ui(ui, next_text.to_uppercase(), text_color, bg_color, |ui| {
self.step = match self.step {
Step::EnterMnemonic => {
if self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate {
Step::ConfirmMnemonic
} else {
Step::SetupConnection
}
}
Step::ConfirmMnemonic => {
Step::SetupConnection
},
Step::SetupConnection => {
// Create wallet at last step.
match Wallet::create(&self.name,
&self.pass,
&self.mnemonic_setup.mnemonic,
&self.network_setup.method) {
Ok(w) => {
self.mnemonic_setup.reset();
// Pass created wallet to callback.
(on_create)(w);
Step::EnterMnemonic
}
Err(e) => {
self.creation_error = Some(format!("{:?}", e));
Step::SetupConnection
}
}
}
};
// Show next step button.
View::colored_text_button_ui(ui, next_text.to_uppercase(), text_color, bg_color, |ui| {
self.step = match self.step {
Step::EnterMnemonic => {
if self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate {
Step::ConfirmMnemonic
} else {
Step::SetupConnection
}
}
Step::ConfirmMnemonic => Step::SetupConnection,
Step::SetupConnection => {
// Create wallet at last step.
match Wallet::create(
&self.name,
&self.pass,
&self.mnemonic_setup.mnemonic,
&self.network_setup.method,
) {
Ok(w) => {
self.mnemonic_setup.reset();
// Pass created wallet to callback.
(on_create)(w);
Step::EnterMnemonic
}
Err(e) => {
self.creation_error = Some(format!("{:?}", e));
Step::SetupConnection
}
}
}
};
// Check external connections availability on connection setup.
if self.step == Step::SetupConnection {
ExternalConnection::check(None, ui.ctx());
}
});
}
// Check external connections availability on connection setup.
if self.step == Step::SetupConnection {
ExternalConnection::check(None, ui.ctx());
}
});
}
/// Draw wallet creation [`Step`] content.
fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match &self.step {
Step::EnterMnemonic => self.mnemonic_setup.enter_ui(ui, cb),
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
Step::SetupConnection => {
// Redraw if node is running.
if Node::is_running() && !Content::is_dual_panel_mode(ui.ctx()) {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
}
self.network_setup.ui(ui, cb);
}
}
}
/// Draw wallet creation [`Step`] content.
fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match &self.step {
Step::EnterMnemonic => self.mnemonic_setup.enter_ui(ui, cb),
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
Step::SetupConnection => {
// Redraw if node is running.
if Node::is_running() && !Content::is_dual_panel_mode(ui.ctx()) {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
}
self.network_setup.ui(ui, cb);
}
}
}
/// Back to previous wallet creation [`Step`], return `true` to close creation.
pub fn on_back(&mut self) -> bool {
match &self.step {
Step::ConfirmMnemonic => {
self.step = Step::EnterMnemonic;
false
},
Step::SetupConnection => {
self.creation_error = None;
self.step = Step::EnterMnemonic;
false
}
_ => true
}
}
}
/// Back to previous wallet creation [`Step`], return `true` to close creation.
pub fn on_back(&mut self) -> bool {
match &self.step {
Step::ConfirmMnemonic => {
self.step = Step::EnterMnemonic;
false
}
Step::SetupConnection => {
self.creation_error = None;
self.step = Step::EnterMnemonic;
false
}
_ => true,
}
}
}
+262 -253
View File
@@ -17,298 +17,307 @@ use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::icons::PENCIL;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View, TextEdit};
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Content, Modal, TextEdit, View};
use crate::wallet::Mnemonic;
use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord};
/// Mnemonic phrase setup content.
pub struct MnemonicSetup {
/// Current mnemonic phrase.
pub mnemonic: Mnemonic,
/// Current mnemonic phrase.
pub mnemonic: Mnemonic,
/// Current word number to edit at [`Modal`].
word_index_edit: usize,
/// Entered word value for [`Modal`].
word_edit: String,
/// Flag to check if entered word is valid at [`Modal`].
valid_word_edit: bool,
/// Current word number to edit at [`Modal`].
word_index_edit: usize,
/// Entered word value for [`Modal`].
word_edit: String,
/// Flag to check if entered word is valid at [`Modal`].
valid_word_edit: bool,
}
/// Identifier for word input [`Modal`].
pub const WORD_INPUT_MODAL: &'static str = "word_input_modal";
impl Default for MnemonicSetup {
fn default() -> Self {
Self {
mnemonic: Mnemonic::default(),
word_index_edit: 0,
word_edit: String::from(""),
valid_word_edit: true,
}
}
fn default() -> Self {
Self {
mnemonic: Mnemonic::default(),
word_index_edit: 0,
word_edit: String::from(""),
valid_word_edit: true,
}
}
}
impl ContentContainer for MnemonicSetup {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
WORD_INPUT_MODAL
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![WORD_INPUT_MODAL]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb),
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb),
_ => {}
}
}
fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {
}
fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {}
}
impl MnemonicSetup {
/// Draw content for phrase enter step.
pub fn enter_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.ui(ui, cb);
ui.add_space(10.0);
/// Draw content for phrase enter step.
pub fn enter_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.ui(ui, cb);
ui.add_space(10.0);
// Show mode setup.
let mut mode = self.mnemonic.mode();
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let create_mode = PhraseMode::Generate;
let create_text = t!("create");
View::radio_value(ui, &mut mode, create_mode, create_text);
});
columns[1].vertical_centered(|ui| {
let import_mode = PhraseMode::Import;
let import_text = t!("wallets.recover");
View::radio_value(ui, &mut mode, import_mode, import_text);
});
});
if mode != self.mnemonic.mode() {
self.mnemonic.set_mode(mode);
}
// Show mode setup.
let mut mode = self.mnemonic.mode();
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let create_mode = PhraseMode::Generate;
let create_text = t!("create");
View::radio_value(ui, &mut mode, create_mode, create_text);
});
columns[1].vertical_centered(|ui| {
let import_mode = PhraseMode::Import;
let import_text = t!("wallets.recover");
View::radio_value(ui, &mut mode, import_mode, import_text);
});
});
if mode != self.mnemonic.mode() {
self.mnemonic.set_mode(mode);
}
ui.add_space(10.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.words_count"))
.size(16.0)
.color(Colors::gray())
);
});
ui.add_space(6.0);
ui.add_space(10.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.words_count"))
.size(16.0)
.color(Colors::gray()),
);
});
ui.add_space(6.0);
// Show mnemonic phrase size setup.
let mut size = self.mnemonic.size();
ui.columns(5, |columns| {
for (index, word) in PhraseSize::VALUES.iter().enumerate() {
columns[index].vertical_centered(|ui| {
let text = word.value().to_string();
View::radio_value(ui, &mut size, word.clone(), text);
});
}
});
if size != self.mnemonic.size() {
self.mnemonic.set_size(size);
}
// Show mnemonic phrase size setup.
let mut size = self.mnemonic.size();
ui.columns(5, |columns| {
for (index, word) in PhraseSize::VALUES.iter().enumerate() {
columns[index].vertical_centered(|ui| {
let text = word.value().to_string();
View::radio_value(ui, &mut size, word.clone(), text);
});
}
});
if size != self.mnemonic.size() {
self.mnemonic.set_size(size);
}
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show words setup.
self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import);
}
/// Draw content for phrase confirmation step.
pub fn confirm_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.ui(ui, cb);
// Show words setup.
self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import);
}
/// Draw content for phrase confirmation step.
pub fn confirm_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.ui(ui, cb);
ui.add_space(4.0);
ui.vertical_centered(|ui| {
let text = format!("{}:", t!("wallets.recovery_phrase"));
ui.label(RichText::new(text).size(16.0).color(Colors::gray()));
});
ui.add_space(4.0);
self.word_list_ui(ui, true);
}
ui.add_space(4.0);
ui.vertical_centered(|ui| {
let text = format!("{}:", t!("wallets.recovery_phrase"));
ui.label(RichText::new(text).size(16.0).color(Colors::gray()));
});
ui.add_space(4.0);
self.word_list_ui(ui, true);
}
/// Draw grid of words for mnemonic phrase.
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool) {
ui.add_space(6.0);
ui.scope(|ui| {
// Setup spacing between columns.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0);
/// Draw grid of words for mnemonic phrase.
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool) {
ui.add_space(6.0);
ui.scope(|ui| {
// Setup spacing between columns.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0);
// Select list of words based on current mode and edit flag.
let words = self.mnemonic.words(edit);
// Select list of words based on current mode and edit flag.
let words = self.mnemonic.words(edit);
let mut word_number = 0;
let cols = list_columns_count(ui);
let _ = words.chunks(cols).map(|chunk| {
let size = chunk.len();
word_number += 1;
if size > 1 {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
columns[1].horizontal(|ui| {
word_number += 1;
let word = chunk.get(1).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
if size > 2 {
columns[2].horizontal(|ui| {
word_number += 1;
let word = chunk.get(2).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
}
if size > 3 {
columns[3].horizontal(|ui| {
word_number += 1;
let word = chunk.get(3).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
}
});
} else {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
});
}
}).collect::<Vec<_>>();
});
ui.add_space(6.0);
}
let mut word_number = 0;
let cols = list_columns_count(ui);
let _ = words
.chunks(cols)
.map(|chunk| {
let size = chunk.len();
word_number += 1;
if size > 1 {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
columns[1].horizontal(|ui| {
word_number += 1;
let word = chunk.get(1).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
if size > 2 {
columns[2].horizontal(|ui| {
word_number += 1;
let word = chunk.get(2).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
}
if size > 3 {
columns[3].horizontal(|ui| {
word_number += 1;
let word = chunk.get(3).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
}
});
} else {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
});
}
})
.collect::<Vec<_>>();
});
ui.add_space(6.0);
}
/// Draw word grid item.
fn word_item_ui(&mut self,
ui: &mut egui::Ui,
num: usize,
word: &PhraseWord,
edit: bool) {
let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) {
Colors::red()
} else {
Colors::white_or_black(true)
};
if edit {
ui.add_space(6.0);
View::button(ui, PENCIL.to_string(), Colors::white_or_black(false), || {
self.word_index_edit = num - 1;
self.word_edit = word.text.clone();
self.valid_word_edit = word.valid;
// Show word edit modal.
Modal::new(WORD_INPUT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.recovery_phrase"))
.show();
});
ui.label(RichText::new(format!("#{} {}", num, word.text))
.size(17.0)
.color(color));
} else {
ui.add_space(12.0);
let text = format!("#{} {}", num, word.text);
ui.label(RichText::new(text).size(17.0).color(color));
}
}
/// Draw word grid item.
fn word_item_ui(&mut self, ui: &mut egui::Ui, num: usize, word: &PhraseWord, edit: bool) {
let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) {
Colors::red()
} else {
Colors::white_or_black(true)
};
if edit {
ui.add_space(6.0);
View::button(
ui,
PENCIL.to_string(),
Colors::white_or_black(false),
|| {
self.word_index_edit = num - 1;
self.word_edit = word.text.clone();
self.valid_word_edit = word.valid;
// Show word edit modal.
Modal::new(WORD_INPUT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.recovery_phrase"))
.show();
},
);
ui.label(
RichText::new(format!("#{} {}", num, word.text))
.size(17.0)
.color(color),
);
} else {
ui.add_space(12.0);
let text = format!("#{} {}", num, word.text);
ui.label(RichText::new(text).size(17.0).color(color));
}
}
/// Reset mnemonic phrase state to default values.
pub fn reset(&mut self) {
self.mnemonic = Mnemonic::default();
}
/// Reset mnemonic phrase state to default values.
pub fn reset(&mut self) {
self.mnemonic = Mnemonic::default();
}
/// Draw word input [`Modal`] content.
fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut MnemonicSetup| {
// Insert word checking validity.
let word = &c.word_edit.trim().to_string();
c.valid_word_edit = c.mnemonic.insert(c.word_index_edit, word);
if !c.valid_word_edit {
return;
}
// Close modal or go to next word to edit.
let next_word = c.mnemonic.get(c.word_index_edit + 1);
let close_modal = next_word.is_none() ||
(!next_word.as_ref().unwrap().text.is_empty() &&
next_word.unwrap().valid);
if close_modal {
Modal::close();
} else {
c.word_index_edit += 1;
c.word_edit = String::from("");
}
};
/// Draw word input [`Modal`] content.
fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut MnemonicSetup| {
// Insert word checking validity.
let word = &c.word_edit.trim().to_string();
c.valid_word_edit = c.mnemonic.insert(c.word_index_edit, word);
if !c.valid_word_edit {
return;
}
// Close modal or go to next word to edit.
let next_word = c.mnemonic.get(c.word_index_edit + 1);
let close_modal = next_word.is_none()
|| (!next_word.as_ref().unwrap().text.is_empty() && next_word.unwrap().valid);
if close_modal {
Modal::close();
} else {
c.word_index_edit += 1;
c.word_edit = String::from("");
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw word value text edit.
let mut word_edit = TextEdit::new(Id::from(modal.id).with(self.word_index_edit));
word_edit.ui(ui, &mut self.word_edit, cb);
if word_edit.enter_pressed {
on_save(self);
}
// Draw word value text edit.
let mut word_edit = TextEdit::new(Id::from(modal.id).with(self.word_index_edit));
word_edit.ui(ui, &mut self.word_edit, cb);
if word_edit.enter_pressed {
on_save(self);
}
// Show error when specified word is not valid.
if !self.valid_word_edit {
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.not_valid_word"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show error when specified word is not valid.
if !self.valid_word_edit {
ui.add_space(12.0);
ui.label(
RichText::new(t!("wallets.not_valid_word"))
.size(17.0)
.color(Colors::red()),
);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Show save button.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
// Show save button.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
}
/// Calculate word list columns count based on available ui width.
fn list_columns_count(ui: &mut egui::Ui) -> usize {
let w = ui.available_width();
let min_panel_w = Content::SIDE_PANEL_WIDTH - 12.0;
let double_min_panel_w = min_panel_w * 2.0;
if w >= min_panel_w * 1.5 && w < double_min_panel_w {
3
} else if w >= double_min_panel_w {
4
} else {
2
}
}
let w = ui.available_width();
let min_panel_w = Content::SIDE_PANEL_WIDTH - 12.0;
let double_min_panel_w = min_panel_w * 2.0;
if w >= min_panel_w * 1.5 && w < double_min_panel_w {
3
} else if w >= double_min_panel_w {
4
} else {
2
}
}
+1 -1
View File
@@ -18,4 +18,4 @@ pub use mnemonic::MnemonicSetup;
mod content;
pub use content::WalletCreationContent;
pub mod types;
pub mod types;
+15 -15
View File
@@ -15,21 +15,21 @@
/// Wallet creation step.
#[derive(PartialEq, Clone)]
pub enum Step {
/// Mnemonic phrase input.
EnterMnemonic,
/// Mnemonic phrase confirmation.
ConfirmMnemonic,
/// Wallet connection setup.
SetupConnection
/// Mnemonic phrase input.
EnterMnemonic,
/// Mnemonic phrase confirmation.
ConfirmMnemonic,
/// Wallet connection setup.
SetupConnection,
}
impl Step {
/// Short name representing creation step.
pub fn name(&self) -> String {
match *self {
Step::EnterMnemonic => "enter_phrase".to_owned(),
Step::ConfirmMnemonic => "confirm_phrase".to_owned(),
Step::SetupConnection => "setup_conn".to_owned(),
}
}
}
/// Short name representing creation step.
pub fn name(&self) -> String {
match *self {
Step::EnterMnemonic => "enter_phrase".to_owned(),
Step::ConfirmMnemonic => "confirm_phrase".to_owned(),
Step::SetupConnection => "setup_conn".to_owned(),
}
}
}
+2 -2
View File
@@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod modals;
mod creation;
pub mod modals;
mod content;
pub use content::*;
mod wallet;
use wallet::*;
use wallet::*;
+84 -75
View File
@@ -21,89 +21,98 @@ use crate::gui::views::{Modal, TextEdit, View};
/// Initial wallet creation [`Modal`] content.
pub struct AddWalletModal {
/// Wallet name.
pub name_edit: String,
/// Password to encrypt created wallet.
pub pass_edit: String,
/// Wallet name.
pub name_edit: String,
/// Password to encrypt created wallet.
pub pass_edit: String,
}
impl Default for AddWalletModal {
fn default() -> Self {
Self {
name_edit: t!("wallets.default_wallet").into(),
pass_edit: "".to_string(),
}
}
fn default() -> Self {
Self {
name_edit: t!("wallets.default_wallet").into(),
pass_edit: "".to_string(),
}
}
}
impl AddWalletModal {
/// Draw creating wallet name/password input [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_input: impl FnMut(String, ZeroingString)) {
let mut on_next = |m: &mut AddWalletModal| {
let name = m.name_edit.clone();
let pass = m.pass_edit.clone();
if name.is_empty() || pass.is_empty() {
return;
}
Modal::close();
on_input(name, ZeroingString::from(pass));
};
ui.vertical_centered(|ui| {
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.name"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
/// Draw creating wallet name/password input [`Modal`] content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_input: impl FnMut(String, ZeroingString),
) {
let mut on_next = |m: &mut AddWalletModal| {
let name = m.name_edit.clone();
let pass = m.pass_edit.clone();
if name.is_empty() || pass.is_empty() {
return;
}
Modal::close();
on_input(name, ZeroingString::from(pass));
};
ui.vertical_centered(|ui| {
ui.add_space(6.0);
ui.label(
RichText::new(t!("wallets.name"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Show wallet name text edit.
let mut name_input = TextEdit::new(Id::from(modal.id).with("name"))
.focus(Modal::first_draw());
name_input.ui(ui, &mut self.name_edit, cb);
// Show wallet name text edit.
let mut name_input = TextEdit::new(Id::from(modal.id).with("name")).focus(false);
name_input.ui(ui, &mut self.name_edit, cb);
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(8.0);
ui.label(
RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Show wallet password text edit.
let mut pass_input = TextEdit::new(Id::from(modal.id).with("pass"))
.password()
.focus(false);
if name_input.enter_pressed {
pass_input.focus_request();
}
pass_input.ui(ui, &mut self.pass_edit, cb);
if pass_input.enter_pressed {
(on_next)(self);
}
ui.add_space(12.0);
});
// Show wallet password text edit.
let mut pass_input = TextEdit::new(Id::from(modal.id).with("pass"))
.password()
.focus(Modal::first_draw());
if name_input.enter_pressed {
pass_input.focus_request();
}
pass_input.ui(ui, &mut self.pass_edit, cb);
if pass_input.enter_pressed {
on_next(self);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
(on_next)(self);
});
});
});
ui.add_space(6.0);
});
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
on_next(self);
});
});
});
ui.add_space(6.0);
});
}
}
+101 -97
View File
@@ -15,14 +15,14 @@
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, OpenUrl, RichText, ScrollArea};
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::gui::icons::{BRACKETS_CURLY, GITHUB_LOGO, TELEGRAM_LOGO};
use crate::gui::views::{Modal, View};
/// Application release changelog content.
pub struct ChangelogContent {
/// Changelog text.
changelog: String,
/// Changelog text.
changelog: String,
}
/// Endpoint for GitHub repository.
@@ -33,105 +33,109 @@ const TELEGRAM_URL: &'static str = "https://t.me/grim_releases";
const GIT_URL: &'static str = "https://code.gri.mw/GUI/grim";
impl ChangelogContent {
/// Create new content instance.
pub fn new(changelog: String) -> Self {
Self { changelog }
}
/// Create new content instance.
pub fn new(changelog: String) -> Self {
Self { changelog }
}
/// Identifier for [`Modal`].
pub const MODAL_ID: &'static str = "release_changelog_modal";
/// Identifier for [`Modal`].
pub const MODAL_ID: &'static str = "release_changelog_modal";
/// Draw changelog [`Modal`] content.
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("changelog")).size(16.0).color(Colors::gray()));
});
ui.add_space(6.0);
/// Draw changelog [`Modal`] content.
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("changelog"))
.size(16.0)
.color(Colors::gray()),
);
});
ui.add_space(6.0);
// Show changelog text.
ui.vertical_centered(|ui| {
let scroll_id = Id::from("release_changelog");
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("_input");
egui::TextEdit::multiline(&mut self.changelog)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
});
// Show changelog text.
ui.vertical_centered(|ui| {
let scroll_id = Id::from("release_changelog");
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("_input");
egui::TextEdit::multiline(&mut self.changelog)
.id(input_id)
.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(8.0);
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(3, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw button to open GitHub link.
let mut github_clicked = false;
View::button(ui, GITHUB_LOGO, Colors::white_or_black(false), || {
github_clicked = true;
});
if github_clicked {
ui.ctx().open_url(OpenUrl {
url: GITHUB_URL.into(),
new_tab: true,
});
}
});
columns[1].vertical_centered_justified(|ui| {
// Draw button to open Telegram link.
let mut tg_clicked = false;
View::button(ui, TELEGRAM_LOGO, Colors::white_or_black(false), || {
tg_clicked = true;
});
if tg_clicked {
ui.ctx().open_url(OpenUrl {
url: TELEGRAM_URL.into(),
new_tab: true,
});
}
});
columns[2].vertical_centered_justified(|ui| {
// Draw button to open repository link.
let mut git_clicked = false;
View::button(ui, BRACKETS_CURLY, Colors::white_or_black(false), || {
git_clicked = true;
});
if git_clicked {
ui.ctx().open_url(OpenUrl {
url: GIT_URL.into(),
new_tab: true,
});
}
});
});
ui.columns(3, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw button to open GitHub link.
let mut github_clicked = false;
View::button(ui, GITHUB_LOGO, Colors::white_or_black(false), || {
github_clicked = true;
});
if github_clicked {
ui.ctx().open_url(OpenUrl {
url: GITHUB_URL.into(),
new_tab: true,
});
}
});
columns[1].vertical_centered_justified(|ui| {
// Draw button to open Telegram link.
let mut tg_clicked = false;
View::button(ui, TELEGRAM_LOGO, Colors::white_or_black(false), || {
tg_clicked = true;
});
if tg_clicked {
ui.ctx().open_url(OpenUrl {
url: TELEGRAM_URL.into(),
new_tab: true,
});
}
});
columns[2].vertical_centered_justified(|ui| {
// Draw button to open repository link.
let mut git_clicked = false;
View::button(ui, BRACKETS_CURLY, Colors::white_or_black(false), || {
git_clicked = true;
});
if git_clicked {
ui.ctx().open_url(OpenUrl {
url: GIT_URL.into(),
new_tab: true,
});
}
});
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
Modal::close();
});
});
ui.add_space(6.0);
}
}
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
Modal::close();
});
});
ui.add_space(6.0);
}
}
+137 -125
View File
@@ -15,150 +15,162 @@
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
use crate::gui::Colors;
use crate::gui::icons::{CHECK, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED};
use crate::gui::views::wallets::wallet::types::wallet_status_text;
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::{Wallet, WalletList};
/// Wallet list [`Modal`] content
pub struct WalletListModal {
/// Selected wallet id.
selected_id: Option<i64>,
/// Selected wallet id.
selected_id: Option<i64>,
/// Optional data to pass after wallet selection.
data: Option<String>,
/// Optional data to pass after wallet selection.
data: Option<String>,
/// Flag to check if wallet can be opened from the list.
can_open: bool,
/// Flag to check if wallet can be opened from the list.
can_open: bool,
}
impl WalletListModal {
/// Create new content instance.
pub fn new(selected_id: Option<i64>, data: Option<String>, can_open: bool) -> Self {
Self { selected_id, data, can_open }
}
/// Create new content instance.
pub fn new(selected_id: Option<i64>, data: Option<String>, can_open: bool) -> Self {
Self {
selected_id,
data,
can_open,
}
}
/// Draw content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallets: &WalletList,
mut on_select: impl FnMut(Wallet, Option<String>)) {
ui.add_space(4.0);
ScrollArea::vertical()
.max_height(373.0)
.id_salt("select_wallet_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2])
.show(ui, |ui| {
ui.add_space(2.0);
ui.vertical_centered(|ui| {
let data = self.data.clone();
for wallet in wallets.list() {
// Draw wallet list item.
self.wallet_item_ui(ui, wallet, || {
Modal::close();
on_select(wallet.clone(), data.clone());
});
ui.add_space(5.0);
}
});
});
/// Draw content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
wallets: &WalletList,
mut on_select: impl FnMut(Wallet, Option<String>),
) {
ui.add_space(4.0);
ScrollArea::vertical()
.max_height(373.0)
.id_salt("select_wallet_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2])
.show(ui, |ui| {
ui.add_space(2.0);
ui.vertical_centered(|ui| {
let data = self.data.clone();
for wallet in wallets.list() {
// Draw wallet list item.
self.wallet_item_ui(ui, wallet, || {
Modal::close();
on_select(wallet.clone(), data.clone());
});
ui.add_space(5.0);
}
});
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.data = None;
Modal::close();
});
});
ui.add_space(6.0);
}
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
self.data = None;
Modal::close();
},
);
});
ui.add_space(6.0);
}
/// Draw wallet list item with provided callback on select.
fn wallet_item_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
on_select: impl FnOnce()) {
let config = wallet.get_config();
let id = config.id;
/// Draw wallet list item with provided callback on select.
fn wallet_item_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, on_select: impl FnOnce()) {
let config = wallet.get_config();
let id = config.id;
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
let rounding = View::item_rounding(0, 1, false);
ui.painter().rect(rect,
rounding,
Colors::fill(),
View::hover_stroke(),
StrokeKind::Outside);
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
let rounding = View::item_rounding(0, 1, false);
ui.painter().rect(
rect,
rounding,
Colors::fill(),
View::hover_stroke(),
StrokeKind::Outside,
);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if self.can_open {
// Show button to select or open closed wallet.
let icon = if wallet.is_open() {
CHECK
} else {
FOLDER_OPEN
};
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
on_select();
});
} else {
// Draw button to select wallet.
if self.selected_id.unwrap_or(0) == id {
View::selected_item_check(ui);
} else {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select();
});
}
}
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if self.can_open {
// Show button to select or open closed wallet.
let icon = if wallet.is_open() { CHECK } else { FOLDER_OPEN };
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
on_select();
});
} else {
// Draw button to select wallet.
if self.selected_id.unwrap_or(0) == id {
View::selected_item_check(ui);
} else {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select();
});
}
}
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);
// Show wallet name text.
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
View::ellipsize_text(ui, config.name, 18.0, Colors::title(false));
});
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);
// Show wallet name text.
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
View::ellipsize_text(ui, config.name, 18.0, Colors::title(false));
});
// Show wallet connection text.
let connection = wallet.get_current_connection();
let conn_text = match connection {
ConnectionMethod::Integrated => {
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
}
ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url)
};
ui.label(RichText::new(conn_text).size(15.0).color(Colors::text(false)));
ui.add_space(1.0);
// Show wallet connection text.
let connection = wallet.get_current_connection();
let conn_text = match connection {
ConnectionMethod::Integrated => {
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
}
ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url),
};
ui.label(
RichText::new(conn_text)
.size(15.0)
.color(Colors::text(false)),
);
ui.add_space(1.0);
// Show wallet API text or open status.
if self.can_open {
ui.label(RichText::new(wallet_status_text(wallet))
.size(15.0)
.color(Colors::gray()));
} else {
let address = if let Some(port) = config.api_port {
format!("127.0.0.1:{}", port)
} else {
"-".to_string()
};
let api_text = format!("{} {}", PLUGS_CONNECTED, address);
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
}
ui.add_space(3.0);
});
});
});
}
}
// Show wallet API text or open status.
if self.can_open {
ui.label(
RichText::new(wallet_status_text(wallet))
.size(15.0)
.color(Colors::gray()),
);
} else {
let address = if let Some(port) = config.api_port {
format!("127.0.0.1:{}", port)
} else {
"-".to_string()
};
let api_text = format!("{} {}", PLUGS_CONNECTED, address);
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
}
ui.add_space(3.0);
});
});
});
}
}
+1 -1
View File
@@ -25,4 +25,4 @@ mod add;
pub use add::*;
mod changelog;
pub use changelog::*;
pub use changelog::*;
+90 -77
View File
@@ -21,88 +21,101 @@ use crate::gui::views::{Modal, TextEdit, View};
/// Wallet opening [`Modal`] content.
pub struct OpenWalletModal {
/// Password to open wallet.
pass_edit: String,
/// Flag to check if wrong password was entered.
wrong_pass: bool,
/// Password to open wallet.
pass_edit: String,
/// Flag to check if wrong password was entered.
wrong_pass: bool,
}
impl OpenWalletModal {
/// Create new content instance.
pub fn new() -> Self {
Self {
pass_edit: "".to_string(),
wrong_pass: false,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_continue: impl FnMut(ZeroingString) -> bool) {
// Callback for button to continue.
let mut on_continue = |m: &mut OpenWalletModal| {
let pass = m.pass_edit.clone();
if pass.is_empty() {
return;
}
m.wrong_pass = !on_continue(ZeroingString::from(pass));
if !m.wrong_pass {
m.pass_edit = "".to_string();
Modal::close();
}
};
/// Create new content instance.
pub fn new() -> Self {
Self {
pass_edit: "".to_string(),
wrong_pass: false,
}
}
/// Draw [`Modal`] content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_continue: impl FnMut(ZeroingString) -> bool,
) {
// Callback for button to continue.
let mut on_continue = |m: &mut OpenWalletModal| {
let pass = m.pass_edit.clone();
if pass.is_empty() {
return;
}
m.wrong_pass = !on_continue(ZeroingString::from(pass));
if !m.wrong_pass {
m.pass_edit = "".to_string();
Modal::close();
}
};
ui.vertical_centered(|ui| {
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.add_space(6.0);
ui.label(
RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Show password input.
let mut pass_edit = TextEdit::new(Id::from(modal.id).with("pass_edit")).password();
pass_edit.ui(ui, &mut self.pass_edit, cb);
if pass_edit.enter_pressed {
(on_continue)(self);
}
// Show password input.
let mut pass_edit = TextEdit::new(Id::from(modal.id).with("pass_edit")).password();
pass_edit.ui(ui, &mut self.pass_edit, cb);
if pass_edit.enter_pressed {
(on_continue)(self);
}
// Show information when password is empty.
if self.pass_edit.is_empty() {
self.wrong_pass = false;
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()));
} else if self.wrong_pass {
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show information when password is empty.
if self.pass_edit.is_empty() {
self.wrong_pass = false;
ui.add_space(10.0);
ui.label(
RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()),
);
} else if self.wrong_pass {
ui.add_space(10.0);
ui.label(
RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()),
);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
(on_continue)(self);
});
});
});
ui.add_space(6.0);
});
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
(on_continue)(self);
});
});
});
ui.add_space(6.0);
});
}
}
+152 -125
View File
@@ -15,151 +15,178 @@
use egui::scroll_area::ScrollBarVisibility;
use egui::{RichText, ScrollArea};
use crate::gui::icons::{CHECK, PLUS_CIRCLE, TRASH};
use crate::gui::Colors;
use crate::gui::icons::{PLUS_CIRCLE, TRASH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::gui::views::network::ConnectionsContent;
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::WalletsContent;
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Wallet connection selection [`Modal`] content.
pub struct WalletSettingsModal {
/// Current connection method.
pub conn: ConnectionMethod,
/// Current connection method.
pub conn: ConnectionMethod,
/// External connection creation content.
new_ext_conn_content: Option<ExternalConnectionModal>
/// External connection creation content.
new_ext_conn_content: Option<ExternalConnectionModal>,
}
impl WalletSettingsModal {
/// Create from provided wallet connection.
pub fn new(conn: ConnectionMethod) -> Self {
Self {
conn,
new_ext_conn_content: None,
}
}
/// Create from provided wallet connection.
pub fn new(conn: ConnectionMethod) -> Self {
Self {
conn,
new_ext_conn_content: None,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
on_select: impl Fn(ConnectionMethod)) {
// Draw external connection creation content.
if let Some(ext_content) = self.new_ext_conn_content.as_mut() {
ext_content.ui(ui, cb, modal, |conn| {
on_select(ConnectionMethod::External(conn.id, conn.url));
});
return;
}
/// Draw [`Modal`] content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
on_select: impl Fn(ConnectionMethod),
) {
// Draw external connection creation content.
if let Some(ext_content) = self.new_ext_conn_content.as_mut() {
ext_content.ui(ui, cb, modal, |conn| {
on_select(ConnectionMethod::External(conn.id, conn.url));
});
return;
}
// Check connections state on first draw.
if Modal::first_draw() {
ExternalConnection::check(None, ui.ctx());
}
// Check connections state on first draw.
if Modal::first_draw() {
ExternalConnection::check(None, ui.ctx());
}
ui.add_space(4.0);
ui.add_space(4.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list();
ScrollArea::vertical()
.max_height(if ext_conn_list.len() < 4 {
330.0
} else {
350.0
})
.id_salt("connections_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2])
.show(ui, |ui| {
ui.add_space(2.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list();
ScrollArea::vertical()
.max_height(if ext_conn_list.len() < 4 {
330.0
} else {
350.0
})
.id_salt("connections_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2])
.show(ui, |ui| {
ui.add_space(2.0);
// Show integrated node selection.
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
match self.conn {
ConnectionMethod::Integrated => {
View::selected_item_check(ui);
}
_ => {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select(ConnectionMethod::Integrated);
Modal::close();
});
}
}
});
// Show integrated node selection.
let cur_integrated = self.conn == ConnectionMethod::Integrated;
let bg = if cur_integrated {
Colors::fill()
} else {
Colors::fill_lite()
};
ConnectionsContent::integrated_node_item_ui(
ui,
bg,
(!cur_integrated, || {
on_select(ConnectionMethod::Integrated);
Modal::close();
}),
|ui| {
if cur_integrated {
View::selected_item_check(ui);
}
cur_integrated
},
);
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.ext_conn"))
.size(16.0)
.color(Colors::gray()));
ui.add_space(6.0);
// Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.new_ext_conn_content = Some(ExternalConnectionModal::new(None));
});
});
ui.add_space(4.0);
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.ext_conn"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.new_ext_conn_content = Some(ExternalConnectionModal::new(None));
});
});
ui.add_space(4.0);
if !ext_conn_list.is_empty() {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
let len = ext_conn_list.len();
ConnectionsContent::ext_conn_item_ui(ui, conn, index, len, |ui| {
let current_ext_conn = match self.conn {
ConnectionMethod::Integrated => false,
ConnectionMethod::External(id, _) => id == conn.id
};
if current_ext_conn {
View::selected_item_check(ui);
} else {
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || {
on_select(
ConnectionMethod::External(conn.id, conn.url.clone())
);
Modal::close();
});
}
});
});
}
}
ui.add_space(4.0);
});
if !ext_conn_list.is_empty() {
ui.add_space(8.0);
}
for (i, c) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
let len = ext_conn_list.len();
let is_current = match self.conn {
ConnectionMethod::External(id, _) => id == c.id,
_ => false,
};
let bg = if is_current {
Colors::fill()
} else {
Colors::fill_lite()
};
ConnectionsContent::ext_conn_item_ui(
ui,
bg,
c,
i,
len,
(!is_current, || {
on_select(ConnectionMethod::External(c.id, c.url.clone()));
Modal::close();
}),
|ui| {
if is_current {
View::selected_item_check(ui);
}
},
);
});
}
ui.add_space(4.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Draw button to delete the wallet.
View::colored_text_button(ui,
format!("{} {}", TRASH, t!("wallets.delete")),
Colors::red(),
Colors::white_or_black(false), || {
Modal::new(WalletsContent::DELETE_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
});
});
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Draw button to delete the wallet.
View::colored_text_button(
ui,
format!("{} {}", TRASH, t!("wallets.delete")),
Colors::red(),
Colors::white_or_black(false),
|| {
Modal::new(WalletsContent::DELETE_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
},
);
});
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
Modal::close();
});
});
ui.add_space(6.0);
}
}
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Modal::close();
},
);
});
ui.add_space(6.0);
}
}
+366 -345
View File
@@ -12,34 +12,34 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
use grin_core::core::amount_to_hr_string;
use crate::gui::icons::{CHECK, FOLDER_USER, PACKAGE, PATH, SCAN, SPINNER, USERS_THREE, USER_PLUS};
use crate::gui::Colors;
use crate::gui::icons::{CHECK, FOLDER_USER, PACKAGE, PATH, SCAN, SPINNER, USER_PLUS, USERS_THREE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ModalPosition, QrScanResult};
use crate::gui::views::wallets::wallet::account::create::CreateAccountContent;
use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN};
use crate::gui::views::{CameraContent, CameraScanContent, Content, Modal, View};
use crate::gui::Colors;
use crate::gui::views::wallets::wallet::request::SendRequestContent;
use crate::wallet::{Wallet, WalletConfig};
use crate::gui::views::wallets::wallet::types::{GRIN, WalletContentContainer};
use crate::gui::views::{CameraContent, CameraScanContent, Content, Modal, View};
use crate::wallet::types::{WalletAccount, WalletTask};
use crate::wallet::{Wallet, WalletConfig};
/// Wallet account panel content.
pub struct WalletAccountContent {
/// Flag to show account list content.
pub show_list: bool,
/// Account creation [`Modal`] content.
create_account_content: CreateAccountContent,
/// Flag to show account list content.
pub show_list: bool,
/// Account creation [`Modal`] content.
create_account_content: CreateAccountContent,
/// QR code scan content.
qr_scan_content: Option<CameraContent>,
/// QR code scan result
qr_scan_result: Option<QrScanResult>,
/// Send request creation [`Modal`] content.
send_content: Option<SendRequestContent>,
/// QR code scan content.
qr_scan_content: Option<CameraContent>,
/// QR code scan result
qr_scan_result: Option<QrScanResult>,
/// Send request creation [`Modal`] content.
send_content: Option<SendRequestContent>,
}
/// Account creation [`Modal`] identifier.
@@ -48,366 +48,387 @@ const CREATE_MODAL_ID: &'static str = "create_account_modal";
const SEND_MODAL_ID: &'static str = "account_send_request_modal";
impl WalletContentContainer for WalletAccountContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
CREATE_MODAL_ID,
SEND_MODAL_ID
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![CREATE_MODAL_ID, SEND_MODAL_ID]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
CREATE_MODAL_ID => self.create_account_content.ui(ui, wallet, modal, cb),
SEND_MODAL_ID => {
if let Some(c) = self.send_content.as_mut() {
c.modal_ui(ui, wallet, modal, cb);
}
}
_ => {}
}
}
fn modal_ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
) {
match modal.id {
CREATE_MODAL_ID => self.create_account_content.ui(ui, wallet, modal, cb),
SEND_MODAL_ID => {
if let Some(c) = self.send_content.as_mut() {
c.ui(ui, wallet, modal, cb);
}
}
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if self.qr_scan_showing() {
self.qr_scan_ui(ui, wallet, cb);
} else {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
if self.show_list {
self.list_ui(ui, wallet);
} else {
// Show account content.
self.account_ui(ui, wallet, cb);
}
});
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if self.qr_scan_showing() {
self.qr_scan_ui(ui, wallet, cb);
} else {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
if self.show_list {
self.list_ui(ui, wallet);
} else {
// Show account content.
self.account_ui(ui, wallet, cb);
}
});
}
}
}
impl Default for WalletAccountContent {
fn default() -> Self {
Self {
show_list: false,
create_account_content: CreateAccountContent::default(),
qr_scan_content: None,
qr_scan_result: None,
send_content: None,
}
}
fn default() -> Self {
Self {
show_list: false,
create_account_content: CreateAccountContent::default(),
qr_scan_content: None,
qr_scan_result: None,
send_content: None,
}
}
}
const ACCOUNT_ITEM_HEIGHT: f32 = 75.0;
impl WalletAccountContent {
/// Check if QR code scanner was opened.
pub fn qr_scan_showing(&self) -> bool {
self.qr_scan_content.is_some() || self.qr_scan_result.is_some()
}
/// Check if QR code scanner was opened.
pub fn qr_scan_showing(&self) -> bool {
self.qr_scan_content.is_some() || self.qr_scan_result.is_some()
}
/// Close QR code scanner.
pub fn close_qr_scan(&mut self, cb: &dyn PlatformCallbacks) {
if !self.qr_scan_showing() {
return;
}
cb.stop_camera();
self.qr_scan_content = None;
self.qr_scan_result = None;
}
/// Close QR code scanner.
pub fn close_qr_scan(&mut self, cb: &dyn PlatformCallbacks) {
if !self.qr_scan_showing() {
return;
}
cb.stop_camera();
self.qr_scan_content = None;
self.qr_scan_result = None;
}
/// Check if it's possible to go back at navigation stack.
pub fn can_back(&self) -> bool {
self.qr_scan_showing() || self.show_list
}
/// Check if it's possible to go back at navigation stack.
pub fn can_back(&self) -> bool {
self.qr_scan_showing() || self.show_list
}
/// Navigate back on navigation stack.
pub fn back(&mut self, cb: &dyn PlatformCallbacks) {
if self.qr_scan_showing() {
self.close_qr_scan(cb);
} else if self.show_list {
self.show_list = false;
}
}
/// Navigate back on navigation stack.
pub fn back(&mut self, cb: &dyn PlatformCallbacks) {
if self.qr_scan_showing() {
self.close_qr_scan(cb);
} else if self.show_list {
self.show_list = false;
}
}
/// Draw wallet account content.
fn account_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
// Check wallet data.
if wallet.get_data().is_none() {
return;
}
/// Draw wallet account content.
fn account_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Check wallet data.
let data = wallet.get_data();
if data.is_none() {
return;
}
let data = data.unwrap();
let data = wallet.get_data().unwrap();
let mut rect = ui.available_rect_before_wrap();
rect.set_height(75.0);
let mut rect = ui.available_rect_before_wrap();
rect.set_height(75.0);
// Draw round background.
let rounding = View::item_rounding(0, 2, false);
ui.painter().rect(
rect,
rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside,
);
// Draw round background.
let rounding = View::item_rounding(0, 2, false);
ui.painter().rect(rect,
rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to show QR code scanner.
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
self.qr_scan_content = Some(CameraContent::default());
cb.start_camera();
});
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to show QR code scanner.
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
self.qr_scan_content = Some(CameraContent::default());
cb.start_camera();
});
// Draw button to show list of accounts.
let accounts = wallet.accounts();
let accounts_icon = if accounts.len() > 1 {
USERS_THREE
} else {
USER_PLUS
};
let rounding = View::item_rounding(1, 3, true);
View::item_button(ui, rounding, accounts_icon, None, || {
if accounts.len() == 1 {
self.create_account_content = CreateAccountContent::default();
Modal::new(CREATE_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.accounts"))
.show();
} else {
self.show_list = true;
}
});
// Draw button to show list of accounts.
let accounts = wallet.accounts();
let accounts_icon = if accounts.len() > 1 {
USERS_THREE
} else {
USER_PLUS
};
let rounding = View::item_rounding(1, 3, true);
View::item_button(ui, rounding, accounts_icon, None, || {
if accounts.len() == 1 {
self.create_account_content = CreateAccountContent::default();
Modal::new(CREATE_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.accounts"))
.show();
} else {
self.show_list = true;
}
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(8.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show spendable amount.
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(
RichText::new(amount_text)
.size(18.0)
.color(Colors::white_or_black(true)),
);
});
ui.add_space(-2.0);
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(8.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show spendable amount.
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(amount_text)
.size(18.0)
.color(Colors::white_or_black(true)));
});
ui.add_space(-2.0);
// Show account label.
let account = wallet.get_config().account;
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if account == default_acc_label {
t!("wallets.default_account").into()
} else {
account.to_owned()
};
let acc_text = format!("{} {}", FOLDER_USER, acc_label);
View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false));
// Show account label.
let account = wallet.get_config().account;
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if account == default_acc_label {
t!("wallets.default_account").into()
} else {
account.to_owned()
};
let acc_text = format!("{} {}", FOLDER_USER, acc_label);
View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false));
// Show confirmed height or sync progress.
let status_text = if wallet.message_opening() {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else if !wallet.syncing() {
format!("{} {}", PACKAGE, data.info.last_confirmed_height)
} else {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_loading"))
} else {
if wallet.is_repairing() {
let rep_progress = wallet.repairing_progress();
if rep_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_checking"))
} else {
format!(
"{} {}: {}%",
SPINNER,
t!("wallets.wallet_checking"),
rep_progress
)
}
} else {
format!(
"{} {}: {}%",
SPINNER,
t!("wallets.wallet_loading"),
info_progress
)
}
}
};
let animate = wallet.syncing() || wallet.message_opening();
View::animate_text(ui, status_text, 15.0, Colors::gray(), animate);
})
});
});
}
// Show confirmed height or sync progress.
let status_text = if wallet.message_opening() {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else if !wallet.syncing() {
format!("{} {}", PACKAGE, data.info.last_confirmed_height)
} else {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_loading"))
} else {
if wallet.is_repairing() {
let rep_progress = wallet.repairing_progress();
if rep_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.wallet_checking"),
rep_progress)
}
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.wallet_loading"),
info_progress)
}
}
};
let animate = wallet.syncing() || wallet.message_opening();
View::animate_text(ui, status_text, 15.0, Colors::gray(), animate);
})
});
});
}
/// Draw account list content.
fn list_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
let accounts = wallet.accounts();
let size = accounts.len();
ScrollArea::vertical()
.id_salt("account_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(411.0)
.auto_shrink([true; 2])
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
for index in row_range {
let acc = accounts.get(index).unwrap().clone();
let current = wallet.get_config().account == acc.label;
account_item_ui(ui, &acc, current, index, size, || {
let _ = wallet.set_active_account(&acc.label);
self.show_list = false;
});
if index == size - 1 {
ui.add_space(4.0);
}
}
});
/// Draw account list content.
fn list_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
let accounts = wallet.accounts();
let size = accounts.len();
ScrollArea::vertical()
.id_salt("account_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(411.0)
.auto_shrink([true; 2])
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
for index in row_range {
let acc = accounts.get(index).unwrap().clone();
let current = wallet.get_config().account == acc.label;
account_item_ui(ui, &acc, current, index, size, || {
let _ = wallet.set_active_account(&acc.label);
self.show_list = false;
});
if index == size - 1 {
ui.add_space(4.0);
}
}
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
self.show_list = false;
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.add"), Colors::white_or_black(false), || {
self.show_list = false;
self.create_account_content = CreateAccountContent::default();
Modal::new(CREATE_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.accounts"))
.show();
});
});
});
ui.add_space(6.0);
}
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.show_list = false;
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.add"), Colors::white_or_black(false), || {
self.show_list = false;
self.create_account_content = CreateAccountContent::default();
Modal::new(CREATE_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.accounts"))
.show();
});
});
});
ui.add_space(6.0);
}
/// Draw QR code scanner content.
fn qr_scan_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| {
if self.qr_scan_content.is_some() {
if let Some(result) = self.qr_scan_content.as_ref().unwrap().qr_scan_result() {
cb.stop_camera();
self.qr_scan_content = None;
match result {
QrScanResult::Address(a) => {
if let Some(data) = wallet.get_data() {
if data.info.amount_currently_spendable > 0 {
let address = Some(a.to_string());
self.send_content = Some(SendRequestContent::new(address));
Modal::new(SEND_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
}
}
}
QrScanResult::Slatepack(m) => {
wallet.task(WalletTask::OpenMessage(m));
}
_ => {
self.qr_scan_result = Some(result);
}
}
} else {
// Draw QR code scan content.
self.qr_scan_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.close_qr_scan(cb);
});
});
}
} else if let Some(res) = &self.qr_scan_result.clone() {
CameraScanContent::result_ui(ui, res, cb, || {
self.qr_scan_result = None;
}, || {
self.qr_scan_content = Some(CameraContent::default());
cb.start_camera();
});
}
ui.add_space(6.0);
});
}
/// Draw QR code scanner content.
fn qr_scan_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| {
if self.qr_scan_content.is_some() {
if let Some(result) = self.qr_scan_content.as_ref().unwrap().qr_scan_result() {
cb.stop_camera();
self.qr_scan_content = None;
match result {
QrScanResult::Address(a) => {
if let Some(data) = wallet.get_data() {
if data.info.amount_currently_spendable > 0 {
let address = Some(a.to_string());
self.send_content = Some(SendRequestContent::new(address));
Modal::new(SEND_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
}
}
}
QrScanResult::Slatepack(m) => {
wallet.task(WalletTask::OpenMessage(m));
}
_ => {
self.qr_scan_result = Some(result);
}
}
} else {
// Draw QR code scan content.
self.qr_scan_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.close_qr_scan(cb);
});
});
}
} else if let Some(res) = &self.qr_scan_result.clone() {
CameraScanContent::result_ui(
ui,
res,
cb,
|| {
self.qr_scan_result = None;
},
|| {
self.qr_scan_content = Some(CameraContent::default());
cb.start_camera();
},
);
}
ui.add_space(6.0);
});
}
}
/// Draw account item.
fn account_item_ui(ui: &mut egui::Ui,
acc: &WalletAccount,
current: bool,
index: usize,
size: usize,
mut on_select: impl FnMut()) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(ACCOUNT_ITEM_HEIGHT);
fn account_item_ui(
ui: &mut egui::Ui,
acc: &WalletAccount,
current: bool,
index: usize,
size: usize,
mut on_select: impl FnMut(),
) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(ACCOUNT_ITEM_HEIGHT);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, size, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, size, false);
ui.painter().rect(
bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside,
);
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select account.
if current {
View::selected_item_check(ui);
} else {
let button_rounding = View::item_rounding(index, size, true);
View::item_button(ui, button_rounding, CHECK, None, || {
on_select();
});
}
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select account.
if current {
View::selected_item_check(ui);
} else {
let button_rounding = View::item_rounding(index, size, true);
View::item_button(ui, button_rounding, CHECK, None, || {
on_select();
});
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(8.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show spendable amount.
let amount = amount_to_hr_string(acc.spendable_amount, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(amount_text)
.size(18.0)
.color(Colors::white_or_black(true)));
});
ui.add_space(-2.0);
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(8.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show spendable amount.
let amount = amount_to_hr_string(acc.spendable_amount, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(
RichText::new(amount_text)
.size(18.0)
.color(Colors::white_or_black(true)),
);
});
ui.add_space(-2.0);
// Show account name.
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if acc.label == default_acc_label {
t!("wallets.default_account").into()
} else {
acc.label.to_owned()
};
let acc_name = format!("{} {}", FOLDER_USER, acc_label);
View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false));
// Show account name.
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if acc.label == default_acc_label {
t!("wallets.default_account").into()
} else {
acc.label.to_owned()
};
let acc_name = format!("{} {}", FOLDER_USER, acc_label);
View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false));
// Show account BIP32 derivation path.
let acc_path = format!("{} {}", PATH, acc.path);
ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
});
}
// Show account BIP32 derivation path.
let acc_path = format!("{} {}", PATH, acc.path);
ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
});
}
+77 -70
View File
@@ -14,90 +14,97 @@
use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::Colors;
use crate::wallet::Wallet;
/// Account creation [`Modal`] content.
pub struct CreateAccountContent {
/// Account label value.
account_label_edit: String,
/// Flag to check if error occurred during account creation.
account_creation_error: bool,
/// Account label value.
account_label_edit: String,
/// Flag to check if error occurred during account creation.
account_creation_error: bool,
}
impl Default for CreateAccountContent {
fn default() -> Self {
Self {
account_label_edit: "".to_string(),
account_creation_error: false,
}
}
fn default() -> Self {
Self {
account_label_edit: "".to_string(),
account_creation_error: false,
}
}
}
impl CreateAccountContent {
/// Draw account creation [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
let on_create = |m: &mut CreateAccountContent| {
if m.account_label_edit.is_empty() {
return;
}
let label = &m.account_label_edit;
match wallet.create_account(label) {
Ok(_) => {
let _ = wallet.set_active_account(label);
Modal::close();
},
Err(_) => m.account_creation_error = true
};
};
/// Draw account creation [`Modal`] content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
) {
let on_create = |m: &mut CreateAccountContent| {
if m.account_label_edit.is_empty() {
return;
}
let label = &m.account_label_edit;
match wallet.create_account(label) {
Ok(_) => {
let _ = wallet.set_active_account(label);
Modal::close();
}
Err(_) => m.account_creation_error = true,
};
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.new_account_desc"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.new_account_desc"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw account name edit.
let mut name_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id));
name_edit.ui(ui, &mut self.account_label_edit, cb);
if name_edit.enter_pressed {
on_create(self);
}
// Draw account name edit.
let mut name_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id));
name_edit.ui(ui, &mut self.account_label_edit, cb);
if name_edit.enter_pressed {
on_create(self);
}
// Show error occurred during account creation.
if self.account_creation_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("error"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show error occurred during account creation.
if self.account_creation_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("error")).size(17.0).color(Colors::red()));
}
ui.add_space(12.0);
});
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("create"), Colors::white_or_black(false), || {
on_create(self);
});
});
});
ui.add_space(6.0);
}
}
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("create"), Colors::white_or_black(false), || {
on_create(self);
});
});
});
ui.add_space(6.0);
}
}
+1 -1
View File
@@ -15,4 +15,4 @@
mod content;
pub use content::*;
mod create;
mod create;
File diff suppressed because it is too large Load Diff
+213 -204
View File
@@ -15,235 +15,244 @@
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, RichText, ScrollArea};
use crate::gui::Colors;
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, SCAN, SEAL_CHECK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, View};
use crate::gui::Colors;
use crate::gui::views::wallets::wallet::proof::PaymentProofContent;
use crate::wallet::types::WalletTask;
use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, View};
use crate::wallet::Wallet;
use crate::wallet::types::WalletTask;
pub struct MessageInputContent {
/// Slatepack input text.
message_edit: String,
/// Flag to check if error happened at Slatepack message parsing.
parse_error: bool,
/// QR code scanner content.
scan_qr_content: Option<CameraContent>,
/// Button to parse picked file content.
file_pick_button: FilePickContent,
/// Slatepack input text.
message_edit: String,
/// Flag to check if error happened at Slatepack message parsing.
parse_error: bool,
/// Button to parse picked file content.
file_pick_button: FilePickContent,
/// Payment proof input content.
pub proof_content: Option<PaymentProofContent>,
/// QR code scanner content.
scan_qr_content: Option<CameraContent>,
/// Payment proof input content.
pub proof_content: Option<PaymentProofContent>,
}
/// Hint for Slatepack message input.
const SLATEPACK_MESSAGE_HINT: &'static str = "BEGINSLATEPACK.\n...\n...\n...\nENDSLATEPACK.";
impl Default for MessageInputContent {
fn default() -> Self {
Self {
message_edit: "".to_string(),
parse_error: false,
scan_qr_content: None,
file_pick_button: FilePickContent::new(
FilePickContentType::Button(t!("choose_file").into())
),
proof_content: None,
}
}
fn default() -> Self {
Self {
message_edit: "".to_string(),
parse_error: false,
file_pick_button: FilePickContent::new(FilePickContentType::Button(
t!("choose_file").into(),
)),
scan_qr_content: None,
proof_content: None,
}
}
}
impl MessageInputContent {
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
if let Some(scan_content) = self.scan_qr_content.as_mut() {
if let Some(result) = scan_content.qr_scan_result() {
cb.stop_camera();
modal.enable_closing();
self.scan_qr_content = None;
// Parse scan result.
self.on_message_input(result.text(), wallet);
} else {
scan_content.ui(ui, cb);
}
ui.add_space(8.0);
/// Identifier for [`Modal`].
pub const MODAL_ID: &'static str = "input_message_modal";
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
/// Draw [`Modal`] content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
) {
if let Some(scan_content) = self.scan_qr_content.as_mut() {
if let Some(result) = scan_content.qr_scan_result() {
cb.stop_camera();
modal.enable_closing();
self.scan_qr_content = None;
// Parse scan result.
self.on_message_input(result.text(), wallet);
} else {
scan_content.ui(ui, cb);
}
ui.add_space(8.0);
// Show buttons to close modal or scanner.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scan_qr_content = None;
Modal::close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scan_qr_content = None;
modal.enable_closing();
});
});
});
} else if let Some(proof_content) = self.proof_content.as_mut() {
proof_content.input_ui(ui, wallet, cb);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.message_edit = "".to_string();
Modal::close();
});
});
} else {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let (text, color) = if self.parse_error {
(t!("wallets.parse_slatepack_err"), Colors::red())
} else {
(t!("wallets.input_slatepack_desc"), Colors::gray())
};
ui.label(RichText::new(text).size(16.0).color(color));
});
ui.add_space(6.0);
// Show buttons to close modal or scanner.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scan_qr_content = None;
Modal::close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scan_qr_content = None;
modal.enable_closing();
});
});
});
} else if let Some(proof_content) = self.proof_content.as_mut() {
proof_content.input_ui(ui, wallet, cb);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Draw slatepack message content.
let message_before = self.message_edit.clone();
ui.vertical_centered(|ui| {
let scroll_id = Id::from("message_input").with(wallet.identifier());
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("_input");
let resp = egui::TextEdit::multiline(&mut self.message_edit)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(true)
.hint_text(SLATEPACK_MESSAGE_HINT)
.desired_width(f32::INFINITY)
.show(ui)
.response;
if View::is_desktop() {
resp.request_focus();
}
ui.add_space(6.0);
});
});
// Parse message on input change.
if message_before != self.message_edit {
self.on_message_input(self.message_edit.clone(), wallet);
}
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.message_edit = "".to_string();
self.parse_error = false;
Modal::close();
});
});
} else {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let (text, color) = if self.parse_error {
(t!("wallets.parse_slatepack_err"), Colors::red())
} else {
(t!("wallets.input_slatepack_desc"), Colors::gray())
};
ui.label(RichText::new(text).size(16.0).color(color));
});
ui.add_space(6.0);
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Draw slatepack message content.
let message_before = self.message_edit.clone();
ui.vertical_centered(|ui| {
let scroll_id = Id::from("message_input").with(wallet.identifier());
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("_input");
let resp = egui::TextEdit::multiline(&mut self.message_edit)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(true)
.hint_text(SLATEPACK_MESSAGE_HINT)
.desired_width(f32::INFINITY)
.show(ui)
.response;
if View::is_desktop() {
resp.request_focus();
}
ui.add_space(6.0);
});
});
// Parse message on input change.
if message_before != self.message_edit {
self.on_message_input(self.message_edit.clone(), wallet);
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
if self.parse_error {
// Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::white_or_black(false), || {
self.message_edit = "".to_string();
self.parse_error = false;
});
} else {
// Draw button to scan Slatepack message QR code.
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::white_or_black(false), || {
modal.disable_closing();
self.scan_qr_content = Some(CameraContent::default());
cb.start_camera();
});
}
});
columns[1].vertical_centered_justified(|ui| {
// Draw paste button.
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::white_or_black(false), || {
self.on_message_input(cb.get_string_from_buffer(), wallet);
});
});
});
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Draw button to pick Slatepack message file.
ui.add_space(8.0);
ui.vertical_centered(|ui| {
let mut picked_data = None;
self.file_pick_button.ui(ui, cb, |data| {
picked_data = Some(data);
});
if let Some(data) = picked_data {
self.on_message_input(data, wallet);
}
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
if self.parse_error {
// Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::white_or_black(false), || {
self.message_edit = "".to_string();
self.parse_error = false;
});
} else {
// Draw button to scan Slatepack message QR code.
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::white_or_black(false), || {
modal.disable_closing();
self.scan_qr_content = Some(CameraContent::default());
cb.start_camera();
});
}
});
columns[1].vertical_centered_justified(|ui| {
// Draw paste button.
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::white_or_black(false), || {
self.on_message_input(cb.get_string_from_buffer(), wallet);
});
});
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Draw button to pick Slatepack message file.
ui.add_space(8.0);
ui.vertical_centered(|ui| {
let mut picked_data = None;
self.file_pick_button.ui(ui, cb, |data| {
picked_data = Some(data);
});
if let Some(data) = picked_data {
self.on_message_input(data, wallet);
}
});
ui.vertical_centered(|ui| {
let proof_label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof"));
View::colored_text_button(ui,
proof_label,
Colors::gold_dark(),
Colors::white_or_black(false), || {
Modal::set_title(t!("wallets.payment_proof"));
self.proof_content = Some(PaymentProofContent::new(None));
});
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.message_edit = "".to_string();
Modal::close();
});
});
}
ui.add_space(6.0);
}
ui.vertical_centered(|ui| {
let proof_label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof"));
View::colored_text_button(
ui,
proof_label,
Colors::gold_dark(),
Colors::white_or_black(false),
|| {
Modal::set_title(t!("wallets.payment_proof"));
self.proof_content = Some(PaymentProofContent::new(None));
},
);
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
/// Parse Slatepack message on input change.
fn on_message_input(&mut self, text: String, wallet: &Wallet) {
self.parse_error = false;
self.message_edit = text;
if self.message_edit.is_empty() {
return;
}
match wallet.parse_slatepack(&self.message_edit) {
Ok(_) => {
wallet.task(WalletTask::OpenMessage(self.message_edit.to_string()));
self.message_edit = "".to_string();
Modal::close();
}
Err(_) => {
self.parse_error = true;
}
}
}
}
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.message_edit = "".to_string();
Modal::close();
});
});
}
ui.add_space(6.0);
}
/// Parse Slatepack message on input change.
fn on_message_input(&mut self, text: String, wallet: &Wallet) {
self.parse_error = false;
self.message_edit = text.trim().to_string();
if self.message_edit.is_empty() {
return;
}
if self.message_edit.starts_with("BEGINSLATEPACK.")
&& self.message_edit.ends_with("ENDSLATEPACK.")
{
wallet.task(WalletTask::OpenMessage(self.message_edit.to_string()));
self.message_edit = "".to_string();
Modal::close();
} else {
self.parse_error = true;
}
}
}
+3 -3
View File
@@ -24,7 +24,7 @@ mod content;
pub use content::WalletContent;
mod account;
mod transport;
mod request;
mod message;
mod proof;
mod proof;
mod request;
mod transport;
+209 -200
View File
@@ -3,221 +3,230 @@ use egui::{Id, RichText, ScrollArea};
use grin_util::ToHex;
use grin_wallet_libwallet::{Error, PaymentProof, TxLogEntryType};
use crate::gui::Colors;
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, FILE_TEXT, SEAL_CHECK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{FilePickContent, FilePickContentType, Modal, View};
use crate::gui::Colors;
use crate::wallet::types::{WalletTask, WalletTransaction};
use crate::wallet::Wallet;
use crate::wallet::types::{WalletTask, WalletTx};
pub struct PaymentProofContent {
/// Payment proof text.
input_edit: String,
/// Button to pick payment proof file.
pick_button: FilePickContent,
/// Flag to check if an error occurred during proof parsing.
parse_error: bool,
/// Proof validation result.
pub validation_result: Option<Result<(u32, bool, bool), Error>>,
/// Payment proof text.
input_edit: String,
/// Button to pick payment proof file.
pick_button: FilePickContent,
/// Flag to check if an error occurred during proof parsing.
parse_error: bool,
/// Proof validation result.
pub validation_result: Option<Result<(u32, bool, bool), Error>>,
}
impl PaymentProofContent {
/// Create new content to share or validate payment proof.
pub fn new(proof_text: Option<String>) -> Self {
Self {
input_edit: proof_text.unwrap_or("".to_string()),
pick_button: FilePickContent::new(FilePickContentType::Button(t!("file").into())),
parse_error: false,
validation_result: None,
}
}
/// Create new content to share or validate payment proof.
pub fn new(proof_text: Option<String>) -> Self {
Self {
input_edit: proof_text.unwrap_or("".to_string()),
pick_button: FilePickContent::new(FilePickContentType::Button(t!("file").into())),
parse_error: false,
validation_result: None,
}
}
/// Draw transaction payment proof input.
pub fn input_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
if self.parse_error {
let label_text = t!("wallets.payment_proof_error");
ui.label(RichText::new(label_text).size(16.0).color(Colors::red()));
} else if let Some(proof) = self.validation_result.as_ref() {
match proof {
Ok(_) => {
let label_text = t!("wallets.payment_proof_valid");
ui.label(RichText::new(label_text).size(16.0).color(Colors::green()));
}
Err(e) => {
let error_text = t!("wallets.payment_proof_error");
let label_text = format!("{} ({:?})", error_text, e);
ui.label(RichText::new(label_text).size(16.0).color(Colors::red()));
}
}
} else {
let desc_label = t!("wallets.payment_proof_desc");
ui.label(RichText::new(desc_label).size(16.0).color(Colors::inactive_text()));
}
});
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let scroll_id = Id::from("tx_info_payment_proof_input");
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("edit");
let proof_input_before = self.input_edit.clone();
let resp = egui::TextEdit::multiline(&mut self.input_edit)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(!wallet.payment_proof_verifying())
.desired_width(f32::INFINITY)
.show(ui)
.response;
if View::is_desktop() {
resp.request_focus();
}
// Parse payment proof on input change.
if self.input_edit != proof_input_before {
self.on_proof_edit_change(wallet);
}
ui.add_space(6.0);
});
});
/// Draw transaction payment proof input.
pub fn input_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
if self.parse_error {
let label_text = t!("wallets.payment_proof_error");
ui.label(RichText::new(label_text).size(16.0).color(Colors::red()));
} else if let Some(proof) = self.validation_result.as_ref() {
match proof {
Ok(_) => {
let label_text = t!("wallets.payment_proof_valid");
ui.label(RichText::new(label_text).size(16.0).color(Colors::green()));
}
Err(e) => {
let error_text = t!("wallets.payment_proof_error");
let label_text = format!("{} ({:?})", error_text, e);
ui.label(RichText::new(label_text).size(16.0).color(Colors::red()));
}
}
} else {
let desc_label = t!("wallets.payment_proof_desc");
ui.label(
RichText::new(desc_label)
.size(16.0)
.color(Colors::inactive_text()),
);
}
});
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let scroll_id = Id::from("tx_info_payment_proof_input");
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("edit");
let proof_input_before = self.input_edit.clone();
let resp = egui::TextEdit::multiline(&mut self.input_edit)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(!wallet.payment_proof_verifying())
.desired_width(f32::INFINITY)
.show(ui)
.response;
if View::is_desktop() {
resp.request_focus();
}
// Parse payment proof on input change.
if self.input_edit != proof_input_before {
self.on_proof_edit_change(wallet);
}
ui.add_space(6.0);
});
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
if wallet.payment_proof_verifying() {
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
});
} else {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
if wallet.payment_proof_verifying() {
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
});
} else {
// 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| {
if self.parse_error || (self.validation_result.is_some() &&
self.validation_result.as_ref().unwrap().is_err()) {
// Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::white_or_black(false), || {
self.input_edit = "".to_string();
self.parse_error = false;
self.validation_result = None;
});
} else {
// Draw button to paste proof text.
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::white_or_black(false), || {
self.input_edit = cb.get_string_from_buffer();
self.on_proof_edit_change(wallet);
});
}
});
columns[1].vertical_centered_justified(|ui| {
let mut changed = false;
self.pick_button.ui(ui, cb, |data| {
self.input_edit = data.clone();
changed = true;
});
if changed {
self.on_proof_edit_change(wallet);
}
});
});
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
if self.parse_error
|| (self.validation_result.is_some()
&& self.validation_result.as_ref().unwrap().is_err())
{
// Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::white_or_black(false), || {
self.input_edit = "".to_string();
self.parse_error = false;
self.validation_result = None;
});
} else {
// Draw button to paste proof text.
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::white_or_black(false), || {
self.input_edit = cb.get_string_from_buffer();
self.on_proof_edit_change(wallet);
});
}
});
columns[1].vertical_centered_justified(|ui| {
let mut changed = false;
self.pick_button.ui(ui, cb, |data| {
self.input_edit = data.clone();
changed = true;
});
if changed {
self.on_proof_edit_change(wallet);
}
});
});
}
}
/// Callback on payment proof input change.
fn on_proof_edit_change(&mut self, wallet: &Wallet) {
if wallet.payment_proof_verifying() {
return;
}
if self.input_edit.is_empty() {
self.parse_error = false;
return;
}
if let Ok(p) = serde_json::from_str::<PaymentProof>(self.input_edit.as_str()) {
wallet.task(WalletTask::VerifyProof(p, None));
} else {
self.parse_error = true;
}
}
/// Callback on payment proof input change.
fn on_proof_edit_change(&mut self, wallet: &Wallet) {
if wallet.payment_proof_verifying() {
return;
}
if self.input_edit.is_empty() {
self.parse_error = false;
return;
}
if let Ok(p) = serde_json::from_str::<PaymentProof>(self.input_edit.as_str()) {
wallet.task(WalletTask::VerifyProof(p, None));
} else {
self.parse_error = true;
}
}
/// Draw transaction payment proof content to share.
pub fn share_ui(&mut self,
ui: &mut egui::Ui,
tx: &WalletTransaction,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let (desc_text, color) = if tx.data.tx_type == TxLogEntryType::TxReceived {
(t!("wallets.payment_proof_valid").into(), Colors::green())
} else {
(format!("{}:", t!("wallets.payment_proof")), Colors::inactive_text())
};
let desc = format!("{} {}", SEAL_CHECK, desc_text);
ui.label(RichText::new(desc).size(16.0).color(color));
});
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let scroll_id = Id::from("tx_info_payment_proof_share");
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("edit");
egui::TextEdit::multiline(&mut self.input_edit)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
});
/// Draw transaction payment proof content to share.
pub fn share_ui(&mut self, ui: &mut egui::Ui, tx: &WalletTx, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let (desc_text, color) = if tx.data.tx_type == TxLogEntryType::TxReceived {
(t!("wallets.payment_proof_valid").into(), Colors::green())
} else {
(
format!("{}:", t!("wallets.payment_proof")),
Colors::inactive_text(),
)
};
let desc = format!("{} {}", SEAL_CHECK, desc_text);
ui.label(RichText::new(desc).size(16.0).color(color));
});
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let scroll_id = Id::from("tx_info_payment_proof_share");
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("edit");
egui::TextEdit::multiline(&mut self.input_edit)
.id(input_id)
.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(8.0);
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// 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| {
// Draw copy button.
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.input_edit.clone());
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
let share_text = format!("{} {}", FILE_TEXT, t!("share"));
View::colored_text_button(ui,
share_text,
Colors::blue(),
Colors::white_or_black(false), || {
let file_name = format!("{}.txt", tx.data.kernel_excess.unwrap().to_hex());
let data = self.input_edit.as_bytes().to_vec();
cb.share_data(file_name, data).unwrap_or_default();
Modal::close();
});
});
});
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw copy button.
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.input_edit.clone());
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
let share_text = format!("{} {}", FILE_TEXT, t!("share"));
View::colored_text_button(
ui,
share_text,
Colors::blue(),
Colors::white_or_black(false),
|| {
let file_name = format!("{}.txt", tx.data.kernel_excess.unwrap().to_hex());
let data = self.input_edit.as_bytes().to_vec();
cb.share_data(file_name, data).unwrap_or_default();
Modal::close();
},
);
});
});
}
}
+183 -96
View File
@@ -12,117 +12,204 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, TextEdit, View};
use crate::wallet::Wallet;
use crate::wallet::types::WalletTask;
use egui::{Id, RichText};
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::Colors;
use crate::wallet::types::WalletTask;
use crate::wallet::Wallet;
use grin_wallet_libwallet::SlatepackAddress;
/// Invoice request creation content.
pub struct InvoiceRequestContent {
/// Amount to receive.
amount_edit: String,
/// Amount to receive.
amount_edit: String,
/// Sender address.
address_edit: String,
/// Flag to check if entered address is incorrect.
address_error: bool,
/// Address QR code scanner content.
address_scan_content: Option<CameraContent>,
}
impl Default for InvoiceRequestContent {
fn default() -> Self {
Self {
amount_edit: "".to_string(),
}
}
fn default() -> Self {
Self {
amount_edit: "".to_string(),
address_edit: "".to_string(),
address_error: false,
address_scan_content: None,
}
}
}
impl InvoiceRequestContent {
/// Draw [`Modal`] content.
pub fn modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Setup callback on continue.
let on_continue = |m: &mut InvoiceRequestContent| {
if m.amount_edit.is_empty() {
return;
}
if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) {
m.amount_edit = "".to_string();
wallet.task(WalletTask::Receive(a));
Modal::close();
}
};
/// Draw [`Modal`] content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
) {
// Setup callback on continue.
let on_continue = |m: &mut InvoiceRequestContent| {
if m.amount_edit.is_empty() {
return;
}
if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) {
let addr_str = m.address_edit.as_str();
let addr = if let Ok(r) = SlatepackAddress::try_from(addr_str.trim()) {
Some(r)
} else {
None
};
wallet.task(WalletTask::Receive(a, addr));
Modal::close();
}
};
ui.add_space(6.0);
ui.add_space(6.0);
// Draw amount input content.
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.enter_amount_receive"))
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw QR code scanner content if requested.
if let Some(content) = self.address_scan_content.as_mut() {
let mut close_scan = true;
content.modal_ui(ui, cb, |result| {
if let Some(result) = result {
self.address_edit = result.text();
} else {
modal.enable_closing();
close_scan = true;
}
});
if close_scan {
self.address_scan_content = None;
}
return;
}
// Draw request amount text input.
let amount_edit_before = self.amount_edit.clone();
let mut amount_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id))
.h_center()
.numeric();
amount_edit.ui(ui, &mut self.amount_edit, cb);
if amount_edit.enter_pressed {
on_continue(self);
}
// Draw amount input content.
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.enter_amount_receive"))
.size(17.0)
.color(Colors::gray()),
);
});
ui.add_space(8.0);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
if !self.amount_edit.is_empty() {
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(amount) => {
if !self.amount_edit.contains(".") {
// To avoid input of several `0` before `.` and put `.` after first `0`.
if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") {
let amount_text = amount_to_hr_string(amount, true);
let amount_parts = amount_text.split(".").collect::<Vec<&str>>();
self.amount_edit = format!("0.{}", amount_parts[0]);
amount_edit.cursor_to_end(self.amount_edit.len(), ui);
}
} else {
// Check input after `.`.
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
if parts.len() == 2 && (parts[1].len() > 9 ||
(amount == 0 && parts[1].len() > 8)) {
self.amount_edit = amount_edit_before;
}
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
// Draw request amount text input.
let amount_edit_before = self.amount_edit.clone();
let mut amount_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id))
.h_center()
.numeric()
.focus(Modal::first_draw());
amount_edit.ui(ui, &mut self.amount_edit, cb);
ui.add_space(12.0);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
if !self.amount_edit.is_empty() {
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(amount) => {
if !self.amount_edit.contains(".") {
// To avoid input of several `0` before `.` and put `.` after first `0`.
if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") {
let amount_text = amount_to_hr_string(amount, true);
let amount_parts = amount_text.split(".").collect::<Vec<&str>>();
self.amount_edit = format!("0.{}", amount_parts[0]);
amount_edit.cursor_to_end(self.amount_edit.len(), ui);
}
} else {
// Check input after `.`.
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
if parts.len() == 2
&& (parts[1].len() > 9 || (amount == 0 && parts[1].len() > 8))
{
self.amount_edit = amount_edit_before;
}
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.add_space(8.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.amount_edit = "".to_string();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Button to create Slatepack message request.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
on_continue(self);
});
});
});
ui.add_space(6.0);
}
}
// Show address error or input description.
ui.vertical_centered(|ui| {
if self.address_error {
ui.label(
RichText::new(t!("transport.incorrect_addr_err"))
.size(17.0)
.color(Colors::red()),
);
} else {
ui.label(
RichText::new(t!("transport.sender_address"))
.size(17.0)
.color(Colors::gray()),
);
}
});
ui.add_space(6.0);
// Show address text edit.
let addr_edit_before = self.address_edit.clone();
let address_edit_id = Id::from(modal.id)
.with("_address")
.with(wallet.get_config().id);
let mut address_edit = TextEdit::new(address_edit_id)
.paste()
.focus(false)
.scan_qr();
if amount_edit.enter_pressed {
address_edit.focus_request();
}
address_edit.ui(ui, &mut self.address_edit, cb);
// Check if scan button was pressed.
if address_edit.scan_pressed {
modal.disable_closing();
self.address_scan_content = Some(CameraContent::default());
}
ui.add_space(12.0);
// Check value if input was changed.
if addr_edit_before != self.address_edit {
self.address_error = false;
}
// Continue on Enter press.
if address_edit.enter_pressed {
on_continue(self);
}
// 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!("modal.cancel"),
Colors::white_or_black(false),
|| {
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
// Button to create Slatepack message request.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
on_continue(self);
});
});
});
ui.add_space(6.0);
}
}
+1 -1
View File
@@ -16,4 +16,4 @@ mod invoice;
pub use invoice::*;
mod send;
pub use send::*;
pub use send::*;
+279 -277
View File
@@ -17,309 +17,311 @@ use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_core::global::get_accept_fee_base;
use grin_wallet_libwallet::SlatepackAddress;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, TextEdit, View};
use crate::gui::Colors;
use crate::wallet::types::WalletTask;
use crate::wallet::Wallet;
use crate::wallet::types::WalletTask;
/// Content to create a request to send funds.
pub struct SendRequestContent {
/// Amount to send.
amount_edit: String,
/// Flag to check if maximum amount is calculating.
pub max_calculating: bool,
/// Amount to send.
amount_edit: String,
/// Flag to check if maximum amount is calculating.
pub max_calculating: bool,
/// Fee amount.
fee_edit: String,
/// Fee amount.
fee_edit: String,
/// Receiver address.
address_edit: String,
/// Flag to check if entered address is incorrect.
address_error: bool,
/// Receiver address.
address_edit: String,
/// Flag to check if entered address is incorrect.
address_error: bool,
/// Address QR code scanner content.
address_scan_content: Option<CameraContent>,
/// Address QR code scanner content.
address_scan_content: Option<CameraContent>,
}
impl SendRequestContent {
/// Create new content instance with optional receiver address.
pub fn new(addr: Option<String>) -> Self {
Self {
amount_edit: "".to_string(),
max_calculating: false,
fee_edit: "".to_string(),
address_edit: addr.unwrap_or("".to_string()),
address_error: false,
address_scan_content: None,
}
}
/// Create new content instance with optional receiver address.
pub fn new(addr: Option<String>) -> Self {
Self {
amount_edit: "".to_string(),
max_calculating: false,
fee_edit: "".to_string(),
address_edit: addr.unwrap_or("".to_string()),
address_error: false,
address_scan_content: None,
}
}
/// Setup fee amount.
pub fn on_fee_calculated(&mut self, fee: u64) {
self.fee_edit = amount_to_hr_string(fee, true);
}
/// Setup fee amount.
pub fn on_fee_calculated(&mut self, fee: u64) {
self.fee_edit = amount_to_hr_string(fee, true);
}
/// Setup maximum amount to send and fee.
pub fn on_max_amount_calculated(&mut self, amount: u64, fee: u64) {
self.max_calculating = false;
if amount == 0 {
self.amount_edit = "".to_string();
self.fee_edit = "".to_string();
} else {
self.amount_edit = amount_to_hr_string(amount, true);
self.fee_edit = amount_to_hr_string(fee, true);
}
}
/// Setup maximum amount to send and fee.
pub fn on_max_amount_calculated(&mut self, amount: u64, fee: u64) {
self.max_calculating = false;
if amount == 0 {
self.amount_edit = "".to_string();
self.fee_edit = "".to_string();
} else {
self.amount_edit = amount_to_hr_string(amount, true);
self.fee_edit = amount_to_hr_string(fee, true);
}
}
/// Draw [`Modal`] content.
pub fn modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
/// Draw [`Modal`] content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
) {
let data = wallet.get_data();
if data.is_none() {
Modal::close();
return;
}
let data = data.unwrap();
// Draw QR code scanner content if requested.
if let Some(scanner) = self.address_scan_content.as_mut() {
let on_stop = || {
cb.stop_camera();
modal.enable_closing();
};
ui.add_space(6.0);
if let Some(result) = scanner.qr_scan_result() {
self.address_edit = result.text();
on_stop();
self.address_scan_content = None;
} else {
ui.add_space(6.0);
scanner.ui(ui, cb);
ui.add_space(6.0);
// Draw QR code scanner content if requested.
if let Some(content) = self.address_scan_content.as_mut() {
let mut close_scan = true;
content.modal_ui(ui, cb, |result| {
if let Some(result) = result {
self.address_edit = result.text();
} else {
modal.enable_closing();
close_scan = true;
}
});
if close_scan {
self.address_scan_content = None;
}
return;
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.vertical_centered(|ui| {
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let enter_text = t!("wallets.enter_amount_send","amount" => amount);
ui.label(RichText::new(enter_text).size(17.0).color(Colors::gray()));
});
ui.add_space(8.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop();
self.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
on_stop();
self.address_scan_content = None;
});
});
});
ui.add_space(6.0);
}
return;
}
// Draw amount text edit.
let amount_edit_id = Id::from(modal.id)
.with("amount")
.with(wallet.get_config().id);
let mut amount_edit = TextEdit::new(amount_edit_id)
.h_center()
.numeric()
.focus(Modal::first_draw());
if self.max_calculating {
amount_edit = amount_edit.disable();
}
let amount_edit_before = self.amount_edit.clone();
ui.vertical_centered(|ui| {
let data = wallet.get_data().unwrap();
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let enter_text = t!("wallets.enter_amount_send","amount" => amount);
ui.label(RichText::new(enter_text)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw button to calculate maximum amount to send.
let mut calculate_max = false;
amount_edit.custom_buttons_ui(ui, &mut self.amount_edit, cb, |ui| {
if self.max_calculating {
ui.add_space(12.0);
View::loading_spinner(ui, 40.0);
ui.add_space(12.0);
} else {
View::button(ui, t!("max_short"), Colors::white_or_black(false), || {
calculate_max = true;
});
ui.add_space(8.0);
}
});
if calculate_max {
self.max_calculating = true;
let max = data.info.amount_currently_spendable;
self.amount_edit = amount_to_hr_string(max, true);
}
ui.add_space(8.0);
// Draw amount text edit.
let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id);
let mut amount_edit = TextEdit::new(amount_edit_id)
.h_center()
.numeric()
.focus(Modal::first_draw());
if self.max_calculating {
amount_edit = amount_edit.disable();
}
let amount_edit_before = self.amount_edit.clone();
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
if !self.amount_edit.is_empty() {
// Trim text, replace `,` by `.` and parse amount.
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(mut amount) => {
if !self.amount_edit.contains(".") {
// To avoid input of several `0` before `.` and put `.` after first `0`.
if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") {
let amount_text = amount_to_hr_string(amount, true);
let amount_parts = amount_text.split(".").collect::<Vec<&str>>();
self.amount_edit = format!("0.{}", amount_parts[0]);
amount = amount_from_hr_string(self.amount_edit.as_str())
.unwrap_or_else(|_| amount);
amount_edit.cursor_to_end(self.amount_edit.len(), ui);
}
// Reset fee amount on `0`.
if amount == 0 {
self.fee_edit = "".to_string();
}
} else {
// Check input after `.`.
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
if parts.len() == 2
&& (parts[1].len() > 9 || (amount == 0 && parts[1].len() > 8))
{
self.amount_edit = amount_edit_before.clone();
}
}
// Do not input amount more than balance.
if amount != 0 && self.amount_edit != amount_edit_before {
let fee = amount_from_hr_string(self.fee_edit.as_str()).unwrap_or(0);
let max = data.info.amount_currently_spendable;
if amount > max || amount + fee > max {
self.max_calculating = true;
wallet.task(WalletTask::CalculateFee(max, 0));
} else {
wallet.task(WalletTask::CalculateFee(amount, 0));
}
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
} else {
self.fee_edit = "".to_string();
}
}
// Draw button to calculate maximum amount to send.
let mut calculate_max = false;
amount_edit.custom_buttons_ui(ui, &mut self.amount_edit, cb, |ui| {
if self.max_calculating {
ui.add_space(12.0);
View::loading_spinner(ui, 40.0);
ui.add_space(12.0);
} else {
View::button(ui, t!("max_short"), Colors::white_or_black(false), || {
calculate_max = true;
});
ui.add_space(8.0);
}
});
if calculate_max {
self.max_calculating = true;
let max = wallet.get_data().unwrap().info.amount_currently_spendable;
self.amount_edit = amount_to_hr_string(max, true);
}
ui.add_space(8.0);
// Show fee value.
ui.vertical_centered(|ui| {
let fee_label = t!(
"wallets.fee_base_desc",
"value" => format!(": {}", get_accept_fee_base())
);
ui.label(RichText::new(fee_label).size(17.0).color(Colors::gray()));
});
ui.add_space(6.0);
let fee_edit_id = Id::from(modal.id).with("_fee").with(wallet.get_config().id);
let mut fee_edit = TextEdit::new(fee_edit_id).focus(false).h_center().disable();
let mut loading_label = format!("{}...", t!("wallets.loading"));
fee_edit.ui(
ui,
if wallet.fee_calculating() {
&mut loading_label
} else {
&mut self.fee_edit
},
cb,
);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
if !self.amount_edit.is_empty() {
// Trim text, replace `,` by `.` and parse amount.
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(mut amount) => {
if !self.amount_edit.contains(".") {
// To avoid input of several `0` before `.` and put `.` after first `0`.
if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") {
let amount_text = amount_to_hr_string(amount, true);
let amount_parts = amount_text.split(".").collect::<Vec<&str>>();
self.amount_edit = format!("0.{}", amount_parts[0]);
amount = amount_from_hr_string(self.amount_edit.as_str())
.unwrap_or_else(|_| amount);
amount_edit.cursor_to_end(self.amount_edit.len(), ui);
}
// Reset fee amount on `0`.
if amount == 0 {
self.fee_edit = "".to_string();
}
} else {
// Check input after `.`.
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
if parts.len() == 2 && (parts[1].len() > 9 ||
(amount == 0 && parts[1].len() > 8)) {
self.amount_edit = amount_edit_before.clone();
}
}
// Do not input amount more than balance.
if amount != 0 && self.amount_edit != amount_edit_before {
let fee = amount_from_hr_string(self.fee_edit.as_str()).unwrap_or(0);
let max = wallet.get_data().unwrap().info.amount_currently_spendable;
if amount > max || amount + fee > max {
self.max_calculating = true;
wallet.task(WalletTask::CalculateFee(max, 0));
} else {
wallet.task(WalletTask::CalculateFee(amount, 0));
}
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
} else {
self.fee_edit = "".to_string();
}
}
ui.add_space(8.0);
// Show fee value.
ui.vertical_centered(|ui| {
let fee_label = t!(
"wallets.fee_base_desc",
"value" => format!(": {}", get_accept_fee_base())
);
ui.label(RichText::new(fee_label)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(6.0);
let fee_edit_id = Id::from(modal.id).with("_fee").with(wallet.get_config().id);
let mut fee_edit = TextEdit::new(fee_edit_id)
.focus(false)
.h_center()
.disable();
let mut loading_label = format!("{}...", t!("wallets.loading"));
fee_edit.ui(ui, if wallet.fee_calculating() {
&mut loading_label
} else {
&mut self.fee_edit
}, cb);
// Show address error or input description.
ui.vertical_centered(|ui| {
if self.address_error {
ui.label(
RichText::new(t!("transport.incorrect_addr_err"))
.size(17.0)
.color(Colors::red()),
);
} else {
ui.label(
RichText::new(t!("transport.receiver_address"))
.size(17.0)
.color(Colors::gray()),
);
}
});
ui.add_space(6.0);
ui.add_space(8.0);
// Show address text edit.
let addr_edit_before = self.address_edit.clone();
let address_edit_id = Id::from(modal.id)
.with("_address")
.with(wallet.get_config().id);
let mut address_edit = TextEdit::new(address_edit_id)
.paste()
.focus(false)
.scan_qr();
if amount_edit.enter_pressed {
address_edit.focus_request();
}
address_edit.ui(ui, &mut self.address_edit, cb);
// Check if scan button was pressed.
if address_edit.scan_pressed {
modal.disable_closing();
self.address_scan_content = Some(CameraContent::default());
}
ui.add_space(12.0);
// Check value if input was changed.
if addr_edit_before != self.address_edit {
self.address_error = false;
}
// Continue on Enter press.
if address_edit.enter_pressed {
self.on_continue(wallet);
}
// Show address error or input description.
ui.vertical_centered(|ui| {
if self.address_error {
ui.label(RichText::new(t!("transport.incorrect_addr_err"))
.size(17.0)
.color(Colors::red()));
} else {
ui.label(RichText::new(t!("transport.receiver_address"))
.size(17.0)
.color(Colors::gray()));
}
});
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show address text edit.
let addr_edit_before = self.address_edit.clone();
let address_edit_id = Id::from(modal.id).with("_address").with(wallet.get_config().id);
let mut address_edit = TextEdit::new(address_edit_id)
.paste()
.focus(false)
.scan_qr();
if amount_edit.enter_pressed {
address_edit.focus_request();
}
address_edit.ui(ui, &mut self.address_edit, cb);
// Check if scan button was pressed.
if address_edit.scan_pressed {
modal.disable_closing();
self.address_scan_content = Some(CameraContent::default());
}
ui.add_space(12.0);
// Check value if input was changed.
if addr_edit_before != self.address_edit {
self.address_error = false;
}
// Continue on Enter press.
if address_edit.enter_pressed {
self.on_continue(wallet);
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
self.close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
// Button to create Slatepack message request.
if self.max_calculating || wallet.fee_calculating() {
ui.add_space(4.0);
View::small_loading_spinner(ui);
} else {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
self.on_continue(wallet);
});
}
});
});
ui.add_space(6.0);
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
/// Callback when Continue button was pressed.
fn on_continue(&mut self, wallet: &Wallet) {
if self.amount_edit.is_empty() {
return;
}
// Check address to send over Tor if enabled.
let addr_str = self.address_edit.as_str();
if let Ok(r) = SlatepackAddress::try_from(addr_str.trim()) {
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
wallet.task(WalletTask::Send(a, Some(r)));
Modal::close();
}
} else if !addr_str.is_empty() {
self.address_error = true;
} else if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
wallet.task(WalletTask::Send(a, None));
Modal::close();
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Button to create Slatepack message request.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
self.on_continue(wallet);
});
});
});
ui.add_space(6.0);
}
/// Callback when Continue button was pressed.
fn on_continue(&mut self, wallet: &Wallet) {
if self.amount_edit.is_empty() || self.max_calculating || wallet.fee_calculating() {
return;
}
// Check address to send over Tor if enabled.
let addr_str = self.address_edit.as_str();
if let Ok(r) = SlatepackAddress::try_from(addr_str.trim()) {
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
wallet.task(WalletTask::Send(a, Some(r)));
Modal::close();
}
} else if !addr_str.is_empty() {
self.address_error = true;
} else if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
wallet.task(WalletTask::Send(a, None));
Modal::close();
}
}
/// Close modal and clear data.
fn close(&mut self) {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
self.address_scan_content = None;
Modal::close();
}
}
/// Close modal and clear data.
fn close(&mut self) {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
self.address_scan_content = None;
Modal::close();
}
}
+505 -412
View File
@@ -13,36 +13,36 @@
// limitations under the License.
use eframe::emath::Align;
use eframe::epaint::StrokeKind;
use egui::{Id, Layout, RichText};
use eframe::epaint::{RectShape, StrokeKind};
use egui::{CursorIcon, Id, Layout, RichText, Sense, UiBuilder};
use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDERS, FOLDER_USER, PASSWORD, PENCIL};
use crate::gui::Colors;
use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDER_USER, FOLDERS, PASSWORD};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
use crate::gui::views::{FilePickContent, FilePickContentType, Modal, TextEdit, View};
use crate::gui::Colors;
use crate::wallet::Wallet;
/// Common wallet settings content.
pub struct CommonSettings {
/// Wallet name [`Modal`] value.
name_edit: String,
/// Wallet name [`Modal`] value.
name_edit: String,
/// Flag to check if wrong password was entered.
wrong_pass: bool,
/// Current wallet password [`Modal`] value.
old_pass_edit: String,
/// New wallet password [`Modal`] value.
new_pass_edit: String,
/// Flag to check if wrong password was entered.
wrong_pass: bool,
/// Current wallet password [`Modal`] value.
old_pass_edit: String,
/// New wallet password [`Modal`] value.
new_pass_edit: String,
/// Data path value value for [`Modal`].
data_path_edit: String,
/// Button to pick directory for wallet data.
pick_data_dir: FilePickContent,
/// Data path value value for [`Modal`].
data_path_edit: String,
/// Button to pick directory for wallet data.
pick_data_dir: FilePickContent,
/// Minimum confirmations number value.
min_confirmations_edit: String,
/// Minimum confirmations number value.
min_confirmations_edit: String,
}
/// Identifier for wallet name [`Modal`].
@@ -55,434 +55,527 @@ const DATA_PATH_MODAL: &'static str = "wallet_data_path";
const MIN_CONFIRMATIONS_EDIT_MODAL: &'static str = "wallet_min_conf_edit_modal";
impl WalletContentContainer for CommonSettings {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
NAME_EDIT_MODAL,
PASS_EDIT_MODAL,
DATA_PATH_MODAL,
MIN_CONFIRMATIONS_EDIT_MODAL
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![
NAME_EDIT_MODAL,
PASS_EDIT_MODAL,
DATA_PATH_MODAL,
MIN_CONFIRMATIONS_EDIT_MODAL,
]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
NAME_EDIT_MODAL => self.name_modal_ui(ui, wallet, modal, cb),
PASS_EDIT_MODAL => self.pass_modal_ui(ui, wallet, modal, cb),
DATA_PATH_MODAL => self.data_path_modal_ui(ui, wallet, cb),
MIN_CONFIRMATIONS_EDIT_MODAL => self.min_conf_modal_ui(ui, wallet, modal, cb),
_ => {}
}
}
fn modal_ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
) {
match modal.id {
NAME_EDIT_MODAL => self.name_modal_ui(ui, wallet, modal, cb),
PASS_EDIT_MODAL => self.pass_modal_ui(ui, wallet, modal, cb),
DATA_PATH_MODAL => self.data_path_modal_ui(ui, wallet, cb),
MIN_CONFIRMATIONS_EDIT_MODAL => self.min_conf_modal_ui(ui, wallet, modal, cb),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
let config = wallet.get_config();
// Show wallet name.
self.name_ui(ui, config.name);
// Show data dir for desktop.
if View::is_desktop() {
ui.add_space(-4.0);
self.data_dir_ui(ui, wallet, cb);
}
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.min_tx_conf_count")).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if View::is_desktop() {
ui.add_space(1.0);
} else {
ui.add_space(8.0);
}
ui.vertical_centered(|ui| {
let config = wallet.get_config();
// Show wallet name.
self.name_ui(ui, config.name);
// Show data dir for desktop.
if View::is_desktop() {
ui.add_space(-4.0);
self.data_dir_ui(ui, wallet, cb);
}
ui.add_space(6.0);
ui.label(
RichText::new(t!("wallets.min_tx_conf_count"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Show minimum amount of confirmations value setup.
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations);
View::button(ui, min_conf_text, Colors::white_or_black(false), || {
self.min_confirmations_edit = config.min_confirmations.to_string();
// Show minimum amount of confirmations value modal.
Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
// Show minimum amount of confirmations value setup.
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations);
View::button(ui, min_conf_text, Colors::white_or_black(false), || {
self.min_confirmations_edit = config.min_confirmations.to_string();
// Show minimum amount of confirmations value modal.
Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(8.0);
ui.add_space(8.0);
// Setup ability to post wallet transactions with Dandelion.
View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || {
wallet.update_use_dandelion(!wallet.can_use_dandelion());
});
// Ability to post wallet transactions with Dandelion.
View::checkbox(
ui,
wallet.can_use_dandelion(),
t!("wallets.use_dandelion"),
|| {
wallet.update_use_dandelion(!wallet.can_use_dandelion());
},
);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
});
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
});
}
}
impl Default for CommonSettings {
fn default() -> Self {
Self {
name_edit: "".to_string(),
wrong_pass: false,
old_pass_edit: "".to_string(),
new_pass_edit: "".to_string(),
data_path_edit: "".to_string(),
pick_data_dir: FilePickContent::new(
FilePickContentType::ItemButton(View::item_rounding(1, 2, true))
).no_parse().pick_folder(),
min_confirmations_edit: "".to_string(),
}
}
fn default() -> Self {
Self {
name_edit: "".to_string(),
wrong_pass: false,
old_pass_edit: "".to_string(),
new_pass_edit: "".to_string(),
data_path_edit: "".to_string(),
pick_data_dir: FilePickContent::new(FilePickContentType::ItemButton(
View::item_rounding(1, 2, true),
))
.no_parse()
.pick_folder(),
min_confirmations_edit: "".to_string(),
}
}
}
impl CommonSettings {
/// Draw content to change wallet name and password.
fn name_ui(&mut self, ui: &mut egui::Ui, name: String) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
/// Draw content to change wallet name and password.
fn name_ui(&mut self, ui: &mut egui::Ui, name: String) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let r = if View::is_desktop() {
View::item_rounding(0, 2, false)
} else {
View::item_rounding(0, 1, false)
};
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = if View::is_desktop() {
View::item_rounding(0, 2, false)
} else {
View::item_rounding(0, 1, false)
};
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
let r = if View::is_desktop() {
View::item_rounding(0, 2, true)
} else {
View::item_rounding(0, 1, true)
};
View::item_button(ui, r, PASSWORD, None, || {
self.old_pass_edit = "".to_string();
self.new_pass_edit = "".to_string();
self.wrong_pass = false;
// Show wallet password modal.
Modal::new(PASS_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(
layout_size,
Layout::left_to_right(Align::Center),
|ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
View::ellipsize_text(ui, name.clone(), 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!(
"{} {}",
FOLDER_USER,
t!("wallets.name").replace(":", "")
);
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
});
},
);
},
)
.response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
self.name_edit = name;
// Show wallet name modal.
Modal::new(NAME_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
}
}
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
let r = if View::is_desktop() {
View::item_rounding(0, 2, true)
} else {
View::item_rounding(0, 1, true)
};
View::item_button(ui, r, PASSWORD, None, || {
self.old_pass_edit = "".to_string();
self.new_pass_edit = "".to_string();
self.wrong_pass = false;
// Show wallet password modal.
Modal::new(PASS_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
self.name_edit = name.clone();
// Show wallet name modal.
Modal::new(NAME_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
View::ellipsize_text(ui, name, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDER_USER, t!("wallets.name").replace(":", ""));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
});
});
});
}
/// Draw wallet name [`Modal`] content.
fn name_modal_ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
) {
let on_save = |c: &mut CommonSettings| {
if !c.name_edit.is_empty() {
wallet.change_name(c.name_edit.clone());
Modal::close();
}
};
/// Draw wallet name [`Modal`] content.
fn name_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut CommonSettings| {
if !c.name_edit.is_empty() {
wallet.change_name(c.name_edit.clone());
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.name"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Show wallet name text edit.
let mut name_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id));
name_edit.ui(ui, &mut self.name_edit, cb);
if name_edit.enter_pressed {
on_save(self);
}
ui.add_space(12.0);
});
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.name"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show wallet name text edit.
let mut name_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id));
name_edit.ui(ui, &mut self.name_edit, cb);
if name_edit.enter_pressed {
on_save(self);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
/// Draw wallet pass [`Modal`] content.
fn pass_modal_ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
) {
let wallet_id = wallet.get_config().id;
let on_continue = |c: &mut CommonSettings| {
if c.new_pass_edit.is_empty() {
return;
}
let old_pass = c.old_pass_edit.clone();
let new_pass = c.new_pass_edit.clone();
match wallet.change_password(old_pass, new_pass) {
Ok(_) => {
// Clear password values.
c.old_pass_edit = "".to_string();
c.new_pass_edit = "".to_string();
// Close modal.
Modal::close();
}
Err(_) => c.wrong_pass = true,
}
};
/// Draw wallet pass [`Modal`] content.
fn pass_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
let wallet_id = wallet.get_config().id;
let on_continue = |c: &mut CommonSettings| {
if c.new_pass_edit.is_empty() {
return;
}
let old_pass = c.old_pass_edit.clone();
let new_pass = c.new_pass_edit.clone();
match wallet.change_password(old_pass, new_pass) {
Ok(_) => {
// Clear password values.
c.old_pass_edit = "".to_string();
c.new_pass_edit = "".to_string();
// Close modal.
Modal::close();
}
Err(_) => c.wrong_pass = true
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.current_pass"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.current_pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw old password text edit.
let pass_edit_id = Id::from(modal.id).with(wallet_id).with("old_pass");
let mut pass_edit = TextEdit::new(pass_edit_id)
.password()
.focus(Modal::first_draw());
pass_edit.ui(ui, &mut self.old_pass_edit, cb);
ui.add_space(8.0);
// Draw old password text edit.
let pass_edit_id = Id::from(modal.id).with(wallet_id).with("old_pass");
let mut pass_edit = TextEdit::new(pass_edit_id)
.password()
.focus(Modal::first_draw());
pass_edit.ui(ui, &mut self.old_pass_edit, cb);
ui.add_space(8.0);
ui.label(
RichText::new(t!("wallets.new_pass"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.new_pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw new password text edit.
let new_pass_edit_id = Id::from(modal.id).with(wallet_id).with("new_pass");
let mut new_pass_edit = TextEdit::new(new_pass_edit_id).password().focus(false);
if pass_edit.enter_pressed {
new_pass_edit.focus_request();
}
new_pass_edit.ui(ui, &mut self.new_pass_edit, cb);
if new_pass_edit.enter_pressed {
on_continue(self);
}
// Draw new password text edit.
let new_pass_edit_id = Id::from(modal.id).with(wallet_id).with("new_pass");
let mut new_pass_edit = TextEdit::new(new_pass_edit_id)
.password()
.focus(false);
if pass_edit.enter_pressed {
new_pass_edit.focus_request();
}
new_pass_edit.ui(ui, &mut self.new_pass_edit, cb);
if new_pass_edit.enter_pressed {
on_continue(self);
}
// Show information when password is empty.
if self.old_pass_edit.is_empty() || self.new_pass_edit.is_empty() {
ui.add_space(10.0);
ui.label(
RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()),
);
} else if self.wrong_pass {
ui.add_space(10.0);
ui.label(
RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()),
);
}
ui.add_space(12.0);
});
// Show information when password is empty.
if self.old_pass_edit.is_empty() || self.new_pass_edit.is_empty() {
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()));
} else if self.wrong_pass {
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("change"), Colors::white_or_black(false), || {
on_continue(self);
});
});
});
ui.add_space(6.0);
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("change"), Colors::white_or_black(false), || {
on_continue(self);
});
});
});
ui.add_space(6.0);
});
}
/// Draw content to change wallet data directory.
fn data_dir_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let r = View::item_rounding(1, 2, false);
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
/// Draw content to change wallet data directory.
fn data_dir_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
self.pick_data_dir.ui(ui, cb, |path| {
wallet.change_data_path(path);
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(
layout_size,
Layout::left_to_right(Align::Center),
|ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
let path = wallet.get_config().data_path.unwrap_or_default();
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDERS, t!("files_location"));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
});
},
);
},
)
.response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
self.data_path_edit = wallet.get_config().data_path.unwrap_or_default();
// Show chain data path edit modal.
Modal::new(DATA_PATH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
}
}
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(1, 2, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
/// Draw data path input [`Modal`] content.
fn data_path_modal_ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks,
) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let on_save = |path: &String| {
wallet.change_data_path(path.clone());
Modal::close();
};
ui.label(
RichText::new(format!("{}:", t!("files_location")))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
self.pick_data_dir.ui(ui, cb, |path| {
wallet.change_data_path(path);
});
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
self.data_path_edit = wallet.get_config().data_path.unwrap_or_default();
// Show chain data path edit modal.
Modal::new(DATA_PATH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
let path = wallet.get_config().data_path.unwrap_or_default();
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDERS, t!("files_location"));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
});
});
});
}
// Draw chain data path text edit.
let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste();
edit.ui(ui, &mut self.data_path_edit, cb);
if edit.enter_pressed {
on_save(&self.data_path_edit);
}
ui.add_space(12.0);
/// Draw data path input [`Modal`] content.
fn data_path_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let on_save = |path: &String| {
wallet.change_data_path(path.clone());
Modal::close();
};
ui.label(RichText::new(format!("{}:", t!("files_location")))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Draw chain data path text edit.
let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste();
edit.ui(ui, &mut self.data_path_edit, cb);
if edit.enter_pressed {
on_save(&self.data_path_edit);
}
ui.add_space(12.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(&self.data_path_edit);
});
});
});
ui.add_space(6.0);
});
});
}
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
/// Draw wallet name [`Modal`] content.
fn min_conf_modal_ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
) {
let on_save = |c: &mut CommonSettings| {
if let Ok(min_conf) = c.min_confirmations_edit.parse::<u64>() {
wallet.update_min_confirmations(min_conf);
Modal::close();
}
};
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(&self.data_path_edit);
});
});
});
ui.add_space(6.0);
});
});
}
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.min_tx_conf_count"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
/// Draw wallet name [`Modal`] content.
fn min_conf_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut CommonSettings| {
if let Ok(min_conf) = c.min_confirmations_edit.parse::<u64>() {
wallet.update_min_confirmations(min_conf);
Modal::close();
}
};
// Minimum amount of confirmations text edit.
let mut min_confirmations_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
min_confirmations_edit.ui(ui, &mut self.min_confirmations_edit, cb);
if min_confirmations_edit.enter_pressed {
on_save(self);
}
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.min_tx_conf_count"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.min_confirmations_edit.parse::<u64>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
}
ui.add_space(12.0);
});
// Minimum amount of confirmations text edit.
let mut min_confirmations_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
min_confirmations_edit.ui(ui, &mut self.min_confirmations_edit, cb);
if min_confirmations_edit.enter_pressed {
on_save(self);
}
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.min_confirmations_edit.parse::<u64>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
}
@@ -12,190 +12,165 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Layout, RichText, StrokeKind};
use egui::RichText;
use crate::gui::icons::{CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, GLOBE, GLOBE_SIMPLE, PLUS_CIRCLE, X_CIRCLE};
use crate::gui::Colors;
use crate::gui::icons::{GLOBE, PLUS_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::gui::views::network::ConnectionsContent;
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::node::Node;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Wallet connection settings content.
pub struct ConnectionSettings {
/// Selected connection method.
pub method: ConnectionMethod,
/// Selected connection method.
pub method: ConnectionMethod,
/// External connection [`Modal`] content.
ext_conn_modal: ExternalConnectionModal,
/// External connection [`Modal`] content.
ext_conn_modal: ExternalConnectionModal,
}
impl Default for ConnectionSettings {
fn default() -> Self {
Self {
method: ConnectionMethod::Integrated,
ext_conn_modal: ExternalConnectionModal::new(None),
}
}
fn default() -> Self {
let method = {
let ext_conn_list = ConnectionsConfig::ext_conn_list();
if Node::is_running() || Node::is_starting() || ext_conn_list.is_empty() {
ConnectionMethod::Integrated
} else {
let ext_conn = ext_conn_list.get(0).unwrap();
ConnectionMethod::External(ext_conn.id, ext_conn.url.clone())
}
};
Self {
method,
ext_conn_modal: ExternalConnectionModal::new(None),
}
}
}
impl ContentContainer for ConnectionSettings {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
ExternalConnectionModal::WALLET_ID
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![ExternalConnectionModal::WALLET_ID]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
ExternalConnectionModal::WALLET_ID => {
self.ext_conn_modal.ui(ui, cb, modal, |_| {});
},
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
ExternalConnectionModal::WALLET_ID => {
self.ext_conn_modal.ui(ui, cb, modal, |_| {});
}
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
ui.add_space(2.0);
View::sub_title(ui, format!("{} {}", GLOBE, t!("wallets.conn_method")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
ui.add_space(2.0);
View::sub_title(ui, format!("{} {}", GLOBE, t!("wallets.conn_method")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.add_space(6.0);
// Show integrated node selection.
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
let is_current_method = self.method == ConnectionMethod::Integrated;
if !is_current_method {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
self.method = ConnectionMethod::Integrated;
});
} else {
View::selected_item_check(ui);
}
});
ui.vertical_centered(|ui| {
ui.add_space(6.0);
// Show integrated node selection.
let cur_integrated = self.method == ConnectionMethod::Integrated;
let bg = if cur_integrated {
Colors::fill_deep()
} else {
Colors::fill_lite()
};
ConnectionsContent::integrated_node_item_ui(
ui,
bg,
(!cur_integrated, || {
self.method = ConnectionMethod::Integrated;
}),
|ui| {
if cur_integrated {
View::selected_item_check(ui);
}
cur_integrated
},
);
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
ui.add_space(8.0);
ui.label(
RichText::new(t!("wallets.ext_conn"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.ext_conn_modal = ExternalConnectionModal::new(None);
Modal::new(ExternalConnectionModal::WALLET_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add_node"))
.show();
});
ui.add_space(4.0);
// Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.ext_conn_modal = ExternalConnectionModal::new(None);
Modal::new(ExternalConnectionModal::WALLET_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add_node"))
.show();
});
ui.add_space(4.0);
// Check for removed active connection.
let cur_method = &self.method.clone();
let mut ext_conn_list = ConnectionsConfig::ext_conn_list();
let has_method = !ext_conn_list.iter().filter(|c| {
match cur_method {
ConnectionMethod::Integrated => true,
ConnectionMethod::External(id, url) => id == &c.id || url == &c.url
}
}).collect::<Vec<&ExternalConnection>>().is_empty();
if !has_method {
match cur_method {
ConnectionMethod::External(id, url) => {
ext_conn_list.push(ExternalConnection {
id: *id,
url: url.clone(),
secret: None,
available: Some(true),
})
}
_ => {}
}
}
// Check for removed active connection.
let cur_method = &self.method.clone();
let mut ext_conn_list = ConnectionsConfig::ext_conn_list();
let has_method = !ext_conn_list
.iter()
.filter(|c| match cur_method {
ConnectionMethod::Integrated => true,
ConnectionMethod::External(id, url) => id == &c.id || url == &c.url,
})
.collect::<Vec<&ExternalConnection>>()
.is_empty();
if !has_method {
match cur_method {
ConnectionMethod::External(id, url) => ext_conn_list.push(ExternalConnection {
id: *id,
url: url.clone(),
username: Some("grin".to_string()),
secret: None,
available: Some(true),
}),
_ => {}
}
}
let ext_size = ext_conn_list.len();
if ext_size != 0 {
ui.add_space(8.0);
for (i, c) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
// Draw external connection item.
let is_current = match cur_method {
ConnectionMethod::External(id, url) => id == &c.id || url == &c.url,
_ => false
};
Self::ext_conn_item_ui(ui, c, is_current, i, ext_size, || {
self.method = ConnectionMethod::External(c.id, c.url.clone());
});
});
}
}
});
}
let len = ext_conn_list.len();
if len != 0 {
ui.add_space(8.0);
for (i, c) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
// Draw external connection item.
let is_current = match cur_method {
ConnectionMethod::External(id, url) => id == &c.id || url == &c.url,
_ => false,
};
let bg = if is_current {
Colors::fill()
} else {
Colors::fill_lite()
};
ConnectionsContent::ext_conn_item_ui(
ui,
bg,
c,
i,
len,
(!is_current, || {
self.method = ConnectionMethod::External(c.id, c.url.clone());
}),
|ui| {
if is_current {
View::selected_item_check(ui);
}
},
);
});
}
}
});
}
}
impl ConnectionSettings {
/// Draw external connection item content.
fn ext_conn_item_ui(ui: &mut egui::Ui,
conn: &ExternalConnection,
is_current: bool,
index: usize,
len: usize,
mut on_select: impl FnMut()) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if is_current {
View::selected_item_check(ui);
} else {
// Draw button to select connection.
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || {
on_select();
});
}
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| {
// Draw connections URL.
ui.add_space(4.0);
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
ui.add_space(1.0);
// Setup connection status text.
let status_text = if let Some(available) = conn.available {
if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
} else {
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);
});
});
});
});
}
}
@@ -14,52 +14,49 @@
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ContentContainer;
use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings};
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings};
use crate::wallet::Wallet;
/// Wallet settings tab content.
pub struct WalletSettingsContent {
/// Common setup content.
common_setup: CommonSettings,
/// Connection setup content.
conn_setup: ConnectionSettings,
/// Recovery setup content.
recovery_setup: RecoverySettings
/// Common setup content.
common_setup: CommonSettings,
/// Connection setup content.
conn_setup: ConnectionSettings,
/// Recovery setup content.
recovery_setup: RecoverySettings,
}
impl Default for WalletSettingsContent {
fn default() -> Self {
Self {
common_setup: CommonSettings::default(),
conn_setup: ConnectionSettings::default(),
recovery_setup: RecoverySettings::default()
}
}
fn default() -> Self {
Self {
common_setup: CommonSettings::default(),
conn_setup: ConnectionSettings::default(),
recovery_setup: RecoverySettings::default(),
}
}
}
impl WalletSettingsContent {
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
// Show common wallet setup.
self.common_setup.ui(ui, wallet, cb);
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Show common wallet setup.
self.common_setup.ui(ui, wallet, cb);
// Show wallet connections setup.
self.conn_setup.method = wallet.get_current_connection();
let method = self.conn_setup.method.clone();
self.conn_setup.ui(ui, cb);
if method != self.conn_setup.method {
wallet.update_connection(&self.conn_setup.method);
// Reopen wallet if connection changed.
if !wallet.reopen_needed() {
wallet.set_reopen(true);
wallet.close();
}
}
// Show wallet connections setup.
self.conn_setup.method = wallet.get_current_connection();
let method = self.conn_setup.method.clone();
self.conn_setup.ui(ui, cb);
if method != self.conn_setup.method {
wallet.update_connection(&self.conn_setup.method);
// Reopen wallet if connection changed.
if !wallet.reopen_needed() {
wallet.set_reopen(true);
wallet.close();
}
}
// Show wallet recovery setup.
self.recovery_setup.ui(ui, wallet, cb);
}
}
// Show wallet recovery setup.
self.recovery_setup.ui(ui, wallet, cb);
}
}
+1 -1
View File
@@ -22,4 +22,4 @@ mod common;
pub use common::CommonSettings;
mod recovery;
pub use recovery::RecoverySettings;
pub use recovery::RecoverySettings;
+268 -229
View File
@@ -16,25 +16,25 @@ use egui::{Id, RichText};
use grin_chain::SyncStatus;
use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::icons::{EYE, KEY, LIFEBUOY, STETHOSCOPE, TRASH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::Colors;
use crate::node::Node;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::Wallet;
use crate::wallet::types::ConnectionMethod;
/// Wallet recovery settings content.
pub struct RecoverySettings {
/// Wallet password [`Modal`] value.
pass_edit: String,
/// Flag to check if wrong password was entered.
wrong_pass: bool,
/// Wallet password [`Modal`] value.
pass_edit: String,
/// Flag to check if wrong password was entered.
wrong_pass: bool,
/// Recovery phrase value.
recovery_phrase: Option<ZeroingString>,
/// Recovery phrase value.
recovery_phrase: Option<ZeroingString>,
}
/// Identifier for recovery phrase [`Modal`].
@@ -43,251 +43,290 @@ const RECOVERY_PHRASE_MODAL: &'static str = "recovery_phrase_modal";
const DELETE_CONFIRMATION_MODAL: &'static str = "delete_wallet_confirmation_modal";
impl WalletContentContainer for RecoverySettings {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
RECOVERY_PHRASE_MODAL,
DELETE_CONFIRMATION_MODAL
]
}
fn modal_ids(&self) -> Vec<&'static str> {
vec![RECOVERY_PHRASE_MODAL, DELETE_CONFIRMATION_MODAL]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
RECOVERY_PHRASE_MODAL => {
self.recovery_phrase_modal_ui(ui, wallet, modal, cb);
}
DELETE_CONFIRMATION_MODAL => {
Self::deletion_modal_ui(ui, wallet);
}
_ => {}
}
}
fn modal_ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
) {
match modal.id {
RECOVERY_PHRASE_MODAL => {
self.recovery_phrase_modal_ui(ui, wallet, modal, cb);
}
DELETE_CONFIRMATION_MODAL => {
Self::deletion_modal_ui(ui, wallet);
}
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, _: &dyn PlatformCallbacks) {
ui.add_space(10.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
View::sub_title(ui, format!("{} {}", KEY, t!("wallets.recovery")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, _: &dyn PlatformCallbacks) {
ui.add_space(10.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
View::sub_title(ui, format!("{} {}", KEY, t!("wallets.recovery")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
ui.vertical_centered(|ui| {
let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
if wallet.sync_error() || (integrated_node && !integrated_node_ready) {
ui.add_space(2.0);
ui.label(RichText::new(t!("wallets.repair_unavailable"))
.size(16.0)
.color(Colors::red()));
ui.add_space(2.0);
} else if !wallet.is_repairing() {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
if wallet.sync_error() || (integrated_node && !integrated_node_ready) {
ui.add_space(2.0);
ui.label(
RichText::new(t!("wallets.repair_unavailable"))
.size(16.0)
.color(Colors::red()),
);
ui.add_space(2.0);
} else if !wallet.is_repairing() {
ui.add_space(6.0);
// Draw button to repair the wallet.
let repair_text = format!("{} {}", STETHOSCOPE, t!("wallets.repair_wallet"));
View::action_button(ui, repair_text, || {
wallet.repair();
});
// Draw button to repair the wallet.
let repair_text = format!("{} {}", STETHOSCOPE, t!("wallets.repair_wallet"));
View::action_button(ui, repair_text, || {
wallet.repair();
});
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.repair_desc"))
.size(16.0)
.color(Colors::inactive_text()));
}
ui.add_space(6.0);
ui.label(
RichText::new(t!("wallets.repair_desc"))
.size(16.0)
.color(Colors::inactive_text()),
);
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Draw button to restore the wallet.
ui.add_space(4.0);
View::colored_text_button(ui,
format!("{} {}", LIFEBUOY, t!("wallets.recover")),
Colors::green(),
Colors::white_or_black(false), || {
wallet.delete_db();
});
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.restore_wallet_desc"))
.size(16.0)
.color(Colors::inactive_text()));
// Draw button to restore the wallet.
ui.add_space(4.0);
View::colored_text_button(
ui,
format!("{} {}", LIFEBUOY, t!("wallets.recover")),
Colors::green(),
Colors::white_or_black(false),
|| {
wallet.delete_db();
},
);
ui.add_space(6.0);
ui.label(
RichText::new(t!("wallets.restore_wallet_desc"))
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
let recovery_text = format!("{}:", t!("wallets.recovery_phrase"));
ui.label(RichText::new(recovery_text).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
let recovery_text = format!("{}:", t!("wallets.recovery_phrase"));
ui.label(
RichText::new(recovery_text)
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Draw button to show recovery phrase.
let show_text = format!("{} {}", EYE, t!("show"));
View::button(ui, show_text, Colors::white_or_black(false), || {
self.show_recovery_phrase_modal();
});
// Draw button to show recovery phrase.
let show_text = format!("{} {}", EYE, t!("show"));
View::button(ui, show_text, Colors::white_or_black(false), || {
self.show_recovery_phrase_modal();
});
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.delete_desc")).size(16.0).color(Colors::red()));
ui.add_space(6.0);
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.label(
RichText::new(t!("wallets.delete_desc"))
.size(16.0)
.color(Colors::red()),
);
ui.add_space(6.0);
// Draw button to delete the wallet.
View::colored_text_button(ui,
format!("{} {}", TRASH, t!("wallets.delete")),
Colors::red(),
Colors::white_or_black(false), || {
Modal::new(DELETE_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
});
ui.add_space(8.0);
});
}
// Draw button to delete the wallet.
View::colored_text_button(
ui,
format!("{} {}", TRASH, t!("wallets.delete")),
Colors::red(),
Colors::white_or_black(false),
|| {
Modal::new(DELETE_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
},
);
ui.add_space(8.0);
});
}
}
impl Default for RecoverySettings {
fn default() -> Self {
Self {
wrong_pass: false,
pass_edit: "".to_string(),
recovery_phrase: None,
}
}
fn default() -> Self {
Self {
wrong_pass: false,
pass_edit: "".to_string(),
recovery_phrase: None,
}
}
}
impl RecoverySettings {
/// Show recovery phrase [`Modal`].
fn show_recovery_phrase_modal(&mut self) {
// Setup modal values.
self.pass_edit = "".to_string();
self.wrong_pass = false;
self.recovery_phrase = None;
// Show recovery phrase modal.
Modal::new(RECOVERY_PHRASE_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.recovery_phrase"))
.show();
}
/// Show recovery phrase [`Modal`].
fn show_recovery_phrase_modal(&mut self) {
// Setup modal values.
self.pass_edit = "".to_string();
self.wrong_pass = false;
self.recovery_phrase = None;
// Show recovery phrase modal.
Modal::new(RECOVERY_PHRASE_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.recovery_phrase"))
.show();
}
/// Draw recovery phrase [`Modal`] content.
fn recovery_phrase_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
let on_next = |c: &mut RecoverySettings| {
match wallet.get_recovery(c.pass_edit.clone()) {
Ok(phrase) => {
c.wrong_pass = false;
c.recovery_phrase = Some(phrase);
}
Err(_) => {
c.wrong_pass = true;
}
}
};
/// Draw recovery phrase [`Modal`] content.
fn recovery_phrase_modal_ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
) {
let on_next = |c: &mut RecoverySettings| match wallet.get_recovery(c.pass_edit.clone()) {
Ok(phrase) => {
c.wrong_pass = false;
c.recovery_phrase = Some(phrase);
}
Err(_) => {
c.wrong_pass = true;
}
};
ui.add_space(6.0);
if self.recovery_phrase.is_some() {
ui.vertical_centered(|ui| {
ui.label(RichText::new(self.recovery_phrase.clone().unwrap().to_string())
.size(17.0)
.color(Colors::white_or_black(true)));
});
ui.add_space(10.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.recovery_phrase = None;
Modal::close();
});
});
} else {
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
ui.add_space(6.0);
if self.recovery_phrase.is_some() {
ui.vertical_centered(|ui| {
ui.label(
RichText::new(self.recovery_phrase.clone().unwrap().to_string())
.size(17.0)
.color(Colors::white_or_black(true)),
);
});
ui.add_space(10.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.recovery_phrase = None;
Modal::close();
});
});
} else {
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw current wallet password text edit.
let pass_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let mut pass_edit = TextEdit::new(pass_edit_id)
.password();
pass_edit.ui(ui, &mut self.pass_edit, cb);
if pass_edit.enter_pressed {
on_next(self);
}
// Draw current wallet password text edit.
let pass_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let mut pass_edit = TextEdit::new(pass_edit_id).password();
pass_edit.ui(ui, &mut self.pass_edit, cb);
if pass_edit.enter_pressed {
on_next(self);
}
// Show information when password is empty or wrong.
if self.pass_edit.is_empty() {
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()));
} else if self.wrong_pass {
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()));
}
});
ui.add_space(12.0);
// Show information when password is empty or wrong.
if self.pass_edit.is_empty() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()),
);
} else if self.wrong_pass {
ui.add_space(12.0);
ui.label(
RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()),
);
}
});
ui.add_space(12.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
self.recovery_phrase = None;
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || {
on_next(self);
});
});
});
});
}
ui.add_space(6.0);
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
self.recovery_phrase = None;
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || {
on_next(self);
});
});
});
});
}
ui.add_space(6.0);
}
/// Draw wallet deletion [`Modal`] content.
pub fn deletion_modal_ui(ui: &mut egui::Ui, wallet: &Wallet) {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.delete_conf"))
.size(17.0)
.color(Colors::text(false)));
});
ui.add_space(12.0);
/// Draw wallet deletion [`Modal`] content.
pub fn deletion_modal_ui(ui: &mut egui::Ui, wallet: &Wallet) {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.delete_conf"))
.size(17.0)
.color(Colors::text(false)),
);
});
ui.add_space(12.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// 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!("modal.cancel"), Colors::white_or_black(false), || {
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("delete"), Colors::white_or_black(false), || {
wallet.delete_wallet();
Modal::close();
});
});
});
ui.add_space(6.0);
});
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("delete"), Colors::white_or_black(false), || {
wallet.delete_wallet();
Modal::close();
});
});
});
ui.add_space(6.0);
});
}
}
+193 -176
View File
@@ -15,210 +15,227 @@
use egui::{Align, CornerRadius, Layout, RichText, StrokeKind};
use crate::AppConfig;
use crate::gui::icons::{CIRCLE_HALF, DOTS_THREE_CIRCLE, PLUGS, PLUGS_CONNECTED, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, WRENCH};
use crate::gui::Colors;
use crate::gui::icons::{
CIRCLE_HALF, DOTS_THREE_CIRCLE, PLUGS, PLUGS_CONNECTED, POWER, QR_CODE, SHIELD_CHECKERED,
SHIELD_SLASH, WARNING_CIRCLE, WRENCH,
};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::transport::settings::WalletTransportSettingsContent;
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
use crate::gui::views::{Modal, QrCodeContent, View};
use crate::gui::Colors;
use crate::tor::{Tor, TorConfig};
use crate::wallet::Wallet;
use crate::wallet::types::WalletTask;
/// Wallet transport panel content.
pub struct WalletTransportContent {
/// QR code address content.
pub qr_address_content: Option<QrCodeContent>,
/// QR code address content.
pub qr_address_content: Option<QrCodeContent>,
/// Settings content.
pub settings_content: Option<WalletTransportSettingsContent>,
/// Settings content.
pub settings_content: Option<WalletTransportSettingsContent>,
}
impl WalletContentContainer for WalletTransportContent {
fn modal_ids(&self) -> Vec<&'static str> { vec![] }
fn modal_ids(&self) -> Vec<&'static str> {
vec![]
}
fn modal_ui(&mut self, _: &mut egui::Ui, _: &Wallet, _: &Modal, _: &dyn PlatformCallbacks) {
}
fn modal_ui(&mut self, _: &mut egui::Ui, _: &Wallet, _: &Modal, _: &dyn PlatformCallbacks) {}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if let Some(content) = self.qr_address_content.as_mut() {
let dark_theme = AppConfig::dark_theme().unwrap_or(false);
// Set light theme for better scanning.
AppConfig::set_dark_theme(false);
crate::setup_visuals(ui.ctx());
// Draw QR code content.
ui.add_space(6.0);
content.ui(ui, cb);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_address_content = None;
});
});
ui.add_space(6.0);
// Set color theme back.
AppConfig::set_dark_theme(dark_theme);
crate::setup_visuals(ui.ctx());
} else if let Some(content) = self.settings_content.as_mut() {
let mut closed = false;
content.ui(ui, wallet, cb, || {
closed = true;
});
if closed {
self.settings_content = None;
}
} else {
self.tor_header_ui(ui, wallet);
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if let Some(content) = self.qr_address_content.as_mut() {
let dark_theme = AppConfig::dark_theme().unwrap_or(false);
// Set light theme for better scanning.
AppConfig::set_dark_theme(false);
crate::setup_visuals(ui.ctx());
// Draw QR code content.
ui.add_space(6.0);
content.ui(ui, cb);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_address_content = None;
});
});
ui.add_space(6.0);
// Set color theme back.
AppConfig::set_dark_theme(dark_theme);
crate::setup_visuals(ui.ctx());
} else if let Some(content) = self.settings_content.as_mut() {
let mut closed = false;
content.ui(ui, wallet, cb, || {
closed = true;
});
if closed {
self.settings_content = None;
}
} else {
self.tor_header_ui(ui, wallet);
}
}
}
impl Default for WalletTransportContent {
fn default() -> Self {
Self {
qr_address_content: None,
settings_content: None,
}
}
fn default() -> Self {
Self {
qr_address_content: None,
settings_content: None,
}
}
}
impl WalletTransportContent {
/// Check if it's possible to go back at navigation stack.
pub fn can_back(&self) -> bool {
self.settings_content.is_some() || self.qr_address_content.is_some()
}
/// Check if it's possible to go back at navigation stack.
pub fn can_back(&self) -> bool {
self.settings_content.is_some() || self.qr_address_content.is_some()
}
/// Navigate back on navigation stack.
pub fn back(&mut self) {
if let Some(content) = self.settings_content.as_ref() {
if content.tor_settings_content.settings_changed {
Tor::restart_services();
}
self.settings_content = None;
} else if self.qr_address_content.is_some() {
self.qr_address_content = None;
}
}
/// Navigate back on navigation stack.
pub fn back(&mut self) {
if let Some(content) = self.settings_content.as_ref() {
if content.tor_settings_content.settings_changed {
Tor::restart();
}
self.settings_content = None;
} else if self.qr_address_content.is_some() {
self.qr_address_content = None;
}
}
/// Draw Tor transport header content.
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
let wallet_data = wallet.get_data();
if wallet_data.is_none() {
return;
}
let addr = wallet.slatepack_address().unwrap();
/// Draw Tor transport header content.
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
let data = wallet.get_data();
if data.is_none() {
return;
}
let data = data.unwrap();
let addr = wallet.slatepack_address().unwrap();
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
// Draw round background.
let info = wallet.get_data().unwrap().info;
let awaiting_balance = info.amount_awaiting_confirmation > 0 ||
info.amount_awaiting_finalization > 0 || info.amount_locked > 0;
let rounding = if awaiting_balance {
View::item_rounding(1, 3, false)
} else {
View::item_rounding(1, 2, false)
};
ui.painter().rect(rect,
rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
// Draw round background.
let info = data.info;
let awaiting_balance = info.amount_awaiting_confirmation > 0
|| info.amount_awaiting_finalization > 0
|| info.amount_locked > 0;
let rounding = if awaiting_balance {
View::item_rounding(1, 3, false)
} else {
View::item_rounding(1, 2, false)
};
ui.painter().rect(
rect,
rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside,
);
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Show button to show QR code address.
let r = if awaiting_balance {
View::item_rounding(1, 3, true)
} else {
View::item_rounding(1, 2, true)
};
View::item_button(ui, r, QR_CODE, None, || {
self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false)
.with_max_size(320.0));
});
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Show button to show QR code address.
let r = if awaiting_balance {
View::item_rounding(1, 3, true)
} else {
View::item_rounding(1, 2, true)
};
View::item_button(ui, r, QR_CODE, None, || {
self.qr_address_content =
Some(QrCodeContent::new(addr.clone(), false).with_max_size(320.0));
});
let service_id = &wallet.identifier();
// Draw button to enable/disable Tor listener for current wallet.
if wallet.foreign_api_port().is_some() && wallet.secret_key().is_some() {
let port = wallet.foreign_api_port().unwrap();
let key = wallet.secret_key().unwrap();
if !Tor::is_service_starting(service_id) {
if !Tor::is_service_running(service_id) {
let r = CornerRadius::default();
View::item_button(ui, r, POWER, Some(Colors::green()), || {
Tor::start_service(port, key.clone(), service_id);
});
} else {
let r = CornerRadius::default();
View::item_button(ui, r, POWER, Some(Colors::red()), || {
Tor::stop_service(service_id);
});
}
}
}
// Draw button to show Tor transport settings.
let button_rounding = View::item_rounding(1, 3, true);
View::item_button(ui, button_rounding, WRENCH, None, || {
self.settings_content = Some(WalletTransportSettingsContent::default());
});
let service_id = &wallet.identifier();
// Draw button to enable/disable Tor listener for current wallet.
if wallet.foreign_api_port().is_some() {
if !Tor::is_service_starting(service_id) {
if !Tor::is_service_running(service_id) {
let r = CornerRadius::default();
View::item_button(ui, r, POWER, Some(Colors::green()), || {
wallet.task(WalletTask::StartTor);
});
} else {
let r = CornerRadius::default();
View::item_button(ui, r, POWER, Some(Colors::red()), || {
Tor::stop_service(service_id);
});
}
}
}
// Draw button to show Tor transport settings.
let button_rounding = View::item_rounding(1, 3, true);
View::item_button(ui, button_rounding, WRENCH, None, || {
self.settings_content = Some(WalletTransportSettingsContent::default());
});
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);
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);
let is_running = Tor::is_service_running(service_id);
let has_error = Tor::is_service_failed(service_id);
let is_starting = Tor::is_service_starting(service_id);
let address_color = if is_running && !is_starting {
Colors::green()
} else if has_error {
Colors::red()
} else {
Colors::inactive_text()
};
// Show slatepack address text.
View::animate_text(ui, addr.clone(), 17.0, address_color, is_starting);
ui.add_space(1.0);
let is_running = Tor::is_service_running(service_id);
let has_error = Tor::is_service_failed(service_id);
let is_starting = Tor::is_service_starting(service_id);
let address_color = if is_running && !is_starting {
Colors::green()
} else if has_error {
Colors::red()
} else {
Colors::inactive_text()
};
// Show slatepack address text.
View::animate_text(ui, addr.clone(), 17.0, address_color, is_starting);
ui.add_space(1.0);
let (icon, text) = if is_starting {
(DOTS_THREE_CIRCLE, t!("transport.connecting"))
} else if has_error {
(WARNING_CIRCLE, t!("transport.conn_error"))
} else if is_running {
(PLUGS_CONNECTED, t!("transport.connected"))
} else if let Some(_) = TorConfig::get_proxy() {
(PLUGS_CONNECTED, t!("app_settings.proxy"))
} else {
(PLUGS, t!("transport.disconnected"))
};
let status_text = format!("{} {}", icon, text);
// Show connection status text.
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0);
let (icon, text) = if is_starting {
(DOTS_THREE_CIRCLE, t!("transport.connecting"))
} else if has_error {
(WARNING_CIRCLE, t!("transport.conn_error"))
} else if is_running {
(PLUGS_CONNECTED, t!("transport.connected"))
} else if let Some(_) = TorConfig::get_proxy() {
(PLUGS_CONNECTED, t!("app_settings.proxy"))
} else {
(PLUGS, t!("transport.disconnected"))
};
let status_text = format!("{} {}", icon, text);
// Show connection status text.
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0);
let bridges_text = if is_starting || has_error {
match TorConfig::get_bridge() {
None => {
format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled"))
}
Some(b) => {
let name = b.protocol_name().to_uppercase();
format!("{} {}",
SHIELD_CHECKERED,
t!("transport.bridge_name", "b" = name))
}
}
} else {
format!("{} {}", CIRCLE_HALF, t!("transport.tor_network"))
};
// Show bridge info text.
ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray()));
});
});
});
});
}
}
let bridges_text = if is_starting || has_error {
match TorConfig::get_bridge() {
None => {
format!(
"{} {}",
SHIELD_SLASH,
t!("transport.bridges_disabled")
)
}
Some(b) => {
let name = b.protocol_name().to_uppercase();
format!(
"{} {}",
SHIELD_CHECKERED,
t!("transport.bridge_name", "b" = name)
)
}
}
} else {
format!("{} {}", CIRCLE_HALF, t!("transport.tor_network"))
};
// Show bridge info text.
ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray()));
});
},
);
});
});
}
}
@@ -15,4 +15,4 @@
mod content;
pub use content::*;
mod settings;
mod settings;
@@ -14,60 +14,64 @@
use egui::RichText;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View;
use crate::gui::views::settings::TorSettingsContent;
use crate::gui::views::types::ContentContainer;
use crate::gui::views::View;
use crate::gui::Colors;
use crate::tor::Tor;
use crate::wallet::Wallet;
/// Wallet transport settings content.
pub struct WalletTransportSettingsContent {
/// Tor transport content settings.
pub tor_settings_content: TorSettingsContent,
/// Tor transport content settings.
pub tor_settings_content: TorSettingsContent,
}
impl Default for WalletTransportSettingsContent {
fn default() -> Self {
Self {
tor_settings_content: TorSettingsContent::default()
}
}
fn default() -> Self {
Self {
tor_settings_content: TorSettingsContent::default(),
}
}
}
impl WalletTransportSettingsContent {
/// Draw transport settings content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks,
on_close: impl FnOnce()) {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
// Show Tor settings.
self.tor_settings_content.ui(ui, cb);
ui.add_space(4.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.label(RichText::new(t!("transport.tor_autorun_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Show Tor service autorun checkbox.
let autorun = wallet.auto_start_tor_listener();
View::checkbox(ui, autorun, t!("network.autorun"), || {
wallet.update_auto_start_tor_listener(!autorun);
});
});
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
if self.tor_settings_content.settings_changed {
Tor::restart_services();
}
on_close();
});
});
ui.add_space(6.0);
}
}
/// Draw transport settings content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks,
on_close: impl FnOnce(),
) {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
// Show Tor settings.
self.tor_settings_content.ui(ui, cb);
ui.add_space(4.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.label(
RichText::new(t!("transport.tor_autorun_desc"))
.size(17.0)
.color(Colors::inactive_text()),
);
// Show Tor service autorun checkbox.
let autorun = wallet.auto_start_tor_listener();
View::checkbox(ui, autorun, t!("network.autorun"), || {
wallet.update_auto_start_tor_listener(!autorun);
});
});
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
if self.tor_settings_content.settings_changed {
Tor::restart();
}
on_close();
});
});
ui.add_space(6.0);
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -16,4 +16,4 @@ mod content;
pub use content::*;
mod tx;
pub use tx::*;
pub use tx::*;
+380 -322
View File
@@ -12,6 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{
CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, FILE_X, HASH_STRAIGHT, PROHIBIT, QR_CODE,
SEAL_CHECK,
};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::wallet::message::MessageInputContent;
use crate::gui::views::wallets::wallet::proof::PaymentProofContent;
use crate::gui::views::wallets::wallet::txs::WalletTransactionsContent;
use crate::gui::views::{Modal, QrCodeContent, View};
use crate::wallet::Wallet;
use crate::wallet::types::{WalletTask, WalletTx};
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, CornerRadius, Id, Layout, RichText, ScrollArea, StrokeKind};
use grin_core::core::amount_to_hr_string;
@@ -19,359 +34,402 @@ use grin_util::ToHex;
use grin_wallet_libwallet::TxLogEntryType;
use std::fs;
use crate::AppConfig;
use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SEAL_CHECK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::proof::PaymentProofContent;
use crate::gui::views::wallets::wallet::txs::WalletTransactionsContent;
use crate::gui::views::{Modal, QrCodeContent, View};
use crate::gui::Colors;
use crate::wallet::types::{WalletTask, WalletTransaction};
use crate::wallet::Wallet;
/// Transaction information [`Modal`] content.
pub struct WalletTransactionContent {
/// Transaction identifier.
tx_id: u32,
/// Slatepack message text.
message: Option<String>,
/// Transaction identifier.
tx_id: u32,
/// Slatepack message text.
message: Option<String>,
/// QR code Slatepack message image content.
qr_code_content: Option<QrCodeContent>,
/// QR code Slatepack message image content.
qr_code_content: Option<QrCodeContent>,
/// Payment proof sharing content.
pub proof_content: Option<PaymentProofContent>,
/// Payment proof sharing content.
pub proof_content: Option<PaymentProofContent>,
}
impl WalletTransactionContent {
/// Create new content instance with [`Wallet`] from provided [`WalletTransaction`].
pub fn new(tx_id: u32) -> Self {
Self {
tx_id,
message: None,
qr_code_content: None,
proof_content: None,
}
}
/// Create new content instance with [`Wallet`] from provided [`WalletTx`].
pub fn new(tx_id: u32) -> Self {
Self {
tx_id,
message: None,
qr_code_content: None,
proof_content: None,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
// Check values and setup transaction data.
let wallet_data = wallet.get_data();
if wallet_data.is_none() {
Modal::close();
return;
}
let data = wallet_data.unwrap();
let data_txs = data.txs.clone().unwrap();
let txs = data_txs.into_iter()
.filter(|tx| tx.data.id == self.tx_id)
.collect::<Vec<WalletTransaction>>();
if txs.is_empty() {
Modal::close();
return;
}
let tx = txs.get(0).unwrap();
/// Draw [`Modal`] content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
modal: &Modal,
wallet: &Wallet,
cb: &dyn PlatformCallbacks,
on_delete: impl FnOnce(u32),
) {
// Check values and setup transaction data.
let wallet_data = wallet.get_data();
if wallet_data.is_none() {
Modal::close();
return;
}
let data = wallet_data.unwrap();
let data_txs = data.txs.clone().unwrap();
let txs = data_txs
.into_iter()
.filter(|tx| tx.data.id == self.tx_id)
.collect::<Vec<WalletTx>>();
if txs.is_empty() {
Modal::close();
return;
}
let tx = txs.get(0).unwrap();
if let Some(content) = self.qr_code_content.as_mut() {
let dark_theme = AppConfig::dark_theme().unwrap_or(false);
// Set light theme for better scanning.
AppConfig::set_dark_theme(false);
crate::setup_visuals(ui.ctx());
modal.set_background_color(Colors::FILL_DEEP);
if let Some(content) = self.qr_code_content.as_mut() {
let dark_theme = AppConfig::dark_theme().unwrap_or(false);
// Set light theme for better scanning.
AppConfig::set_dark_theme(false);
crate::setup_visuals(ui.ctx());
modal.set_background_color(Colors::FILL_DEEP);
ui.add_space(6.0);
content.ui(ui, cb);
ui.add_space(6.0);
content.ui(ui, cb);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to text request content.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_code_content = None;
Modal::close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
self.qr_code_content = None;
});
});
});
// Set color theme back.
AppConfig::set_dark_theme(dark_theme);
crate::setup_visuals(ui.ctx());
} else {
modal.set_background_color(Colors::fill());
// Show transaction information.
self.info_ui(ui, tx, wallet, cb);
// Show buttons to close modal or come back to text request content.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_code_content = None;
Modal::close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
self.qr_code_content = None;
});
});
});
// Set color theme back.
AppConfig::set_dark_theme(dark_theme);
crate::setup_visuals(ui.ctx());
} else {
modal.set_background_color(Colors::fill());
// Show transaction information.
self.info_ui(ui, tx, wallet, cb, on_delete);
// Show transaction sharing content or payment proof.
if self.proof_content.is_none() && tx.can_cancel() && !tx.finalized() {
self.share_ui(ui, wallet, tx, cb);
} else {
if let Some(proof_content) = self.proof_content.as_mut() {
// Draw payment proof sharing content.
proof_content.share_ui(ui, tx, cb);
} else if tx.proof.is_some() && !tx.sending_tor() &&
tx.action_error.is_none() {
ui.vertical_centered(|ui| {
ui.add_space(8.0);
let label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof"));
let text_color = Colors::gold_dark();
let btn_color = Colors::white_or_black(false);
// Draw button to show payment proof sharing content.
View::colored_text_button(ui, label, text_color, btn_color, || {
if let Ok(p) = serde_json::to_string_pretty(&tx.proof) {
let c = PaymentProofContent::new(Some(p));
self.proof_content = Some(c);
}
});
});
}
}
// Show transaction sharing content or payment proof.
if self.proof_content.is_none() && tx.can_cancel() && !tx.finalized() {
self.share_ui(ui, wallet, tx, cb);
} else {
if let Some(proof_content) = self.proof_content.as_mut() {
// Draw payment proof sharing content.
proof_content.share_ui(ui, tx, cb);
} else if tx.proof.is_some() && !tx.sending_tor() && tx.action_error.is_none() {
ui.vertical_centered(|ui| {
ui.add_space(8.0);
let label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof"));
let text_color = Colors::gold_dark();
let btn_color = Colors::white_or_black(false);
// Draw button to show payment proof sharing content.
View::colored_text_button(ui, label, text_color, btn_color, || {
if let Ok(p) = serde_json::to_string_pretty(&tx.proof) {
let c = PaymentProofContent::new(Some(p));
self.proof_content = Some(c);
}
});
});
}
}
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
Modal::close();
});
});
}
ui.add_space(6.0);
}
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
Modal::close();
});
});
}
ui.add_space(6.0);
}
/// Draw transaction sharing content.
fn share_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
tx: &WalletTransaction,
cb: &dyn PlatformCallbacks) {
if self.message.is_none() {
let slatepack_path = wallet.get_config().get_tx_slate_path(tx);
self.message = Some(fs::read_to_string(slatepack_path).unwrap_or("".to_string()));
}
if let Some(m) = &self.message {
if m.is_empty() {
return;
}
/// Draw transaction sharing content.
fn share_ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
tx: &WalletTx,
cb: &dyn PlatformCallbacks,
) {
if self.message.is_none() {
if let Some(slate_state) = tx.data.tx_slate_state.as_ref() {
let slatepack_path = wallet
.get_config()
.get_slate_path(tx.data.tx_slate_id.unwrap(), slate_state);
self.message = Some(fs::read_to_string(slatepack_path).unwrap_or("".to_string()));
}
}
if let Some(m) = &self.message {
if m.is_empty() {
return;
}
let amount = amount_to_hr_string(tx.amount, true);
let desc_text = if tx.can_finalize() {
if tx.data.tx_type == TxLogEntryType::TxSent {
t!("wallets.send_request_desc", "amount" => amount)
} else {
t!("wallets.invoice_desc", "amount" => amount)
}
} else {
if tx.data.tx_type == TxLogEntryType::TxSent {
t!("wallets.parse_i1_slatepack_desc", "amount" => amount)
} else {
t!("wallets.parse_s1_slatepack_desc", "amount" => amount)
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(desc_text).size(16.0).color(Colors::inactive_text()));
});
ui.add_space(6.0);
let amount = amount_to_hr_string(tx.amount, true);
let desc_text = if tx.can_finalize() {
if tx.data.tx_type == TxLogEntryType::TxSent {
t!("wallets.send_request_desc", "amount" => amount)
} else {
t!("wallets.invoice_desc", "amount" => amount)
}
} else {
if tx.data.tx_type == TxLogEntryType::TxSent {
t!("wallets.parse_i1_slatepack_desc", "amount" => amount)
} else {
t!("wallets.parse_s1_slatepack_desc", "amount" => amount)
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(desc_text)
.size(16.0)
.color(Colors::inactive_text()),
);
});
ui.add_space(6.0);
let mut message = m.clone();
// Draw Slatepack message content.
ui.vertical_centered(|ui| {
let scroll_id = Id::from("tx_info_message_request").with(tx.data.id);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("_input");
egui::TextEdit::multiline(&mut message)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
});
let mut message = m.clone();
// Draw Slatepack message content.
ui.vertical_centered(|ui| {
let scroll_id = Id::from("tx_info_message_request").with(tx.data.id);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("_input");
egui::TextEdit::multiline(&mut message)
.id(input_id)
.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(8.0);
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// 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| {
// Draw button to show Slatepack message as QR code.
let qr_text = format!("{} {}", QR_CODE, t!("qr_code"));
View::button(ui, qr_text, Colors::white_or_black(false), || {
self.qr_code_content = Some(QrCodeContent::new(message, true));
});
});
columns[1].vertical_centered_justified(|ui| {
// Draw copy button.
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(m.clone());
Modal::close();
});
});
});
let mut finalization_needed = false;
// Draw button to share response as file.
ui.add_space(8.0);
ui.vertical_centered(|ui| {
let share_text = format!("{} {}", FILE_TEXT, t!("share"));
View::colored_text_button(ui,
share_text,
Colors::blue(),
Colors::white_or_black(false), || {
if let Some(slate_id) = tx.data.tx_slate_id {
let name = format!("{}.{}.slatepack", slate_id, tx.state);
let data = m.as_bytes().to_vec();
cb.share_data(name, data).unwrap_or_default();
Modal::close();
}
});
});
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw button to show Slatepack message as QR code.
let qr_text = format!("{} {}", QR_CODE, t!("qr_code"));
View::button(ui, qr_text, Colors::white_or_black(false), || {
self.qr_code_content = Some(QrCodeContent::new(message, true));
});
});
columns[1].vertical_centered_justified(|ui| {
// Draw copy button.
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(m.clone());
// Show message input or close modal.
if tx.can_finalize() {
finalization_needed = true;
} else {
Modal::close();
}
});
});
});
/// Draw transaction information content.
fn info_ui(&mut self,
ui: &mut egui::Ui,
tx: &WalletTransaction,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw button to share response as file.
if let Some(slate_id) = tx.data.tx_slate_id {
if let Some(slate_state) = tx.data.tx_slate_state.as_ref() {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
let share_text = format!("{} {}", FILE_TEXT, t!("share"));
View::colored_text_button(
ui,
share_text,
Colors::blue(),
Colors::white_or_black(false),
|| {
let name = format!("{}.{}.slatepack", slate_id, slate_state);
let data = m.as_bytes().to_vec();
cb.share_data(name, data).unwrap_or_default();
// Show message input or close modal.
if tx.can_finalize() {
finalization_needed = true;
} else {
Modal::close();
}
},
);
});
}
}
let mut rect = ui.available_rect_before_wrap();
rect.set_height(WalletTransactionsContent::TX_ITEM_HEIGHT);
if finalization_needed {
Modal::new(MessageInputContent::MODAL_ID)
.position(ModalPosition::Center)
.title(t!("wallets.messages"))
.show();
}
}
}
// Draw tx item background.
let p = ui.painter();
let r = View::item_rounding(0, 2, false);
p.rect(rect, r, Colors::TRANSPARENT, View::item_stroke(), StrokeKind::Outside);
/// Draw transaction information content.
fn info_ui(
&mut self,
ui: &mut egui::Ui,
tx: &WalletTx,
wallet: &Wallet,
cb: &dyn PlatformCallbacks,
on_delete: impl FnOnce(u32),
) {
let data = wallet.get_data();
if data.is_none() {
Modal::close();
return;
}
let data = wallet.get_data().unwrap();
// Show transaction amount status and time.
let data = wallet.get_data().unwrap();
WalletTransactionsContent::tx_item_ui(ui, tx, rect, &data, |ui| {
// 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;
}
ui.add_space(6.0);
// Transaction item background setup.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(WalletTransactionsContent::TX_ITEM_HEIGHT);
let rounding = View::item_rounding(0, 2, false);
let bg = Colors::TRANSPARENT;
// Show transaction amount status and time.
let on_click = (false, || {});
WalletTransactionsContent::tx_item_ui(ui, tx, rect, bg, rounding, &data, on_click, |ui| {
// Show button to delete transaction from database.
if tx.data.confirmed || tx.cancelled() {
let r = View::item_rounding(0, 2, true);
View::item_button(ui, r, FILE_X, Some(Colors::inactive_text()), || {
on_delete(tx.data.id);
});
}
// 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;
}
if wallet.synced_from_node() && !tx.cancelled() && !tx.cancelling() && !tx.posting() {
let repeat = tx.broadcasting_timed_out(&wallet);
// Draw button to cancel transaction.
if tx.can_cancel() || repeat {
let r = View::item_rounding(0, 2, true);
View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || {
wallet.task(WalletTask::Cancel(tx.data.id));
Modal::close();
});
}
// Draw button to repeat transaction action.
if tx.can_repeat_action(wallet) || repeat {
let r = if tx.can_finalize() || tx.can_cancel() {
CornerRadius::default()
} else {
View::item_rounding(0, 2, true)
};
WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, repeat);
}
}
});
if wallet.synced_from_node() && !tx.cancelled() && !tx.cancelling() && !tx.posting() {
let rebroadcast = tx.broadcasting_timed_out(&wallet);
// Draw button to cancel transaction.
if tx.can_cancel() || rebroadcast {
let r = View::item_rounding(0, 2, true);
View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || {
wallet.task(WalletTask::Cancel(tx.data.clone()));
Modal::close();
});
}
// Draw button to repeat transaction action.
if tx.can_repeat_action() || rebroadcast {
let r = if tx.can_finalize() || tx.can_cancel() {
CornerRadius::default()
} else {
View::item_rounding(0, 2, true)
};
WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, rebroadcast);
}
}
});
// 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 or sender address.
let addr = if tx.data.tx_type == TxLogEntryType::TxSent {
&tx.receiver
} else {
&tx.sender
};
if let Some(addr) = addr {
let label = format!("{} {}", CIRCLE_HALF, t!("network_mining.address"));
info_item_ui(ui, addr.to_string(), label, true, cb);
}
}
// 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 or sender address.
let (addr, label) = if tx.data.tx_type == TxLogEntryType::TxSent {
(&tx.receiver, t!("transport.receiver_address"))
} else {
(&tx.sender, t!("transport.sender_address"))
};
if let Some(addr) = addr {
let label = format!("{} {}", CIRCLE_HALF, label.replace(":", ""));
info_item_ui(ui, addr.to_string(), label, true, cb);
}
}
}
/// 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);
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);
// 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(), StrokeKind::Outside);
ui.painter().rect(
bg_rect,
rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside,
);
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 as u8;
rounding.sw = 0.0 as u8;
View::item_button(ui, rounding, COPY, None, || {
cb.copy_string_to_buffer(value.clone());
});
}
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 as u8;
rounding.sw = 0.0 as u8;
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);
});
});
});
}
// 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);
});
});
});
}
+53 -48
View File
@@ -22,55 +22,60 @@ pub const GRIN: &str = "ツ";
/// Content container to simplify modals management and navigation.
pub trait WalletContentContainer {
/// List of allowed [`Modal`] identifiers.
fn modal_ids(&self) -> Vec<&'static str>;
/// Draw modal content.
fn modal_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks);
/// Draw container content.
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks);
/// Draw content, to call by parent container.
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Draw modal content.
if let Some(id) = Modal::opened() {
if self.modal_ids().contains(&id) {
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
self.modal_ui(ui, wallet, modal, cb);
});
}
}
self.container_ui(ui, wallet, cb);
}
/// List of allowed [`Modal`] identifiers.
fn modal_ids(&self) -> Vec<&'static str>;
/// Draw modal content.
fn modal_ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks,
);
/// Draw container content.
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks);
/// Draw content, to call by parent container.
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Draw modal content.
if let Some(id) = Modal::opened() {
if self.modal_ids().contains(&id) {
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
self.modal_ui(ui, wallet, modal, cb);
});
}
}
self.container_ui(ui, wallet, cb);
}
}
/// Get wallet status text.
pub fn wallet_status_text(wallet: &Wallet) -> String {
if wallet.sync_error() {
format!("{} {}", WARNING_CIRCLE, t!("error"))
} else if wallet.is_closing() {
format!("{} {}", SPINNER, t!("wallets.closing"))
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
if repair_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.checking"),
repair_progress)
}
} else if wallet.syncing() {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.loading"),
info_progress)
}
} else if wallet.is_open() {
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
} else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
}
}
if wallet.sync_error() && wallet.is_open() {
format!("{} {}", WARNING_CIRCLE, t!("error"))
} else if wallet.is_closing() {
format!("{} {}", SPINNER, t!("wallets.closing"))
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
if repair_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.checking"))
} else {
format!(
"{} {}: {}%",
SPINNER,
t!("wallets.checking"),
repair_progress
)
}
} else if wallet.syncing() {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else {
format!("{} {}: {}%", SPINNER, t!("wallets.loading"), info_progress)
}
} else if wallet.is_open() {
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
} else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
}
}
+62 -54
View File
@@ -23,61 +23,69 @@ use serde::de::StdError;
use crate::AppConfig;
/// Handles http requests.
pub struct HttpClient {
}
pub struct HttpClient {}
impl HttpClient {
/// Send request.
pub async fn send<B>(req: Request<B>) -> Result<Response<Incoming>, Error>
where B: Body + Send + 'static + Unpin, <B as Body>::Data: Send,
B::Data: Send,
B::Error: Into<Box<dyn StdError + Send + Sync>>,
{
if AppConfig::use_proxy() {
if let Some(url) = AppConfig::socks_proxy_url() {
Self::send_socks_proxy(url, req).await
} else {
Self::send_http_proxy(AppConfig::http_proxy_url().unwrap(), req).await
}
} else {
let client = Client::builder(TokioExecutor::new())
.build::<_, B>(HttpsConnector::new());
client.request(req).await
}
}
/// Send request.
pub async fn send<B>(req: Request<B>) -> Result<Response<Incoming>, Error>
where
B: Body + Send + 'static + Unpin,
<B as Body>::Data: Send,
B::Data: Send,
B::Error: Into<Box<dyn StdError + Send + Sync>>,
{
if AppConfig::use_proxy() {
if let Some(url) = AppConfig::socks_proxy_url() {
Self::send_socks_proxy(url, req).await
} else {
Self::send_http_proxy(AppConfig::http_proxy_url().unwrap(), req).await
}
} else {
let client = Client::builder(TokioExecutor::new()).build::<_, B>(HttpsConnector::new());
client.request(req).await
}
}
/// Create socks proxy client.
pub async fn send_socks_proxy<B>(proxy_url: String, req: Request<B>)
-> Result<Response<Incoming>, Error>
where B: Body + Send + 'static + Unpin, <B as Body>::Data: Send,
B::Data: Send,
B::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let connector = HttpsConnector::new();
let uri = proxy_url.parse().unwrap();
let proxy = hyper_socks2::SocksConnector {
proxy_addr: uri,
auth: None,
connector,
}.with_tls().unwrap();
let client = Client::builder(TokioExecutor::new())
.build::<_, B>(proxy);
client.request(req).await
}
/// Create socks proxy client.
pub async fn send_socks_proxy<B>(
proxy_url: String,
req: Request<B>,
) -> Result<Response<Incoming>, Error>
where
B: Body + Send + 'static + Unpin,
<B as Body>::Data: Send,
B::Data: Send,
B::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let connector = HttpsConnector::new();
let uri = proxy_url.parse().unwrap();
let proxy = hyper_socks2::SocksConnector {
proxy_addr: uri,
auth: None,
connector,
}
.with_tls()
.unwrap();
let client = Client::builder(TokioExecutor::new()).build::<_, B>(proxy);
client.request(req).await
}
/// Create http proxy client.
pub async fn send_http_proxy<B>(proxy_url: String, req: Request<B>)
-> Result<Response<Incoming>, Error>
where B: Body + Send + 'static + Unpin, <B as Body>::Data: Send,
B::Data: Send,
B::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let uri = proxy_url.parse().unwrap();
let proxy = Proxy::new(Intercept::All, uri);
let connector = HttpsConnector::new();
let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap();
let client = Client::builder(TokioExecutor::new())
.build::<_, B>(proxy_connector);
client.request(req).await
}
}
/// Create http proxy client.
pub async fn send_http_proxy<B>(
proxy_url: String,
req: Request<B>,
) -> Result<Response<Incoming>, Error>
where
B: Body + Send + 'static + Unpin,
<B as Body>::Data: Send,
B::Data: Send,
B::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let uri = proxy_url.parse().unwrap();
let proxy = Proxy::new(Intercept::All, uri);
let connector = HttpsConnector::new();
let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap();
let client = Client::builder(TokioExecutor::new()).build::<_, B>(proxy_connector);
client.request(req).await
}
}

Some files were not shown because too many files have changed in this diff Show More