diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 4f8f07d037f..ecc81e5c521 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -456,6 +456,14 @@ pub struct WebOptions { /// /// Defaults to true. pub dithering: bool, + + /// If the web event corresponding to an egui event should be propagated + /// to the rest of the web page. + /// + /// The default is `false`, meaning + /// [`stopPropagation`](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation) + /// is called on every event. + pub should_propagate_event: Box bool>, } #[cfg(target_arch = "wasm32")] @@ -471,6 +479,8 @@ impl Default for WebOptions { wgpu_options: egui_wgpu::WgpuConfiguration::default(), dithering: true, + + should_propagate_event: Box::new(|_| false), } } } diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 6ef30004482..999bf803a3c 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -6,7 +6,7 @@ use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter, NeedRepaint pub struct AppRunner { #[allow(dead_code)] - web_options: crate::WebOptions, + pub(crate) web_options: crate::WebOptions, pub(crate) frame: epi::Frame, egui_ctx: egui::Context, painter: super::ActiveWebPainter, diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 6755e2b6483..ebb1bda2551 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -138,14 +138,18 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J && !runner.text_agent.has_focus() { if let Some(text) = text_from_keyboard_event(&event) { - runner.input.raw.events.push(egui::Event::Text(text)); + let egui_event = egui::Event::Text(text); + let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); // If this is indeed text, then prevent any other action. event.prevent_default(); - // Assume egui uses all key events, and don't let them propagate to parent elements. - event.stop_propagation(); + // Use web options to tell if the event should be propagated to parent elements. + if !should_propagate { + event.stop_propagation(); + } } } @@ -173,13 +177,15 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) let egui_key = translate_key(&key); if let Some(egui_key) = egui_key { - runner.input.raw.events.push(egui::Event::Key { + let egui_event = egui::Event::Key { key: egui_key, physical_key: None, // TODO(fornwall) pressed: true, repeat: false, // egui will fill this in for us! modifiers, - }); + }; + let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); let prevent_default = should_prevent_default_for_key(runner, &modifiers, egui_key); @@ -194,8 +200,10 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) event.prevent_default(); } - // Assume egui uses all key events, and don't let them propagate to parent elements. - event.stop_propagation(); + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !should_propagate { + event.stop_propagation(); + } } } @@ -246,14 +254,18 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) { let modifiers = modifiers_from_kb_event(&event); runner.input.raw.modifiers = modifiers; + let mut propagate_event = false; + if let Some(key) = translate_key(&event.key()) { - runner.input.raw.events.push(egui::Event::Key { + let egui_event = egui::Event::Key { key, physical_key: None, // TODO(fornwall) pressed: false, repeat: false, modifiers, - }); + }; + propagate_event |= (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); } if event.key() == "Meta" || event.key() == "Control" { @@ -264,21 +276,23 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) { let keys_down = runner.egui_ctx().input(|i| i.keys_down.clone()); for key in keys_down { - runner.input.raw.events.push(egui::Event::Key { + let egui_event = egui::Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers, - }); + }; + propagate_event |= (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); } } runner.needs_repaint.repaint_asap(); + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. let has_focus = runner.input.raw.focused; - if has_focus { - // Assume egui uses all key events, and don't let them propagate to parent elements. + if has_focus && !propagate_event { event.stop_propagation(); } } @@ -288,11 +302,19 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul if let Some(data) = event.clipboard_data() { if let Ok(text) = data.get_data("text") { let text = text.replace("\r\n", "\n"); + + let mut should_propagate = false; if !text.is_empty() && runner.input.raw.focused { - runner.input.raw.events.push(egui::Event::Paste(text)); + let egui_event = egui::Event::Paste(text); + should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); } - event.stop_propagation(); + + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !should_propagate { + event.stop_propagation(); + } event.prevent_default(); } } @@ -310,7 +332,10 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul runner.needs_repaint.repaint_asap(); } - event.stop_propagation(); + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !(runner.web_options.should_propagate_event)(&egui::Event::Cut) { + event.stop_propagation(); + } event.prevent_default(); })?; @@ -326,7 +351,10 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul runner.needs_repaint.repaint_asap(); } - event.stop_propagation(); + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !(runner.web_options.should_propagate_event)(&egui::Event::Copy) { + event.stop_propagation(); + } event.prevent_default(); })?; @@ -400,15 +428,18 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<( |event: web_sys::PointerEvent, runner: &mut AppRunner| { let modifiers = modifiers_from_mouse_event(&event); runner.input.raw.modifiers = modifiers; + let mut should_propagate = false; if let Some(button) = button_from_mouse_event(&event) { let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); let modifiers = runner.input.raw.modifiers; - runner.input.raw.events.push(egui::Event::PointerButton { + let egui_event = egui::Event::PointerButton { pos, button, pressed: true, modifiers, - }); + }; + should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); // In Safari we are only allowed to write to the clipboard during the // event callback, which is why we run the app logic here and now: @@ -417,7 +448,11 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<( // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); } - event.stop_propagation(); + + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !should_propagate { + event.stop_propagation(); + } // Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here. }, ) @@ -439,12 +474,14 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), ) { if let Some(button) = button_from_mouse_event(&event) { let modifiers = runner.input.raw.modifiers; - runner.input.raw.events.push(egui::Event::PointerButton { + let egui_event = egui::Event::PointerButton { pos, button, pressed: false, modifiers, - }); + }; + let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); // Previously on iOS, the canvas would not receive focus on // any touch event, which resulted in the on-screen keyboard @@ -463,7 +500,11 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), runner.needs_repaint.repaint_asap(); event.prevent_default(); - event.stop_propagation(); + + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !should_propagate { + event.stop_propagation(); + } } } }, @@ -495,9 +536,15 @@ fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), runner, egui::pos2(event.client_x() as f32, event.client_y() as f32), ) { - runner.input.raw.events.push(egui::Event::PointerMoved(pos)); + let egui_event = egui::Event::PointerMoved(pos); + let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); - event.stop_propagation(); + + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !should_propagate { + event.stop_propagation(); + } event.prevent_default(); } }) @@ -510,7 +557,11 @@ fn install_mouseleave(runner_ref: &WebRunner, target: &EventTarget) -> Result<() |event: web_sys::MouseEvent, runner| { runner.input.raw.events.push(egui::Event::PointerGone); runner.needs_repaint.repaint_asap(); - event.stop_propagation(); + + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !(runner.web_options.should_propagate_event)(&egui::Event::PointerGone) { + event.stop_propagation(); + } event.prevent_default(); }, ) @@ -521,18 +572,25 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<() target, "touchstart", |event: web_sys::TouchEvent, runner| { + let mut should_propagate = false; if let Some((pos, _)) = primary_touch_pos(runner, &event) { - runner.input.raw.events.push(egui::Event::PointerButton { + let egui_event = egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers: runner.input.raw.modifiers, - }); + }; + should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); } push_touches(runner, egui::TouchPhase::Start, &event); runner.needs_repaint.repaint_asap(); - event.stop_propagation(); + + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !should_propagate { + event.stop_propagation(); + } event.prevent_default(); }, ) @@ -545,11 +603,17 @@ fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), runner, egui::pos2(touch.client_x() as f32, touch.client_y() as f32), ) { - runner.input.raw.events.push(egui::Event::PointerMoved(pos)); + let egui_event = egui::Event::PointerMoved(pos); + let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); push_touches(runner, egui::TouchPhase::Move, &event); runner.needs_repaint.repaint_asap(); - event.stop_propagation(); + + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !should_propagate { + event.stop_propagation(); + } event.prevent_default(); } } @@ -564,19 +628,28 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), egui::pos2(touch.client_x() as f32, touch.client_y() as f32), ) { // First release mouse to click: - runner.input.raw.events.push(egui::Event::PointerButton { + let mut should_propagate = false; + let egui_event = egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: runner.input.raw.modifiers, - }); + }; + should_propagate |= (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); // Then remove hover effect: + should_propagate |= + (runner.web_options.should_propagate_event)(&egui::Event::PointerGone); runner.input.raw.events.push(egui::Event::PointerGone); push_touches(runner, egui::TouchPhase::End, &event); runner.needs_repaint.repaint_asap(); - event.stop_propagation(); + + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !should_propagate { + event.stop_propagation(); + } event.prevent_default(); // Fix virtual keyboard IOS @@ -617,7 +690,7 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV let modifiers = modifiers_from_wheel_event(&event); - if modifiers.ctrl && !runner.input.raw.modifiers.ctrl { + let egui_event = if modifiers.ctrl && !runner.input.raw.modifiers.ctrl { // The browser is saying the ctrl key is down, but it isn't _really_. // This happens on pinch-to-zoom on a Mac trackpad. // egui will treat ctrl+scroll as zoom, so it all works. @@ -625,17 +698,23 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV // speed of a native app, without being sensitive to egui's `scroll_zoom_speed` setting. let pinch_to_zoom_sensitivity = 0.01; // Feels good on a Mac trackpad in 2024 let zoom_factor = (pinch_to_zoom_sensitivity * delta.y).exp(); - runner.input.raw.events.push(egui::Event::Zoom(zoom_factor)); + egui::Event::Zoom(zoom_factor) } else { - runner.input.raw.events.push(egui::Event::MouseWheel { + egui::Event::MouseWheel { unit, delta, modifiers, - }); - } + } + }; + let should_propagate = (runner.web_options.should_propagate_event)(&egui_event); + runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); - event.stop_propagation(); + + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if !should_propagate { + event.stop_propagation(); + } event.prevent_default(); }) }