Only repaint on cursor movements of area, or if dragging outside (#4730)

* Closes https://github.com/emilk/egui/issues/4723

Also fix some small bugs in the touch input on web
This commit is contained in:
Emil Ernerfeldt 2024-06-28 17:40:48 +02:00 committed by GitHub
parent 10f092d9d4
commit 0c059ac113
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 142 additions and 120 deletions

View File

@ -15,7 +15,6 @@ pub struct AppRunner {
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>, pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
last_save_time: f64, last_save_time: f64,
pub(crate) text_agent: TextAgent, pub(crate) text_agent: TextAgent,
pub(crate) mutable_text_under_cursor: bool,
// Output for the last run: // Output for the last run:
textures_delta: TexturesDelta, textures_delta: TexturesDelta,
@ -121,7 +120,6 @@ impl AppRunner {
needs_repaint, needs_repaint,
last_save_time: now_sec(), last_save_time: now_sec(),
text_agent, text_agent,
mutable_text_under_cursor: false,
textures_delta: Default::default(), textures_delta: Default::default(),
clipped_primitives: None, clipped_primitives: None,
}; };
@ -275,7 +273,7 @@ impl AppRunner {
#[cfg(not(web_sys_unstable_apis))] #[cfg(not(web_sys_unstable_apis))]
let _ = copied_text; 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()) { if let Err(err) = self.text_agent.move_to(ime, self.canvas()) {
log::error!( log::error!(

View File

@ -12,10 +12,7 @@ use super::percent_decode;
#[derive(Default)] #[derive(Default)]
pub(crate) struct WebInput { pub(crate) struct WebInput {
/// Required because we don't get a position on touched /// Required because we don't get a position on touched
pub latest_touch_pos: Option<egui::Pos2>, pub primary_touch: Option<egui::TouchId>,
/// Required to maintain a stable touch position for multi-touch gestures.
pub latest_touch_pos_id: Option<egui::TouchId>,
/// The raw input to `egui`. /// The raw input to `egui`.
pub raw: egui::RawInput, 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.modifiers = egui::Modifiers::default(); // Avoid sticky modifier keys on alt-tab:
self.raw.focused = focused; self.raw.focused = focused;
self.raw.events.push(egui::Event::WindowFocused(focused)); self.raw.events.push(egui::Event::WindowFocused(focused));
self.latest_touch_pos = None; self.primary_touch = None;
self.latest_touch_pos_id = None;
} }
} }

View File

@ -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> { fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "mousemove", |event: web_sys::MouseEvent, runner| { runner_ref.add_event_listener(target, "mousemove", |event: web_sys::MouseEvent, runner| {
let modifiers = modifiers_from_mouse_event(&event); let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers; runner.input.raw.modifiers = modifiers;
let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); 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(); if is_interested_in_pointer_event(runner.egui_ctx(), pos) {
event.stop_propagation(); runner.input.raw.events.push(egui::Event::PointerMoved(pos));
event.prevent_default(); 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); let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers; 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 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 if is_interested_in_pointer_event(runner.egui_ctx(), pos) {
// event callback, which is why we run the app logic here and now: if let Some(button) = button_from_mouse_event(&event) {
runner.logic(); let modifiers = runner.input.raw.modifiers;
runner.input.raw.events.push(egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
});
runner // In Safari we are only allowed to do certain things
.text_agent // (like playing audio, start a download, etc)
.set_focus(runner.mutable_text_under_cursor); // on user action, such as a click.
// So we need to run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap: // Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_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, target,
"touchstart", "touchstart",
|event: web_sys::TouchEvent, runner| { |event: web_sys::TouchEvent, runner| {
let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; if let Some(pos) = primary_touch_pos(runner, &event) {
let pos = pos_from_touch_event( runner.input.raw.events.push(egui::Event::PointerButton {
runner.canvas(), pos,
&event, button: egui::PointerButton::Primary,
&mut latest_touch_pos_id, pressed: true,
runner.egui_ctx(), modifiers: runner.input.raw.modifiers,
); });
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,
});
push_touches(runner, egui::TouchPhase::Start, &event); push_touches(runner, egui::TouchPhase::Start, &event);
runner.needs_repaint.repaint_asap(); 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> { fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "touchmove", |event: web_sys::TouchEvent, runner| { runner_ref.add_event_listener(target, "touchmove", |event: web_sys::TouchEvent, runner| {
let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; if let Some(pos) = primary_touch_pos(runner, &event) {
let pos = pos_from_touch_event( if is_interested_in_pointer_event(runner.egui_ctx(), pos) {
runner.canvas(), runner.input.raw.events.push(egui::Event::PointerMoved(pos));
&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); push_touches(runner, egui::TouchPhase::Move, &event);
runner.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
}
}
}) })
} }
fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "touchend", |event: web_sys::TouchEvent, runner| { runner_ref.add_event_listener(target, "touchend", |event: web_sys::TouchEvent, runner| {
if let Some(pos) = runner.input.latest_touch_pos { if let Some(pos) = primary_touch_pos(runner, &event) {
let modifiers = runner.input.raw.modifiers; if is_interested_in_pointer_event(runner.egui_ctx(), pos) {
// First release mouse to click: // First release mouse to click:
runner.input.raw.events.push(egui::Event::PointerButton { runner.input.raw.events.push(egui::Event::PointerButton {
pos, pos,
button: egui::PointerButton::Primary, button: egui::PointerButton::Primary,
pressed: false, pressed: false,
modifiers, modifiers: runner.input.raw.modifiers,
}); });
// Then remove hover effect: // Then remove hover effect:
runner.input.raw.events.push(egui::Event::PointerGone); runner.input.raw.events.push(egui::Event::PointerGone);
push_touches(runner, egui::TouchPhase::End, &event); push_touches(runner, egui::TouchPhase::End, &event);
runner runner.needs_repaint.repaint_asap();
.text_agent event.stop_propagation();
.set_focus(runner.mutable_text_under_cursor); event.prevent_default();
}
runner.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
} }
}) })
} }

View File

@ -27,33 +27,44 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option<egui::Poin
/// A single touch is translated to a pointer movement. When a second touch is added, the pointer /// A single touch is translated to a pointer movement. When a second touch is added, the pointer
/// should not jump to a different position. Therefore, we do not calculate the average position /// should not jump to a different position. Therefore, we do not calculate the average position
/// of all touches, but we keep using the same touch as long as it is available. /// of all touches, but we keep using the same touch as long as it is available.
/// pub fn primary_touch_pos(
/// `touch_id_for_pos` is the [`TouchId`](egui::TouchId) of the [`Touch`](web_sys::Touch) we previously used to determine the runner: &mut AppRunner,
/// pointer position.
pub fn pos_from_touch_event(
canvas: &web_sys::HtmlCanvasElement,
event: &web_sys::TouchEvent, event: &web_sys::TouchEvent,
touch_id_for_pos: &mut Option<egui::TouchId>, ) -> Option<egui::Pos2> {
egui_ctx: &egui::Context, let all_touches: Vec<_> = (0..event.touches().length())
) -> egui::Pos2 { .filter_map(|i| event.touches().get(i))
let touch_for_pos = if let Some(touch_id_for_pos) = touch_id_for_pos { // On touchend we don't get anything in `touches`, but we still get `changed_touches`, so include those:
// search for the touch we previously used for the position .chain((0..event.changed_touches().length()).filter_map(|i| event.changed_touches().get(i)))
// (unfortunately, `event.touches()` is not a rust collection): .collect();
(0..event.touches().length())
.map(|i| event.touches().get(i).unwrap()) if let Some(primary_touch) = runner.input.primary_touch {
.find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos) // Is the primary touch is gone?
} else { if !all_touches
None .iter()
}; .any(|touch| primary_touch == egui::TouchId::from(touch.identifier()))
// 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 runner.input.primary_touch = None;
// at least one touch.) }
touch_for_pos }
.or_else(|| event.touches().get(0))
.map_or(Default::default(), |touch| { if runner.input.primary_touch.is_none() {
*touch_id_for_pos = Some(egui::TouchId::from(touch.identifier())); runner.input.primary_touch = all_touches
pos_from_touch(canvas_content_rect(canvas), &touch, egui_ctx) .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( fn pos_from_touch(

View File

@ -2026,8 +2026,10 @@ impl ContextImpl {
viewport.widgets_this_frame.clear(); viewport.widgets_this_frame.clear();
} }
if repaint_needed || viewport.input.wants_repaint() { if repaint_needed {
self.request_repaint(ended_viewport_id, RepaintCause::new()); 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());
} }
// ------------------- // -------------------

View File

@ -2,7 +2,10 @@ mod touch_state;
use crate::data::input::*; use crate::data::input::*;
use crate::{emath::*, util::History}; use crate::{emath::*, util::History};
use std::collections::{BTreeMap, HashSet}; use std::{
collections::{BTreeMap, HashSet},
time::Duration,
};
pub use crate::Key; pub use crate::Key;
pub use touch_state::MultiTouchInfo; 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. /// 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<Duration> {
if self.pointer.wants_repaint()
|| self.unprocessed_scroll_delta.abs().max_elem() > 0.2 || self.unprocessed_scroll_delta.abs().max_elem() > 0.2
|| self.unprocessed_scroll_delta_for_zoom.abs() > 0.2 || self.unprocessed_scroll_delta_for_zoom.abs() > 0.2
|| !self.events.is_empty() || !self.events.is_empty()
{
// Immediate repaint
return Some(Duration::ZERO);
}
// We need to wake up and check for press-and-hold for the context menu. if self.any_touches() && !self.pointer.is_decidedly_dragging() {
// TODO(emilk): wake up after `MAX_CLICK_DURATION` instead of every frame. // We need to wake up and check for press-and-hold for the context menu.
|| (self.any_touches() && !self.pointer.is_decidedly_dragging()) 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));
}
}
}
None
} }
/// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once. /// 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)); ui.collapsing("Raw Input", |ui| raw.ui(ui));
crate::containers::CollapsingHeader::new("🖱 Pointer") crate::containers::CollapsingHeader::new("🖱 Pointer")
.default_open(true) .default_open(false)
.show(ui, |ui| { .show(ui, |ui| {
pointer.ui(ui); pointer.ui(ui);
}); });

View File

@ -90,7 +90,9 @@ impl crate::View for InputEventHistory {
if !self.include_pointer_movements if !self.include_pointer_movements
&& matches!( && matches!(
event, event,
egui::Event::PointerMoved(_) | egui::Event::MouseMoved(_) egui::Event::PointerMoved { .. }
| egui::Event::MouseMoved { .. }
| egui::Event::Touch { .. }
) )
{ {
continue; continue;
@ -121,10 +123,10 @@ impl crate::View for InputEventHistory {
fn event_summary(event: &egui::Event) -> String { fn event_summary(event: &egui::Event) -> String {
match event { match event {
egui::Event::PointerMoved(_) => "PointerMoved { .. }".to_owned(), egui::Event::PointerMoved { .. } => "PointerMoved { .. }".to_owned(),
egui::Event::MouseMoved(_) => "MouseMoved { .. }".to_owned(), egui::Event::MouseMoved { .. } => "MouseMoved { .. }".to_owned(),
egui::Event::Zoom(_) => "Zoom { .. }".to_owned(), egui::Event::Zoom { .. } => "Zoom { .. }".to_owned(),
egui::Event::Touch { phase, .. } => format!("Zoom {{ phase: {phase:?}, .. }}"), egui::Event::Touch { phase, .. } => format!("Touch {{ phase: {phase:?}, .. }}"),
egui::Event::MouseWheel { unit, .. } => format!("MouseWheel {{ unit: {unit:?}, .. }}"), egui::Event::MouseWheel { unit, .. } => format!("MouseWheel {{ unit: {unit:?}, .. }}"),
_ => format!("{event:?}"), _ => format!("{event:?}"),