From 1c4618aa7f0be27c6635553a3517f7aa3f6b21f8 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 3 Jun 2025 12:21:42 -0700 Subject: [PATCH] GUACAMOLE-2080: Automatically map local touch IDs to predictable, sequential integer IDs. --- .../src/main/webapp/modules/Touch.js | 72 ++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Touch.js b/guacamole-common-js/src/main/webapp/modules/Touch.js index 19aa76b88..34416ca1e 100644 --- a/guacamole-common-js/src/main/webapp/modules/Touch.js +++ b/guacamole-common-js/src/main/webapp/modules/Touch.js @@ -42,6 +42,23 @@ Guacamole.Touch = function Touch(element) { */ var guacTouch = this; + /** + * Pool of touch IDs available to be used for Guacamole touch events. + * + * @private + * @type {!Guacamole.IntegerPool} + */ + var identifierPool = new Guacamole.IntegerPool(); + + /** + * Mapping of local touch IDs to their corresponding Guacamole touch IDs + * allocated from the identifierPool. + * + * @private + * @type {!Object.} + */ + var identifierMapping = {}; + /** * The default X/Y radius of each touch if the device or browser does not * expose the size of the contact area. @@ -95,13 +112,62 @@ Guacamole.Touch = function Touch(element) { * event. */ + /** + * Returns the Guacamole touch ID for the given touch ID provided by the + * local browser from a local touch event. New touch IDs will be + * automatically allocated as necessary to ensure that each local touch ID + * has a sequential, predictable ID for the Guacamole event. + * + * NOTE: This is necessary to ensure that the touch IDs included within + * Guacamole touch events are predictable and reasonably sized. There is + * otherwise no guarantee that the browser will supply sequential or small + * integer values. The touch IDs supplied by the local browser may well be + * so large that server-side protocol implementations would each need a + * similar translation layer if we don't take care of this here. + * + * @private + * @param {!number} identifier + * @returns {!number} + */ + var mapIdentifier = function mapIdentifier(identifier) { + + if (identifier in identifierMapping) + return identifierMapping[identifier]; + + return (identifierMapping[identifier] = identifierPool.next()); + + }; + + /** + * Removes any established ID mapping for the given local touch ID, + * allowing the corresponding Guacamole touch ID to be reused by other + * touches. + * + * @private + * @param {!number} identifier + * @returns {!number} + */ + var unmapIdentifier = function unmapIdentifier(identifier) { + + // NOTE: Here, we intentionally return the ID that _would_ have been + // mapped had mapIdentifier() been properly called. This simplifies the + // logic while keeping things consistent. + + var id = mapIdentifier(identifier); + delete identifierMapping[identifier]; + + identifierPool.free(id); + return id; + + }; + element.addEventListener('touchstart', function touchstart(e) { // Fire "ontouchstart" events for all new touches for (var i = 0; i < e.changedTouches.length; i++) { var changedTouch = e.changedTouches[i]; - var identifier = changedTouch.identifier; + var identifier = mapIdentifier(changedTouch.identifier); // Ignore duplicated touches if (guacTouch.touches[identifier]) @@ -130,7 +196,7 @@ Guacamole.Touch = function Touch(element) { for (var i = 0; i < e.changedTouches.length; i++) { var changedTouch = e.changedTouches[i]; - var identifier = changedTouch.identifier; + var identifier = mapIdentifier(changedTouch.identifier); // Ignore any unrecognized touches var touch = guacTouch.touches[identifier]; @@ -161,7 +227,7 @@ Guacamole.Touch = function Touch(element) { for (var i = 0; i < e.changedTouches.length; i++) { var changedTouch = e.changedTouches[i]; - var identifier = changedTouch.identifier; + var identifier = unmapIdentifier(changedTouch.identifier); // Ignore any unrecognized touches var touch = guacTouch.touches[identifier];