Skip to content

Commit

Permalink
Force canvas/text input focus on touch for iOS web browsers (#4848)
Browse files Browse the repository at this point in the history
  • Loading branch information
BKSalman committed Jul 23, 2024
1 parent 56df31a commit 34db001
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 55 deletions.
1 change: 1 addition & 0 deletions crates/eframe/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ web-sys = { workspace = true, features = [
"Storage",
"Touch",
"TouchEvent",
"PointerEvent",
"TouchList",
"WebGl2RenderingContext",
"WebglDebugRendererInfo",
Expand Down
93 changes: 52 additions & 41 deletions crates/eframe/src/web/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal
// so we check if we have focus inside of the handler.
install_copy_cut_paste(runner_ref, &document)?;

install_mousedown(runner_ref, &canvas)?;
// Use `document` here to notice if the user releases a drag outside of the canvas:
// See https://github.com/emilk/egui/issues/3157
install_mousemove(runner_ref, &document)?;
install_mouseup(runner_ref, &document)?;
install_pointerup(runner_ref, &document)?;
install_pointerdown(runner_ref, &canvas)?;
install_mouseleave(runner_ref, &canvas)?;

install_touchstart(runner_ref, &canvas)?;
Expand Down Expand Up @@ -390,11 +390,11 @@ fn prevent_default_and_stop_propagation(
Ok(())
}

fn install_mousedown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(
target,
"mousedown",
|event: web_sys::MouseEvent, runner: &mut AppRunner| {
"pointerdown",
|event: web_sys::PointerEvent, runner: &mut AppRunner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
if let Some(button) = button_from_mouse_event(&event) {
Expand All @@ -420,6 +420,53 @@ fn install_mousedown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
)
}

fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(
target,
"pointerup",
|event: web_sys::PointerEvent, runner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;

let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());

if is_interested_in_pointer_event(
runner,
egui::pos2(event.client_x() as f32, event.client_y() as f32),
) {
if let Some(button) = button_from_mouse_event(&event) {
let modifiers = runner.input.raw.modifiers;
runner.input.raw.events.push(egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
});

// Previously on iOS, the canvas would not receive focus on
// any touch event, which resulted in the on-screen keyboard
// not working when focusing on a text field in an egui app.
// This attempts to fix that by forcing the focus on any
// click on the canvas.
runner.canvas().focus().ok();

// In Safari we are only allowed to do certain things
// (like playing audio, start a download, etc)
// on user action, such as a click.
// So we need to run the app logic here and now:
runner.logic();

// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();

event.prevent_default();
event.stop_propagation();
}
}
},
)
}

/// Returns true if the cursor is above the canvas, or if we're dragging something.
/// Pass in the position in browser viewport coordinates (usually event.clientX/Y).
fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool {
Expand Down Expand Up @@ -453,42 +500,6 @@ fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
})
}

fn install_mouseup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "mouseup", |event: web_sys::MouseEvent, runner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;

let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());

if is_interested_in_pointer_event(
runner,
egui::pos2(event.client_x() as f32, event.client_y() as f32),
) {
if let Some(button) = button_from_mouse_event(&event) {
let modifiers = runner.input.raw.modifiers;
runner.input.raw.events.push(egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
});

// In Safari we are only allowed to do certain things
// (like playing audio, start a download, etc)
// on user action, such as a click.
// So we need to run the app logic here and now:
runner.logic();

// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();

event.prevent_default();
event.stop_propagation();
}
}
})
}

fn install_mouseleave(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(
target,
Expand Down
13 changes: 13 additions & 0 deletions crates/eframe/src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,16 @@ 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)
}
15 changes: 1 addition & 14 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::{AppRunner, WebRunner};
use super::{is_mobile, AppRunner, WebRunner};

pub struct TextAgent {
input: web_sys::HtmlInputElement,
Expand Down Expand Up @@ -173,16 +173,3 @@ impl Drop for TextAgent {
self.input.remove();
}
}

/// Returns `true` if the app is likely running on a mobile device.
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)
}

0 comments on commit 34db001

Please sign in to comment.