blob: cefa311e0ed54a5953a28a95061153fcf15c10d4 [file] [log] [blame] [view]
Gestures
========
TODO(ianh): make it possible for a Gesture to time out and cancel even
without having seen a pointer event, to handle double-tap events
TODO(ianh): even with that, we should keep track of finished-but-valid
candidates so that when double-tap cancels itself, the tap, which was
still valid even though it's done, can be accepted
```javascript
callback GestureCallback void (Event event);
dictionary GestureState {
Boolean valid = false; // if true, the event was part of the current gesture
Boolean forceCommit = 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
}
dictionary SendEventOptions {
Integer? coallesceGroup = null; // when queuing events, only the last event with each group is kept
Boolean precommit = false; // if true, event should just be sent right away, not queued
}
abstract class Gesture {
constructor ();
attribute GestureCallback callback;
// set by GestureChooser to point to itself
GestureState processEvent(EventTarget target, Event event);
// - if this.ready=true:
// - clear the sendEvent() buffer
// - set this.accepted = false
// - let returnValue = this.processEventInternal(...)
// - if this.discarding:
// - assert: returnValue.valid == false
// - if returnValue.valid == false
// - assert: returnValue.forceCommit == false
// - if !returnValue.valid, then:
// - clear the sendEvent() buffer
// - set this.accepted = false
// - set this.canceled = !returnValue.valid
// - set this.ready = returnValue.finished
// - set this.discarding = !returnValue.valid && !returnValue.finished
// - set this.active = returnValue.valid && !returnValue.finished
// - return returnValue
readonly attribute Boolean canceled; // defaults to false
// true if either the last time processEvent was invoked, valid was
// false, or, we have been cancel()ed
readonly attribute Boolean ready; // defaults to true
// true if the last time processEvent was invoked, the gesture was
// over
readonly attribute Boolean discarding; // defaults to false
// true if the last time processEvent was invoked, valid was false
// and finished was false, or, we have been cancel()ed
// (aka canceled && !ready)
readonly attribute Boolean active; // defaults to false
// true if the last time processEvent was invoked, valid was true
// and finished was false, and we haven't been cancel()ed
// (aka !canceled && !ready)
readonly attribute Boolean accepted; // defaults to false
// true accept() was called and we haven't been cancel()ed since
void accept();
// assert: this.canceled == false
// set accepted = true
// send the buffered gesture events to the callback
// - call this immediately after getting a positive result from
// processEvent()
virtual void cancel();
// called to indicate that this gesture isn't going to be chosen,
// or if it was chosen, that it is finished
// - assert: this.canceled == false
// - set this.canceled = true
// - set this.discarding = !this.ready
// - set this.active = false
// - clear the sendEvent() buffer
// - set this.accepted = false
// - descendants may override this if they have more state to drop,
// or if they want to send an event to report that it's canceled,
// especially if this.accepted is true
virtual void reset();
// called immediately after the first pointer-down of a possible
// gesture is sent to processEvents() to indicate that the pointer
// wasn't captured so we are to forget anything ever happened (later
// pointer-downs are always captured)
// - set this.canceled = true
// - set this.ready = true
// - set this.discarding = false
// - set this.active = false
// - clear the sendEvent() buffer
// - set this.accepted = false
// - descendants may override this if they have more state to drop
// internal API:
virtual GestureState processEventInternal(EventTarget target, Event event);
// descendants override this
// default implementation returns { } (defaults)
// - if this.discarding == false, then:
// - optionally, call sendEvent() to fire gesture-specific
// events
// - as the events are received, they get examined to see if they
// fit the pattern for the gesture; if they do, then return an
// object with valid=true; if more events for this gesture could
// still come in, return finished=false.
// - if you returned valid=false finished=false, then the next call
// to this must not return valid=true
// - doing anything with the event or target other than reading
// state is a contract violation
// - you are allowed to call sendEvent() at any time during a
// processEventInternal() call, or after a call to
// processEventInternal(), assuming that the last such call
// returned either valid=true or finished=true, until the next
// call to processEventInternal() or cancel().
// - set forceCommit=true on the return value if you are confident
// that this is the gesture the user meant, even if it's possible
// that another gesture is still claiming it's valid (e.g. a long
// press might forceCommit to override a scroll, if the user
// hasn't moved for a while)
// - if you send events, you can set precommit=true to send the
// event even before the gesture has been accepted
// - if you send precommit events, make sure to send corresponding
// "cancel" events if reset() or cancel() are called
void sendEvent(Event event, SendEventOptions options);
// used internally to queue up or send events
// - assert: this.discarding == false
// - assert: options.precommit is false or options.coallesceGroup
// is null
// - set event.gesture = this
// - if this.accepted is true or if options.precommit is true, then
// send the event straight to the callback
// - otherwise:
// - if the buffer has an entry with the same coallesceGroup
// identifier, drop it
// - add the event to the buffer
}
class GestureChooser : EventTarget {
constructor (EventTarget? target = null, Array<Gesture> candidates = []);
// throws if any of the candidates are active
readonly attribute EventTarget? target;
void setTarget(EventTarget? target);
Array<Gesture> getGestures();
void addGesture(Gesture candidate);
// throw if candidates.active is true
void removeGesture(Gesture candidate);
// if active is true and candidate was the last Gesture in our list
// to be active, set active and accepted to false
// while target is not null and the list of candidates is not empty,
// ensures that it is registered as an event listener for
// pointer-down, pointer-move, and pointer-up events on the target;
// when the target changes, or when the list of candidates is
// emptied, unregisters itself
readonly attribute Boolean active; // at least one of the gestures is active (initially false)
readonly attribute Boolean accepted; // we accepted a gesture since the last time active was false (initially false)
// internal state:
// /candidates/ is a list of Gesture objects, initially empty
//
// any time one of the pointer events is received:
// - if it's pointer-down and it's already captured, ignore the
// event and skip the remaining steps
// - let captured be a boolean
// - if /candidates/ is empty, then:
// - set captured to false
// - if accepted is true, call cancel() on whatever the last
// accepted candidate was, if any
// - add all the registered Gestures to /candidates/
// - otherwise:
// - set captured to true
// - if it's pointer-down, capture the event
// - call processEvent() with the event on all the Gestures in
// /candidates/, collecting their return values (GestureState
// objects)
// - set forcingAccept to false
// - set willAccept to null
// - for each Gesture in /candidates/, in registration order:
// - if it returned valid==false, then
// - if it is our last accepted candidate, then:
// - set this.accepted = false
// - if it returned valid==true:
// - if it's pointer-down, then:
// - set captured to true
// - capture the event
// - if this.accepted == true:
// - assert: this Gesture is the last accepted candidate
// - if it returned forceCommit==true then:
// - assert that its accepted attribute is false
// - if forcingAccept is false, then:
// - set willAccept to this Gesture
// - set forcingAccept to true
// - otherwise:
// - if forcingAccept is false:
// - if willAccept is null:
// - set willAccept to this Gesture
// - otherwise:
// - set willAccept to 'undecided'
// - if it returned finished==true
// - remove the Gesture from /candidates/
// - if willAccept is set to a Gesture:
// - set this.accepted = true
// - call the Gesture's accept() method; this is now the last
// accepted candidate
// - call cancel() on all the other Gesture objects that returned
// valid==true
// - if captured is false:
// - call reset() on all the gestures in /candidates/, and then
// let /candidates/ be empty
// - if /candidates/ is now empty, then set active to false;
// otherwise, set active to true
}
class TapGesture : Gesture {
// internal state:
// Integer numButtons = 0;
// Boolean primaryDown = false;
virtual Boolean internalProcessEvent(EventTarget target, Event event);
// - if the event is a pointer-down:
// - increment this.numButtons
// - otherwise if it is a pointer-up:
// - assert: this.numButtons > 0
// - decrement this.numButtons
// - if this.discarding == true:
// return { valid: false, finished: this.numButtons == 0 }
// - if EventTarget isn't an Element:
// - assert: event is a pointer-down
// - assert: this.numButtons > 0
// - return { valid: false, finished: false }
// - 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 precommit=true
// - return { valid: true, finished: false }
// - otherwise:
// - if this.ready == false:
// - // this is a right-click or similar
// - return { valid: false, finished: false }
// - otherwise, if this.canceled==false:
// - assert: this.active==true
// - // this is some bogus secondary press that we should ignore
// // but it doesn't invalidate the existing primary press
// - return { valid true, finished: false }
// - otherwise:
// - // this is some secondary press but we don't have a first press
// // we have to wait til it's done before we can start a
// // tap gesture again
// - return { valid: false, finished: false }
// - otherwise:
// - assert: this.active
// // if we're ready, forcibly the first event we'll see is a pointer-down,
// // so this.ready will never be true here
// // if we're cancelled, then we won't get to here
// - if the event is pointer-move:
// - assert: this.numButtons > 0
// // because otherwise we would have lost capture and thus not be getting the events
// - if it's primary:
// - if it hit tests within target's bounding box:
// - sendEvent() a tap-move event, with precommit=true
// - return { valid: true, finished: false }
// - otherwise:
// - sendEvent() a tap-cancel event, with precommit=true
// - return { valid: false, finished: false }
// - otherwise:
// - // this is the move of some bogus secondary press
// // ignore it, but continue listening
// - return { valid: true, finished: false }
// - if the event is pointer-up:
// - if it's primary:
// - sendEvent() a tap event
// - this.primaryDown = false
// - return { valid: true, forceCommit: this.numButtons == 0, finished: this.numButtons == 0 }
// - otherwise:
// - // this is the 'up' of some bogus secondary press
// // ignore it, but continue listening for our primary up
// - return { valid: this.primaryDown, finished: this.numButtons == 0 }
}
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(), reset(), 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(), reset(), or finger goes out of bounding box, or it times out
// double-tap: sent when the primary pointer is released the second time
}
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; precommit=true
// scroll, with fields inertia (is this a simulated scroll from inertia or a real scroll?), delta (number of pixels to scroll); precommit=true
// scroll-end, with field inertia (same), delta=0; precommit=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:
// - valid=true pretty much always so long as there's a primary touch (e.g. not for a right-click)
// - forceCommit=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) (precommit)
// zoom-end: sent when cancel()ed after zoom-start, or when the fingers are lifted (precommit)
// 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 precommit
}
class DoubleTapZoomGesture : ZoomGesture {
// a ZoomGesture for the double-tap-slide gesture
// when the slide starts, forceCommit
}
class PanAndZoomGesture : Gesture {
GestureState processEvent(EventTarget target, Event event);
// manipulate-start (precommit)
// manipulate: (precommit)
// panX, panY: pixels
// scaleX, scaleY: a multiplier of the scale at manipulate-start
// rotation: turns
// manipulate-end (precommit)
}
abstract class FlingGesture : Gesture {
GestureState processEvent(EventTarget target, Event event);
// fling-start: when the gesture begins (precommit)
// fling-move: while the user is directly dragging the element (has delta attribute with the distance from fling-start) (precommit)
// 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 (precommit)
// fling-end: cancel(), reset(), or after fling or fling-cancel (precommit)
}
class FlingLeftGesture : FlingGesture { }
class FlingRightGesture : FlingGesture { }
class FlingUpGesture : FlingGesture { }
class FlingDownGesture : FlingGesture { }
```