Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile web virtual keyboard support #279

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[package]
name = "bevy_egui"
version = "0.29.0"
rust-version = "1.80.0" # needed for LazyLock https://doc.rust-lang.org/stable/std/sync/struct.LazyLock.html
authors = ["mvlabat <[email protected]>"]
description = "A plugin for Egui integration into Bevy"
license = "MIT"
Expand Down Expand Up @@ -75,11 +76,16 @@ winit = "0.30"
web-sys = { version = "0.3.63", features = [
"Clipboard",
"ClipboardEvent",
"CompositionEvent",
"DataTransfer",
'Document',
'EventTarget',
"Window",
"Document",
"EventTarget",
"HtmlInputElement",
"InputEvent",
"KeyboardEvent",
"Navigator",
"TouchEvent",
"Window",
] }
js-sys = "0.3.63"
wasm-bindgen = "0.2.84"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ An example WASM project is live at [mvlabat.github.io/bevy_egui_web_showcase](ht
- Opening URLs
- Multiple windows support (see [./examples/two_windows.rs](https://github.com/mvlabat/bevy_egui/blob/v0.29.0/examples/two_windows.rs))
- Paint callback support (see [./examples/paint_callback.rs](https://github.com/mvlabat/bevy_egui/blob/v0.29.0/examples/paint_callback.rs))
- Mobile web virtual keyboard (still rough support and only works without prevent_default_event_handling set to false on the WindowPlugin primary_window)

`bevy_egui` can be compiled with using only `bevy`, `egui` and `bytemuck` as dependencies: `manage_clipboard` and `open_url` features,
that require additional crates, can be disabled.
Expand Down
128 changes: 126 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ pub mod egui_render_to_texture_node;
pub mod render_systems;
/// Plugin systems.
pub mod systems;
/// Clipboard management for web.
/// Mobile web keyboard hacky input support
#[cfg(target_arch = "wasm32")]
mod text_agent;
/// Clipboard management for web
#[cfg(all(
feature = "manage_clipboard",
target_arch = "wasm32",
Expand Down Expand Up @@ -129,6 +132,15 @@ use bevy::{
))]
use std::cell::{RefCell, RefMut};

#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;

#[cfg(target_arch = "wasm32")]
use crate::text_agent::{
install_text_agent, is_mobile_safari, process_safari_virtual_keyboard, propagate_text,
SafariVirtualKeyboardHack, TextAgentChannel, VirtualTouchInfo,
};

/// Adds all Egui resources and render graph nodes.
pub struct EguiPlugin;

Expand Down Expand Up @@ -661,7 +673,16 @@ impl Plugin for EguiPlugin {
target_arch = "wasm32",
web_sys_unstable_apis
))]
world.init_non_send_resource::<web_clipboard::SubscribedEvents>();
world.init_non_send_resource::<SubscribedEvents<web_sys::ClipboardEvent>>();
// virtual keyboard events for text_agent
#[cfg(target_arch = "wasm32")]
world.init_non_send_resource::<SubscribedEvents<web_sys::CompositionEvent>>();
#[cfg(target_arch = "wasm32")]
world.init_non_send_resource::<SubscribedEvents<web_sys::KeyboardEvent>>();
#[cfg(target_arch = "wasm32")]
world.init_non_send_resource::<SubscribedEvents<web_sys::InputEvent>>();
#[cfg(target_arch = "wasm32")]
world.init_non_send_resource::<SubscribedEvents<web_sys::TouchEvent>>();
#[cfg(feature = "render")]
world.init_resource::<EguiUserTextures>();
#[cfg(feature = "render")]
Expand Down Expand Up @@ -714,6 +735,58 @@ impl Plugin for EguiPlugin {
.after(InputSystem)
.after(EguiSet::InitContexts),
);
#[cfg(target_arch = "wasm32")]
{
use std::sync::{LazyLock, Mutex};

let maybe_window_plugin = app.get_added_plugins::<bevy::prelude::WindowPlugin>();

if !maybe_window_plugin.is_empty()
&& maybe_window_plugin[0].primary_window.is_some()
&& maybe_window_plugin[0]
.primary_window
.as_ref()
.unwrap()
.prevent_default_event_handling
{
app.init_resource::<TextAgentChannel>();

let (sender, receiver) = crossbeam_channel::unbounded();
static TOUCH_INFO: LazyLock<Mutex<VirtualTouchInfo>> =
LazyLock::new(|| Mutex::new(VirtualTouchInfo::default()));

app.insert_resource(SafariVirtualKeyboardHack {
sender,
receiver,
touch_info: &TOUCH_INFO,
});

app.add_systems(
PreStartup,
install_text_agent
.in_set(EguiSet::ProcessInput)
.after(process_input_system)
.after(InputSystem)
.after(EguiSet::InitContexts),
);

app.add_systems(
PreUpdate,
propagate_text
.in_set(EguiSet::ProcessInput)
.after(process_input_system)
.after(InputSystem)
.after(EguiSet::InitContexts),
);

if is_mobile_safari() {
app.add_systems(
PostUpdate,
process_safari_virtual_keyboard.after(process_output_system),
);
}
}
}
app.add_systems(
PreUpdate,
begin_frame_system
Expand Down Expand Up @@ -946,6 +1019,57 @@ fn free_egui_textures_system(
}
}

/// Helper function for outputting a String from a JsValue
#[cfg(target_arch = "wasm32")]
pub fn string_from_js_value(value: &JsValue) -> String {
value.as_string().unwrap_or_else(|| format!("{value:#?}"))
}

#[cfg(target_arch = "wasm32")]
struct EventClosure<T> {
target: web_sys::EventTarget,
event_name: String,
closure: wasm_bindgen::closure::Closure<dyn FnMut(T)>,
}

/// Stores event listeners.
#[cfg(target_arch = "wasm32")]
pub struct SubscribedEvents<T> {
event_closures: Vec<EventClosure<T>>,
}

#[cfg(target_arch = "wasm32")]
impl<T> Default for SubscribedEvents<T> {
fn default() -> SubscribedEvents<T> {
Self {
event_closures: vec![],
}
}
}

#[cfg(target_arch = "wasm32")]
impl<T> SubscribedEvents<T> {
/// Use this method to unsubscribe from all stored events, this can be useful
/// for gracefully destroying a Bevy instance in a page.
pub fn unsubscribe_from_events(&mut self) {
let events_to_unsubscribe = std::mem::take(&mut self.event_closures);

if !events_to_unsubscribe.is_empty() {
for event in events_to_unsubscribe {
if let Err(err) = event.target.remove_event_listener_with_callback(
event.event_name.as_str(),
event.closure.as_ref().unchecked_ref(),
) {
log::error!(
"Failed to unsubscribe from event: {}",
string_from_js_value(&err)
);
}
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
19 changes: 19 additions & 0 deletions src/systems.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ use bevy::{
};
use std::{marker::PhantomData, time::Duration};

#[cfg(target_arch = "wasm32")]
use crate::text_agent::{is_mobile_safari, update_text_agent};

#[allow(missing_docs)]
#[derive(SystemParam)]
// IMPORTANT: remember to add the logic to clear event readers to the `clear` method.
Expand Down Expand Up @@ -240,6 +243,17 @@ pub fn process_input_system(
});
}

#[cfg(target_arch = "wasm32")]
let mut editing_text = false;
#[cfg(target_arch = "wasm32")]
for context in context_params.contexts.iter() {
let platform_output = &context.egui_output.platform_output;
if platform_output.ime.is_some() || platform_output.mutable_text_under_cursor {
editing_text = true;
break;
}
}

for event in keyboard_input_events {
let text_event_allowed = !command && !win || !*context_params.is_macos && ctrl && alt;
let Some(mut window_context) = context_params.window_context(event.window) else {
Expand Down Expand Up @@ -413,6 +427,11 @@ pub fn process_input_system(
.egui_input
.events
.push(egui::Event::PointerGone);

#[cfg(target_arch = "wasm32")]
if !is_mobile_safari() {
update_text_agent(editing_text);
}
}
bevy::input::touch::TouchPhase::Canceled => {
window_context.ctx.pointer_touch_id = None;
Expand Down
Loading
Loading