Skip to content

Commit

Permalink
Conditionally propagating web events
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 embedded in a larger 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 `WebConfig` 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.
  • Loading branch information
liamrosenfeld committed Sep 1, 2024
1 parent edea5a4 commit 7fafae7
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 38 deletions.
6 changes: 6 additions & 0 deletions crates/eframe/src/epi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,10 @@ 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.
pub should_propagate_event: Box<dyn Fn(&egui::Event) -> bool>,
}

#[cfg(target_arch = "wasm32")]
Expand All @@ -471,6 +475,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
133 changes: 96 additions & 37 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();
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 @@ -195,7 +201,9 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
}

// Assume egui uses all key events, and don't let them propagate to parent elements.
event.stop_propagation();
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,20 +276,22 @@ 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();

let has_focus = runner.input.raw.focused;
if has_focus {
if has_focus && !propagate_event {
// Assume egui uses all key events, and don't let them propagate to parent elements.
event.stop_propagation();
}
Expand All @@ -288,11 +302,17 @@ 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();
if !should_propagate {
event.stop_propagation();
}
event.prevent_default();
}
}
Expand All @@ -310,7 +330,9 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
runner.needs_repaint.repaint_asap();
}

event.stop_propagation();
if !(runner.web_options.should_propagate_event)(&egui::Event::Cut) {
event.stop_propagation();
}
event.prevent_default();
})?;

Expand All @@ -326,7 +348,9 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
runner.needs_repaint.repaint_asap();
}

event.stop_propagation();
if !(runner.web_options.should_propagate_event)(&egui::Event::Copy) {
event.stop_propagation();
}
event.prevent_default();
})?;

Expand Down Expand Up @@ -400,15 +424,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 +444,9 @@ 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();
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 +468,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 +494,9 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
runner.needs_repaint.repaint_asap();

event.prevent_default();
event.stop_propagation();
if !should_propagate {
event.stop_propagation();
}
}
}
},
Expand Down Expand Up @@ -495,9 +528,13 @@ 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();
if !should_propagate {
event.stop_propagation();
}
event.prevent_default();
}
})
Expand All @@ -510,7 +547,9 @@ 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();
if !(runner.web_options.should_propagate_event)(&egui::Event::PointerGone) {
event.stop_propagation();
}
event.prevent_default();
},
)
Expand All @@ -521,18 +560,23 @@ 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();
if !should_propagate {
event.stop_propagation();
}
event.prevent_default();
},
)
Expand All @@ -545,11 +589,15 @@ 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();
if !should_propagate {
event.stop_propagation();
}
event.prevent_default();
}
}
Expand All @@ -564,19 +612,26 @@ 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();
if !should_propagate {
event.stop_propagation();
}
event.prevent_default();

// Fix virtual keyboard IOS
Expand Down Expand Up @@ -617,25 +672,29 @@ 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();
if !should_propagate {
event.stop_propagation();
}
event.prevent_default();
})
}
Expand Down

0 comments on commit 7fafae7

Please sign in to comment.