diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 894b7dcaae7..4b4aad45fe9 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -15,7 +15,6 @@ pub struct AppRunner { pub(crate) needs_repaint: std::sync::Arc, last_save_time: f64, pub(crate) text_agent: TextAgent, - pub(crate) mutable_text_under_cursor: bool, // Output for the last run: textures_delta: TexturesDelta, @@ -121,7 +120,6 @@ impl AppRunner { needs_repaint, last_save_time: now_sec(), text_agent, - mutable_text_under_cursor: false, textures_delta: Default::default(), clipped_primitives: None, }; @@ -275,7 +273,7 @@ impl AppRunner { #[cfg(not(web_sys_unstable_apis))] let _ = copied_text; - self.mutable_text_under_cursor = mutable_text_under_cursor; + self.text_agent.set_focus(mutable_text_under_cursor); if let Err(err) = self.text_agent.move_to(ime, self.canvas()) { log::error!( diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index fa87bf3b324..5877f424cb7 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -12,10 +12,7 @@ use super::percent_decode; #[derive(Default)] pub(crate) struct WebInput { /// Required because we don't get a position on touched - pub latest_touch_pos: Option, - - /// Required to maintain a stable touch position for multi-touch gestures. - pub latest_touch_pos_id: Option, + pub primary_touch: Option, /// The raw input to `egui`. pub raw: egui::RawInput, @@ -46,8 +43,7 @@ impl WebInput { self.raw.modifiers = egui::Modifiers::default(); // Avoid sticky modifier keys on alt-tab: self.raw.focused = focused; self.raw.events.push(egui::Event::WindowFocused(focused)); - self.latest_touch_pos = None; - self.latest_touch_pos_id = None; + self.primary_touch = None; } } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index ba895f60786..403caf398de 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -405,15 +405,24 @@ fn install_mousedown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), ) } +/// Returns true if the cursor is above the canvas, or if we're dragging something. +fn is_interested_in_pointer_event(egui_ctx: &egui::Context, pos: egui::Pos2) -> bool { + egui_ctx.input(|i| i.screen_rect().contains(pos) || i.pointer.any_down() || i.any_touches()) +} + fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { runner_ref.add_event_listener(target, "mousemove", |event: web_sys::MouseEvent, runner| { let modifiers = modifiers_from_mouse_event(&event); runner.input.raw.modifiers = modifiers; + let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); - runner.input.raw.events.push(egui::Event::PointerMoved(pos)); - runner.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); + + if is_interested_in_pointer_event(runner.egui_ctx(), pos) { + runner.input.raw.events.push(egui::Event::PointerMoved(pos)); + runner.needs_repaint.repaint_asap(); + event.stop_propagation(); + event.prevent_default(); + } }) } @@ -422,29 +431,31 @@ fn install_mouseup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J 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, - }); + let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); - // 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(); + if is_interested_in_pointer_event(runner.egui_ctx(), pos) { + if let Some(button) = button_from_mouse_event(&event) { + 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 do certain things + // (like playing audio, start a download, etc) + // on user action, such as a click. + // So we need to run the app logic here and now: + runner.logic(); - runner - .text_agent - .set_focus(runner.mutable_text_under_cursor); + // 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(); + event.prevent_default(); + event.stop_propagation(); + } } - event.stop_propagation(); - event.prevent_default(); }) } @@ -466,22 +477,14 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<() target, "touchstart", |event: web_sys::TouchEvent, runner| { - let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; - let pos = pos_from_touch_event( - runner.canvas(), - &event, - &mut latest_touch_pos_id, - runner.egui_ctx(), - ); - runner.input.latest_touch_pos_id = latest_touch_pos_id; - runner.input.latest_touch_pos = Some(pos); - let modifiers = runner.input.raw.modifiers; - runner.input.raw.events.push(egui::Event::PointerButton { - pos, - button: egui::PointerButton::Primary, - pressed: true, - modifiers, - }); + if let Some(pos) = primary_touch_pos(runner, &event) { + runner.input.raw.events.push(egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed: true, + modifiers: runner.input.raw.modifiers, + }); + } push_touches(runner, egui::TouchPhase::Start, &event); runner.needs_repaint.repaint_asap(); @@ -493,47 +496,39 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<() fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { runner_ref.add_event_listener(target, "touchmove", |event: web_sys::TouchEvent, runner| { - let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; - let pos = pos_from_touch_event( - runner.canvas(), - &event, - &mut latest_touch_pos_id, - runner.egui_ctx(), - ); - runner.input.latest_touch_pos_id = latest_touch_pos_id; - runner.input.latest_touch_pos = Some(pos); - runner.input.raw.events.push(egui::Event::PointerMoved(pos)); - - push_touches(runner, egui::TouchPhase::Move, &event); - runner.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); + if let Some(pos) = primary_touch_pos(runner, &event) { + if is_interested_in_pointer_event(runner.egui_ctx(), pos) { + runner.input.raw.events.push(egui::Event::PointerMoved(pos)); + + push_touches(runner, egui::TouchPhase::Move, &event); + runner.needs_repaint.repaint_asap(); + event.stop_propagation(); + event.prevent_default(); + } + } }) } fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { runner_ref.add_event_listener(target, "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); + if let Some(pos) = primary_touch_pos(runner, &event) { + if is_interested_in_pointer_event(runner.egui_ctx(), pos) { + // First release mouse to click: + runner.input.raw.events.push(egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed: false, + modifiers: runner.input.raw.modifiers, + }); + // Then remove hover effect: + runner.input.raw.events.push(egui::Event::PointerGone); - runner - .text_agent - .set_focus(runner.mutable_text_under_cursor); + push_touches(runner, egui::TouchPhase::End, &event); - runner.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); + runner.needs_repaint.repaint_asap(); + event.stop_propagation(); + event.prevent_default(); + } } }) } diff --git a/crates/eframe/src/web/input.rs b/crates/eframe/src/web/input.rs index c3ddf34898e..a98dde61313 100644 --- a/crates/eframe/src/web/input.rs +++ b/crates/eframe/src/web/input.rs @@ -27,33 +27,44 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option, - egui_ctx: &egui::Context, -) -> egui::Pos2 { - let touch_for_pos = if let Some(touch_id_for_pos) = touch_id_for_pos { - // search for the touch we previously used for the position - // (unfortunately, `event.touches()` is not a rust collection): - (0..event.touches().length()) - .map(|i| event.touches().get(i).unwrap()) - .find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos) - } else { - None - }; - // Use the touch found above or pick the first, or return a default position if there is no - // touch at all. (The latter is not expected as the current method is only called when there is - // at least one touch.) - touch_for_pos - .or_else(|| event.touches().get(0)) - .map_or(Default::default(), |touch| { - *touch_id_for_pos = Some(egui::TouchId::from(touch.identifier())); - pos_from_touch(canvas_content_rect(canvas), &touch, egui_ctx) - }) +) -> Option { + let all_touches: Vec<_> = (0..event.touches().length()) + .filter_map(|i| event.touches().get(i)) + // On touchend we don't get anything in `touches`, but we still get `changed_touches`, so include those: + .chain((0..event.changed_touches().length()).filter_map(|i| event.changed_touches().get(i))) + .collect(); + + if let Some(primary_touch) = runner.input.primary_touch { + // Is the primary touch is gone? + if !all_touches + .iter() + .any(|touch| primary_touch == egui::TouchId::from(touch.identifier())) + { + runner.input.primary_touch = None; + } + } + + if runner.input.primary_touch.is_none() { + runner.input.primary_touch = all_touches + .first() + .map(|touch| egui::TouchId::from(touch.identifier())); + } + + let primary_touch = runner.input.primary_touch; + + if let Some(primary_touch) = primary_touch { + for touch in all_touches { + if primary_touch == egui::TouchId::from(touch.identifier()) { + let canvas_rect = canvas_content_rect(runner.canvas()); + return Some(pos_from_touch(canvas_rect, &touch, runner.egui_ctx())); + } + } + } + + None } fn pos_from_touch( diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index a02e19c467c..0e8bfe4d66c 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2026,8 +2026,10 @@ impl ContextImpl { viewport.widgets_this_frame.clear(); } - if repaint_needed || viewport.input.wants_repaint() { + if repaint_needed { self.request_repaint(ended_viewport_id, RepaintCause::new()); + } else if let Some(delay) = viewport.input.wants_repaint_after() { + self.request_repaint_after(delay, ended_viewport_id, RepaintCause::new()); } // ------------------- diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index f52bfe24928..e2fd693b786 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -2,7 +2,10 @@ mod touch_state; use crate::data::input::*; use crate::{emath::*, util::History}; -use std::collections::{BTreeMap, HashSet}; +use std::{ + collections::{BTreeMap, HashSet}, + time::Duration, +}; pub use crate::Key; pub use touch_state::MultiTouchInfo; @@ -389,15 +392,30 @@ impl InputState { } /// The [`crate::Context`] will call this at the end of each frame to see if we need a repaint. - pub fn wants_repaint(&self) -> bool { - self.pointer.wants_repaint() + /// + /// Returns how long to wait for a repaint. + pub fn wants_repaint_after(&self) -> Option { + if self.pointer.wants_repaint() || self.unprocessed_scroll_delta.abs().max_elem() > 0.2 || self.unprocessed_scroll_delta_for_zoom.abs() > 0.2 || !self.events.is_empty() + { + // Immediate repaint + return Some(Duration::ZERO); + } + + if self.any_touches() && !self.pointer.is_decidedly_dragging() { + // We need to wake up and check for press-and-hold for the context menu. + if let Some(press_start_time) = self.pointer.press_start_time { + let press_duration = self.time - press_start_time; + if press_duration < MAX_CLICK_DURATION { + let secs_until_menu = MAX_CLICK_DURATION - press_duration; + return Some(Duration::from_secs_f64(secs_until_menu)); + } + } + } - // We need to wake up and check for press-and-hold for the context menu. - // TODO(emilk): wake up after `MAX_CLICK_DURATION` instead of every frame. - || (self.any_touches() && !self.pointer.is_decidedly_dragging()) + None } /// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once. @@ -1208,7 +1226,7 @@ impl InputState { ui.collapsing("Raw Input", |ui| raw.ui(ui)); crate::containers::CollapsingHeader::new("🖱 Pointer") - .default_open(true) + .default_open(false) .show(ui, |ui| { pointer.ui(ui); }); diff --git a/crates/egui_demo_lib/src/demo/tests/input_event_history.rs b/crates/egui_demo_lib/src/demo/tests/input_event_history.rs index a0b3d897c9a..9564e1b6dfd 100644 --- a/crates/egui_demo_lib/src/demo/tests/input_event_history.rs +++ b/crates/egui_demo_lib/src/demo/tests/input_event_history.rs @@ -90,7 +90,9 @@ impl crate::View for InputEventHistory { if !self.include_pointer_movements && matches!( event, - egui::Event::PointerMoved(_) | egui::Event::MouseMoved(_) + egui::Event::PointerMoved { .. } + | egui::Event::MouseMoved { .. } + | egui::Event::Touch { .. } ) { continue; @@ -121,10 +123,10 @@ impl crate::View for InputEventHistory { fn event_summary(event: &egui::Event) -> String { match event { - egui::Event::PointerMoved(_) => "PointerMoved { .. }".to_owned(), - egui::Event::MouseMoved(_) => "MouseMoved { .. }".to_owned(), - egui::Event::Zoom(_) => "Zoom { .. }".to_owned(), - egui::Event::Touch { phase, .. } => format!("Zoom {{ phase: {phase:?}, .. }}"), + egui::Event::PointerMoved { .. } => "PointerMoved { .. }".to_owned(), + egui::Event::MouseMoved { .. } => "MouseMoved { .. }".to_owned(), + egui::Event::Zoom { .. } => "Zoom { .. }".to_owned(), + egui::Event::Touch { phase, .. } => format!("Touch {{ phase: {phase:?}, .. }}"), egui::Event::MouseWheel { unit, .. } => format!("MouseWheel {{ unit: {unit:?}, .. }}"), _ => format!("{event:?}"),