mirror of
https://code.gri.mw/GUI/grim.git
synced 2026-07-12 09:48:55 +00:00
Build 37: fix Android — typing, install-alongside-Grim, status bar, edge padding
- Typing (critical): the Goblin UI used plain egui::TextEdit, which never received input because Grim's native-keyboard path only feeds Grim's own TextEdit widget. The native IME is also aborted by the windowing system on this app. Fix: every Goblin field now uses Grim's TextEdit, opted into Grim's own on-screen keyboard on Android via a new .soft_keyboard() builder (no native-IME dependency). Additive TextEdit options (hint_text/text_color/ body/soft_keyboard) leave Grim's defaults and existing call sites unchanged. - Install alongside Grim: FileProvider authority mw.gri.android.fileprovider → st.goblin.wallet.fileprovider (Android rejects duplicate authorities). - Status bar: app draws edge-to-edge, so dark icons vanished on the dark theme. New theme-aware JNI call sets light/dark status-bar icons per theme. - Edge padding: centered_column now keeps an 18dp side gutter on phones instead of running content flush to the screen edge. Validated on the API-36 emulator: typed into fields, padding gutter present.
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
|
||||
<provider
|
||||
android:name=".FileProvider"
|
||||
android:authorities="mw.gri.android.fileprovider"
|
||||
android:authorities="st.goblin.wallet.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
@@ -40,6 +40,7 @@
|
||||
<activity
|
||||
android:launchMode="singleTask"
|
||||
android:name=".MainActivity"
|
||||
android:windowSoftInputMode="adjustResize|stateUnchanged"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode|density|locale|layoutDirection|fontScale|colorMode"
|
||||
android:exported="true">
|
||||
|
||||
|
||||
@@ -503,13 +503,27 @@ public class MainActivity extends GameActivity {
|
||||
// Called from native code to share data from provided path.
|
||||
public void shareData(String path) {
|
||||
File file = new File(path);
|
||||
Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file);
|
||||
Uri uri = FileProvider.getUriForFile(this, "st.goblin.wallet.fileprovider", file);
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
intent.setType("text/*");
|
||||
startActivity(Intent.createChooser(intent, "Share data"));
|
||||
}
|
||||
|
||||
// Called from native code to set status-bar icon color to contrast the
|
||||
// in-app theme. white = light icons for a dark background. The app draws
|
||||
// edge-to-edge, so the OS status-bar background is the app's own content;
|
||||
// the icons must follow the theme or they vanish (dark-on-dark).
|
||||
public void setStatusBarWhiteIcons(boolean white) {
|
||||
runOnUiThread(() -> {
|
||||
androidx.core.view.WindowInsetsControllerCompat c =
|
||||
androidx.core.view.WindowCompat.getInsetsController(getWindow(),
|
||||
getWindow().getDecorView());
|
||||
// isAppearanceLightStatusBars == true means DARK icons.
|
||||
c.setAppearanceLightStatusBars(!white);
|
||||
});
|
||||
}
|
||||
|
||||
// Called from native code to check if device is using dark theme.
|
||||
public boolean useDarkTheme() {
|
||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
|
||||
@@ -44,6 +44,8 @@ pub struct App<Platform> {
|
||||
resize_direction: Option<ResizeDirection>,
|
||||
/// Flag to check if it's first draw.
|
||||
first_draw: bool,
|
||||
/// Last status-bar icon state pushed to the platform (Android).
|
||||
status_bar_white: Option<bool>,
|
||||
}
|
||||
|
||||
impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
@@ -53,6 +55,7 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
content: Content::default(),
|
||||
resize_direction: None,
|
||||
first_draw: true,
|
||||
status_bar_white: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +77,14 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
self.first_draw = false;
|
||||
}
|
||||
|
||||
// Keep the Android status-bar icons readable against the in-app theme
|
||||
// (the app draws edge-to-edge over the status bar). Only on change.
|
||||
let white_icons = crate::gui::theme::status_bar_white_icons();
|
||||
if self.status_bar_white != Some(white_icons) {
|
||||
self.platform.set_status_bar_white_icons(white_icons);
|
||||
self.status_bar_white = Some(white_icons);
|
||||
}
|
||||
|
||||
// Handle Esc keyboard key event and platform Back button key event.
|
||||
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
|
||||
if back_pressed
|
||||
|
||||
@@ -199,6 +199,14 @@ impl PlatformCallbacks for Android {
|
||||
}
|
||||
|
||||
fn clear_user_attention(&self) {}
|
||||
|
||||
fn set_status_bar_white_icons(&self, white: bool) {
|
||||
self.call_java_method(
|
||||
"setStatusBarWhiteIcons",
|
||||
"(Z)V",
|
||||
&[JValue::Bool(white as u8)],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
||||
@@ -43,4 +43,8 @@ pub trait PlatformCallbacks {
|
||||
fn request_user_attention(&self);
|
||||
fn user_attention_required(&self) -> bool;
|
||||
fn clear_user_attention(&self);
|
||||
|
||||
/// Set the status-bar icon color to contrast the current theme. `white` =
|
||||
/// light icons (for a dark background). No-op off Android.
|
||||
fn set_status_bar_white_icons(&self, _white: bool) {}
|
||||
}
|
||||
|
||||
@@ -196,6 +196,12 @@ pub fn tokens() -> &'static ThemeTokens {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the status bar should use light (white) icons: true on the dark
|
||||
/// theme (dark top), false on the light/yellow themes (bright top).
|
||||
pub fn status_bar_white_icons() -> bool {
|
||||
tokens().dark_base
|
||||
}
|
||||
|
||||
/// Density scales from the design handoff.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum DensityKind {
|
||||
|
||||
+67
-73
@@ -26,7 +26,7 @@ use egui::{Align, Color32, Layout, Margin, RichText, ScrollArea, Sense, Vec2};
|
||||
use crate::gui::icons::{ARROW_DOWN, CHECK, CLOCK, COPY, QR_CODE, USER_CIRCLE, WALLET};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::theme::{self, fonts};
|
||||
use crate::gui::views::{Content, View};
|
||||
use crate::gui::views::{Content, TextEdit, View};
|
||||
use crate::wallet::Wallet;
|
||||
use crate::wallet::types::WalletData;
|
||||
|
||||
@@ -1225,8 +1225,8 @@ impl GoblinWalletView {
|
||||
fn me_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
let t = theme::tokens();
|
||||
match self.settings_page {
|
||||
SettingsPage::Node => return self.node_settings_ui(ui, wallet),
|
||||
SettingsPage::Relays => return self.relays_ui(ui, wallet),
|
||||
SettingsPage::Node => return self.node_settings_ui(ui, wallet, cb),
|
||||
SettingsPage::Relays => return self.relays_ui(ui, wallet, cb),
|
||||
SettingsPage::Nips => return self.nips_ui(ui),
|
||||
SettingsPage::Pairing => return self.pairing_settings_ui(ui),
|
||||
SettingsPage::Main => {}
|
||||
@@ -1395,7 +1395,7 @@ impl GoblinWalletView {
|
||||
if self.claim.is_none() {
|
||||
self.claim = Some(ClaimState::default());
|
||||
}
|
||||
self.claim_ui(ui, wallet);
|
||||
self.claim_ui(ui, wallet, cb);
|
||||
ui.add_space(8.0);
|
||||
w::card(ui, |ui| {
|
||||
if !npub.is_empty() {
|
||||
@@ -1454,7 +1454,7 @@ impl GoblinWalletView {
|
||||
}
|
||||
if self.import_nsec.is_some() {
|
||||
ui.add_space(8.0);
|
||||
self.import_nsec_ui(ui, wallet);
|
||||
self.import_nsec_ui(ui, wallet, cb);
|
||||
}
|
||||
|
||||
ui.add_space(16.0);
|
||||
@@ -1643,7 +1643,7 @@ impl GoblinWalletView {
|
||||
});
|
||||
}
|
||||
|
||||
fn node_settings_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
fn node_settings_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
use crate::wallet::types::ConnectionMethod;
|
||||
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
||||
let t = theme::tokens();
|
||||
@@ -1730,21 +1730,19 @@ impl GoblinWalletView {
|
||||
|
||||
ui.add_space(16.0);
|
||||
settings_group(ui, "Add external node", |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.node_url_input)
|
||||
.hint_text("https://node.example.com:3413")
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("set_node_url"))
|
||||
.hint_text("https://node.example.com:3413")
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut self.node_url_input, cb);
|
||||
ui.add_space(8.0);
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.node_secret_input)
|
||||
.hint_text("API secret (optional)")
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("set_node_secret"))
|
||||
.hint_text("API secret (optional)")
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut self.node_secret_input, cb);
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
let url = self.node_url_input.trim().to_string();
|
||||
@@ -1770,7 +1768,7 @@ impl GoblinWalletView {
|
||||
}
|
||||
|
||||
/// Relay list editor; saving restarts the nostr service live.
|
||||
fn relays_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
fn relays_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
let t = theme::tokens();
|
||||
if self.sub_header(ui, "Relays") {
|
||||
self.settings_page = SettingsPage::Main;
|
||||
@@ -1821,13 +1819,12 @@ impl GoblinWalletView {
|
||||
|
||||
ui.add_space(16.0);
|
||||
settings_group(ui, "Add relay", |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.relay_input)
|
||||
.hint_text("wss://relay.example.com")
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("set_relay"))
|
||||
.hint_text("wss://relay.example.com")
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut self.relay_input, cb);
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
let relay = self.relay_input.trim().to_string();
|
||||
@@ -2049,24 +2046,22 @@ impl GoblinWalletView {
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
w::field_well(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut rotate.reset_input)
|
||||
.hint_text("Type RESET")
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("rotate_reset"))
|
||||
.hint_text("Type RESET")
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut rotate.reset_input, cb);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
w::field_well(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut rotate.password)
|
||||
.hint_text("Wallet password")
|
||||
.password(true)
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("rotate_pass"))
|
||||
.hint_text("Wallet password")
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut rotate.password, cb);
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
let armed = rotate.reset_input.trim() == "RESET" && !rotate.password.is_empty();
|
||||
@@ -2182,7 +2177,7 @@ impl GoblinWalletView {
|
||||
}
|
||||
|
||||
/// Inline nsec-import flow: replaces the identity with an imported key.
|
||||
fn import_nsec_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
fn import_nsec_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
let t = theme::tokens();
|
||||
let import = self.import_nsec.as_mut().unwrap();
|
||||
if import.stage == 3 {
|
||||
@@ -2222,36 +2217,33 @@ impl GoblinWalletView {
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
w::field_well(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut import.nsec)
|
||||
.hint_text("nsec1… or identity backup JSON")
|
||||
.password(true)
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("import_nsec"))
|
||||
.hint_text("nsec1… or identity backup JSON")
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut import.nsec, cb);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
w::field_well(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut import.password)
|
||||
.hint_text("Wallet password")
|
||||
.password(true)
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("import_pass"))
|
||||
.hint_text("Wallet password")
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut import.password, cb);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
w::field_well(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut import.backup_password)
|
||||
.hint_text("Backup password (only if exported elsewhere)")
|
||||
.password(true)
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("import_backup_pass"))
|
||||
.hint_text("Backup password (only if exported elsewhere)")
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut import.backup_password, cb);
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
let pasted = import.nsec.trim();
|
||||
@@ -2357,7 +2349,7 @@ impl GoblinWalletView {
|
||||
}
|
||||
|
||||
/// Inline username-claim widget (availability check + register over Tor).
|
||||
fn claim_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
fn claim_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
let t = theme::tokens();
|
||||
// Poll the worker result; avatar invalidation happens after the
|
||||
// claim borrow is released.
|
||||
@@ -2530,12 +2522,14 @@ impl GoblinWalletView {
|
||||
.font(FontId::new(16.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
let edit = egui::TextEdit::singleline(&mut claim.input)
|
||||
let before = claim.input.clone();
|
||||
TextEdit::new(egui::Id::from("settings_claim"))
|
||||
.hint_text("yourname")
|
||||
.desired_width(ui.available_width() - 20.0)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false);
|
||||
if ui.add(edit).changed() {
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut claim.input, cb);
|
||||
if claim.input != before {
|
||||
claim.available = None;
|
||||
claim.message = None;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::theme::{self, fonts};
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition, QrScanResult};
|
||||
use crate::gui::views::wallets::creation::MnemonicSetup;
|
||||
use crate::gui::views::{CameraScanContent, Content, Modal, View};
|
||||
use crate::gui::views::{CameraScanContent, Content, Modal, TextEdit, View};
|
||||
use crate::node::Node;
|
||||
use crate::wallet::types::{ConnectionMethod, PhraseMode, PhraseSize};
|
||||
use crate::wallet::{ConnectionsConfig, ExternalConnection, Wallet, WalletList};
|
||||
@@ -121,11 +121,11 @@ impl OnboardingContent {
|
||||
ui.add_space(View::get_top_inset() + 24.0);
|
||||
match self.step {
|
||||
Step::Intro => self.intro_ui(ui),
|
||||
Step::Node => self.node_ui(ui),
|
||||
Step::WalletSetup => self.wallet_setup_ui(ui),
|
||||
Step::Node => self.node_ui(ui, cb),
|
||||
Step::WalletSetup => self.wallet_setup_ui(ui, cb),
|
||||
Step::Words => self.words_ui(ui, wallets, cb),
|
||||
Step::ConfirmWords => self.confirm_ui(ui, wallets, cb),
|
||||
Step::Identity => done = self.identity_ui(ui),
|
||||
Step::Identity => done = self.identity_ui(ui, cb),
|
||||
}
|
||||
ui.add_space(View::get_bottom_inset() + 24.0);
|
||||
});
|
||||
@@ -289,7 +289,7 @@ impl OnboardingContent {
|
||||
.clicked()
|
||||
}
|
||||
|
||||
fn node_ui(&mut self, ui: &mut egui::Ui) {
|
||||
fn node_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let t = theme::tokens();
|
||||
self.step_header(
|
||||
ui,
|
||||
@@ -320,13 +320,12 @@ impl OnboardingContent {
|
||||
if !self.integrated {
|
||||
ui.add_space(10.0);
|
||||
w::field_well(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.ext_url)
|
||||
.hint_text("https://node.example.com")
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("onb_ext_url"))
|
||||
.hint_text("https://node.example.com")
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut self.ext_url, cb);
|
||||
});
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
@@ -354,7 +353,7 @@ impl OnboardingContent {
|
||||
|
||||
// ── Wallet name + password, create vs restore ───────────────────────
|
||||
|
||||
fn wallet_setup_ui(&mut self, ui: &mut egui::Ui) {
|
||||
fn wallet_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let t = theme::tokens();
|
||||
self.step_header(ui, "STEP 2 OF 3 · WALLET", "Set up your wallet", Step::Node);
|
||||
|
||||
@@ -381,35 +380,32 @@ impl OnboardingContent {
|
||||
ui.add_space(14.0);
|
||||
|
||||
w::field_well(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.name)
|
||||
.hint_text("Wallet name")
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("onb_name"))
|
||||
.hint_text("Wallet name")
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut self.name, cb);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
w::field_well(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.pass)
|
||||
.hint_text("Password")
|
||||
.password(true)
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("onb_pass"))
|
||||
.hint_text("Password")
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut self.pass, cb);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
w::field_well(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.pass2)
|
||||
.hint_text("Repeat password")
|
||||
.password(true)
|
||||
.desired_width(f32::INFINITY)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false),
|
||||
);
|
||||
TextEdit::new(egui::Id::from("onb_pass2"))
|
||||
.hint_text("Repeat password")
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut self.pass2, cb);
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
@@ -663,7 +659,7 @@ impl OnboardingContent {
|
||||
|
||||
// ── Identity (optional username) ─────────────────────────────────────
|
||||
|
||||
fn identity_ui(&mut self, ui: &mut egui::Ui) -> Option<Wallet> {
|
||||
fn identity_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) -> Option<Wallet> {
|
||||
let t = theme::tokens();
|
||||
// No back from here: the wallet exists now.
|
||||
ui.label(
|
||||
@@ -798,12 +794,14 @@ impl OnboardingContent {
|
||||
.font(FontId::new(16.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
let edit = egui::TextEdit::singleline(&mut self.claim.input)
|
||||
let before = self.claim.input.clone();
|
||||
TextEdit::new(egui::Id::from("onb_claim"))
|
||||
.hint_text("yourname")
|
||||
.desired_width(ui.available_width())
|
||||
.text_color(t.surface_text)
|
||||
.frame(false);
|
||||
if ui.add(edit).changed() {
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut self.claim.input, cb);
|
||||
if self.claim.input != before {
|
||||
self.claim.available = None;
|
||||
self.claim.message = None;
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ use eframe::epaint::FontId;
|
||||
use egui::{Align, Layout, RichText, ScrollArea, Sense, Vec2};
|
||||
use grin_core::core::amount_from_hr_string;
|
||||
|
||||
use crate::gui::icons::{ARROW_LEFT, MAGNIFYING_GLASS, QR_CODE, USERS};
|
||||
use crate::gui::icons::{ARROW_LEFT, MAGNIFYING_GLASS, USERS};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::theme::{self, fonts};
|
||||
use crate::gui::views::types::QrScanResult;
|
||||
use crate::gui::views::{CameraContent, View};
|
||||
use crate::gui::views::{CameraContent, TextEdit, View};
|
||||
use crate::nostr::nip05;
|
||||
use crate::wallet::Wallet;
|
||||
use crate::wallet::types::WalletTask;
|
||||
@@ -200,7 +200,7 @@ impl SendFlow {
|
||||
crate::gui::views::Content::SIDE_PANEL_WIDTH * 1.2,
|
||||
|ui| match self.stage {
|
||||
Stage::Recipient => done = self.recipient_ui(ui, wallet, cb, avatars),
|
||||
Stage::Amount => done = self.amount_ui(ui, wallet, avatars),
|
||||
Stage::Amount => done = self.amount_ui(ui, wallet, avatars, cb),
|
||||
Stage::Review => done = self.review_ui(ui, wallet, avatars),
|
||||
Stage::Sending => self.sending_ui(ui, wallet),
|
||||
Stage::Success => done = self.success_ui(ui),
|
||||
@@ -284,27 +284,18 @@ impl SendFlow {
|
||||
.color(t.surface_text_mute),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let edit = egui::TextEdit::singleline(&mut search)
|
||||
let mut te = TextEdit::new(egui::Id::from("send_search"))
|
||||
.hint_text("@handle, npub, or name")
|
||||
.desired_width(ui.available_width() - 34.0)
|
||||
.text_color(t.surface_text)
|
||||
.frame(false);
|
||||
ui.add(edit);
|
||||
let scan_btn = ui
|
||||
.add(
|
||||
egui::Label::new(
|
||||
RichText::new(QR_CODE)
|
||||
.font(FontId::new(20.0, fonts::regular()))
|
||||
.color(t.surface_text),
|
||||
)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
open_scan = scan_btn.clicked();
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.scan_qr();
|
||||
te.ui(ui, &mut search, cb);
|
||||
// scan_qr() already starts the camera on tap.
|
||||
open_scan = te.scan_pressed;
|
||||
});
|
||||
});
|
||||
if open_scan {
|
||||
cb.start_camera();
|
||||
self.scan = Some(CameraContent::default());
|
||||
self.error = None;
|
||||
}
|
||||
@@ -716,6 +707,7 @@ impl SendFlow {
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
avatars: &mut AvatarTextures,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
) -> bool {
|
||||
let t = theme::tokens();
|
||||
if self.back_header(ui, "Amount") {
|
||||
@@ -796,12 +788,14 @@ impl SendFlow {
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let edit = egui::TextEdit::singleline(&mut self.note)
|
||||
let note_id = egui::Id::from("send_note");
|
||||
TextEdit::new(note_id)
|
||||
.hint_text("Add a note…")
|
||||
.desired_width(ui.available_width())
|
||||
.text_color(t.surface_text)
|
||||
.frame(false);
|
||||
note_focused = ui.add(edit).has_focus();
|
||||
.body()
|
||||
.soft_keyboard()
|
||||
.ui(ui, &mut self.note, cb);
|
||||
note_focused = ui.ctx().memory(|m| m.has_focus(note_id));
|
||||
});
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
|
||||
@@ -643,9 +643,12 @@ pub fn fill_bg(ui: &Ui, color: Color32) {
|
||||
/// would start the row a single line tall, so a `ScrollArea` inside would
|
||||
/// clip everything below the first widget.
|
||||
pub fn centered_column<R>(ui: &mut Ui, width: f32, add: impl FnOnce(&mut Ui) -> R) -> R {
|
||||
// Always keep a side gutter so content never runs flush to the screen
|
||||
// edge on phones (where `width` exceeds the available width).
|
||||
const MIN_SIDE_PAD: f32 = 18.0;
|
||||
let avail = ui.available_width();
|
||||
let w = width.min(avail);
|
||||
let margin = ((avail - w) / 2.0).max(0.0);
|
||||
let w = width.min(avail - MIN_SIDE_PAD * 2.0).max(0.0);
|
||||
let margin = ((avail - w) / 2.0).max(MIN_SIDE_PAD);
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.min.x += margin;
|
||||
rect.max.x = rect.min.x + w;
|
||||
|
||||
@@ -52,6 +52,12 @@ pub struct TextEdit {
|
||||
numeric: bool,
|
||||
/// Flag to not show soft keyboard.
|
||||
no_soft_keyboard: bool,
|
||||
/// Optional placeholder shown when empty.
|
||||
hint: Option<String>,
|
||||
/// Optional text color override (defaults to the theme text color).
|
||||
text_color: Option<egui::Color32>,
|
||||
/// Use the body text style instead of the default heading.
|
||||
body_font: bool,
|
||||
}
|
||||
|
||||
impl TextEdit {
|
||||
@@ -73,6 +79,9 @@ impl TextEdit {
|
||||
enter_pressed: false,
|
||||
numeric: false,
|
||||
no_soft_keyboard: is_android(),
|
||||
hint: None,
|
||||
text_color: None,
|
||||
body_font: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,14 +181,21 @@ impl TextEdit {
|
||||
|
||||
// Show text edit.
|
||||
let text_edit_resp = egui::TextEdit::singleline(input)
|
||||
.text_color(if self.enabled {
|
||||
Colors::text(false)
|
||||
} else {
|
||||
.hint_text(self.hint.clone().unwrap_or_default())
|
||||
.text_color(if !self.enabled {
|
||||
Colors::inactive_text()
|
||||
} else if let Some(c) = self.text_color {
|
||||
c
|
||||
} else {
|
||||
Colors::text(false)
|
||||
})
|
||||
.interactive(self.enabled)
|
||||
.id(self.id)
|
||||
.font(TextStyle::Heading)
|
||||
.font(if self.body_font {
|
||||
TextStyle::Body
|
||||
} else {
|
||||
TextStyle::Heading
|
||||
})
|
||||
.min_size(edit_rect.size())
|
||||
.margin(if View::is_desktop() {
|
||||
egui::Margin::symmetric(4, 2)
|
||||
@@ -206,8 +222,14 @@ impl TextEdit {
|
||||
|
||||
// Reset keyboard state for newly focused.
|
||||
if clicked || self.focus_request {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(ViewportCommand::IMEAllowed(true));
|
||||
// Only ask for the native IME when this field uses it.
|
||||
// Fields that opt into the on-screen keyboard must not
|
||||
// also trip the (flaky, on some devices aborted) native
|
||||
// soft keyboard.
|
||||
if self.no_soft_keyboard {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(ViewportCommand::IMEAllowed(true));
|
||||
}
|
||||
KeyboardContent::reset_window_state();
|
||||
}
|
||||
|
||||
@@ -247,7 +269,10 @@ impl TextEdit {
|
||||
multiline: bool,
|
||||
value: &mut String,
|
||||
) -> bool {
|
||||
let event: Option<KeyboardEvent> = if is_android() {
|
||||
// Native IME (Grim's default on Android) feeds LAST_SOFT_KEYBOARD_EVENT;
|
||||
// the on-screen keyboard feeds KeyboardContent. A field using the
|
||||
// on-screen keyboard reads the latter even on Android.
|
||||
let event: Option<KeyboardEvent> = if is_android() && self.no_soft_keyboard {
|
||||
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
|
||||
w_input.take()
|
||||
} else {
|
||||
@@ -411,6 +436,32 @@ impl TextEdit {
|
||||
self.no_soft_keyboard = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use the on-screen keyboard on every platform, including Android (where
|
||||
/// the default is the native IME). Reliable when the native keyboard is
|
||||
/// unavailable or aborted by the windowing system.
|
||||
pub fn soft_keyboard(mut self) -> Self {
|
||||
self.no_soft_keyboard = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set placeholder text shown when the field is empty.
|
||||
pub fn hint_text(mut self, hint: impl Into<String>) -> Self {
|
||||
self.hint = Some(hint.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the text color (e.g. to match an on-card theme token).
|
||||
pub fn text_color(mut self, color: egui::Color32) -> Self {
|
||||
self.text_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Render with the body text style instead of the default heading.
|
||||
pub fn body(mut self) -> Self {
|
||||
self.body_font = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if current system is Android.
|
||||
|
||||
Reference in New Issue
Block a user