Skip to content

Commit

Permalink
Emulate double click from touch devices (#1787)
Browse files Browse the repository at this point in the history
Resolves #1781.

This PR improves double-click emulation so that both of the consecutive
mouse (touch) events have the exact same location. On a touch device,
you might otherwise accidentally direct the second click to a slightly
different location, or the target operating system might not even
recognise both clicks as proper double click.

## Notes

- I’ve slightly re-ordered the commentary, which I thought was sensible
due to the increased complexity in the file.
- The parameters for detecting a double-click are heuristic at best –
the 500ms delay is from [this
article](https://en.wikipedia.org/wiki/Double-click#:~:text=rely%20upon%20it.-,Speed%20and%20timing,basis%20for%20other%20timed%20actions.),
and the 500px distance just felt okay when trying from an iPad.

I’ve tested with this on an iPad, so from my side a pure code review
would suffice.

<a data-ca-tag
href="https://codeapprove.com/pr/tiny-pilot/tinypilot/1787"><img
src="https://codeapprove.com/external/github-tag-allbg.png" alt="Review
on CodeApprove" /></a>

---------

Co-authored-by: Jan Heuermann <[email protected]>
  • Loading branch information
jotaen4tinypilot and jotaen authored Apr 18, 2024
1 parent ae5f65e commit aead4f1
Showing 1 changed file with 56 additions and 12 deletions.
68 changes: 56 additions & 12 deletions app/static/js/touch.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@
* handling logic as good as possible, and to keep the complexity away from the
* “regular” mouse handling code in the remote screen component.
*
* We currently only provide rudimentary support for touch devices. So for now,
* this adapter is only capable of emulating single left clicks.
* We currently only provide basic support for touch devices. So for now, this
* adapter can emulate the following mouse actions:
* - Single left click
* - Double left click
*/
export class TouchToMouseAdapter {
_lastTouchPosition = { clientX: 0, clientY: 0 };
constructor() {
this._lastTouchInfo = {
timestamp: new Date(),
clientX: 0,
clientY: 0,
};
}

/**
* Synthetic mouse event that includes all properties that the
Expand All @@ -24,15 +32,22 @@ export class TouchToMouseAdapter {
* @returns {SyntheticMouseEvent}
*/
fromTouchStart(evt) {
// The corresponding `touchend` event won’t have the `touches` property
// set, so we need to preserve the latest one to be able to reconstruct the
// cursor position for the touch/mouse release.
const touch = evt.touches[0];
this._lastTouchPosition = {
clientX: touch.clientX,
clientY: touch.clientY,
const touchInfo = {
timestamp: new Date(),
clientX: evt.touches[0].clientX,
clientY: evt.touches[0].clientY,
};
return mouseClickEvent(evt.target, this._lastTouchPosition, 1);

// If this touch was a double click, use the mouse coordinates from the
// previous touch, so that the position is exactly the same. (See comment
// of `isDoubleClick` for why this is important.)
if (isDoubleClick(touchInfo, this._lastTouchInfo)) {
touchInfo.clientX = this._lastTouchInfo.clientX;
touchInfo.clientY = this._lastTouchInfo.clientY;
}

this._lastTouchInfo = touchInfo;
return mouseClickEvent(evt.target, this._lastTouchInfo, 1);
}

/**
Expand All @@ -43,7 +58,10 @@ export class TouchToMouseAdapter {
* @returns {SyntheticMouseEvent}
*/
fromTouchEndOrCancel(evt) {
return mouseClickEvent(evt.target, this._lastTouchPosition, 0);
// A `touchend` or `touchcancel` event doesn’t have the touches attribute
// set, so we have to use the last known touch position instead, to keep
// the mouse cursor in the same position.
return mouseClickEvent(evt.target, this._lastTouchInfo, 0);
}
}

Expand All @@ -57,3 +75,29 @@ function mouseClickEvent(target, touchPosition, buttons) {
deltaY: 0,
};
}

/**
* Checks whether two consecutive touches are intended to be a double click
* (double tap). This is true if both touches occur within a short time span,
* and if their location is close to each other. Note that in contrast to a
* mouse device, tapping twice on a touch screen almost never yields the exact
* same position for both touches. In this case, the user might accidentally
* click in the wrong place, or the target operating system might not recognize
* the two clicks as proper double click.
*/
function isDoubleClick(touchInfo1, touchInfo2) {
return (
distancePx(touchInfo1, touchInfo2) < 50 &&
delayMs(touchInfo1.timestamp, touchInfo2.timestamp) < 500
);
}

function distancePx(touchInfo1, touchInfo2) {
const a = Math.abs(touchInfo1.clientX - touchInfo2.clientX);
const b = Math.abs(touchInfo1.clientY - touchInfo2.clientY);
return Math.hypot(a, b);
}

function delayMs(date1, date2) {
return Math.abs(date1.getTime() - date2.getTime());
}

0 comments on commit aead4f1

Please sign in to comment.