Skip to content

Commit d973afd

Browse files
micmonay486c
authored andcommitted
Fix virtual keyboard on (mobile) web (emilk#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 :)
1 parent 4aae84b commit d973afd

File tree

4 files changed

+51
-26
lines changed

4 files changed

+51
-26
lines changed

crates/eframe/src/web/app_runner.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,10 @@ impl AppRunner {
292292
}
293293
}
294294

295-
if let Err(err) = self.text_agent.move_to(ime, self.canvas()) {
295+
if let Err(err) = self
296+
.text_agent
297+
.move_to(ime, self.canvas(), self.egui_ctx.zoom_factor())
298+
{
296299
log::error!(
297300
"failed to update text agent position: {}",
298301
super::string_from_js_value(&err)

crates/eframe/src/web/events.rs

+7
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,13 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
573573
runner.needs_repaint.repaint_asap();
574574
event.stop_propagation();
575575
event.prevent_default();
576+
577+
// Fix virtual keyboard IOS
578+
// Need call focus at the same time of event
579+
if runner.text_agent.has_focus() {
580+
runner.text_agent.set_focus(false);
581+
runner.text_agent.set_focus(true);
582+
}
576583
}
577584
}
578585
})

crates/eframe/src/web/mod.rs

-13
Original file line numberDiff line numberDiff line change
@@ -252,16 +252,3 @@ pub fn percent_decode(s: &str) -> String {
252252
.decode_utf8_lossy()
253253
.to_string()
254254
}
255-
256-
/// Returns `true` if the app is likely running on a mobile device.
257-
pub(crate) fn is_mobile() -> bool {
258-
fn try_is_mobile() -> Option<bool> {
259-
const MOBILE_DEVICE: [&str; 6] =
260-
["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"];
261-
262-
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
263-
let is_mobile = MOBILE_DEVICE.iter().any(|&name| user_agent.contains(name));
264-
Some(is_mobile)
265-
}
266-
try_is_mobile().unwrap_or(false)
267-
}

crates/eframe/src/web/text_agent.rs

+40-12
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::cell::Cell;
55

66
use wasm_bindgen::prelude::*;
77

8-
use super::{is_mobile, AppRunner, WebRunner};
8+
use super::{AppRunner, WebRunner};
99

1010
pub struct TextAgent {
1111
input: web_sys::HtmlInputElement,
@@ -22,12 +22,17 @@ impl TextAgent {
2222
.create_element("input")?
2323
.dyn_into::<web_sys::HtmlInputElement>()?;
2424
input.set_type("text");
25+
input.set_autofocus(true);
26+
input.set_attribute("autocapitalize", "off")?;
2527

2628
// append it to `<body>` and hide it outside of the viewport
2729
let style = input.style();
28-
style.set_property("opacity", "0")?;
30+
style.set_property("background-color", "transparent")?;
31+
style.set_property("border", "none")?;
32+
style.set_property("outline", "none")?;
2933
style.set_property("width", "1px")?;
3034
style.set_property("height", "1px")?;
35+
style.set_property("caret-color", "transparent")?;
3136
style.set_property("position", "absolute")?;
3237
style.set_property("top", "0")?;
3338
style.set_property("left", "0")?;
@@ -39,6 +44,12 @@ impl TextAgent {
3944
let input = input.clone();
4045
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
4146
let text = input.value();
47+
// Fix android virtual keyboard Gboard
48+
// This removes the virtual keyboard's suggestion.
49+
if !event.is_composing() {
50+
input.blur().ok();
51+
input.focus().ok();
52+
}
4253
// if `is_composing` is true, then user is using IME, for example: emoji, pinyin, kanji, hangul, etc.
4354
// In that case, the browser emits both `input` and `compositionupdate` events,
4455
// and we need to ignore the `input` event.
@@ -103,14 +114,8 @@ impl TextAgent {
103114
&self,
104115
ime: Option<egui::output::IMEOutput>,
105116
canvas: &web_sys::HtmlCanvasElement,
117+
zoom_factor: f32,
106118
) -> Result<(), JsValue> {
107-
// Mobile keyboards don't follow the text input it's writing to,
108-
// instead typically being fixed in place on the bottom of the screen,
109-
// so don't bother moving the text agent on mobile.
110-
if is_mobile() {
111-
return Ok(());
112-
}
113-
114119
// Don't move the text agent unless the position actually changed:
115120
if self.prev_ime_output.get() == ime {
116121
return Ok(());
@@ -119,14 +124,24 @@ impl TextAgent {
119124

120125
let Some(ime) = ime else { return Ok(()) };
121126

122-
let canvas_rect = super::canvas_content_rect(canvas);
127+
let mut canvas_rect = super::canvas_content_rect(canvas);
128+
// Fix for safari with virtual keyboard flapping position
129+
if is_mobile_safari() {
130+
canvas_rect.min.y = canvas.offset_top() as f32;
131+
}
123132
let cursor_rect = ime.cursor_rect.translate(canvas_rect.min.to_vec2());
124133

125134
let style = self.input.style();
126135

127136
// This is where the IME input will point to:
128-
style.set_property("left", &format!("{}px", cursor_rect.center().x))?;
129-
style.set_property("top", &format!("{}px", cursor_rect.center().y))?;
137+
style.set_property(
138+
"left",
139+
&format!("{}px", cursor_rect.center().x * zoom_factor),
140+
)?;
141+
style.set_property(
142+
"top",
143+
&format!("{}px", cursor_rect.center().y * zoom_factor),
144+
)?;
130145

131146
Ok(())
132147
}
@@ -173,3 +188,16 @@ impl Drop for TextAgent {
173188
self.input.remove();
174189
}
175190
}
191+
192+
/// Returns `true` if the app is likely running on a mobile device on navigator Safari.
193+
fn is_mobile_safari() -> bool {
194+
(|| {
195+
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
196+
let is_ios = user_agent.contains("iPhone")
197+
|| user_agent.contains("iPad")
198+
|| user_agent.contains("iPod");
199+
let is_safari = user_agent.contains("Safari");
200+
Some(is_ios && is_safari)
201+
})()
202+
.unwrap_or(false)
203+
}

0 commit comments

Comments
 (0)