typedef PointerID Integer; dictionary GestureState { Boolean cancel = true; // if true, then cancel the gesture at this point Boolean capture = false; // (for pointer-down) if true, then this pointer is relevant Boolean choose = false; // if true, the gesture thinks that other gestures should give up Boolean finished = true; // if true, we're ready for the next gesture to start // choose and cancel are mutually exclusive } dictionary SendEventOptions { Integer? coallesceGroup = null; // when queuing events, only the last event with each group is kept Boolean prechoose = false; // if true, event should just be sent right away, not queued } abstract class Gesture : EventTarget { constructor (EventTarget target); readonly attribute EventTarget target; virtual GestureState processEvent(Event event); // return {} virtual void choose(); // called by GestureManager // make sure to call superclass choose() before // - assert: this.active == true // - assert: this.chosen == false // - set this.chosen = true // - if there are any buffered events, dispatch them on this virtual void cancel(); // called by GestureManager // make sure to call superclass cancel() after // - set active and chosen to false, clear the event buffer readonly attribute Boolean ready; // last event, we were finished readonly attribute Boolean active; // we have not yet been canceled since we last captured a pointer readonly attribute Boolean chosen; // we're the only possible gesture at this point // !ready && !active => we're discarding events until the user gets to a state where a new gesture can begin // active && !chosen => we're collecting events until no other gesture is valid, or until we take command void sendEvent(Event event, SendEventOptions options); // used internally to queue up or send events // - assert: this.active == true // - assert: options.prechoose is false or options.coallesceGroup // is null // - set event.gesture = this // - if this.chosen is true or if options.prechoose is true, then // send the event straight to the callback // - otherwise: // - if the event buffer has an entry with the same // coallesceGroup identifier, drop it // - add the event to the event buffer }
Gesture objects have an Event buffer, initially empty. Each Event in this buffer can be associated with a coallesceGroup, which is identified by integer.
When created, Gesture objects register themselves as pointer-down, pointer-move, and pointer-up event handlers on their target, with the same event handler. That event handler runs the following steps:
processEvent() with the Event objectSubclasses should override processEvent():
dictionary GestureList { Array<Gesture> gestures; Boolean chosen; } class GestureManager { constructor (EventTarget target); readonly attribute EventTarget target; // the ApplicationDocument, normally void addGesture(Event event, Gesture gesture); void cancelGesture(Gesture gesture); void chooseGesture(Gesture gesture); GestureList getActiveGestures(PointerID pointer); }
GestureManager objects have a map of lists of Gesture objects, keyed on pointer IDs, and with each list associated with a “chosen” flag indicating if an entry in the list has already been chosen. Initially the map is empty. It is exposed by the getActiveGestures() method, which returns the list and flag.
When addGesture() is called with an event and a Gesture, it runs the following steps:
cancelGesture() with this GestureA GestureManager, when created, starts listening to pointer-down events on its target. The listener acts as follows:
pointer-down eventchoose() method.When cancelGesture() is called with a Gesture:
choose() method.When chooseGesture() is called with a Gesture:
cancel() on each entry in loserschoose() on the Gestureclass TapGesture : Gesture { // internal state: // Integer numButtons = 0; // Boolean primaryDown = false; virtual GestureState processEvent(Event event); // - let returnValue = { finished = false } // - if the event is a pointer-down: // - increment this.numButtons // - set returnValue.capture = true // - otherwise if it is a pointer-up: // - assert: this.numButtons > 0 // - decrement this.numButtons // - if numButtons == 0: // - set returnValue.finished = true // - if this.ready == false and this.active == false: // - return returnValue // - if EventTarget isn't an Element: // - assert: event is a pointer-down // - return returnValue // - if the event is pointer-down: // - assert: this.numButtons > 0 // - if it's primary: // - assert: this.ready==true // this is the first press // - this.primaryDown = true // - sendEvent() a tap-down event, with prechoose=true // - set returnValue.cancel = false // - return returnValue // - otherwise: // - if this.primaryDown == true and this.active == true: // - // this is some bogus secondary press that we should have prevent // // taps from starting until it's finished, but it doesn't invalidate // // the existing primary press // - set returnValue.cancel = false // - return returnValue // - otherwise: // - // this is some secondary press but we don't have a first press // // (maybe this is all in the context of a right-click or something) // // we have to wait til it's done before we can start a tap gesture again // - return returnValue // - if the event is pointer-move: // - assert: this.numButtons > 0 // - if it's primary: // - if it hit tests within target's bounding box: // - sendEvent() a tap-move event, with prechoose=true // - set returnValue.cancel = false // - return returnValue // - otherwise: // - sendEvent() a tap-cancel event, with prechoose=true // - return returnValue // - otherwise: // - // this is the move of some bogus secondary press // // ignore it, but continue listening if we have a primary button down // - if this.primaryDown == true and this.active == true: // - set returnValue.cancel = false // - return returnValue // - if the event is pointer-up: // - if it's primary: // - sendEvent() a tap event // - set this.primaryDown = false // - set returnValue.cancel = false // - return returnValue // - otherwise: // - // this is the 'up' of some bogus secondary press // // ignore it, but continue listening for our primary up if necessary // - if this.primaryDown == true and this.active == true: // - set returnValue.cancel = false // - return returnValue } class LongPressGesture : Gesture { GestureState processEvent(EventTarget target, Event event); // long-tap-start: sent when the primary pointer goes down // long-tap-cancel: sent when cancel()ed or finger goes out of bounding box // long-tap: sent when the primary pointer is released } class DoubleTapGesture : Gesture { GestureState processEvent(EventTarget target, Event event); // double-tap-start: sent when the primary pointer goes down the first time // double-tap-cancel: sent when cancel()ed or finger goes out of bounding box, or it times out // double-tap: sent when the primary pointer is released the second time within the timeout } abstract class ScrollGesture : Gesture { GestureState processEvent(EventTarget target, Event event); // this fires the following events (inertia is a boolean, delta is a float): // scroll-start, with field inertia=false, delta=0; prechoose=true // scroll, with fields inertia (is this a simulated scroll from inertia or a real scroll?), delta (number of pixels to scroll); prechoose=true // scroll-end, with field inertia (same), delta=0; prechoose=true // scroll-start is fired right away // scroll is sent whenever the primary pointer moves while down // scroll is also sent after the pointer goes back up, based on inertia // scroll-end is sent after the pointer goes back up once the scroll reaches delta=0 // scroll-end is also sent when the gesture is canceled or reset // processEvent() returns: // - cancel=false pretty much always so long as there's a primary touch (e.g. not for a right-click) // - chose=true when you travel a certain distance // - finished=true when the primary pointer goes up } class HorizontalScrollGesture : ScrollGesture { } // a ScrollGesture giving x-axis scrolling class VerticalScrollGesture : ScrollGesture { } // a ScrollGesture giving y-axis scrolling class PanGesture : Gesture { // similar to ScrollGesture, but with two axes // pan-start, pan, pan-end // events have inertia (boolean), dx (float), dy (float) } abstract class ZoomGesture : Gesture { GestureState processEvent(EventTarget target, Event event); // zoom-start: sent when we could start zooming (e.g. for pinch-zoom, when two fingers hit the glass) (prechoose) // zoom-end: sent when cancel()ed after zoom-start, or when the fingers are lifted (prechoose) // zoom, with a 'scale' attribute, whose value is a multiple of the scale factor at zoom-start // e.g. if the user zooms to 2x, you'd get a bunch of 'zoom' events like scale=1.0, scale=1.17, ... scale=1.91, scale=2.0 } class PinchZoomGesture : ZoomGesture { // a ZoomGesture for two-finger-pinch gesture // zoom is prechoose } class DoubleTapZoomGesture : ZoomGesture { // a ZoomGesture for the double-tap-slide gesture // when the slide starts, forceChoose } class PanAndZoomGesture : Gesture { GestureState processEvent(EventTarget target, Event event); // manipulate-start (prechoose) // manipulate: (prechoose) // panX, panY: pixels // scaleX, scaleY: a multiplier of the scale at manipulate-start // rotation: turns // manipulate-end (prechoose) } abstract class FlingGesture : Gesture { GestureState processEvent(EventTarget target, Event event); // fling-start: when the gesture begins (prechoose) // fling-move: while the user is directly dragging the element (has delta attribute with the distance from fling-start) (prechoose) // fling: the user has released the pointer and the decision is it was in fact flung // fling-cancel: cancel(), or the user has released the pointer and the decision is it was not flung (prechoose) // fling-end: cancel(), or after fling or fling-cancel (prechoose) } class FlingLeftGesture : FlingGesture { } class FlingRightGesture : FlingGesture { } class FlingUpGesture : FlingGesture { } class FlingDownGesture : FlingGesture { }