diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:54:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:54:43 +0000 |
commit | e4283f6d48b98e764b988b43bbc86b9d52e6ec94 (patch) | |
tree | c8f7f7a6c2f5faa2942d27cefc6fd46cca492656 /js/ui/swipeTracker.js | |
parent | Initial commit. (diff) | |
download | gnome-shell-e4283f6d48b98e764b988b43bbc86b9d52e6ec94.tar.xz gnome-shell-e4283f6d48b98e764b988b43bbc86b9d52e6ec94.zip |
Adding upstream version 43.9.upstream/43.9upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/ui/swipeTracker.js')
-rw-r--r-- | js/ui/swipeTracker.js | 787 |
1 files changed, 787 insertions, 0 deletions
diff --git a/js/ui/swipeTracker.js b/js/ui/swipeTracker.js new file mode 100644 index 0000000..869f977 --- /dev/null +++ b/js/ui/swipeTracker.js @@ -0,0 +1,787 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported SwipeTracker */ + +const { Clutter, Gio, GObject, Meta } = imports.gi; + +const Main = imports.ui.main; +const Params = imports.misc.params; + +// FIXME: ideally these values matches physical touchpad size. We can get the +// correct values for gnome-shell specifically, since mutter uses libinput +// directly, but GTK apps cannot get it, so use an arbitrary value so that +// it's consistent with apps. +const TOUCHPAD_BASE_HEIGHT = 300; +const TOUCHPAD_BASE_WIDTH = 400; + +const EVENT_HISTORY_THRESHOLD_MS = 150; + +const SCROLL_MULTIPLIER = 10; + +const MIN_ANIMATION_DURATION = 100; +const MAX_ANIMATION_DURATION = 400; +const VELOCITY_THRESHOLD_TOUCH = 0.3; +const VELOCITY_THRESHOLD_TOUCHPAD = 0.6; +const DECELERATION_TOUCH = 0.998; +const DECELERATION_TOUCHPAD = 0.997; +const VELOCITY_CURVE_THRESHOLD = 2; +const DECELERATION_PARABOLA_MULTIPLIER = 0.35; +const DRAG_THRESHOLD_DISTANCE = 16; + +// Derivative of easeOutCubic at t=0 +const DURATION_MULTIPLIER = 3; +const ANIMATION_BASE_VELOCITY = 0.002; +const EPSILON = 0.005; + +const GESTURE_FINGER_COUNT = 3; + +const State = { + NONE: 0, + SCROLLING: 1, +}; + +const TouchpadState = { + NONE: 0, + PENDING: 1, + HANDLING: 2, + IGNORED: 3, +}; + +const EventHistory = class { + constructor() { + this.reset(); + } + + reset() { + this._data = []; + } + + trim(time) { + const thresholdTime = time - EVENT_HISTORY_THRESHOLD_MS; + const index = this._data.findIndex(r => r.time >= thresholdTime); + + this._data.splice(0, index); + } + + append(time, delta) { + this.trim(time); + + this._data.push({ time, delta }); + } + + calculateVelocity() { + if (this._data.length < 2) + return 0; + + const firstTime = this._data[0].time; + const lastTime = this._data[this._data.length - 1].time; + + if (firstTime === lastTime) + return 0; + + const totalDelta = this._data.slice(1).map(a => a.delta).reduce((a, b) => a + b); + const period = lastTime - firstTime; + + return totalDelta / period; + } +}; + +const TouchpadSwipeGesture = GObject.registerClass({ + Properties: { + 'enabled': GObject.ParamSpec.boolean( + 'enabled', 'enabled', 'enabled', + GObject.ParamFlags.READWRITE, + true), + 'orientation': GObject.ParamSpec.enum( + 'orientation', 'orientation', 'orientation', + GObject.ParamFlags.READWRITE, + Clutter.Orientation, Clutter.Orientation.HORIZONTAL), + }, + Signals: { + 'begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] }, + 'update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] }, + 'end': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE] }, + }, +}, class TouchpadSwipeGesture extends GObject.Object { + _init(allowedModes) { + super._init(); + this._allowedModes = allowedModes; + this._state = TouchpadState.NONE; + this._cumulativeX = 0; + this._cumulativeY = 0; + this._touchpadSettings = new Gio.Settings({ + schema_id: 'org.gnome.desktop.peripherals.touchpad', + }); + + global.stage.connectObject( + 'captured-event::touchpad', this._handleEvent.bind(this), this); + } + + _handleEvent(actor, event) { + if (event.type() !== Clutter.EventType.TOUCHPAD_SWIPE) + return Clutter.EVENT_PROPAGATE; + + if (event.get_gesture_phase() === Clutter.TouchpadGesturePhase.BEGIN) + this._state = TouchpadState.NONE; + + if (event.get_touchpad_gesture_finger_count() !== GESTURE_FINGER_COUNT) + return Clutter.EVENT_PROPAGATE; + + if ((this._allowedModes & Main.actionMode) === 0) + return Clutter.EVENT_PROPAGATE; + + if (!this.enabled) + return Clutter.EVENT_PROPAGATE; + + if (this._state === TouchpadState.IGNORED) + return Clutter.EVENT_PROPAGATE; + + let time = event.get_time(); + + const [x, y] = event.get_coords(); + const [dx, dy] = event.get_gesture_motion_delta_unaccelerated(); + + if (this._state === TouchpadState.NONE) { + if (dx === 0 && dy === 0) + return Clutter.EVENT_PROPAGATE; + + this._cumulativeX = 0; + this._cumulativeY = 0; + this._state = TouchpadState.PENDING; + } + + if (this._state === TouchpadState.PENDING) { + this._cumulativeX += dx; + this._cumulativeY += dy; + + const cdx = this._cumulativeX; + const cdy = this._cumulativeY; + const distance = Math.sqrt(cdx * cdx + cdy * cdy); + + if (distance >= DRAG_THRESHOLD_DISTANCE) { + const gestureOrientation = Math.abs(cdx) > Math.abs(cdy) + ? Clutter.Orientation.HORIZONTAL + : Clutter.Orientation.VERTICAL; + + this._cumulativeX = 0; + this._cumulativeY = 0; + + if (gestureOrientation === this.orientation) { + this._state = TouchpadState.HANDLING; + this.emit('begin', time, x, y); + } else { + this._state = TouchpadState.IGNORED; + return Clutter.EVENT_PROPAGATE; + } + } else { + return Clutter.EVENT_PROPAGATE; + } + } + + const vertical = this.orientation === Clutter.Orientation.VERTICAL; + let delta = vertical ? dy : dx; + const distance = vertical ? TOUCHPAD_BASE_HEIGHT : TOUCHPAD_BASE_WIDTH; + + switch (event.get_gesture_phase()) { + case Clutter.TouchpadGesturePhase.BEGIN: + case Clutter.TouchpadGesturePhase.UPDATE: + if (this._touchpadSettings.get_boolean('natural-scroll')) + delta = -delta; + + this.emit('update', time, delta, distance); + break; + + case Clutter.TouchpadGesturePhase.END: + case Clutter.TouchpadGesturePhase.CANCEL: + this.emit('end', time, distance); + this._state = TouchpadState.NONE; + break; + } + + return this._state === TouchpadState.HANDLING + ? Clutter.EVENT_STOP + : Clutter.EVENT_PROPAGATE; + } + + destroy() { + global.stage.disconnectObject(this); + } +}); + +const TouchSwipeGesture = GObject.registerClass({ + Properties: { + 'distance': GObject.ParamSpec.double( + 'distance', 'distance', 'distance', + GObject.ParamFlags.READWRITE, + 0, Infinity, 0), + 'orientation': GObject.ParamSpec.enum( + 'orientation', 'orientation', 'orientation', + GObject.ParamFlags.READWRITE, + Clutter.Orientation, Clutter.Orientation.HORIZONTAL), + }, + Signals: { + 'begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] }, + 'update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] }, + 'end': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE] }, + 'cancel': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE] }, + }, +}, class TouchSwipeGesture extends Clutter.GestureAction { + _init(allowedModes, nTouchPoints, thresholdTriggerEdge) { + super._init(); + this.set_n_touch_points(nTouchPoints); + this.set_threshold_trigger_edge(thresholdTriggerEdge); + + this._allowedModes = allowedModes; + this._distance = global.screen_height; + + global.display.connect('grab-op-begin', () => { + this.cancel(); + }); + + this._lastPosition = 0; + } + + get distance() { + return this._distance; + } + + set distance(distance) { + if (this._distance === distance) + return; + + this._distance = distance; + this.notify('distance'); + } + + vfunc_gesture_prepare(actor) { + if (!super.vfunc_gesture_prepare(actor)) + return false; + + if ((this._allowedModes & Main.actionMode) === 0) + return false; + + let time = this.get_last_event(0).get_time(); + let [xPress, yPress] = this.get_press_coords(0); + let [x, y] = this.get_motion_coords(0); + const [xDelta, yDelta] = [x - xPress, y - yPress]; + const swipeOrientation = Math.abs(xDelta) > Math.abs(yDelta) + ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; + + if (swipeOrientation !== this.orientation) + return false; + + this._lastPosition = + this.orientation === Clutter.Orientation.VERTICAL ? y : x; + + this.emit('begin', time, xPress, yPress); + return true; + } + + vfunc_gesture_progress(_actor) { + let [x, y] = this.get_motion_coords(0); + let pos = this.orientation === Clutter.Orientation.VERTICAL ? y : x; + + let delta = pos - this._lastPosition; + this._lastPosition = pos; + + let time = this.get_last_event(0).get_time(); + + this.emit('update', time, -delta, this._distance); + + return true; + } + + vfunc_gesture_end(_actor) { + let time = this.get_last_event(0).get_time(); + + this.emit('end', time, this._distance); + } + + vfunc_gesture_cancel(_actor) { + let time = Clutter.get_current_event_time(); + + this.emit('cancel', time, this._distance); + } +}); + +const ScrollGesture = GObject.registerClass({ + Properties: { + 'enabled': GObject.ParamSpec.boolean( + 'enabled', 'enabled', 'enabled', + GObject.ParamFlags.READWRITE, + true), + 'orientation': GObject.ParamSpec.enum( + 'orientation', 'orientation', 'orientation', + GObject.ParamFlags.READWRITE, + Clutter.Orientation, Clutter.Orientation.HORIZONTAL), + 'scroll-modifiers': GObject.ParamSpec.flags( + 'scroll-modifiers', 'scroll-modifiers', 'scroll-modifiers', + GObject.ParamFlags.READWRITE, + Clutter.ModifierType, 0), + }, + Signals: { + 'begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] }, + 'update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] }, + 'end': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE] }, + }, +}, class ScrollGesture extends GObject.Object { + _init(actor, allowedModes) { + super._init(); + this._allowedModes = allowedModes; + this._began = false; + this._enabled = true; + + actor.connect('scroll-event', this._handleEvent.bind(this)); + } + + get enabled() { + return this._enabled; + } + + set enabled(enabled) { + if (this._enabled === enabled) + return; + + this._enabled = enabled; + this._began = false; + + this.notify('enabled'); + } + + canHandleEvent(event) { + if (event.type() !== Clutter.EventType.SCROLL) + return false; + + if (event.get_scroll_source() !== Clutter.ScrollSource.FINGER && + event.get_source_device().get_device_type() !== Clutter.InputDeviceType.TOUCHPAD_DEVICE) + return false; + + if (!this.enabled) + return false; + + if ((this._allowedModes & Main.actionMode) === 0) + return false; + + if (!this._began && this.scrollModifiers !== 0 && + (event.get_state() & this.scrollModifiers) === 0) + return false; + + return true; + } + + _handleEvent(actor, event) { + if (!this.canHandleEvent(event)) + return Clutter.EVENT_PROPAGATE; + + if (event.get_scroll_direction() !== Clutter.ScrollDirection.SMOOTH) + return Clutter.EVENT_PROPAGATE; + + const vertical = this.orientation === Clutter.Orientation.VERTICAL; + const distance = vertical ? TOUCHPAD_BASE_HEIGHT : TOUCHPAD_BASE_WIDTH; + + let time = event.get_time(); + let [dx, dy] = event.get_scroll_delta(); + if (dx === 0 && dy === 0) { + this.emit('end', time, distance); + this._began = false; + return Clutter.EVENT_STOP; + } + + if (!this._began) { + let [x, y] = event.get_coords(); + this.emit('begin', time, x, y); + this._began = true; + } + + const delta = (vertical ? dy : dx) * SCROLL_MULTIPLIER; + + this.emit('update', time, delta, distance); + + return Clutter.EVENT_STOP; + } +}); + +// USAGE: +// +// To correctly implement the gesture, there must be handlers for the following +// signals: +// +// begin(tracker, monitor) +// The handler should check whether a deceleration animation is currently +// running. If it is, it should stop the animation (without resetting +// progress). Then it should call: +// tracker.confirmSwipe(distance, snapPoints, currentProgress, cancelProgress) +// If it's not called, the swipe would be ignored. +// The parameters are: +// * distance: the page size; +// * snapPoints: an (sorted with ascending order) array of snap points; +// * currentProgress: the current progress; +// * cancelprogress: a non-transient value that would be used if the gesture +// is cancelled. +// If no animation was running, currentProgress and cancelProgress should be +// same. The handler may set 'orientation' property here. +// +// update(tracker, progress) +// The handler should set the progress to the given value. +// +// end(tracker, duration, endProgress) +// The handler should animate the progress to endProgress. If endProgress is +// 0, it should do nothing after the animation, otherwise it should change the +// state, e.g. change the current page or switch workspace. +// NOTE: duration can be 0 in some cases, in this case it should finish +// instantly. + +/** A class for handling swipe gestures */ +var SwipeTracker = GObject.registerClass({ + Properties: { + 'enabled': GObject.ParamSpec.boolean( + 'enabled', 'enabled', 'enabled', + GObject.ParamFlags.READWRITE, + true), + 'orientation': GObject.ParamSpec.enum( + 'orientation', 'orientation', 'orientation', + GObject.ParamFlags.READWRITE, + Clutter.Orientation, Clutter.Orientation.HORIZONTAL), + 'distance': GObject.ParamSpec.double( + 'distance', 'distance', 'distance', + GObject.ParamFlags.READWRITE, + 0, Infinity, 0), + 'allow-long-swipes': GObject.ParamSpec.boolean( + 'allow-long-swipes', 'allow-long-swipes', 'allow-long-swipes', + GObject.ParamFlags.READWRITE, + false), + 'scroll-modifiers': GObject.ParamSpec.flags( + 'scroll-modifiers', 'scroll-modifiers', 'scroll-modifiers', + GObject.ParamFlags.READWRITE, + Clutter.ModifierType, 0), + }, + Signals: { + 'begin': { param_types: [GObject.TYPE_UINT] }, + 'update': { param_types: [GObject.TYPE_DOUBLE] }, + 'end': { param_types: [GObject.TYPE_UINT64, GObject.TYPE_DOUBLE] }, + }, +}, class SwipeTracker extends GObject.Object { + _init(actor, orientation, allowedModes, params) { + super._init(); + params = Params.parse(params, { allowDrag: true, allowScroll: true }); + + this.orientation = orientation; + this._allowedModes = allowedModes; + this._enabled = true; + this._distance = global.screen_height; + this._history = new EventHistory(); + this._reset(); + + this._touchpadGesture = new TouchpadSwipeGesture(allowedModes); + this._touchpadGesture.connect('begin', this._beginGesture.bind(this)); + this._touchpadGesture.connect('update', this._updateGesture.bind(this)); + this._touchpadGesture.connect('end', this._endTouchpadGesture.bind(this)); + this.bind_property('enabled', this._touchpadGesture, 'enabled', 0); + this.bind_property('orientation', this._touchpadGesture, 'orientation', + GObject.BindingFlags.SYNC_CREATE); + + this._touchGesture = new TouchSwipeGesture(allowedModes, + GESTURE_FINGER_COUNT, + Clutter.GestureTriggerEdge.AFTER); + this._touchGesture.connect('begin', this._beginTouchSwipe.bind(this)); + this._touchGesture.connect('update', this._updateGesture.bind(this)); + this._touchGesture.connect('end', this._endTouchGesture.bind(this)); + this._touchGesture.connect('cancel', this._cancelTouchGesture.bind(this)); + this.bind_property('enabled', this._touchGesture, 'enabled', 0); + this.bind_property('orientation', this._touchGesture, 'orientation', + GObject.BindingFlags.SYNC_CREATE); + this.bind_property('distance', this._touchGesture, 'distance', 0); + global.stage.add_action_full('swipe', Clutter.EventPhase.CAPTURE, this._touchGesture); + + if (params.allowDrag) { + this._dragGesture = new TouchSwipeGesture(allowedModes, 1, + Clutter.GestureTriggerEdge.AFTER); + this._dragGesture.connect('begin', this._beginGesture.bind(this)); + this._dragGesture.connect('update', this._updateGesture.bind(this)); + this._dragGesture.connect('end', this._endTouchGesture.bind(this)); + this._dragGesture.connect('cancel', this._cancelTouchGesture.bind(this)); + this.bind_property('enabled', this._dragGesture, 'enabled', 0); + this.bind_property('orientation', this._dragGesture, 'orientation', + GObject.BindingFlags.SYNC_CREATE); + this.bind_property('distance', this._dragGesture, 'distance', 0); + actor.add_action_full('drag', Clutter.EventPhase.CAPTURE, this._dragGesture); + } else { + this._dragGesture = null; + } + + if (params.allowScroll) { + this._scrollGesture = new ScrollGesture(actor, allowedModes); + this._scrollGesture.connect('begin', this._beginGesture.bind(this)); + this._scrollGesture.connect('update', this._updateGesture.bind(this)); + this._scrollGesture.connect('end', this._endTouchpadGesture.bind(this)); + this.bind_property('enabled', this._scrollGesture, 'enabled', 0); + this.bind_property('orientation', this._scrollGesture, 'orientation', + GObject.BindingFlags.SYNC_CREATE); + this.bind_property('scroll-modifiers', + this._scrollGesture, 'scroll-modifiers', 0); + } else { + this._scrollGesture = null; + } + } + + /** + * canHandleScrollEvent: + * @param {Clutter.Event} scrollEvent: an event to check + * @returns {bool} whether the event can be handled by the tracker + * + * This function can be used to combine swipe gesture and mouse + * scrolling. + */ + canHandleScrollEvent(scrollEvent) { + if (!this.enabled || this._scrollGesture === null) + return false; + + return this._scrollGesture.canHandleEvent(scrollEvent); + } + + get enabled() { + return this._enabled; + } + + set enabled(enabled) { + if (this._enabled === enabled) + return; + + this._enabled = enabled; + if (!enabled && this._state === State.SCROLLING) + this._interrupt(); + this.notify('enabled'); + } + + get distance() { + return this._distance; + } + + set distance(distance) { + if (this._distance === distance) + return; + + this._distance = distance; + this.notify('distance'); + } + + _reset() { + this._state = State.NONE; + + this._snapPoints = []; + this._initialProgress = 0; + this._cancelProgress = 0; + + this._prevOffset = 0; + this._progress = 0; + + this._cancelled = false; + + this._history.reset(); + } + + _interrupt() { + this.emit('end', 0, this._cancelProgress); + this._reset(); + } + + _beginTouchSwipe(gesture, time, x, y) { + if (this._dragGesture) + this._dragGesture.cancel(); + + this._beginGesture(gesture, time, x, y); + } + + _beginGesture(gesture, time, x, y) { + if (this._state === State.SCROLLING) + return; + + this._history.append(time, 0); + + let rect = new Meta.Rectangle({ x, y, width: 1, height: 1 }); + let monitor = global.display.get_monitor_index_for_rect(rect); + + this.emit('begin', monitor); + } + + _findClosestPoint(pos) { + const distances = this._snapPoints.map(x => Math.abs(x - pos)); + const min = Math.min(...distances); + return distances.indexOf(min); + } + + _findNextPoint(pos) { + return this._snapPoints.findIndex(p => p >= pos); + } + + _findPreviousPoint(pos) { + const reversedIndex = this._snapPoints.slice().reverse().findIndex(p => p <= pos); + return this._snapPoints.length - 1 - reversedIndex; + } + + _findPointForProjection(pos, velocity) { + const initial = this._findClosestPoint(this._initialProgress); + const prev = this._findPreviousPoint(pos); + const next = this._findNextPoint(pos); + + if ((velocity > 0 ? prev : next) === initial) + return velocity > 0 ? next : prev; + + return this._findClosestPoint(pos); + } + + _getBounds(pos) { + if (this.allowLongSwipes) + return [this._snapPoints[0], this._snapPoints[this._snapPoints.length - 1]]; + + const closest = this._findClosestPoint(pos); + + let prev, next; + if (Math.abs(this._snapPoints[closest] - pos) < EPSILON) { + prev = next = closest; + } else { + prev = this._findPreviousPoint(pos); + next = this._findNextPoint(pos); + } + + const lowerIndex = Math.max(prev - 1, 0); + const upperIndex = Math.min(next + 1, this._snapPoints.length - 1); + + return [this._snapPoints[lowerIndex], this._snapPoints[upperIndex]]; + } + + _updateGesture(gesture, time, delta, distance) { + if (this._state !== State.SCROLLING) + return; + + if ((this._allowedModes & Main.actionMode) === 0 || !this.enabled) { + this._interrupt(); + return; + } + + if (this.orientation === Clutter.Orientation.HORIZONTAL && + Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) + delta = -delta; + + this._progress += delta / distance; + this._history.append(time, delta); + + this._progress = Math.clamp(this._progress, ...this._getBounds(this._initialProgress)); + + this.emit('update', this._progress); + } + + _getEndProgress(velocity, distance, isTouchpad) { + if (this._cancelled) + return this._cancelProgress; + + const threshold = isTouchpad ? VELOCITY_THRESHOLD_TOUCHPAD : VELOCITY_THRESHOLD_TOUCH; + + if (Math.abs(velocity) < threshold) + return this._snapPoints[this._findClosestPoint(this._progress)]; + + const decel = isTouchpad ? DECELERATION_TOUCHPAD : DECELERATION_TOUCH; + const slope = decel / (1.0 - decel) / 1000.0; + + let pos; + if (Math.abs(velocity) > VELOCITY_CURVE_THRESHOLD) { + const c = slope / 2 / DECELERATION_PARABOLA_MULTIPLIER; + const x = Math.abs(velocity) - VELOCITY_CURVE_THRESHOLD + c; + + pos = slope * VELOCITY_CURVE_THRESHOLD + + DECELERATION_PARABOLA_MULTIPLIER * x * x - + DECELERATION_PARABOLA_MULTIPLIER * c * c; + } else { + pos = Math.abs(velocity) * slope; + } + + pos = pos * Math.sign(velocity) + this._progress; + pos = Math.clamp(pos, ...this._getBounds(this._initialProgress)); + + const index = this._findPointForProjection(pos, velocity); + + return this._snapPoints[index]; + } + + _endTouchGesture(_gesture, time, distance) { + this._endGesture(time, distance, false); + } + + _endTouchpadGesture(_gesture, time, distance) { + this._endGesture(time, distance, true); + } + + _endGesture(time, distance, isTouchpad) { + if (this._state !== State.SCROLLING) + return; + + if ((this._allowedModes & Main.actionMode) === 0 || !this.enabled) { + this._interrupt(); + return; + } + + this._history.trim(time); + + let velocity = this._history.calculateVelocity(); + const endProgress = this._getEndProgress(velocity, distance, isTouchpad); + + velocity /= distance; + + if ((endProgress - this._progress) * velocity <= 0) + velocity = ANIMATION_BASE_VELOCITY; + + const nPoints = Math.max(1, Math.ceil(Math.abs(this._progress - endProgress))); + const maxDuration = MAX_ANIMATION_DURATION * Math.log2(1 + nPoints); + + let duration = Math.abs((this._progress - endProgress) / velocity * DURATION_MULTIPLIER); + if (duration > 0) + duration = Math.clamp(duration, MIN_ANIMATION_DURATION, maxDuration); + + this._reset(); + this.emit('end', duration, endProgress); + } + + _cancelTouchGesture(_gesture, time, distance) { + if (this._state !== State.SCROLLING) + return; + + this._cancelled = true; + this._endGesture(time, distance, false); + } + + /** + * confirmSwipe: + * @param {number} distance: swipe distance in pixels + * @param {number[]} snapPoints: + * An array of snap points, sorted in ascending order + * @param {number} currentProgress: initial progress value + * @param {number} cancelProgress: the value to be used on cancelling + * + * Confirms a swipe. User has to call this in 'begin' signal handler, + * otherwise the swipe wouldn't start. If there's an animation running, + * it should be stopped first. + * + * @cancel_progress must always be a snap point, or a value matching + * some other non-transient state. + */ + confirmSwipe(distance, snapPoints, currentProgress, cancelProgress) { + this.distance = distance; + this._snapPoints = snapPoints; + this._initialProgress = currentProgress; + this._progress = currentProgress; + this._cancelProgress = cancelProgress; + + this._state = State.SCROLLING; + } + + destroy() { + if (this._touchpadGesture) { + this._touchpadGesture.destroy(); + delete this._touchpadGesture; + } + + if (this._touchGesture) { + global.stage.remove_action(this._touchGesture); + delete this._touchGesture; + } + } +}); |