Skip to content

Commit

Permalink
Only repaint on cursor movements of area, or if dragging outside (#4730)
Browse files Browse the repository at this point in the history
* Closes #4723

Also fix some small bugs in the touch input on web
  • Loading branch information
emilk authored Jun 28, 2024
1 parent 10f092d commit 0c059ac
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 121 deletions.
4 changes: 1 addition & 3 deletions crates/eframe/src/web/app_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ pub struct AppRunner {
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
last_save_time: f64,
pub(crate) text_agent: TextAgent,
pub(crate) mutable_text_under_cursor: bool,

// Output for the last run:
textures_delta: TexturesDelta,
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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!(
Expand Down
8 changes: 2 additions & 6 deletions crates/eframe/src/web/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<egui::Pos2>,

/// Required to maintain a stable touch position for multi-touch gestures.
pub latest_touch_pos_id: Option<egui::TouchId>,
pub primary_touch: Option<egui::TouchId>,

/// The raw input to `egui`.
pub raw: egui::RawInput,
Expand Down Expand Up @@ -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;
}
}

Expand Down
141 changes: 68 additions & 73 deletions crates/eframe/src/web/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
})
}

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

Expand All @@ -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();
Expand All @@ -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();
}
}
})
}
Expand Down
63 changes: 37 additions & 26 deletions crates/eframe/src/web/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// 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.
///
/// `touch_id_for_pos` is the [`TouchId`](egui::TouchId) of the [`Touch`](web_sys::Touch) we previously used to determine the
/// pointer position.
pub fn pos_from_touch_event(
canvas: &web_sys::HtmlCanvasElement,
pub fn primary_touch_pos(
runner: &mut AppRunner,
event: &web_sys::TouchEvent,
touch_id_for_pos: &mut Option<egui::TouchId>,
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<egui::Pos2> {
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(
Expand Down
4 changes: 3 additions & 1 deletion crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

// -------------------
Expand Down
32 changes: 25 additions & 7 deletions crates/egui/src/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Duration> {
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.
Expand Down Expand Up @@ -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);
});
Expand Down
12 changes: 7 additions & 5 deletions crates/egui_demo_lib/src/demo/tests/input_event_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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:?}"),
Expand Down

0 comments on commit 0c059ac

Please sign in to comment.