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:
parent
10f092d9d4
commit
0c059ac113
|
|
@ -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,
|
||||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
runner
|
||||
.text_agent
|
||||
.set_focus(runner.mutable_text_under_cursor);
|
||||
// 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();
|
||||
|
||||
// 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));
|
||||
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();
|
||||
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);
|
||||
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);
|
||||
|
||||
push_touches(runner, egui::TouchPhase::End, &event);
|
||||
push_touches(runner, egui::TouchPhase::End, &event);
|
||||
|
||||
runner
|
||||
.text_agent
|
||||
.set_focus(runner.mutable_text_under_cursor);
|
||||
|
||||
runner.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
runner.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
// -------------------
|
||||
|
|
|
|||
|
|
@ -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<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);
|
||||
}
|
||||
|
||||
// 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())
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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:?}"),
|
||||
|
|
|
|||
Loading…
Reference in New Issue