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
This commit is contained in:
William Casarin
2025-08-07 11:25:41 -07:00
parent 3aa4d00053
commit 77ac91e810
24 changed files with 276 additions and 94 deletions

40
Cargo.lock generated
View File

@@ -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",

View File

@@ -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" }

View File

@@ -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

View File

@@ -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"]

View File

@@ -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<AndroidApp>,
}
/// 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<P: AsRef<Path>>(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(),
}
}

View File

@@ -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<Rect> {
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<Rect> {
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))
}

View File

@@ -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;

View File

@@ -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

View File

@@ -20,3 +20,13 @@ pub fn virtual_keyboard_height(virt: bool) -> i32 {
0
}
}
pub fn virtual_keyboard_rect(ui: &egui::Ui, virt: bool) -> Option<egui::Rect> {
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))
}

View File

@@ -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"
>
<intent-filter>
@@ -37,4 +38,4 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
</manifest>

View File

@@ -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 theyre 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
// doesnt 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) {

View File

@@ -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);

View File

@@ -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<egui::RepaintCause,
});
}
fn virtual_keyboard_ui(ui: &mut egui::Ui) {
let height = notedeck::platform::virtual_keyboard_height(true);
let screen_rect = ui.ctx().screen_rect();
let min = egui::Pos2::new(0.0, screen_rect.max.y - height as f32);
let rect = Rect::from_min_max(min, screen_rect.max);
fn virtual_keyboard_ui(ui: &mut egui::Ui, rect: egui::Rect) {
let painter = ui.painter_at(rect);
painter.rect_filled(rect, 0.0, Color32::from_black_alpha(200));

View File

@@ -64,18 +64,15 @@ pub struct Damus {
pub onboarding: Onboarding,
}
fn handle_key_events(input: &egui::InputState, columns: &mut Columns) {
fn handle_egui_events(input: &egui::InputState, columns: &mut Columns) {
for event in &input.raw.events {
#[allow(clippy::collapsible_match)]
if let egui::Event::Key {
key, pressed: true, ..
} = event
{
match key {
/*
match event {
egui::Event::Key { key, pressed, .. } if *pressed => 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 || {

View File

@@ -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()),

View File

@@ -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();

View File

@@ -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,

View File

@@ -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)) {

View File

@@ -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 {

View File

@@ -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:",

View File

@@ -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));

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -62,3 +62,34 @@ pub fn secondary_label(ui: &mut egui::Ui, s: impl Into<String>) -> 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::<egui::Rect>(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<egui::Rect> {
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::Rect>(egui::Id::new(INPUT_RECT_KEY)))
}