Skip to content

Commit

Permalink
Conditionally propagate web events using a filter in WebOptions (#5056)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
liamrosenfeld authored Sep 5, 2024
1 parent 2be93ac commit df9cd21
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 41 deletions.
10 changes: 10 additions & 0 deletions crates/eframe/src/epi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -472,6 +480,8 @@ impl Default for WebOptions {
wgpu_options: egui_wgpu::WgpuConfiguration::default(),

dithering: true,

should_propagate_event: Box::new(|_| false),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/eframe/src/web/app_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
159 changes: 119 additions & 40 deletions crates/eframe/src/web/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}

Expand Down Expand Up @@ -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);
Expand All @@ -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();
}
}
}

Expand Down Expand Up @@ -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" {
Expand All @@ -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();
}
}
Expand All @@ -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();
}
}
Expand All @@ -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();
})?;

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

Expand Down Expand Up @@ -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:
Expand All @@ -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.
},
)
Expand All @@ -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
Expand All @@ -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();
}
}
}
},
Expand Down Expand Up @@ -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();
}
})
Expand All @@ -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();
},
)
Expand All @@ -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();
},
)
Expand All @@ -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();
}
}
Expand All @@ -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
Expand Down Expand Up @@ -617,25 +690,31 @@ 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.
// However, we explicitly handle it here in order to better match the pinch-to-zoom
// 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();
})
}
Expand Down

0 comments on commit df9cd21

Please sign in to comment.