From c8578c9a6b78ff4b37b86f23264fbf78d826365c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 22 May 2024 11:48:34 +0200 Subject: [PATCH] Fix: still track mouse when dragging outside web canvas (#4522) * Closes https://github.com/emilk/egui/issues/3157 If the mouse leaves the canvas when dragging a slider, the slider will still move. --- To support this, I had to revert https://github.com/emilk/egui/pull/4419 Despite that, I fail to reproduce the two issues it claimed to solve: * https://github.com/emilk/egui/issues/4406 may have been solved in another way by this PR * https://github.com/emilk/egui/issues/4418 I cannot reproduce on Mac. If it is still a problem, I think it should be solved by triggering a `PointerEvent::Released` when focus is lost (i.e. on alt-tab), and not on `PointerGone` --- crates/eframe/src/web/events.rs | 107 +++++++++++++++---------- crates/egui/src/context.rs | 2 +- crates/egui/src/input_state.rs | 6 +- crates/egui/src/interaction.rs | 2 +- crates/egui_demo_lib/src/demo/tests.rs | 12 +-- 5 files changed, 73 insertions(+), 56 deletions(-) diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index f7234d76fad..66b90d3e0c2 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -284,6 +284,8 @@ pub(crate) fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Resul pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { let canvas = runner_ref.try_lock().unwrap().canvas().clone(); + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); { let prevent_default_events = [ @@ -333,8 +335,11 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu }, )?; + // NOTE: we register "mousemove" on `document` instead of just the canvas + // in order to track a dragged mouse outside the canvas. + // See https://github.com/emilk/egui/issues/3157 runner_ref.add_event_listener( - &canvas, + &document, "mousemove", |event: web_sys::MouseEvent, runner| { let modifiers = modifiers_from_mouse_event(&event); @@ -347,31 +352,37 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu }, )?; - runner_ref.add_event_listener(&canvas, "mouseup", |event: web_sys::MouseEvent, runner| { - let modifiers = modifiers_from_mouse_event(&event); - runner.input.raw.modifiers = modifiers; - 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 { - pos, - button, - pressed: false, - modifiers, - }); + // Use `document` here to notice if the user releases a drag outside of the canvas. + // See https://github.com/emilk/egui/issues/3157 + runner_ref.add_event_listener( + &document, + "mouseup", + |event: web_sys::MouseEvent, runner| { + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; + 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 { + pos, + button, + pressed: false, + modifiers, + }); - // 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: - runner.logic(); + // 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: + runner.logic(); - // Make sure we paint the output of the above logic call asap: - runner.needs_repaint.repaint_asap(); + // Make sure we paint the output of the above logic call asap: + runner.needs_repaint.repaint_asap(); - text_agent::update_text_agent(runner); - } - event.stop_propagation(); - event.prevent_default(); - })?; + text_agent::update_text_agent(runner); + } + event.stop_propagation(); + event.prevent_default(); + }, + )?; runner_ref.add_event_listener( &canvas, @@ -412,8 +423,10 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu }, )?; + // Use `document` here to notice if the user drag outside of the canvas. + // See https://github.com/emilk/egui/issues/3157 runner_ref.add_event_listener( - &canvas, + &document, "touchmove", |event: web_sys::TouchEvent, runner| { let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; @@ -434,28 +447,34 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu }, )?; - runner_ref.add_event_listener(&canvas, "touchend", |event: web_sys::TouchEvent, runner| { - if let Some(pos) = runner.input.latest_touch_pos { - let modifiers = runner.input.raw.modifiers; - // First release mouse to click: - runner.input.raw.events.push(egui::Event::PointerButton { - pos, - button: egui::PointerButton::Primary, - pressed: false, - modifiers, - }); - // Then remove hover effect: - runner.input.raw.events.push(egui::Event::PointerGone); + // Use `document` here to notice if the user releases a drag outside of the canvas. + // See https://github.com/emilk/egui/issues/3157 + runner_ref.add_event_listener( + &document, + "touchend", + |event: web_sys::TouchEvent, runner| { + if let Some(pos) = runner.input.latest_touch_pos { + let modifiers = runner.input.raw.modifiers; + // First release mouse to click: + runner.input.raw.events.push(egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed: false, + modifiers, + }); + // Then remove hover effect: + runner.input.raw.events.push(egui::Event::PointerGone); - push_touches(runner, egui::TouchPhase::End, &event); - runner.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); - } + push_touches(runner, egui::TouchPhase::End, &event); + runner.needs_repaint.repaint_asap(); + event.stop_propagation(); + event.prevent_default(); + } - // Finally, focus or blur text agent to toggle mobile keyboard: - text_agent::update_text_agent(runner); - })?; + // Finally, focus or blur text agent to toggle mobile keyboard: + text_agent::update_text_agent(runner); + }, + )?; runner_ref.add_event_listener( &canvas, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 2d58be7e126..04bdd16432a 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1875,8 +1875,8 @@ impl Context { drag_started: _, dragged, drag_stopped: _, - hovered, contains_pointer, + hovered, } = interact_widgets; if true { diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 22851a470b6..e82fd62b75e 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -800,10 +800,8 @@ impl PointerState { } Event::PointerGone => { self.latest_pos = None; - self.pointer_events.push(PointerEvent::Released { - click: None, - button: PointerButton::Primary, - }); + // When dragging a slider and the mouse leaves the viewport, we still want the drag to work, + // so we don't treat this as a `PointerEvent::Released`. // NOTE: we do NOT clear `self.interact_pos` here. It will be cleared next frame. } Event::MouseMoved(delta) => *self.motion.get_or_insert(Vec2::ZERO) += *delta, diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index e25e1c4aa13..8a7b2d0948c 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -283,7 +283,7 @@ pub(crate) fn interact( drag_started, dragged, drag_stopped, - hovered, contains_pointer, + hovered, } } diff --git a/crates/egui_demo_lib/src/demo/tests.rs b/crates/egui_demo_lib/src/demo/tests.rs index 6a8348ac56a..44e355d0e8e 100644 --- a/crates/egui_demo_lib/src/demo/tests.rs +++ b/crates/egui_demo_lib/src/demo/tests.rs @@ -466,23 +466,23 @@ fn response_summary(response: &egui::Response, show_hovers: bool) -> String { // These are in inverse logical/chonological order, because we show them in the ui that way: if response.triple_clicked_by(button) { - writeln!(new_info, "Triple_clicked_by{button_suffix}").ok(); + writeln!(new_info, "Triple-clicked{button_suffix}").ok(); } if response.double_clicked_by(button) { - writeln!(new_info, "Double_clicked_by{button_suffix}").ok(); + writeln!(new_info, "Double-clicked{button_suffix}").ok(); } if response.clicked_by(button) { - writeln!(new_info, "Clicked_by{button_suffix}").ok(); + writeln!(new_info, "Clicked{button_suffix}").ok(); } if response.drag_stopped_by(button) { - writeln!(new_info, "Drag_stopped_by{button_suffix}").ok(); + writeln!(new_info, "Drag stopped{button_suffix}").ok(); } if response.dragged_by(button) { - writeln!(new_info, "Dragged_by{button_suffix}").ok(); + writeln!(new_info, "Dragged{button_suffix}").ok(); } if response.drag_started_by(button) { - writeln!(new_info, "Drag_started_by{button_suffix}").ok(); + writeln!(new_info, "Drag started{button_suffix}").ok(); } }