From 77ac91e810b82ac05e74d9219861822d277359d2 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 7 Aug 2025 11:25:41 -0700 Subject: [PATCH] Implement soft keyboard visibility on Android - Added `SoftKeyboardContext` enum and support for calculating keyboard insets from both virtual and platform sources - Updated `AppContext` to provide `soft_keyboard_rect` for determining visible keyboard area - Adjusted UI rendering to shift content when input boxes intersect with the soft keyboard, preventing overlap - Modified `MainActivity` and Android manifest to use `windowSoftInputMode="adjustResize"` and updated window inset handling - Introduced helper functions (`include_input`, `input_rect`, `clear_input_rect`) in `notedeck_ui` for tracking focused input boxes - Fixed Android JNI keyboard height reporting to clamp negative values Together, these changes allow the app to correctly detect and respond to soft keyboard visibility on Android, ensuring input fields remain accessible when typing. Fixes: https://github.com/damus-io/notedeck/issues/946 Fixes: https://github.com/damus-io/notedeck/issues/1043 --- Cargo.lock | 40 ++++++------- Cargo.toml | 18 +++--- Makefile | 2 +- crates/notedeck/Cargo.toml | 1 + crates/notedeck/src/app.rs | 15 +++++ crates/notedeck/src/context.rs | 48 ++++++++++++++++ crates/notedeck/src/lib.rs | 2 +- crates/notedeck/src/platform/android.rs | 2 +- crates/notedeck/src/platform/mod.rs | 10 ++++ .../android/app/src/main/AndroidManifest.xml | 3 +- .../java/com/damus/notedeck/MainActivity.java | 25 +++++++-- crates/notedeck_chrome/src/android.rs | 5 +- crates/notedeck_chrome/src/chrome.rs | 45 ++++++++++++--- crates/notedeck_columns/src/app.rs | 23 ++++---- crates/notedeck_columns/src/nav.rs | 2 +- .../src/ui/account_login_view.rs | 2 +- crates/notedeck_columns/src/ui/note/post.rs | 1 + .../notedeck_columns/src/ui/profile/edit.rs | 56 ++++++++++++------- crates/notedeck_columns/src/ui/search/mod.rs | 2 +- crates/notedeck_columns/src/ui/settings.rs | 10 ++-- crates/notedeck_columns/src/ui/wallet.rs | 22 +++++--- crates/notedeck_dave/src/ui/dave.rs | 1 + crates/notedeck_ui/src/context_menu.rs | 4 ++ crates/notedeck_ui/src/lib.rs | 31 ++++++++++ 24 files changed, 276 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96e9bbc0..6e32d21c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,7 +105,8 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-activity" version = "0.6.0" -source = "git+https://github.com/damus-io/android-activity?rev=a8948332c7c551303d32eb26a59d0abd676e47a5#a8948332c7c551303d32eb26a59d0abd676e47a5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", "bitflags 2.9.1", @@ -125,7 +126,7 @@ dependencies = [ [[package]] name = "android-activity" version = "0.6.0" -source = "git+https://github.com/damus-io/android-activity?rev=c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9#c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9" +source = "git+https://github.com/damus-io/android-activity?rev=092a83b747937a2890ac219617a4252c001842ea#092a83b747937a2890ac219617a4252c001842ea" dependencies = [ "android-properties", "bitflags 2.9.1", @@ -1402,7 +1403,7 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "dpi" version = "0.1.1" -source = "git+https://github.com/damus-io/winit?rev=eaff639ab0a14fccf595241f687be883154b267c#eaff639ab0a14fccf595241f687be883154b267c" +source = "git+https://github.com/damus-io/winit?rev=9e4ea9de75222d2523a20f18d3a0a108c573737d#9e4ea9de75222d2523a20f18d3a0a108c573737d" [[package]] name = "dpi" @@ -1419,17 +1420,17 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecolor" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" +source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f" dependencies = [ "bytemuck", - "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)", + "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f)", "serde", ] [[package]] name = "eframe" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" +source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f" dependencies = [ "ahash", "bytemuck", @@ -1465,13 +1466,13 @@ dependencies = [ [[package]] name = "egui" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" +source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f" dependencies = [ "accesskit", "ahash", "backtrace", "bitflags 2.9.1", - "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)", + "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f)", "epaint", "log", "nohash-hasher", @@ -1483,7 +1484,7 @@ dependencies = [ [[package]] name = "egui-wgpu" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" +source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f" dependencies = [ "ahash", "bytemuck", @@ -1502,7 +1503,7 @@ dependencies = [ [[package]] name = "egui-winit" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" +source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f" dependencies = [ "ahash", "arboard", @@ -1520,7 +1521,7 @@ dependencies = [ [[package]] name = "egui_extras" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" +source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f" dependencies = [ "ahash", "egui", @@ -1537,7 +1538,7 @@ dependencies = [ [[package]] name = "egui_glow" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" +source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f" dependencies = [ "ahash", "bytemuck", @@ -1616,7 +1617,7 @@ checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" [[package]] name = "emath" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" +source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f" dependencies = [ "bytemuck", "serde", @@ -1714,13 +1715,13 @@ dependencies = [ [[package]] name = "epaint" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" +source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f" dependencies = [ "ab_glyph", "ahash", "bytemuck", "ecolor", - "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)", + "emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f)", "epaint_default_fonts", "log", "nohash-hasher", @@ -1732,7 +1733,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" version = "0.31.1" -source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd" +source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f" [[package]] name = "equator" @@ -3506,6 +3507,7 @@ dependencies = [ name = "notedeck" version = "0.6.0" dependencies = [ + "android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=092a83b747937a2890ac219617a4252c001842ea)", "base32", "bech32", "bincode", @@ -7445,10 +7447,10 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winit" version = "0.30.8" -source = "git+https://github.com/damus-io/winit?rev=eaff639ab0a14fccf595241f687be883154b267c#eaff639ab0a14fccf595241f687be883154b267c" +source = "git+https://github.com/damus-io/winit?rev=9e4ea9de75222d2523a20f18d3a0a108c573737d#9e4ea9de75222d2523a20f18d3a0a108c573737d" dependencies = [ "ahash", - "android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9)", + "android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=092a83b747937a2890ac219617a4252c001842ea)", "atomic-waker", "bitflags 2.9.1", "block2 0.5.1", @@ -7500,7 +7502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4" dependencies = [ "ahash", - "android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=a8948332c7c551303d32eb26a59d0abd676e47a5)", + "android-activity 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "atomic-waker", "bitflags 2.9.1", "block2 0.5.1", diff --git a/Cargo.toml b/Cargo.toml index 9240f7b8..cd1c309b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,7 @@ openai-api-rs = "6.0.3" re_memory = "0.23.4" oot_bitset = "0.1.1" blurhash = "0.2.3" - +android-activity = { git = "https://github.com/damus-io/android-activity", rev = "092a83b747937a2890ac219617a4252c001842ea", features = [ "game-activity" ] } [profile.small] inherits = 'release' @@ -106,15 +106,15 @@ strip = true # Strip symbols from binary* #egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" } #epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" } -egui = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } -eframe = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } -egui-winit = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } -egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } -egui_extras = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } -epaint = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" } +egui = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" } +eframe = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" } +egui-winit = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" } +egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" } +egui_extras = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" } +epaint = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" } puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" } puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" } -#winit = { git = "https://github.com/damus-io/winit", rev = "14d61a74bee0c9863abe7ef28efae2c4d8bd3743" } +#winit = { git = "https://github.com/damus-io/winit", rev = "701a43d3c6479b0a3869acd2cebbfd410d399a59" } #winit = { path = "/home/jb55/dev/github/rust-windowing/winit" } -android-activity = { git = "https://github.com/damus-io/android-activity", rev = "a8948332c7c551303d32eb26a59d0abd676e47a5" } +#android-activity = { git = "https://github.com/damus-io/android-activity", rev = "f56c974aa5182d5fbd361879f5899eb8f11a37ec" } #android-activity = { path = "/home/jb55/dev/github/rust-mobile/android-activity/android-activity" } diff --git a/Makefile b/Makefile index 84eb3cb5..e49f3a8c 100644 --- a/Makefile +++ b/Makefile @@ -27,4 +27,4 @@ push-android-config: android: jni cd $(ANDROID_DIR) && ./gradlew installDebug adb shell am start -n com.damus.notedeck/.MainActivity - adb logcat -v color -s RustStdoutStderr -s threaded_app | tee logcat.txt + adb logcat -v color -s GameActivity -s RustStdoutStderr -s threaded_app | tee logcat.txt diff --git a/crates/notedeck/Cargo.toml b/crates/notedeck/Cargo.toml index 25105484..ed79826f 100644 --- a/crates/notedeck/Cargo.toml +++ b/crates/notedeck/Cargo.toml @@ -57,6 +57,7 @@ tokio = { workspace = true } [target.'cfg(target_os = "android")'.dependencies] jni = { workspace = true } +android-activity = { workspace = true } [features] puffin = ["puffin_egui", "dep:puffin"] diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs index 92c60fa6..5c7c1e17 100644 --- a/crates/notedeck/src/app.rs +++ b/crates/notedeck/src/app.rs @@ -22,6 +22,9 @@ use std::rc::Rc; use tracing::{error, info}; use unic_langid::{LanguageIdentifier, LanguageIdentifierError}; +#[cfg(target_os = "android")] +use android_activity::AndroidApp; + pub enum AppAction { Note(NoteAction), ToggleChrome, @@ -51,6 +54,9 @@ pub struct Notedeck { frame_history: FrameHistory, job_pool: JobPool, i18n: Localization, + + #[cfg(target_os = "android")] + android_app: Option, } /// Our chrome, which is basically nothing @@ -138,6 +144,11 @@ fn setup_puffin() { } impl Notedeck { + #[cfg(target_os = "android")] + pub fn set_android_context(&mut self, context: AndroidApp) { + self.android_app = Some(context); + } + pub fn new>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self { #[cfg(feature = "puffin")] setup_puffin(); @@ -272,6 +283,8 @@ impl Notedeck { zaps, job_pool, i18n, + #[cfg(target_os = "android")] + android_app: None, } } @@ -335,6 +348,8 @@ impl Notedeck { frame_history: &mut self.frame_history, job_pool: &mut self.job_pool, i18n: &mut self.i18n, + #[cfg(target_os = "android")] + android: self.android_app.as_ref().unwrap().clone(), } } diff --git a/crates/notedeck/src/context.rs b/crates/notedeck/src/context.rs index feac9eda..949b5a6e 100644 --- a/crates/notedeck/src/context.rs +++ b/crates/notedeck/src/context.rs @@ -8,6 +8,9 @@ use egui_winit::clipboard::Clipboard; use enostr::RelayPool; use nostrdb::Ndb; +#[cfg(target_os = "android")] +use android_activity::AndroidApp; +use egui::{Pos2, Rect}; // TODO: make this interface more sandboxed pub struct AppContext<'a> { @@ -26,4 +29,49 @@ pub struct AppContext<'a> { pub frame_history: &'a mut FrameHistory, pub job_pool: &'a mut JobPool, pub i18n: &'a mut Localization, + + #[cfg(target_os = "android")] + pub android: AndroidApp, +} + +#[derive(Debug, Clone)] +pub enum SoftKeyboardContext { + Virtual, + Platform { ppp: f32 }, +} + +impl<'a> AppContext<'a> { + pub fn soft_keyboard_rect(&self, screen_rect: Rect, ctx: SoftKeyboardContext) -> Option { + match ctx { + SoftKeyboardContext::Virtual => { + let height = 400.0; + skb_rect_from_screen_rect(screen_rect, height) + } + + #[allow(unused_variables)] + SoftKeyboardContext::Platform { ppp } => { + #[cfg(target_os = "android")] + { + use android_activity::InsetType; + let inset = self.android.get_window_insets(InsetType::Ime); + let height = inset.bottom as f32 / ppp; + skb_rect_from_screen_rect(screen_rect, height) + } + + #[cfg(not(target_os = "android"))] + { + None + } + } + } + } +} + +#[inline] +fn skb_rect_from_screen_rect(screen_rect: Rect, height: f32) -> Option { + if height == 0.0 { + return None; + } + let min = Pos2::new(0.0, screen_rect.max.y - height); + Some(Rect::from_min_max(min, screen_rect.max)) } diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs index e0562544..4aa24617 100644 --- a/crates/notedeck/src/lib.rs +++ b/crates/notedeck/src/lib.rs @@ -46,7 +46,7 @@ pub use account::relay::RelayAction; pub use account::FALLBACK_PUBKEY; pub use app::{App, AppAction, Notedeck}; pub use args::Args; -pub use context::AppContext; +pub use context::{AppContext, SoftKeyboardContext}; pub use error::{show_one_error_message, Error, FilterError, ZapError}; pub use filter::{FilterState, FilterStates, UnifiedSubscription}; pub use fonts::NamedFontFamily; diff --git a/crates/notedeck/src/platform/android.rs b/crates/notedeck/src/platform/android.rs index 7497346c..1417e05a 100644 --- a/crates/notedeck/src/platform/android.rs +++ b/crates/notedeck/src/platform/android.rs @@ -16,7 +16,7 @@ pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHei debug!("updating virtual keyboard height {}", height); // Convert and store atomically - KEYBOARD_HEIGHT.store(height, Ordering::SeqCst); + KEYBOARD_HEIGHT.store(height.max(0), Ordering::SeqCst); } /// Gets the current Android virtual keyboard height. Useful for transforming diff --git a/crates/notedeck/src/platform/mod.rs b/crates/notedeck/src/platform/mod.rs index d251128e..dac0b363 100644 --- a/crates/notedeck/src/platform/mod.rs +++ b/crates/notedeck/src/platform/mod.rs @@ -20,3 +20,13 @@ pub fn virtual_keyboard_height(virt: bool) -> i32 { 0 } } + +pub fn virtual_keyboard_rect(ui: &egui::Ui, virt: bool) -> Option { + let height = virtual_keyboard_height(virt); + if height <= 0 { + return None; + } + let screen_rect = ui.ctx().screen_rect(); + let min = egui::Pos2::new(0.0, screen_rect.max.y - height as f32); + Some(egui::Rect::from_min_max(min, screen_rect.max)) +} diff --git a/crates/notedeck_chrome/android/app/src/main/AndroidManifest.xml b/crates/notedeck_chrome/android/app/src/main/AndroidManifest.xml index 799a1e30..ece6da0d 100644 --- a/crates/notedeck_chrome/android/app/src/main/AndroidManifest.xml +++ b/crates/notedeck_chrome/android/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ android:name=".MainActivity" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode|fontScale|smallestScreenSize" android:exported="true" + android:windowSoftInputMode="adjustResize" android:launchMode="singleTask" > @@ -37,4 +38,4 @@ - \ No newline at end of file + diff --git a/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java b/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java index 93658f05..61a20681 100644 --- a/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java +++ b/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java @@ -26,12 +26,15 @@ public class MainActivity extends GameActivity { @Override protected void onCreate(Bundle savedInstanceState) { // Shrink view so it does not get covered by insets. + super.onCreate(savedInstanceState); setupInsets(); + //setupFullscreen() - keyboardHelper = new KeyboardHeightHelper(this); + + //keyboardHelper = new KeyboardHeightHelper(this); - super.onCreate(savedInstanceState); + } private void setupFullscreen() { @@ -61,6 +64,18 @@ public class MainActivity extends GameActivity { } private void setupInsets() { + + // NOTE(jb55): This is needed for keyboard visibility. Without this the + // window still gets the right insets, but they’re consumed before they + // reach the NDK side. + //WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + + // NOTE(jb55): This is needed for keyboard visibility. If the bars are + // permanently gone, Android routes the keyboard over the GL surface and + // doesn’t change insets. + //WindowInsetsControllerCompat ic = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); + //ic.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + View content = getContent(); ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> { Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); @@ -72,12 +87,13 @@ public class MainActivity extends GameActivity { mlp.rightMargin = insets.right; v.setLayoutParams(mlp); - return WindowInsetsCompat.CONSUMED; + return windowInsets; }); - WindowCompat.setDecorFitsSystemWindows(getWindow(), true); + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); } + /* @Override public void onResume() { super.onResume(); @@ -95,6 +111,7 @@ public class MainActivity extends GameActivity { super.onDestroy(); keyboardHelper.close(); } + */ @Override public boolean onTouchEvent(MotionEvent event) { diff --git a/crates/notedeck_chrome/src/android.rs b/crates/notedeck_chrome/src/android.rs index aec9f020..f36bf87a 100644 --- a/crates/notedeck_chrome/src/android.rs +++ b/crates/notedeck_chrome/src/android.rs @@ -17,7 +17,7 @@ pub async fn android_main(app: AndroidApp) { //std::env::set_var("DAVE_MODEL", "hhao/qwen2.5-coder-tools:latest"); std::env::set_var( "RUST_LOG", - "egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug", + "egui=debug,egui-winit=debug,winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug", ); //std::env::set_var( @@ -57,7 +57,7 @@ pub async fn android_main(app: AndroidApp) { options.android_app = Some(app.clone()); - let app_args = get_app_args(app); + let app_args = get_app_args(app.clone()); let _res = eframe::run_native( "Damus Notedeck", @@ -65,6 +65,7 @@ pub async fn android_main(app: AndroidApp) { Box::new(move |cc| { let ctx = &cc.egui_ctx; let mut notedeck = Notedeck::new(ctx, path, &app_args); + notedeck.set_android_context(app.clone()); notedeck.setup(ctx); let chrome = Chrome::new_with_apps(cc, &app_args, &mut notedeck)?; notedeck.set_app(chrome); diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs index 20327c9f..804ef9c9 100644 --- a/crates/notedeck_chrome/src/chrome.rs +++ b/crates/notedeck_chrome/src/chrome.rs @@ -8,6 +8,7 @@ use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference use egui_extras::{Size, StripBuilder}; use nostrdb::{ProfileRecord, Transaction}; use notedeck::Error; +use notedeck::SoftKeyboardContext; use notedeck::{ tr, App, AppAction, AppContext, Localization, Notedeck, NotedeckOptions, NotedeckTextStyle, UserAccount, WalletType, @@ -267,9 +268,40 @@ impl Chrome { let amt_open = self.amount_open(ui); let r = self.panel(ctx, StripBuilder::new(ui), amt_open); - // virtual keyboard - if self.options.contains(ChromeOptions::VirtualKeyboard) { - virtual_keyboard_ui(ui); + let skb_ctx = if self.options.contains(ChromeOptions::VirtualKeyboard) { + SoftKeyboardContext::Virtual + } else { + SoftKeyboardContext::Platform { + ppp: ui.ctx().pixels_per_point(), + } + }; + + // move screen up if virtual keyboard intersects with input_rect + let screen_rect = ui.ctx().screen_rect(); + let mut keyboard_height = 0.0; + if let Some(vkb_rect) = ctx.soft_keyboard_rect(screen_rect, skb_ctx.clone()) { + if let SoftKeyboardContext::Virtual = skb_ctx { + virtual_keyboard_ui(ui, vkb_rect); + } + if let Some(input_rect) = notedeck_ui::input_rect(ui) { + if input_rect.intersects(vkb_rect) { + tracing::debug!("screen:{screen_rect} skb:{vkb_rect}"); + keyboard_height = vkb_rect.height(); + } + } + } else { + // clear last input box position state + notedeck_ui::clear_input_rect(ui); + } + + let anim_height = + ui.ctx() + .animate_value_with_time(egui::Id::new("keyboard_anim"), keyboard_height, 0.1); + if anim_height > 0.0 { + ui.ctx().transform_layer_shapes( + ui.layer_id(), + egui::emath::TSTransform::from_translation(egui::Vec2::new(0.0, -anim_height)), + ); } r @@ -912,12 +944,7 @@ fn repaint_causes_window(ui: &mut egui::Ui, causes: &HashMap match key { egui::Key::J => { - columns.select_down(); + //columns.select_down(); + {} } + /* egui::Key::K => { columns.select_up(); } @@ -90,7 +87,13 @@ fn handle_key_events(input: &egui::InputState, columns: &mut Columns) { columns.get_selected_router().go_back(); } _ => {} + }, + + egui::Event::InsetsChanged => { + tracing::debug!("insets have changed!"); } + + _ => {} } } } @@ -102,7 +105,7 @@ fn try_process_event( ) -> Result<()> { let current_columns = get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, &mut damus.decks_cache); - ctx.input(|i| handle_key_events(i, current_columns)); + ctx.input(|i| handle_egui_events(i, current_columns)); let ctx2 = ctx.clone(); let wakeup = move || { diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index e002398f..d14594d7 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -832,7 +832,7 @@ fn render_nav_body( return action; }; - if EditProfileView::new(ctx.i18n, state, ctx.img_cache).ui(ui) { + if EditProfileView::new(ctx.i18n, state, ctx.img_cache, ctx.clipboard).ui(ui) { if let Some(state) = app.view_state.pubkey_to_profile_state.get(kp.pubkey) { action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges( SaveProfileChanges::new(kp.to_full(), state.clone()), diff --git a/crates/notedeck_columns/src/ui/account_login_view.rs b/crates/notedeck_columns/src/ui/account_login_view.rs index bfa60db5..5cc9ec09 100644 --- a/crates/notedeck_columns/src/ui/account_login_view.rs +++ b/crates/notedeck_columns/src/ui/account_login_view.rs @@ -59,7 +59,7 @@ impl<'a> AccountLoginView<'a> { let text_edit_width = available_width - button_width; let textedit_resp = ui.add_sized([text_edit_width, 40.0], login_textedit(self.manager, self.i18n)); - input_context(&textedit_resp, self.clipboard, self.manager.input_buffer(), PasteBehavior::Clear); + input_context(ui, &textedit_resp, self.clipboard, self.manager.input_buffer(), PasteBehavior::Clear); if eye_button(ui, self.manager.password_visible()).clicked() { self.manager.toggle_password_visibility(); diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs index 2e45b602..48443803 100644 --- a/crates/notedeck_columns/src/ui/note/post.rs +++ b/crates/notedeck_columns/src/ui/note/post.rs @@ -209,6 +209,7 @@ impl<'a, 'd> PostView<'a, 'd> { let out = textedit.show(ui); input_context( + ui, &out.response, self.note_context.clipboard, &mut self.draft.buffer.text_buffer, diff --git a/crates/notedeck_columns/src/ui/profile/edit.rs b/crates/notedeck_columns/src/ui/profile/edit.rs index d1c3720d..2bbd6f5b 100644 --- a/crates/notedeck_columns/src/ui/profile/edit.rs +++ b/crates/notedeck_columns/src/ui/profile/edit.rs @@ -1,12 +1,15 @@ use core::f32; use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit}; +use egui_winit::clipboard::Clipboard; use enostr::ProfileState; use notedeck::{profile::unwrap_profile_url, tr, Images, Localization, NotedeckTextStyle}; +use notedeck_ui::context_menu::{input_context, PasteBehavior}; use notedeck_ui::{profile::banner, ProfilePic}; pub struct EditProfileView<'a> { state: &'a mut ProfileState, + clipboard: &'a mut Clipboard, img_cache: &'a mut Images, i18n: &'a mut Localization, } @@ -16,11 +19,13 @@ impl<'a> EditProfileView<'a> { i18n: &'a mut Localization, state: &'a mut ProfileState, img_cache: &'a mut Images, + clipboard: &'a mut Clipboard, ) -> Self { Self { i18n, state, img_cache, + clipboard, } } @@ -95,14 +100,14 @@ impl<'a> EditProfileView<'a> { ) .as_str(), )); - ui.add(singleline_textedit(self.state.str_mut("display_name"))); + singleline_textedit(ui, self.state.str_mut("display_name"), self.clipboard); }); in_frame(ui, |ui| { ui.add(label( tr!(self.i18n, "Username", "Profile username field label").as_str(), )); - ui.add(singleline_textedit(self.state.str_mut("name"))); + singleline_textedit(ui, self.state.str_mut("name"), self.clipboard); }); in_frame(ui, |ui| { @@ -114,28 +119,28 @@ impl<'a> EditProfileView<'a> { ) .as_str(), )); - ui.add(multiline_textedit(self.state.str_mut("picture"))); + multiline_textedit(ui, self.state.str_mut("picture"), self.clipboard); }); in_frame(ui, |ui| { ui.add(label( tr!(self.i18n, "Banner", "Profile banner URL field label").as_str(), )); - ui.add(multiline_textedit(self.state.str_mut("banner"))); + multiline_textedit(ui, self.state.str_mut("banner"), self.clipboard); }); in_frame(ui, |ui| { ui.add(label( tr!(self.i18n, "About", "Profile about/bio field label").as_str(), )); - ui.add(multiline_textedit(self.state.str_mut("about"))); + multiline_textedit(ui, self.state.str_mut("about"), self.clipboard); }); in_frame(ui, |ui| { ui.add(label( tr!(self.i18n, "Website", "Profile website field label").as_str(), )); - ui.add(singleline_textedit(self.state.str_mut("website"))); + singleline_textedit(ui, self.state.str_mut("website"), self.clipboard); }); in_frame(ui, |ui| { @@ -147,7 +152,7 @@ impl<'a> EditProfileView<'a> { ) .as_str(), )); - ui.add(multiline_textedit(self.state.str_mut("lud16"))); + multiline_textedit(ui, self.state.str_mut("lud16"), self.clipboard); }); in_frame(ui, |ui| { @@ -159,7 +164,8 @@ impl<'a> EditProfileView<'a> { ) .as_str(), )); - ui.add(singleline_textedit(self.state.str_mut("nip05"))); + + singleline_textedit(ui, self.state.str_mut("nip05"), self.clipboard); let Some(nip05) = self.state.nip05() else { return; @@ -208,21 +214,29 @@ fn label(text: &str) -> impl egui::Widget + '_ { } } -fn singleline_textedit(data: &mut String) -> impl egui::Widget + '_ { - TextEdit::singleline(data) - .min_size(vec2(0.0, 40.0)) - .vertical_align(egui::Align::Center) - .margin(Margin::symmetric(12, 10)) - .desired_width(f32::INFINITY) +fn singleline_textedit(ui: &mut egui::Ui, data: &mut String, clipboard: &mut Clipboard) { + let r = ui.add( + TextEdit::singleline(data) + .min_size(vec2(0.0, 40.0)) + .vertical_align(egui::Align::Center) + .margin(Margin::symmetric(12, 10)) + .desired_width(f32::INFINITY), + ); + + input_context(ui, &r, clipboard, data, PasteBehavior::Clear); } -fn multiline_textedit(data: &mut String) -> impl egui::Widget + '_ { - TextEdit::multiline(data) - // .min_size(vec2(0.0, 40.0)) - .vertical_align(egui::Align::TOP) - .margin(Margin::symmetric(12, 10)) - .desired_width(f32::INFINITY) - .desired_rows(1) +fn multiline_textedit(ui: &mut egui::Ui, data: &mut String, clipboard: &mut Clipboard) { + let r = ui.add( + TextEdit::multiline(data) + // .min_size(vec2(0.0, 40.0)) + .vertical_align(egui::Align::TOP) + .margin(Margin::symmetric(12, 10)) + .desired_width(f32::INFINITY) + .desired_rows(1), + ); + + input_context(ui, &r, clipboard, data, PasteBehavior::Clear); } fn in_frame(ui: &mut egui::Ui, contents: impl FnOnce(&mut egui::Ui)) { diff --git a/crates/notedeck_columns/src/ui/search/mod.rs b/crates/notedeck_columns/src/ui/search/mod.rs index 0b66b3d2..f3e658c9 100644 --- a/crates/notedeck_columns/src/ui/search/mod.rs +++ b/crates/notedeck_columns/src/ui/search/mod.rs @@ -311,7 +311,7 @@ fn search_box( .frame(false), ); - input_context(&response, clipboard, input, PasteBehavior::Append); + input_context(ui, &response, clipboard, input, PasteBehavior::Append); let mut requested_focus = false; if focus_state == FocusState::ShouldRequestFocus { diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs index 31e846b7..a7644732 100644 --- a/crates/notedeck_columns/src/ui/settings.rs +++ b/crates/notedeck_columns/src/ui/settings.rs @@ -147,7 +147,7 @@ impl<'a> SettingsView<'a> { "Label for appearance settings section", ); settings_group(ui, title, |ui| { - ui.horizontal(|ui| { + ui.horizontal_wrapped(|ui| { ui.label(richtext_small(tr!( self.note_context.i18n, "Font size:", @@ -207,7 +207,7 @@ impl<'a> SettingsView<'a> { let current_zoom = ui.ctx().zoom_factor(); - ui.horizontal(|ui| { + ui.horizontal_wrapped(|ui| { ui.label(richtext_small(tr!( self.note_context.i18n, "Zoom Level:", @@ -260,7 +260,7 @@ impl<'a> SettingsView<'a> { } }); - ui.horizontal(|ui| { + ui.horizontal_wrapped(|ui| { ui.label(richtext_small(tr!( self.note_context.i18n, "Language:", @@ -288,7 +288,7 @@ impl<'a> SettingsView<'a> { }); }); - ui.horizontal(|ui| { + ui.horizontal_wrapped(|ui| { ui.label(richtext_small(tr!( self.note_context.i18n, "Theme:", @@ -441,7 +441,7 @@ impl<'a> SettingsView<'a> { "Label for others settings section" ); settings_group(ui, title, |ui| { - ui.horizontal(|ui| { + ui.horizontal_wrapped(|ui| { ui.label(richtext_small(tr!( self.note_context.i18n, "Sort replies newest first:", diff --git a/crates/notedeck_columns/src/ui/wallet.rs b/crates/notedeck_columns/src/ui/wallet.rs index e9436492..5fd84227 100644 --- a/crates/notedeck_columns/src/ui/wallet.rs +++ b/crates/notedeck_columns/src/ui/wallet.rs @@ -237,8 +237,10 @@ fn show_no_wallet( .password(true); // add paste context menu + let text_edit_resp = ui.add(text_edit); input_context( - &ui.add(text_edit), + ui, + &text_edit_resp, clipboard, &mut state.buf, PasteBehavior::Clear, @@ -388,13 +390,17 @@ fn show_default_zap( }; let id = ui.id().with("default_zap_amount"); - ui.add( - egui::TextEdit::singleline(text) - .desired_width(desired_width) - .margin(egui::Margin::same(8)) - .font(font) - .id(id), - ); + + { + let r = ui.add( + egui::TextEdit::singleline(text) + .desired_width(desired_width) + .margin(egui::Margin::same(8)) + .font(font) + .id(id)); + + notedeck_ui::include_input(ui, &r); + } ui.memory_mut(|m| m.request_focus(id)); diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs index 1ca8e6db..792280a9 100644 --- a/crates/notedeck_dave/src/ui/dave.rs +++ b/crates/notedeck_dave/src/ui/dave.rs @@ -342,6 +342,7 @@ impl<'a> DaveUi<'a> { ) .frame(false), ); + notedeck_ui::include_input(ui, &r); if r.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { DaveResponse::send() diff --git a/crates/notedeck_ui/src/context_menu.rs b/crates/notedeck_ui/src/context_menu.rs index 8292d8c3..bec37064 100644 --- a/crates/notedeck_ui/src/context_menu.rs +++ b/crates/notedeck_ui/src/context_menu.rs @@ -20,6 +20,7 @@ fn handle_paste(clipboard: &mut Clipboard, input: &mut String, paste_behavior: P } pub fn input_context( + ui: &mut egui::Ui, response: &egui::Response, clipboard: &mut Clipboard, input: &mut String, @@ -46,4 +47,7 @@ pub fn input_context( if response.middle_clicked() { handle_paste(clipboard, input, paste_behavior) } + + // for keyboard visibility + crate::include_input(ui, response) } diff --git a/crates/notedeck_ui/src/lib.rs b/crates/notedeck_ui/src/lib.rs index 423fc5be..ff0b69e3 100644 --- a/crates/notedeck_ui/src/lib.rs +++ b/crates/notedeck_ui/src/lib.rs @@ -62,3 +62,34 @@ pub fn secondary_label(ui: &mut egui::Ui, s: impl Into) -> egui::Respons let color = ui.style().visuals.noninteractive().fg_stroke.color; ui.add(Label::new(RichText::new(s).size(10.0).color(color)).selectable(false)) } + +const INPUT_RECT_KEY: &str = "notedeck_input_rect"; + +/// Includes an input rect for keyboard visibility purposes. We use this to move the screen up if +/// a soft keyboard intersects with the input box +pub fn include_input(ui: &mut egui::Ui, resp: &egui::Response) { + // only include input if we have focus + if !resp.has_focus() { + return; + } + + ui.data_mut(|d| { + let id = egui::Id::new(INPUT_RECT_KEY); + match d.get_temp::(id) { + Some(r) => d.insert_temp(id, resp.rect.union(r)), + None => d.insert_temp(id, resp.rect), + } + }) +} + +/// Set the last input rect for keyboard visibility purposes. We use this to move the screen up if +/// a soft keyboard intersects with the input box +pub fn input_rect(ui: &mut egui::Ui) -> Option { + ui.data(|d| d.get_temp(egui::Id::new(INPUT_RECT_KEY))) +} + +/// Set the last input rect for keyboard visibility purposes. We use this to move the screen up if +/// a soft keyboard intersects with the input box +pub fn clear_input_rect(ui: &mut egui::Ui) { + ui.data_mut(|d| d.remove::(egui::Id::new(INPUT_RECT_KEY))) +}