Skip to content

Commit

Permalink
janky SubscribedEvents
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivy committed Jul 15, 2024
1 parent 7b8438b commit 5985a6a
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 64 deletions.
71 changes: 67 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ use bevy::{
app::Last,
asset::{load_internal_asset, AssetEvent, Assets, Handle},
ecs::{event::EventReader, system::ResMut},
prelude::NonSendMut,
prelude::Shader,
render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
Expand Down Expand Up @@ -127,6 +128,8 @@ use bevy::{
))]
use std::cell::{RefCell, RefMut};

use wasm_bindgen::prelude::*;

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

Expand Down Expand Up @@ -648,7 +651,12 @@ 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::InputEvent>>();
#[cfg(target_arch = "wasm32")]
world.init_non_send_resource::<SubscribedEvents<web_sys::KeyboardEvent>>();
#[cfg(feature = "render")]
world.init_resource::<EguiUserTextures>();
#[cfg(feature = "render")]
Expand Down Expand Up @@ -702,9 +710,21 @@ impl Plugin for EguiPlugin {
use bevy::prelude::Res;
app.init_resource::<text_agent::TextAgentChannel>();

app.add_systems(PreStartup, |channel: Res<text_agent::TextAgentChannel>| {
text_agent::install_text_agent(channel.sender.clone()).unwrap();
});
app.add_systems(
PreStartup,
|channel: Res<text_agent::TextAgentChannel>,
mut subscribed_input_events: NonSendMut<SubscribedEvents<web_sys::InputEvent>>,
mut subscribed_keyboard_events: NonSendMut<
SubscribedEvents<web_sys::KeyboardEvent>,
>| {
text_agent::install_text_agent(
&mut subscribed_input_events,
&mut subscribed_keyboard_events,
channel.sender.clone(),
)
.unwrap();
},
);

app.add_systems(
PreStartup,
Expand Down Expand Up @@ -920,6 +940,49 @@ fn free_egui_textures_system(
}
}

/// Stores the clipboard event listeners.

pub struct SubscribedEvents<T> {
event_closures: Vec<EventClosure<T>>,
}

impl<T> Default for SubscribedEvents<T> {
fn default() -> SubscribedEvents<T> {
Self {
event_closures: vec![],
}
}
}

impl<T> SubscribedEvents<T> {
/// Use this method to unsubscribe from all the clipboard 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: {}",
crate::web_clipboard::string_from_js_value(&err)
);
}
}
}
}
}

struct EventClosure<T> {
target: web_sys::EventTarget,
event_name: String,
// closure: Closure<dyn FnMut(web_sys::ClipboardEvent)>,
closure: wasm_bindgen::closure::Closure<dyn FnMut(T)>,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
71 changes: 50 additions & 21 deletions src/text_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crossbeam_channel::Sender;
use once_cell::sync::Lazy;
use wasm_bindgen::prelude::*;

use crate::systems::ContextSystemParams;
use crate::{systems::ContextSystemParams, EventClosure, SubscribedEvents};

static AGENT_ID: &str = "egui_text_agent";

Expand Down Expand Up @@ -67,7 +67,11 @@ fn is_mobile() -> Option<bool> {
}

/// Text event handler,
pub fn install_text_agent(sender: Sender<egui::Event>) -> Result<(), JsValue> {
pub fn install_text_agent(
subscribed_input_events: &mut SubscribedEvents<web_sys::InputEvent>,
subscribed_keyboard_events: &mut SubscribedEvents<web_sys::KeyboardEvent>,
sender: Sender<egui::Event>,
) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().expect("document should have a body");
Expand All @@ -92,6 +96,30 @@ pub fn install_text_agent(sender: Sender<egui::Event>) -> Result<(), JsValue> {
input.set_autofocus(true);
input.set_hidden(true);

{
let input_clone = input.clone();
let sender_clone = sender.clone();
let closure = Closure::wrap(Box::new(move |_event: web_sys::InputEvent| {
let text = input_clone.value();

if !text.is_empty() {
input_clone.set_value("");
if text.len() == 1 {
let _ = sender_clone.send(egui::Event::Text(text.clone()));
}
}
}) as Box<dyn FnMut(_)>);
input.add_event_listener_with_callback("input", closure.as_ref().unchecked_ref())?;
subscribed_input_events.event_closures.push(EventClosure {
target: <web_sys::Document as std::convert::AsRef<web_sys::EventTarget>>::as_ref(
&document,
)
.clone(),
event_name: "virtual_keyboard_input".to_owned(),
closure,
});
}

if let Some(true) = is_mobile() {
// keydown
let sender_clone = sender.clone();
Expand All @@ -111,7 +139,16 @@ pub fn install_text_agent(sender: Sender<egui::Event>) -> Result<(), JsValue> {
}
}) as Box<dyn FnMut(_)>);
document.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())?;
closure.forget();
subscribed_keyboard_events
.event_closures
.push(EventClosure {
target: <web_sys::Document as std::convert::AsRef<web_sys::EventTarget>>::as_ref(
&document,
)
.clone(),
event_name: "virtual_keyboard_keydown".to_owned(),
closure,
});
}

if let Some(true) = is_mobile() {
Expand All @@ -129,24 +166,16 @@ pub fn install_text_agent(sender: Sender<egui::Event>) -> Result<(), JsValue> {
}
}) as Box<dyn FnMut(_)>);
document.add_event_listener_with_callback("keyup", closure.as_ref().unchecked_ref())?;
closure.forget();
}

{
let input_clone = input.clone();
let sender_clone = sender.clone();
let on_input = Closure::wrap(Box::new(move |_event: web_sys::InputEvent| {
let text = input_clone.value();

if !text.is_empty() {
input_clone.set_value("");
if text.len() == 1 {
let _ = sender_clone.send(egui::Event::Text(text.clone()));
}
}
}) as Box<dyn FnMut(_)>);
input.add_event_listener_with_callback("input", on_input.as_ref().unchecked_ref())?;
on_input.forget();
subscribed_keyboard_events
.event_closures
.push(EventClosure {
target: <web_sys::Document as std::convert::AsRef<web_sys::EventTarget>>::as_ref(
&document,
)
.clone(),
event_name: "virtual_keyboard_keyup".to_owned(),
closure,
});
}

body.append_child(&input)?;
Expand Down
56 changes: 17 additions & 39 deletions src/web_clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ use crossbeam_channel::{Receiver, Sender};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;

use crate::{EventClosure, SubscribedEvents};

/// Startup system to initialize web clipboard events.
pub fn startup_setup_web_events(
mut egui_clipboard: ResMut<EguiClipboard>,
mut subscribed_events: NonSendMut<SubscribedEvents>,
mut subscribed_events: NonSendMut<SubscribedEvents<web_sys::ClipboardEvent>>,
) {
let (tx, rx) = crossbeam_channel::unbounded();
egui_clipboard.clipboard.event_receiver = Some(rx);
Expand Down Expand Up @@ -71,41 +73,10 @@ impl WebClipboard {
}
}

/// Stores the clipboard event listeners.
#[derive(Default)]
pub struct SubscribedEvents {
event_closures: Vec<EventClosure>,
}

impl SubscribedEvents {
/// Use this method to unsubscribe from all the clipboard 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)
);
}
}
}
}
}

struct EventClosure {
target: web_sys::EventTarget,
event_name: String,
closure: Closure<dyn FnMut(web_sys::ClipboardEvent)>,
}

fn setup_clipboard_copy(subscribed_events: &mut SubscribedEvents, tx: Sender<WebClipboardEvent>) {
fn setup_clipboard_copy(
subscribed_events: &mut SubscribedEvents<web_sys::ClipboardEvent>,
tx: Sender<WebClipboardEvent>,
) {
let Some(window) = web_sys::window() else {
log::error!("Failed to add the \"copy\" listener: no window object");
return;
Expand Down Expand Up @@ -139,7 +110,10 @@ fn setup_clipboard_copy(subscribed_events: &mut SubscribedEvents, tx: Sender<Web
});
}

fn setup_clipboard_cut(subscribed_events: &mut SubscribedEvents, tx: Sender<WebClipboardEvent>) {
fn setup_clipboard_cut(
subscribed_events: &mut SubscribedEvents<web_sys::ClipboardEvent>,
tx: Sender<WebClipboardEvent>,
) {
let Some(window) = web_sys::window() else {
log::error!("Failed to add the \"cut\" listener: no window object");
return;
Expand Down Expand Up @@ -173,7 +147,10 @@ fn setup_clipboard_cut(subscribed_events: &mut SubscribedEvents, tx: Sender<WebC
});
}

fn setup_clipboard_paste(subscribed_events: &mut SubscribedEvents, tx: Sender<WebClipboardEvent>) {
fn setup_clipboard_paste(
subscribed_events: &mut SubscribedEvents<web_sys::ClipboardEvent>,
tx: Sender<WebClipboardEvent>,
) {
let Some(window) = web_sys::window() else {
log::error!("Failed to add the \"paste\" listener: no window object");
return;
Expand Down Expand Up @@ -245,6 +222,7 @@ fn clipboard_copy(contents: String) {
});
}

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

0 comments on commit 5985a6a

Please sign in to comment.