Conditionally propagate web events using a filter in WebOptions (#5056)

Currently egui will prevent all web events from propagating. This causes
issues in contexts where you are using egui in a larger web context that
wants to receive events that egui does not directly respond to. For
example, currently using egui in a VSCode extension will block all app
hotkeys, such as saving and opening the panel.

This adds a closure to `WebOptions` that takes in a reference to the
egui event that is generated from a web event and returns if the
corresponding web event should be propagated or not. The default for it
is to always return false.

Alternatives I considered were:
1. Having the propagation filter be a property of the focus in memory.
That way it could be configured by the view currently selected. I opted
away from that because I wanted to avoid lowering eframe implementation
specific stuff into egui.
2. Having events contain a `web_propagate` flag that could be set when
handling them. However, that would not be compatible with the current
system of egui events being handled outside of the web event handler.

I just recently started using egui so I am not sure how idiomatic my
approach here is. I would be happy to switch this over to a different
architecture if there are suggestions.

<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

* [x] I have followed the instructions in the PR template
This commit is contained in:
Liam Rosenfeld 2024-09-05 02:48:13 -04:00 committed by GitHub
parent 2be93aca5d
commit df9cd21248
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 130 additions and 41 deletions

View File

@ -457,6 +457,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<dyn Fn(&egui::Event) -> bool>,
}
#[cfg(target_arch = "wasm32")]
@ -472,6 +480,8 @@ impl Default for WebOptions {
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
dithering: true,
should_propagate_event: Box::new(|_| false),
}
}
}

View File

@ -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,

View File

@ -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();
})
}