Skip to content

Commit

Permalink
Fix virtual keyboard on (mobile) web (emilk#4855)
Browse files Browse the repository at this point in the history
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 :)
  • Loading branch information
micmonay authored and hacknus committed Oct 30, 2024
1 parent ce08d8d commit 36ab82e
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 26 deletions.
5 changes: 4 additions & 1 deletion crates/eframe/src/web/app_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions crates/eframe/src/web/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
})
Expand Down
13 changes: 0 additions & 13 deletions crates/eframe/src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> {
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)
}
52 changes: 40 additions & 12 deletions crates/eframe/src/web/text_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,12 +22,17 @@ impl TextAgent {
.create_element("input")?
.dyn_into::<web_sys::HtmlInputElement>()?;
input.set_type("text");
input.set_autofocus(true);
input.set_attribute("autocapitalize", "off")?;

// append it to `<body>` 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")?;
Expand All @@ -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.
Expand Down Expand Up @@ -103,14 +114,8 @@ impl TextAgent {
&self,
ime: Option<egui::output::IMEOutput>,
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(());
Expand All @@ -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(())
}
Expand Down Expand Up @@ -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)
}

0 comments on commit 36ab82e

Please sign in to comment.