1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 05:10:03 -04:00

Basic multi touch support (issue #279) (#306)

* translate touch events from glium to egui

Unfortunately, winit does not seem to create _Touch_ events for the touch pad
on my mac.  Only _TouchpadPressure_ events are sent.

Found some issues (like
[this](https://github.com/rust-windowing/winit/issues/54)), but I am not sure
what they exactly mean:  Sometimes, touch events are mixed with
touch-to-pointer translation in the discussions.

* translate touch events from web_sys to egui

The are a few open topics:
- egui_web currently translates touch events into pointer events.
  I guess this should change, such that egui itself performs this kind of
  conversion.
- `pub fn egui_web::pos_from_touch_event` is a public function, but I
  would like to change the return type to an `Option`.  Shouldn't this
  function be private, anyway?

* introduce `TouchState` and `Gesture`

InputState.touch was introduced with type `TouchState`, just as
InputState.pointer is of type `Pointer`.

The TouchState internally relies on a collection of `Gesture`s.  This commit
provides the first rudimentary implementation of a Gesture, but has no
functionality, yet.

* add method InputState::zoom()

So far, the method always returns `None`, but it should work as soon as the
`Zoom` gesture is implemented.

* manage one `TouchState` per individual device

Although quite unlikely, it is still possible to connect more than one touch
device. (I have three touch pads connected to my MacBook in total, but
unfortunately `winit` sends touch events for none of them.)

We do not want to mix-up the touches from different devices.

* implement control loop for gesture detection

The basic idea is that each gesture can focus on detection logic and does not
have to care (too much) about managing touch state in general.

* streamline `Gesture` trait, simplifying impl's

* implement first version of Zoom gesture

* fix failing doctest

a simple `TODO` should be enough

* get rid of `Gesture`s

* Provide a Zoom/Rotate window in the demo app

For now, it works for two fingers only.  The third finger interrupts the
gesture.

Bugs:
- Pinching in the demo window also moves the window -> Pointer events must be
  ignored when touch is active
- Pinching also works when doing it outside the demo window -> it would be nice
  to return the touch info in the `Response` of the painter allocation

* fix comments and non-idiomatic code

* update touch state *each frame*

* change egui_demo to use *relative* touch data

* support more than two fingers

This commit includes an improved Demo Window for egui_demo, and a complete
re-write of the gesture detection.  The PR should be ready for review, soon.

* cleanup code and comments for review

* minor code simplifications

* oops – forgot the changelog

* resolve comment fee8ed83db (r623226656)

* accept suggestion https://github.com/emilk/egui/pull/306#discussion_r623229228

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* fix syntax error (dough!)

* remove `dbg!` (why didnt clippy see this?)

* apply suggested diffs from review

* fix conversion of physical location to Pos2

* remove redundanct type `TouchAverages`

* remove trailing space

* avoid initial translation jump in plot demo

* extend the demo so it shows off translation

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Ivo Vollrath
2021-05-06 21:01:10 +02:00
committed by GitHub
parent 0d71017ad4
commit 03721dbfd8
11 changed files with 686 additions and 15 deletions

View File

@@ -84,6 +84,9 @@ pub 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 raw: egui::RawInput,
}

View File

@@ -114,13 +114,65 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option<egui::Poin
}
}
pub fn pos_from_touch_event(canvas_id: &str, event: &web_sys::TouchEvent) -> egui::Pos2 {
let canvas = canvas_element(canvas_id).unwrap();
let rect = canvas.get_bounding_client_rect();
let t = event.touches().get(0).unwrap();
/// 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` of the `Touch` we previously used to determine the
/// pointer position.
pub fn pos_from_touch_event(
canvas_id: &str,
event: &web_sys::TouchEvent,
touch_id_for_pos: &mut Option<egui::TouchId>,
) -> 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):
touch_for_pos = (0..event.touches().length())
.into_iter()
.map(|i| event.touches().get(i).unwrap())
.find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos);
} else {
touch_for_pos = 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_origin(canvas_id), &touch)
})
}
fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 {
egui::Pos2 {
x: t.page_x() as f32 - rect.left() as f32,
y: t.page_y() as f32 - rect.top() as f32,
x: touch.page_x() as f32 - canvas_origin.x as f32,
y: touch.page_y() as f32 - canvas_origin.y as f32,
}
}
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
let rect = canvas_element(canvas_id)
.unwrap()
.get_bounding_client_rect();
egui::Pos2::new(rect.left() as f32, rect.top() as f32)
}
fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) {
let canvas_origin = canvas_origin(runner.canvas_id());
for touch_idx in 0..event.changed_touches().length() {
if let Some(touch) = event.changed_touches().item(touch_idx) {
runner.input.raw.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId::from(touch.identifier()),
phase,
pos: pos_from_touch(canvas_origin, &touch),
force: touch.force(),
});
}
}
}
@@ -876,7 +928,10 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
let mut runner_lock = runner_ref.0.lock();
let pos = pos_from_touch_event(runner_lock.canvas_id(), &event);
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
let pos =
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
runner_lock.input.latest_touch_pos = Some(pos);
runner_lock.input.is_touch = true;
let modifiers = runner_lock.input.raw.modifiers;
@@ -890,6 +945,8 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
pressed: true,
modifiers,
});
push_touches(&mut *runner_lock, egui::TouchPhase::Start, &event);
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
@@ -903,7 +960,10 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
let mut runner_lock = runner_ref.0.lock();
let pos = pos_from_touch_event(runner_lock.canvas_id(), &event);
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
let pos =
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
runner_lock.input.latest_touch_pos = Some(pos);
runner_lock.input.is_touch = true;
runner_lock
@@ -911,6 +971,8 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
.raw
.events
.push(egui::Event::PointerMoved(pos));
push_touches(&mut *runner_lock, egui::TouchPhase::Move, &event);
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
@@ -940,6 +1002,8 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
});
// Then remove hover effect:
runner_lock.input.raw.events.push(egui::Event::PointerGone);
push_touches(&mut *runner_lock, egui::TouchPhase::End, &event);
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
@@ -952,6 +1016,20 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
closure.forget();
}
{
let event_name = "touchcancel";
let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
let mut runner_lock = runner_ref.0.lock();
runner_lock.input.is_touch = true;
push_touches(&mut *runner_lock, egui::TouchPhase::Cancel, &event);
event.stop_propagation();
event.prevent_default();
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let event_name = "wheel";
let runner_ref = runner_ref.clone();