From 82814c4fffc2fe3ed99e666b517a7a19b4a1c201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Monayron?= Date: Tue, 27 Aug 2024 09:42:35 +0200 Subject: [PATCH] Fix virtual keyboard on (mobile) web (#4855) Hello, I have made several corrections to stabilize the virtual keyboard on Android and IOS (Chrome and Safari). I don't know if these corrections can have a negative impact in certain situations, but at the moment they don't cause me any problems. I'll be happy to answer any questions you may have about these fixes. These fixes correct several issues with the display of the virtual keyboard, particularly since update 0.28, which can be reproduced on the egui demo site. We hope to be able to help you. Thanks a lot for your work, I'm having a lot of fun with egui :) --- crates/eframe/src/web/app_runner.rs | 5 ++- crates/eframe/src/web/events.rs | 7 ++++ crates/eframe/src/web/mod.rs | 13 -------- crates/eframe/src/web/text_agent.rs | 52 ++++++++++++++++++++++------- 4 files changed, 51 insertions(+), 26 deletions(-) diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index c99f27f0..6ef30004 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -292,7 +292,10 @@ impl AppRunner { } } - if let Err(err) = self.text_agent.move_to(ime, self.canvas()) { + if let Err(err) = self + .text_agent + .move_to(ime, self.canvas(), self.egui_ctx.zoom_factor()) + { log::error!( "failed to update text agent position: {}", super::string_from_js_value(&err) diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index af1cf81a..c5e58d35 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -573,6 +573,13 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); + + // Fix virtual keyboard IOS + // Need call focus at the same time of event + if runner.text_agent.has_focus() { + runner.text_agent.set_focus(false); + runner.text_agent.set_focus(true); + } } } }) diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 73ecb540..7e6a5101 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -252,16 +252,3 @@ pub fn percent_decode(s: &str) -> String { .decode_utf8_lossy() .to_string() } - -/// Returns `true` if the app is likely running on a mobile device. -pub(crate) fn is_mobile() -> bool { - fn try_is_mobile() -> Option { - const MOBILE_DEVICE: [&str; 6] = - ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"]; - - let user_agent = web_sys::window()?.navigator().user_agent().ok()?; - let is_mobile = MOBILE_DEVICE.iter().any(|&name| user_agent.contains(name)); - Some(is_mobile) - } - try_is_mobile().unwrap_or(false) -} diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index bc59d905..f0eb67d6 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -5,7 +5,7 @@ use std::cell::Cell; use wasm_bindgen::prelude::*; -use super::{is_mobile, AppRunner, WebRunner}; +use super::{AppRunner, WebRunner}; pub struct TextAgent { input: web_sys::HtmlInputElement, @@ -22,12 +22,17 @@ impl TextAgent { .create_element("input")? .dyn_into::()?; input.set_type("text"); + input.set_autofocus(true); + input.set_attribute("autocapitalize", "off")?; // append it to `` and hide it outside of the viewport let style = input.style(); - style.set_property("opacity", "0")?; + style.set_property("background-color", "transparent")?; + style.set_property("border", "none")?; + style.set_property("outline", "none")?; style.set_property("width", "1px")?; style.set_property("height", "1px")?; + style.set_property("caret-color", "transparent")?; style.set_property("position", "absolute")?; style.set_property("top", "0")?; style.set_property("left", "0")?; @@ -39,6 +44,12 @@ impl TextAgent { let input = input.clone(); move |event: web_sys::InputEvent, runner: &mut AppRunner| { let text = input.value(); + // Fix android virtual keyboard Gboard + // This removes the virtual keyboard's suggestion. + if !event.is_composing() { + input.blur().ok(); + input.focus().ok(); + } // if `is_composing` is true, then user is using IME, for example: emoji, pinyin, kanji, hangul, etc. // In that case, the browser emits both `input` and `compositionupdate` events, // and we need to ignore the `input` event. @@ -103,14 +114,8 @@ impl TextAgent { &self, ime: Option, canvas: &web_sys::HtmlCanvasElement, + zoom_factor: f32, ) -> Result<(), JsValue> { - // Mobile keyboards don't follow the text input it's writing to, - // instead typically being fixed in place on the bottom of the screen, - // so don't bother moving the text agent on mobile. - if is_mobile() { - return Ok(()); - } - // Don't move the text agent unless the position actually changed: if self.prev_ime_output.get() == ime { return Ok(()); @@ -119,14 +124,24 @@ impl TextAgent { let Some(ime) = ime else { return Ok(()) }; - let canvas_rect = super::canvas_content_rect(canvas); + let mut canvas_rect = super::canvas_content_rect(canvas); + // Fix for safari with virtual keyboard flapping position + if is_mobile_safari() { + canvas_rect.min.y = canvas.offset_top() as f32; + } let cursor_rect = ime.cursor_rect.translate(canvas_rect.min.to_vec2()); let style = self.input.style(); // This is where the IME input will point to: - style.set_property("left", &format!("{}px", cursor_rect.center().x))?; - style.set_property("top", &format!("{}px", cursor_rect.center().y))?; + style.set_property( + "left", + &format!("{}px", cursor_rect.center().x * zoom_factor), + )?; + style.set_property( + "top", + &format!("{}px", cursor_rect.center().y * zoom_factor), + )?; Ok(()) } @@ -173,3 +188,16 @@ impl Drop for TextAgent { self.input.remove(); } } + +/// Returns `true` if the app is likely running on a mobile device on navigator Safari. +fn is_mobile_safari() -> bool { + (|| { + let user_agent = web_sys::window()?.navigator().user_agent().ok()?; + let is_ios = user_agent.contains("iPhone") + || user_agent.contains("iPad") + || user_agent.contains("iPod"); + let is_safari = user_agent.contains("Safari"); + Some(is_ios && is_safari) + })() + .unwrap_or(false) +}